v2.7.4 (#579)
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai> Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai> Co-authored-by: Peter Evers <pevers90@gmail.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Pierre Rossinès <pierre.rossines@protonmail.com> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
|
|
@ -239,6 +239,32 @@ def test_vibe_acp_setup_shows_onboarding_and_exits_on_cancel(
|
|||
assert "Setup cancelled" in output
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vibe_acp_survives_broken_config(vibe_home_dir: Path) -> None:
|
||||
vibe_home_dir.mkdir(parents=True, exist_ok=True)
|
||||
(vibe_home_dir / "config.toml").write_text("{{{{invalid toml content!!")
|
||||
|
||||
proc, _initialize_response, conn = await _connect_and_initialize(
|
||||
vibe_home_dir=vibe_home_dir, include_api_key=True
|
||||
)
|
||||
|
||||
try:
|
||||
# new_session should return a structured JSON-RPC error, not crash the server
|
||||
with pytest.raises(RequestError):
|
||||
await asyncio.wait_for(
|
||||
conn.new_session(cwd=str(Path.cwd()), mcp_servers=[]), timeout=10
|
||||
)
|
||||
assert proc.returncode is None, "Server crashed after broken config"
|
||||
|
||||
(vibe_home_dir / "config.toml").write_text("")
|
||||
session = await asyncio.wait_for(
|
||||
conn.new_session(cwd=str(Path.cwd()), mcp_servers=[]), timeout=10
|
||||
)
|
||||
assert session.session_id
|
||||
finally:
|
||||
await _terminate_process(proc)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_vibe_acp_new_session_fails_without_api_key(vibe_home_dir: Path) -> None:
|
||||
proc, _initialize_response, conn = await _connect_and_initialize(
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ def acp_bash_tool(mock_client: MockClient) -> Bash:
|
|||
session_id="test_session_123",
|
||||
tool_call_id="test_tool_call_456",
|
||||
)
|
||||
return Bash(config=config, state=state)
|
||||
return Bash(config_getter=lambda: config, state=state)
|
||||
|
||||
|
||||
class TestAcpBashBasic:
|
||||
|
|
@ -154,7 +154,7 @@ class TestAcpBashExecution:
|
|||
self, mock_client: MockClient
|
||||
) -> None:
|
||||
tool = Bash(
|
||||
config=BashToolConfig(),
|
||||
config_getter=lambda: BashToolConfig(),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -174,7 +174,7 @@ class TestAcpBashExecution:
|
|||
mock_client._terminal_handle = custom_handle
|
||||
|
||||
tool = Bash(
|
||||
config=BashToolConfig(),
|
||||
config_getter=lambda: BashToolConfig(),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -194,7 +194,7 @@ class TestAcpBashExecution:
|
|||
mock_client._create_terminal_error = RuntimeError("Connection failed")
|
||||
|
||||
tool = Bash(
|
||||
config=BashToolConfig(),
|
||||
config_getter=lambda: BashToolConfig(),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -212,7 +212,7 @@ class TestAcpBashExecution:
|
|||
@pytest.mark.asyncio
|
||||
async def test_run_without_client(self) -> None:
|
||||
tool = Bash(
|
||||
config=BashToolConfig(),
|
||||
config_getter=lambda: BashToolConfig(),
|
||||
state=AcpBashState.model_construct(
|
||||
client=None, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -231,7 +231,7 @@ class TestAcpBashExecution:
|
|||
async def test_run_without_session_id(self) -> None:
|
||||
mock_client = MockClient()
|
||||
tool = Bash(
|
||||
config=BashToolConfig(),
|
||||
config_getter=lambda: BashToolConfig(),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id=None, tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -254,7 +254,7 @@ class TestAcpBashExecution:
|
|||
mock_client._terminal_handle = custom_handle
|
||||
|
||||
tool = Bash(
|
||||
config=BashToolConfig(),
|
||||
config_getter=lambda: BashToolConfig(),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -281,7 +281,7 @@ class TestAcpBashTimeout:
|
|||
|
||||
# Use a config with different default timeout to verify args timeout overrides it
|
||||
tool = Bash(
|
||||
config=BashToolConfig(default_timeout=30),
|
||||
config_getter=lambda: BashToolConfig(default_timeout=30),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -310,7 +310,7 @@ class TestAcpBashTimeout:
|
|||
custom_handle.kill = failing_kill
|
||||
|
||||
tool = Bash(
|
||||
config=BashToolConfig(),
|
||||
config_getter=lambda: BashToolConfig(),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -328,7 +328,7 @@ class TestAcpBashEmbedding:
|
|||
@pytest.mark.asyncio
|
||||
async def test_run_with_embedding(self, mock_client: MockClient) -> None:
|
||||
tool = Bash(
|
||||
config=BashToolConfig(),
|
||||
config_getter=lambda: BashToolConfig(),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -344,7 +344,7 @@ class TestAcpBashEmbedding:
|
|||
self, mock_client: MockClient
|
||||
) -> None:
|
||||
tool = Bash(
|
||||
config=BashToolConfig(),
|
||||
config_getter=lambda: BashToolConfig(),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id=None
|
||||
),
|
||||
|
|
@ -367,7 +367,7 @@ class TestAcpBashEmbedding:
|
|||
mock_client.session_update = failing_session_update
|
||||
|
||||
tool = Bash(
|
||||
config=BashToolConfig(),
|
||||
config_getter=lambda: BashToolConfig(),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -393,7 +393,7 @@ class TestAcpBashConfig:
|
|||
mock_client._terminal_handle = custom_handle
|
||||
|
||||
tool = Bash(
|
||||
config=BashToolConfig(default_timeout=30),
|
||||
config_getter=lambda: BashToolConfig(default_timeout=30),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -423,7 +423,7 @@ class TestAcpBashCleanup:
|
|||
custom_handle.release = mock_release
|
||||
|
||||
tool = Bash(
|
||||
config=BashToolConfig(),
|
||||
config_getter=lambda: BashToolConfig(),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -455,7 +455,7 @@ class TestAcpBashCleanup:
|
|||
custom_handle.release = mock_release
|
||||
|
||||
tool = Bash(
|
||||
config=BashToolConfig(),
|
||||
config_getter=lambda: BashToolConfig(),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -481,7 +481,7 @@ class TestAcpBashCleanup:
|
|||
mock_client._terminal_handle = custom_handle
|
||||
|
||||
tool = Bash(
|
||||
config=BashToolConfig(),
|
||||
config_getter=lambda: BashToolConfig(),
|
||||
state=AcpBashState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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.3"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.7.4"
|
||||
)
|
||||
|
||||
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.3"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.7.4"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
|
|
@ -291,6 +291,42 @@ class TestLoadSession:
|
|||
assert len(thought_updates) == 1
|
||||
assert thought_updates[0].update.content.text == "Let me think step by step..."
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_replays_reasoning_before_assistant_message(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
session_id = "replay-order-1234"
|
||||
cwd = str(Path.cwd())
|
||||
messages = [
|
||||
{"role": "user", "content": "Think about this"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Here is my answer",
|
||||
"reasoning_content": "Let me think step by step...",
|
||||
},
|
||||
]
|
||||
create_test_session(temp_session_dir, session_id, cwd, messages=messages)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=session_id)
|
||||
|
||||
response_updates = [
|
||||
update.update
|
||||
for update in client._session_updates
|
||||
if isinstance(update.update, (AgentThoughtChunk, AgentMessageChunk))
|
||||
]
|
||||
|
||||
assert [type(update) for update in response_updates] == [
|
||||
AgentThoughtChunk,
|
||||
AgentMessageChunk,
|
||||
]
|
||||
assert response_updates[0].content.text == "Let me think step by step..."
|
||||
assert response_updates[1].content.text == "Here is my answer"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_session_not_found_raises_error(
|
||||
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ def acp_read_file_tool(
|
|||
session_id="test_session_123",
|
||||
tool_call_id="test_tool_call_456",
|
||||
)
|
||||
return ReadFile(config=config, state=state)
|
||||
return ReadFile(config_getter=lambda: config, state=state)
|
||||
|
||||
|
||||
class TestAcpReadFileBasic:
|
||||
|
|
@ -116,7 +116,7 @@ class TestAcpReadFileExecution:
|
|||
test_file = tmp_path / "test_file.txt"
|
||||
test_file.touch()
|
||||
tool = ReadFile(
|
||||
config=ReadFileToolConfig(),
|
||||
config_getter=lambda: ReadFileToolConfig(),
|
||||
state=AcpReadFileState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -139,7 +139,7 @@ class TestAcpReadFileExecution:
|
|||
test_file = tmp_path / "test_file.txt"
|
||||
test_file.touch()
|
||||
tool = ReadFile(
|
||||
config=ReadFileToolConfig(),
|
||||
config_getter=lambda: ReadFileToolConfig(),
|
||||
state=AcpReadFileState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -162,7 +162,7 @@ class TestAcpReadFileExecution:
|
|||
test_file = tmp_path / "test_file.txt"
|
||||
test_file.touch()
|
||||
tool = ReadFile(
|
||||
config=ReadFileToolConfig(),
|
||||
config_getter=lambda: ReadFileToolConfig(),
|
||||
state=AcpReadFileState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -187,7 +187,7 @@ class TestAcpReadFileExecution:
|
|||
test_file = tmp_path / "test.txt"
|
||||
test_file.touch()
|
||||
tool = ReadFile(
|
||||
config=ReadFileToolConfig(),
|
||||
config_getter=lambda: ReadFileToolConfig(),
|
||||
state=AcpReadFileState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -207,7 +207,7 @@ class TestAcpReadFileExecution:
|
|||
test_file = tmp_path / "test.txt"
|
||||
test_file.touch()
|
||||
tool = ReadFile(
|
||||
config=ReadFileToolConfig(),
|
||||
config_getter=lambda: ReadFileToolConfig(),
|
||||
state=AcpReadFileState.model_construct(
|
||||
client=None, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -231,7 +231,7 @@ class TestAcpReadFileExecution:
|
|||
test_file.touch()
|
||||
mock_client = MockClient()
|
||||
tool = ReadFile(
|
||||
config=ReadFileToolConfig(),
|
||||
config_getter=lambda: ReadFileToolConfig(),
|
||||
state=AcpReadFileState.model_construct(
|
||||
client=mock_client, session_id=None, tool_call_id="test_call"
|
||||
),
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ def acp_search_replace_tool(
|
|||
session_id="test_session_123",
|
||||
tool_call_id="test_tool_call_456",
|
||||
)
|
||||
return SearchReplace(config=config, state=state)
|
||||
return SearchReplace(config_getter=lambda: config, state=state)
|
||||
|
||||
|
||||
class TestAcpSearchReplaceBasic:
|
||||
|
|
@ -139,7 +139,7 @@ class TestAcpSearchReplaceExecution:
|
|||
monkeypatch.chdir(tmp_path)
|
||||
config = SearchReplaceConfig(create_backup=True)
|
||||
tool = SearchReplace(
|
||||
config=config,
|
||||
config_getter=lambda: config,
|
||||
state=AcpSearchReplaceState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -169,7 +169,7 @@ class TestAcpSearchReplaceExecution:
|
|||
mock_client._read_error = RuntimeError("File not found")
|
||||
|
||||
tool = SearchReplace(
|
||||
config=SearchReplaceConfig(),
|
||||
config_getter=lambda: SearchReplaceConfig(),
|
||||
state=AcpSearchReplaceState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -200,7 +200,7 @@ class TestAcpSearchReplaceExecution:
|
|||
mock_client._file_content = "old" # Update mock to return correct content
|
||||
|
||||
tool = SearchReplace(
|
||||
config=SearchReplaceConfig(),
|
||||
config_getter=lambda: SearchReplaceConfig(),
|
||||
state=AcpSearchReplaceState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -243,7 +243,7 @@ class TestAcpSearchReplaceExecution:
|
|||
test_file = tmp_path / "test.txt"
|
||||
test_file.touch()
|
||||
tool = SearchReplace(
|
||||
config=SearchReplaceConfig(),
|
||||
config_getter=lambda: SearchReplaceConfig(),
|
||||
state=AcpSearchReplaceState.model_construct(
|
||||
client=client, session_id=session_id, tool_call_id="test_call"
|
||||
),
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ def acp_write_file_tool(
|
|||
session_id="test_session_123",
|
||||
tool_call_id="test_tool_call_456",
|
||||
)
|
||||
return WriteFile(config=config, state=state)
|
||||
return WriteFile(config_getter=lambda: config, state=state)
|
||||
|
||||
|
||||
class TestAcpWriteFileBasic:
|
||||
|
|
@ -95,7 +95,7 @@ class TestAcpWriteFileExecution:
|
|||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
tool = WriteFile(
|
||||
config=WriteFileConfig(),
|
||||
config_getter=lambda: WriteFileConfig(),
|
||||
state=AcpWriteFileState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -130,7 +130,7 @@ class TestAcpWriteFileExecution:
|
|||
mock_client._write_error = RuntimeError("Permission denied")
|
||||
|
||||
tool = WriteFile(
|
||||
config=WriteFileConfig(),
|
||||
config_getter=lambda: WriteFileConfig(),
|
||||
state=AcpWriteFileState.model_construct(
|
||||
client=mock_client, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -149,7 +149,7 @@ class TestAcpWriteFileExecution:
|
|||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
tool = WriteFile(
|
||||
config=WriteFileConfig(),
|
||||
config_getter=lambda: WriteFileConfig(),
|
||||
state=AcpWriteFileState.model_construct(
|
||||
client=None, session_id="test_session", tool_call_id="test_call"
|
||||
),
|
||||
|
|
@ -171,7 +171,7 @@ class TestAcpWriteFileExecution:
|
|||
monkeypatch.chdir(tmp_path)
|
||||
mock_client = MockClient()
|
||||
tool = WriteFile(
|
||||
config=WriteFileConfig(),
|
||||
config_getter=lambda: WriteFileConfig(),
|
||||
state=AcpWriteFileState.model_construct(
|
||||
client=mock_client, session_id=None, tool_call_id="test_call"
|
||||
),
|
||||
|
|
|
|||
|
|
@ -276,6 +276,20 @@ async def test_finds_files_recursively_with_partial_path(
|
|||
assert popup.styles.display == "block"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_popup_is_positioned_near_cursor(vibe_app: VibeApp) -> None:
|
||||
async with vibe_app.run_test() as pilot:
|
||||
popup = vibe_app.query_one(CompletionPopup)
|
||||
|
||||
await pilot.press(*"/com")
|
||||
|
||||
assert popup.styles.display == "block"
|
||||
offset = popup.styles.offset
|
||||
# The popup should have an explicit offset set by _position_popup
|
||||
assert offset.x is not None
|
||||
assert offset.y is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_does_not_trigger_completion_when_navigating_history(
|
||||
file_tree: Path, vibe_app: VibeApp
|
||||
|
|
|
|||
|
|
@ -529,26 +529,6 @@ class TestMistralMapperPrepareMessage:
|
|||
result = mapper.prepare_message(msg)
|
||||
assert result.content == "Hello!"
|
||||
|
||||
def test_strip_reasoning_removes_reasoning_from_assistant(
|
||||
self, mapper: MistralMapper
|
||||
) -> None:
|
||||
msg = LLMMessage(
|
||||
role=Role.assistant,
|
||||
content="Answer",
|
||||
reasoning_content="thinking...",
|
||||
reasoning_signature="sig",
|
||||
)
|
||||
stripped = mapper.strip_reasoning(msg)
|
||||
assert stripped.content == "Answer"
|
||||
assert stripped.reasoning_content is None
|
||||
assert stripped.reasoning_signature is None
|
||||
|
||||
def test_strip_reasoning_leaves_non_assistant_unchanged(
|
||||
self, mapper: MistralMapper
|
||||
) -> None:
|
||||
msg = LLMMessage(role=Role.user, content="hello")
|
||||
assert mapper.strip_reasoning(msg) is msg
|
||||
|
||||
|
||||
class TestMistralBackendReasoningEffort:
|
||||
"""Tests that MistralBackend correctly passes reasoning_effort to the SDK."""
|
||||
|
|
@ -612,53 +592,6 @@ class TestMistralBackendReasoningEffort:
|
|||
assert call_kwargs["reasoning_effort"] == expected_effort
|
||||
assert call_kwargs["temperature"] == expected_temperature
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_complete_strips_reasoning_when_thinking_off(
|
||||
self, backend: MistralBackend
|
||||
) -> None:
|
||||
model = ModelConfig(
|
||||
name="mistral-small-latest",
|
||||
provider="mistral",
|
||||
alias="mistral-small",
|
||||
thinking="off",
|
||||
)
|
||||
messages = [
|
||||
LLMMessage(role=Role.user, content="hi"),
|
||||
LLMMessage(
|
||||
role=Role.assistant, content="answer", reasoning_content="thinking..."
|
||||
),
|
||||
LLMMessage(role=Role.user, content="follow up"),
|
||||
]
|
||||
|
||||
with patch.object(backend, "_get_client") as mock_get_client:
|
||||
mock_client = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = "response"
|
||||
mock_response.choices[0].message.tool_calls = None
|
||||
mock_response.usage.prompt_tokens = 10
|
||||
mock_response.usage.completion_tokens = 5
|
||||
mock_client.chat.complete_async = AsyncMock(return_value=mock_response)
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
await backend.complete(
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=0.2,
|
||||
tools=None,
|
||||
max_tokens=None,
|
||||
tool_choice=None,
|
||||
extra_headers=None,
|
||||
)
|
||||
|
||||
call_kwargs = mock_client.chat.complete_async.call_args.kwargs
|
||||
assert call_kwargs["reasoning_effort"] is None
|
||||
# The assistant message should have reasoning stripped
|
||||
converted_msgs = call_kwargs["messages"]
|
||||
assistant_msg = converted_msgs[1]
|
||||
assert isinstance(assistant_msg, AssistantMessage)
|
||||
assert assistant_msg.content == "answer"
|
||||
|
||||
|
||||
class TestBuildHttpErrorBodyReading:
|
||||
_MESSAGES: ClassVar[list[LLMMessage]] = [LLMMessage(role=Role.user, content="hi")]
|
||||
|
|
|
|||
|
|
@ -87,18 +87,6 @@ class TestThinkingBlocksConversion:
|
|||
{"type": "text", "text": "Answer"},
|
||||
]
|
||||
|
||||
def test_reasoning_stripped_when_thinking_off(self, adapter, provider):
|
||||
messages = [
|
||||
LLMMessage(role=Role.user, content="Hi"),
|
||||
LLMMessage(
|
||||
role=Role.assistant,
|
||||
content="Answer",
|
||||
reasoning_content="Let me think...",
|
||||
),
|
||||
]
|
||||
payload = _prepare(adapter, provider, messages, thinking="off")
|
||||
assert payload["messages"][1]["content"] == "Answer"
|
||||
|
||||
def test_assistant_without_reasoning_is_plain_string(self, adapter, provider):
|
||||
messages = [
|
||||
LLMMessage(role=Role.user, content="Hi"),
|
||||
|
|
|
|||
113
tests/browser_sign_in/stubs.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from vibe.setup.auth import (
|
||||
BrowserSignInError,
|
||||
BrowserSignInErrorCode,
|
||||
BrowserSignInGateway,
|
||||
BrowserSignInPollResult,
|
||||
BrowserSignInProcess,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExchangeRequestPayload:
|
||||
process_id: str
|
||||
exchange_token: str
|
||||
code_verifier: str
|
||||
|
||||
|
||||
class StubBrowserSignInGateway(BrowserSignInGateway):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
process: BrowserSignInProcess | None = None,
|
||||
processes: list[BrowserSignInProcess] | None = None,
|
||||
poll_results: list[BrowserSignInPollResult | BrowserSignInError] | None = None,
|
||||
exchange_result: str = "sk-browser-key",
|
||||
exchange_error: BrowserSignInError | None = None,
|
||||
) -> None:
|
||||
if process is not None and processes is not None:
|
||||
msg = "StubBrowserSignInGateway accepts either process or processes."
|
||||
raise AssertionError(msg)
|
||||
|
||||
self._processes = list(processes or ([] if process is None else [process]))
|
||||
self._poll_results = list(poll_results or [])
|
||||
self.exchange_result = exchange_result
|
||||
self.exchange_error = exchange_error
|
||||
self.code_challenges: list[str] = []
|
||||
self.polled_urls: list[str] = []
|
||||
self.exchange_requests: list[ExchangeRequestPayload] = []
|
||||
self.closed = False
|
||||
self.process_number = 0
|
||||
|
||||
async def create_process(self, code_challenge: str) -> BrowserSignInProcess:
|
||||
self.code_challenges.append(code_challenge)
|
||||
if not self._processes:
|
||||
msg = "StubBrowserSignInGateway requires at least one scripted process."
|
||||
raise AssertionError(msg)
|
||||
|
||||
self.process_number += 1
|
||||
return self._processes.pop(0)
|
||||
|
||||
async def poll(self, poll_url: str) -> BrowserSignInPollResult:
|
||||
self.polled_urls.append(poll_url)
|
||||
if not self._poll_results:
|
||||
msg = "StubBrowserSignInGateway requires scripted poll results."
|
||||
raise AssertionError(msg)
|
||||
|
||||
result = self._poll_results.pop(0)
|
||||
if isinstance(result, BrowserSignInError):
|
||||
raise result
|
||||
return result
|
||||
|
||||
async def exchange(
|
||||
self, process_id: str, exchange_token: str, code_verifier: str
|
||||
) -> str:
|
||||
self.exchange_requests.append(
|
||||
ExchangeRequestPayload(
|
||||
process_id=process_id,
|
||||
exchange_token=exchange_token,
|
||||
code_verifier=code_verifier,
|
||||
)
|
||||
)
|
||||
if self.exchange_error is not None:
|
||||
raise self.exchange_error
|
||||
return self.exchange_result
|
||||
|
||||
async def aclose(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
def build_sign_in_process(
|
||||
now: datetime, process_id: str = "process-1"
|
||||
) -> BrowserSignInProcess:
|
||||
fragment = urlencode({
|
||||
"process_id": process_id,
|
||||
"complete_token": f"complete-token-{process_id}",
|
||||
"state": f"state-{process_id}",
|
||||
})
|
||||
return BrowserSignInProcess(
|
||||
process_id=process_id,
|
||||
sign_in_url=(
|
||||
f"https://console.mistral.ai/codestral/cli/authenticate#{fragment}"
|
||||
),
|
||||
poll_url=(
|
||||
f"https://api.mistral.ai/api/vibe/sign-in/poll/poll-token-{process_id}"
|
||||
),
|
||||
expires_at=now + timedelta(minutes=5),
|
||||
)
|
||||
|
||||
|
||||
def build_poll_failed_error() -> BrowserSignInError:
|
||||
return BrowserSignInError(
|
||||
"Browser sign-in status could not be retrieved.",
|
||||
code=BrowserSignInErrorCode.POLL_FAILED,
|
||||
)
|
||||
|
||||
|
||||
async def noop_sleep(_: float) -> None:
|
||||
return None
|
||||
325
tests/browser_sign_in/test_browser_sign_in.py
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import hashlib
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.browser_sign_in.stubs import (
|
||||
StubBrowserSignInGateway,
|
||||
build_poll_failed_error,
|
||||
build_sign_in_process,
|
||||
noop_sleep,
|
||||
)
|
||||
from vibe.setup.auth import (
|
||||
BrowserSignInError,
|
||||
BrowserSignInErrorCode,
|
||||
BrowserSignInPollResult,
|
||||
BrowserSignInProcess,
|
||||
BrowserSignInService,
|
||||
)
|
||||
|
||||
TEST_NOW = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
|
||||
TEST_PROCESS_ID = "process-1"
|
||||
TEST_SIGN_IN_URL = "https://console.mistral.ai/codestral/cli/authenticate#" + urlencode({
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"complete_token": f"complete-token-{TEST_PROCESS_ID}",
|
||||
"state": f"state-{TEST_PROCESS_ID}",
|
||||
})
|
||||
TEST_POLL_URL = "https://api.mistral.ai/api/vibe/sign-in/poll/poll-token-process-1"
|
||||
|
||||
|
||||
def build_code_challenge(verifier: str) -> str:
|
||||
digest = hashlib.sha256(verifier.encode("ascii")).digest()
|
||||
return base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=")
|
||||
|
||||
|
||||
def build_test_service(
|
||||
*,
|
||||
poll_results: list[BrowserSignInPollResult | BrowserSignInError],
|
||||
exchange_error: BrowserSignInError | None = None,
|
||||
open_browser: Callable[[str], bool] | None = None,
|
||||
sleep: Callable[[float], Awaitable[None]] = noop_sleep,
|
||||
now: Callable[[], datetime] | None = None,
|
||||
process_time: datetime = TEST_NOW,
|
||||
) -> tuple[StubBrowserSignInGateway, BrowserSignInService]:
|
||||
gateway = StubBrowserSignInGateway(
|
||||
process=build_sign_in_process(process_time),
|
||||
poll_results=poll_results,
|
||||
exchange_error=exchange_error,
|
||||
)
|
||||
service = BrowserSignInService(
|
||||
gateway,
|
||||
open_browser=open_browser or (lambda _: True),
|
||||
sleep=sleep,
|
||||
now=now or (lambda: process_time),
|
||||
poll_interval=0,
|
||||
)
|
||||
return gateway, service
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_returns_api_key_after_pending_poll() -> None:
|
||||
opened_urls: list[str] = []
|
||||
statuses: list[str] = []
|
||||
gateway, service = build_test_service(
|
||||
poll_results=[
|
||||
BrowserSignInPollResult(status="pending"),
|
||||
BrowserSignInPollResult(status="completed", exchange_token="exchange-1"),
|
||||
],
|
||||
open_browser=lambda url: opened_urls.append(url) or True,
|
||||
)
|
||||
|
||||
api_key = await service.authenticate(status_callback=statuses.append)
|
||||
|
||||
code_verifier = gateway.exchange_requests[0].code_verifier
|
||||
assert gateway.code_challenges == [build_code_challenge(code_verifier)]
|
||||
assert api_key == "sk-browser-key"
|
||||
assert opened_urls == [TEST_SIGN_IN_URL]
|
||||
assert statuses == [
|
||||
"opening_browser",
|
||||
"waiting_for_browser_sign_in",
|
||||
"exchanging",
|
||||
"completed",
|
||||
]
|
||||
assert gateway.polled_urls == [TEST_POLL_URL, TEST_POLL_URL]
|
||||
assert gateway.exchange_requests[0].exchange_token == "exchange-1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_raises_when_polling_expires() -> None:
|
||||
opened_urls: list[str] = []
|
||||
_, service = build_test_service(
|
||||
poll_results=[BrowserSignInPollResult(status="expired")],
|
||||
open_browser=lambda url: opened_urls.append(url) or True,
|
||||
)
|
||||
|
||||
with pytest.raises(BrowserSignInError, match="expired"):
|
||||
await service.authenticate()
|
||||
|
||||
assert opened_urls == [TEST_SIGN_IN_URL]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_retries_after_transient_poll_failure() -> None:
|
||||
gateway, service = build_test_service(
|
||||
poll_results=[
|
||||
build_poll_failed_error(),
|
||||
BrowserSignInPollResult(status="completed", exchange_token="exchange-1"),
|
||||
]
|
||||
)
|
||||
|
||||
api_key = await service.authenticate()
|
||||
|
||||
assert api_key == "sk-browser-key"
|
||||
assert gateway.polled_urls == [TEST_POLL_URL, TEST_POLL_URL]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_fails_after_three_consecutive_poll_failures() -> None:
|
||||
_, service = build_test_service(
|
||||
poll_results=[
|
||||
build_poll_failed_error(),
|
||||
build_poll_failed_error(),
|
||||
build_poll_failed_error(),
|
||||
]
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
BrowserSignInError, match="status could not be retrieved"
|
||||
) as err:
|
||||
await service.authenticate()
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.POLL_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_resets_poll_failure_streak_after_successful_poll() -> None:
|
||||
gateway, service = build_test_service(
|
||||
poll_results=[
|
||||
build_poll_failed_error(),
|
||||
BrowserSignInPollResult(status="pending"),
|
||||
build_poll_failed_error(),
|
||||
BrowserSignInPollResult(status="completed", exchange_token="exchange-1"),
|
||||
]
|
||||
)
|
||||
|
||||
api_key = await service.authenticate()
|
||||
|
||||
assert api_key == "sk-browser-key"
|
||||
assert len(gateway.polled_urls) == 4
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_raises_on_unknown_poll_state() -> None:
|
||||
class UnknownStateGateway:
|
||||
def __init__(self) -> None:
|
||||
self.process = build_sign_in_process(TEST_NOW)
|
||||
|
||||
async def create_process(self, code_challenge: str):
|
||||
return self.process
|
||||
|
||||
async def poll(self, poll_url: str) -> BrowserSignInPollResult:
|
||||
return cast(
|
||||
BrowserSignInPollResult,
|
||||
SimpleNamespace(status="unexpected", exchange_token=None, message=None),
|
||||
)
|
||||
|
||||
async def exchange(
|
||||
self, process_id: str, exchange_token: str, code_verifier: str
|
||||
) -> str:
|
||||
return "sk-browser-key"
|
||||
|
||||
async def aclose(self) -> None:
|
||||
return None
|
||||
|
||||
service = BrowserSignInService(
|
||||
UnknownStateGateway(),
|
||||
open_browser=lambda _: True,
|
||||
sleep=noop_sleep,
|
||||
now=lambda: TEST_NOW,
|
||||
poll_interval=0,
|
||||
)
|
||||
|
||||
with pytest.raises(BrowserSignInError, match="unknown state") as err:
|
||||
await service.authenticate()
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.UNKNOWN_STATE
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_raises_when_browser_cannot_be_opened() -> None:
|
||||
_, service = build_test_service(poll_results=[], open_browser=lambda _: False)
|
||||
|
||||
with pytest.raises(BrowserSignInError, match="open browser"):
|
||||
await service.authenticate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_raises_when_exchange_fails() -> None:
|
||||
_, service = build_test_service(
|
||||
poll_results=[
|
||||
BrowserSignInPollResult(status="completed", exchange_token="exchange-1")
|
||||
],
|
||||
exchange_error=BrowserSignInError(
|
||||
"Failed to exchange browser sign-in for an API key."
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(BrowserSignInError, match="exchange"):
|
||||
await service.authenticate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_can_be_cancelled_before_start() -> None:
|
||||
gateway, service = build_test_service(poll_results=[])
|
||||
task = asyncio.create_task(service.authenticate())
|
||||
task.cancel()
|
||||
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
assert gateway.code_challenges == []
|
||||
assert gateway.exchange_requests == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_can_be_cancelled_while_waiting_for_sign_in() -> None:
|
||||
blocker = asyncio.Event()
|
||||
|
||||
async def wait_forever(_: float) -> None:
|
||||
await blocker.wait()
|
||||
|
||||
gateway, service = build_test_service(
|
||||
poll_results=[BrowserSignInPollResult(status="pending")], sleep=wait_forever
|
||||
)
|
||||
task = asyncio.create_task(service.authenticate())
|
||||
|
||||
while not gateway.polled_urls:
|
||||
await asyncio.sleep(0)
|
||||
|
||||
task.cancel()
|
||||
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
assert gateway.exchange_requests == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticate_times_out_when_process_never_completes() -> None:
|
||||
current_time = TEST_NOW
|
||||
|
||||
async def advance_time(_: float) -> None:
|
||||
nonlocal current_time
|
||||
current_time += timedelta(minutes=3)
|
||||
|
||||
_, service = build_test_service(
|
||||
poll_results=[
|
||||
BrowserSignInPollResult(status="pending"),
|
||||
BrowserSignInPollResult(status="pending"),
|
||||
],
|
||||
sleep=advance_time,
|
||||
now=lambda: current_time,
|
||||
process_time=current_time,
|
||||
)
|
||||
|
||||
with pytest.raises(BrowserSignInError, match="timed out"):
|
||||
await service.authenticate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"first_poll_result",
|
||||
[BrowserSignInPollResult(status="pending"), build_poll_failed_error()],
|
||||
ids=["pending", "transient_poll_failure"],
|
||||
)
|
||||
async def test_authenticate_caps_sleep_to_remaining_sign_in_lifetime(
|
||||
first_poll_result: BrowserSignInPollResult | BrowserSignInError,
|
||||
) -> None:
|
||||
current_time = TEST_NOW
|
||||
sleep_durations: list[float] = []
|
||||
|
||||
async def advance_time(duration: float) -> None:
|
||||
nonlocal current_time
|
||||
sleep_durations.append(duration)
|
||||
current_time += timedelta(seconds=duration)
|
||||
|
||||
gateway = StubBrowserSignInGateway(
|
||||
process=BrowserSignInProcess(
|
||||
process_id=TEST_PROCESS_ID,
|
||||
sign_in_url=TEST_SIGN_IN_URL,
|
||||
poll_url=TEST_POLL_URL,
|
||||
expires_at=TEST_NOW + timedelta(seconds=1),
|
||||
),
|
||||
poll_results=[first_poll_result],
|
||||
)
|
||||
service = BrowserSignInService(
|
||||
gateway,
|
||||
open_browser=lambda _: True,
|
||||
sleep=advance_time,
|
||||
now=lambda: current_time,
|
||||
poll_interval=3.0,
|
||||
)
|
||||
|
||||
with pytest.raises(BrowserSignInError, match="timed out") as err:
|
||||
await service.authenticate()
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.TIMED_OUT
|
||||
assert sleep_durations == [1.0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aclose_closes_underlying_api() -> None:
|
||||
gateway, service = build_test_service(poll_results=[])
|
||||
|
||||
await service.aclose()
|
||||
|
||||
assert gateway.closed is True
|
||||
654
tests/browser_sign_in/test_browser_sign_in_http.py
Normal file
|
|
@ -0,0 +1,654 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import logging
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from vibe.setup.auth import (
|
||||
BrowserSignInError,
|
||||
BrowserSignInErrorCode,
|
||||
HttpBrowserSignInGateway,
|
||||
)
|
||||
|
||||
AUTH_ORIGIN = "https://api.mistral.ai"
|
||||
AUTH_BROWSER_BASE_URL = "https://console.mistral.ai"
|
||||
AUTH_API_BASE_URL = AUTH_ORIGIN
|
||||
TEST_PROCESS_ID = "process-1"
|
||||
TEST_COMPLETE_TOKEN = "complete-token-1"
|
||||
TEST_STATE = "state-1"
|
||||
TEST_POLL_TOKEN = "poll-token-1"
|
||||
|
||||
|
||||
def _iso(value: datetime) -> str:
|
||||
return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def build_sign_in_url(
|
||||
*,
|
||||
process_id: str = TEST_PROCESS_ID,
|
||||
base_url: str = AUTH_BROWSER_BASE_URL,
|
||||
complete_token: str = TEST_COMPLETE_TOKEN,
|
||||
state: str = TEST_STATE,
|
||||
) -> str:
|
||||
fragment = urlencode({
|
||||
"process_id": process_id,
|
||||
"complete_token": complete_token,
|
||||
"state": state,
|
||||
})
|
||||
return f"{base_url}/codestral/cli/authenticate#{fragment}"
|
||||
|
||||
|
||||
def build_poll_url(
|
||||
*, poll_token: str = TEST_POLL_TOKEN, api_base_url: str = AUTH_API_BASE_URL
|
||||
) -> str:
|
||||
return f"{api_base_url}/api/vibe/sign-in/poll/{poll_token}"
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def build_gateway(
|
||||
handler: Callable[[httpx.Request], httpx.Response],
|
||||
*,
|
||||
origin: str = AUTH_ORIGIN,
|
||||
browser_base_url: str = AUTH_BROWSER_BASE_URL,
|
||||
api_base_url: str = AUTH_API_BASE_URL,
|
||||
):
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.MockTransport(handler), base_url=origin
|
||||
) as client:
|
||||
yield HttpBrowserSignInGateway(
|
||||
browser_base_url=browser_base_url, api_base_url=api_base_url, client=client
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_creates_process_with_pkce_payload() -> None:
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
captured_body: str | None = None
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
nonlocal captured_body
|
||||
assert request.url.path == "/vibe/sign-in"
|
||||
captured_body = request.content.decode("utf-8")
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(),
|
||||
"poll_url": build_poll_url(),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
process = await gateway.create_process("challenge-123")
|
||||
|
||||
assert process.process_id == TEST_PROCESS_ID
|
||||
assert process.sign_in_url == build_sign_in_url()
|
||||
assert process.poll_url == build_poll_url()
|
||||
assert captured_body is not None
|
||||
assert '"code_challenge":"challenge-123"' in captured_body
|
||||
assert '"code_challenge_method":"S256"' in captured_body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_polls_process_state() -> None:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.url.path == "/api/vibe/sign-in/poll/poll-token-1"
|
||||
return httpx.Response(
|
||||
200, json={"status": "completed", "exchange_token": "exchange-1"}
|
||||
)
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
result = await gateway.poll(build_poll_url())
|
||||
|
||||
assert result.status == "completed"
|
||||
assert result.exchange_token == "exchange-1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_maps_410_poll_response_to_expired_status() -> None:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.url.path == "/api/vibe/sign-in/poll/poll-token-1"
|
||||
return httpx.Response(410)
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
result = await gateway.poll(build_poll_url())
|
||||
|
||||
assert result.status == "expired"
|
||||
assert result.exchange_token is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_exchanges_token_for_api_key() -> None:
|
||||
captured_body: str | None = None
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
nonlocal captured_body
|
||||
assert request.url.path == "/vibe/sign-in/process-1/exchange"
|
||||
captured_body = request.content.decode("utf-8")
|
||||
return httpx.Response(200, json={"api_key": "sk-browser-key"})
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
api_key = await gateway.exchange("process-1", "exchange-1", "verifier-1")
|
||||
|
||||
assert api_key == "sk-browser-key"
|
||||
assert captured_body is not None
|
||||
assert '"exchange_token":"exchange-1"' in captured_body
|
||||
assert '"code_verifier":"verifier-1"' in captured_body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_translates_transport_errors() -> None:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
raise httpx.ConnectError("boom", request=request)
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
await gateway.create_process("challenge-123")
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.START_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_assigns_poll_failed_code_on_transport_error() -> None:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
raise httpx.ConnectError("boom", request=request)
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
with pytest.raises(
|
||||
BrowserSignInError, match="status could not be retrieved"
|
||||
) as err:
|
||||
await gateway.poll(build_poll_url())
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.POLL_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_assigns_poll_failed_code_on_invalid_poll_url() -> None:
|
||||
async with build_gateway(lambda _: httpx.Response(200)) as gateway:
|
||||
with pytest.raises(
|
||||
BrowserSignInError, match="status could not be retrieved"
|
||||
) as err:
|
||||
await gateway.poll("https://evil.example/poll/secret-1")
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.POLL_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_translates_non_json_start_response() -> None:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(200, text="<html>not json</html>")
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
await gateway.create_process("challenge-123")
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.START_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_translates_missing_start_fields() -> None:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(),
|
||||
"poll_url": build_poll_url(),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
await gateway.create_process("challenge-123")
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.START_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_accepts_poll_url_under_configured_api_base_url() -> None:
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(),
|
||||
"poll_url": build_poll_url(),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
process = await gateway.create_process("challenge-123")
|
||||
|
||||
assert process.poll_url == build_poll_url()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_accepts_poll_url_under_configured_api_base_path() -> None:
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.url.path == "/v1/vibe/sign-in"
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(),
|
||||
"poll_url": build_poll_url(
|
||||
poll_token=TEST_POLL_TOKEN, api_base_url="https://api.mistral.ai/v1"
|
||||
),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(
|
||||
handler,
|
||||
origin="https://api.mistral.ai",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
) as gateway:
|
||||
process = await gateway.create_process("challenge-123")
|
||||
|
||||
assert process.poll_url == build_poll_url(api_base_url="https://api.mistral.ai/v1")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_accepts_same_origin_urls_with_explicit_default_https_ports() -> (
|
||||
None
|
||||
):
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(
|
||||
base_url="https://console.mistral.ai:443"
|
||||
),
|
||||
"poll_url": build_poll_url(api_base_url="https://api.mistral.ai:443"),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
process = await gateway.create_process("challenge-123")
|
||||
|
||||
assert process.sign_in_url == build_sign_in_url(
|
||||
base_url="https://console.mistral.ai:443"
|
||||
)
|
||||
assert process.poll_url == build_poll_url(api_base_url="https://api.mistral.ai:443")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_accepts_poll_url_without_explicit_default_https_port_when_base_has_one() -> (
|
||||
None
|
||||
):
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert request.url.path == "/api/vibe/sign-in/poll/poll-token-1"
|
||||
return httpx.Response(
|
||||
200, json={"status": "completed", "exchange_token": "exchange-1"}
|
||||
)
|
||||
|
||||
async with build_gateway(
|
||||
handler,
|
||||
origin="https://api.mistral.ai:443",
|
||||
api_base_url="https://api.mistral.ai:443",
|
||||
) as gateway:
|
||||
result = await gateway.poll(build_poll_url())
|
||||
|
||||
assert result.status == "completed"
|
||||
assert result.exchange_token == "exchange-1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_accepts_sign_in_url_under_configured_browser_base_url() -> None:
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(),
|
||||
"poll_url": build_poll_url(),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
process = await gateway.create_process("challenge-123")
|
||||
|
||||
assert process.sign_in_url == build_sign_in_url()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_rejects_sign_in_url_outside_browser_base_url() -> None:
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(base_url="https://evil.example"),
|
||||
"poll_url": build_poll_url(),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
await gateway.create_process("challenge-123")
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.START_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_rejects_sign_in_url_outside_browser_base_path_after_normalization() -> (
|
||||
None
|
||||
):
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(
|
||||
base_url="https://console.mistral.ai/v1/.."
|
||||
),
|
||||
"poll_url": build_poll_url(api_base_url="https://api.mistral.ai/v1"),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(
|
||||
handler,
|
||||
browser_base_url="https://console.mistral.ai/v1",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
await gateway.create_process("challenge-123")
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.START_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_rejects_sign_in_url_with_encoded_dot_segments_outside_browser_base_path() -> (
|
||||
None
|
||||
):
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(
|
||||
base_url="https://console.mistral.ai/v1/%2e%2e"
|
||||
),
|
||||
"poll_url": build_poll_url(api_base_url="https://api.mistral.ai/v1"),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(
|
||||
handler,
|
||||
browser_base_url="https://console.mistral.ai/v1",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
await gateway.create_process("challenge-123")
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.START_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_rejects_poll_url_outside_api_base_url() -> None:
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(),
|
||||
"poll_url": "https://evil.example/api/vibe/sign-in/poll/poll-token-1",
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
await gateway.create_process("challenge-123")
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.START_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_rejects_returned_poll_url_outside_api_base_path_after_normalization() -> (
|
||||
None
|
||||
):
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(
|
||||
base_url="https://console.mistral.ai/v1"
|
||||
),
|
||||
"poll_url": build_poll_url(
|
||||
poll_token=TEST_POLL_TOKEN,
|
||||
api_base_url="https://api.mistral.ai/v1/..",
|
||||
),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(
|
||||
handler,
|
||||
origin="https://api.mistral.ai",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
browser_base_url="https://console.mistral.ai/v1",
|
||||
) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
await gateway.create_process("challenge-123")
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.START_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_rejects_returned_poll_url_with_encoded_dot_segments_outside_api_base_path() -> (
|
||||
None
|
||||
):
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(
|
||||
base_url="https://console.mistral.ai/v1"
|
||||
),
|
||||
"poll_url": build_poll_url(
|
||||
poll_token=TEST_POLL_TOKEN,
|
||||
api_base_url="https://api.mistral.ai/v1/%2e%2e",
|
||||
),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(
|
||||
handler,
|
||||
origin="https://api.mistral.ai",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
browser_base_url="https://console.mistral.ai/v1",
|
||||
) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
await gateway.create_process("challenge-123")
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.START_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_rejects_poll_url_outside_api_base_path() -> None:
|
||||
async with build_gateway(
|
||||
lambda _: httpx.Response(200),
|
||||
origin="https://api.mistral.ai",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
) as gateway:
|
||||
with pytest.raises(
|
||||
BrowserSignInError, match="status could not be retrieved"
|
||||
) as err:
|
||||
await gateway.poll(
|
||||
"https://api.mistral.ai/v1evil/api/vibe/sign-in/poll/poll-token-1"
|
||||
)
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.POLL_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_rejects_poll_url_outside_api_base_path_after_normalization() -> (
|
||||
None
|
||||
):
|
||||
async with build_gateway(
|
||||
lambda _: httpx.Response(200),
|
||||
origin="https://api.mistral.ai",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
) as gateway:
|
||||
with pytest.raises(
|
||||
BrowserSignInError, match="status could not be retrieved"
|
||||
) as err:
|
||||
await gateway.poll(
|
||||
"https://api.mistral.ai/v1/../api/vibe/sign-in/poll/poll-token-1"
|
||||
)
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.POLL_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_translates_invalid_returned_poll_url_port() -> None:
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(),
|
||||
"poll_url": "https://api.mistral.ai:99999/api/vibe/sign-in/poll/poll-token-1",
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in") as err:
|
||||
await gateway.create_process("challenge-123")
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.START_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_assigns_poll_failed_code_on_invalid_poll_url_port() -> None:
|
||||
async with build_gateway(lambda _: httpx.Response(200)) as gateway:
|
||||
with pytest.raises(
|
||||
BrowserSignInError, match="status could not be retrieved"
|
||||
) as err:
|
||||
await gateway.poll(
|
||||
"https://api.mistral.ai:99999/api/vibe/sign-in/poll/poll-token-1"
|
||||
)
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.POLL_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_translates_non_json_poll_response() -> None:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(200, text="<html>not json</html>")
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
with pytest.raises(
|
||||
BrowserSignInError, match="status could not be retrieved"
|
||||
) as err:
|
||||
await gateway.poll(build_poll_url())
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.POLL_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_translates_non_json_exchange_response() -> None:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(200, text="<html>not json</html>")
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
with pytest.raises(BrowserSignInError, match="exchange browser sign-in") as err:
|
||||
await gateway.exchange("process-1", "exchange-1", "verifier-1")
|
||||
|
||||
assert err.value.code is BrowserSignInErrorCode.EXCHANGE_FAILED
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_does_not_log_sign_in_or_poll_secrets_on_start_validation_failure(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
now = datetime(2026, 3, 16, tzinfo=UTC)
|
||||
complete_token = "complete-token-secret"
|
||||
state = "state-secret"
|
||||
poll_token = "poll-token-secret"
|
||||
|
||||
def handler(_: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"process_id": TEST_PROCESS_ID,
|
||||
"sign_in_url": build_sign_in_url(
|
||||
base_url="https://evil.example",
|
||||
complete_token=complete_token,
|
||||
state=state,
|
||||
),
|
||||
"poll_url": build_poll_url(poll_token=poll_token),
|
||||
"expires_at": _iso(now + timedelta(minutes=5)),
|
||||
},
|
||||
)
|
||||
|
||||
async with build_gateway(handler) as gateway:
|
||||
with caplog.at_level(logging.WARNING, logger="vibe"):
|
||||
with pytest.raises(BrowserSignInError, match="start browser sign-in"):
|
||||
await gateway.create_process("challenge-123")
|
||||
|
||||
assert complete_token not in caplog.text
|
||||
assert state not in caplog.text
|
||||
assert poll_token not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_api_does_not_log_poll_secret_on_poll_url_validation_failure(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
poll_token = "poll-token-secret"
|
||||
|
||||
async with build_gateway(
|
||||
lambda _: httpx.Response(200),
|
||||
origin="https://api.mistral.ai",
|
||||
api_base_url="https://api.mistral.ai/v1",
|
||||
) as gateway:
|
||||
with caplog.at_level(logging.WARNING, logger="vibe"):
|
||||
with pytest.raises(
|
||||
BrowserSignInError, match="status could not be retrieved"
|
||||
):
|
||||
await gateway.poll(
|
||||
f"https://api.mistral.ai/v1evil/api/vibe/sign-in/poll/{poll_token}"
|
||||
)
|
||||
|
||||
assert poll_token not in caplog.text
|
||||
|
|
@ -124,10 +124,7 @@ def test_copy_selection_to_clipboard_success(
|
|||
assert result == "selected text"
|
||||
mock_copy_to_clipboard.assert_called_once_with("selected text")
|
||||
mock_app.notify.assert_called_once_with(
|
||||
'"selected text" copied to clipboard',
|
||||
severity="information",
|
||||
timeout=2,
|
||||
markup=False,
|
||||
"Selection copied to clipboard", severity="information", timeout=2, markup=False
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -170,32 +167,13 @@ def test_copy_selection_to_clipboard_multiple_widgets(mock_app: MagicMock) -> No
|
|||
"first selection\nsecond selection"
|
||||
)
|
||||
mock_app.notify.assert_called_once_with(
|
||||
'"first selection\u23cesecond selection" copied to clipboard',
|
||||
"Selection copied to clipboard",
|
||||
severity="information",
|
||||
timeout=2,
|
||||
markup=False,
|
||||
)
|
||||
|
||||
|
||||
def test_copy_selection_to_clipboard_preview_shortening(mock_app: MagicMock) -> None:
|
||||
long_text = "a" * 100
|
||||
widget = MockWidget(
|
||||
text_selection=SimpleNamespace(), get_selection_result=(long_text, None)
|
||||
)
|
||||
mock_app.query.return_value = [widget]
|
||||
|
||||
with patch("vibe.cli.clipboard._copy_to_clipboard") as mock_copy_to_clipboard:
|
||||
result = copy_selection_to_clipboard(mock_app)
|
||||
assert result == long_text
|
||||
|
||||
mock_copy_to_clipboard.assert_called_once_with(long_text)
|
||||
notification_call = mock_app.notify.call_args
|
||||
assert notification_call is not None
|
||||
assert '"' in notification_call[0][0]
|
||||
assert "copied to clipboard" in notification_call[0][0]
|
||||
assert len(notification_call[0][0]) < len(long_text) + 30
|
||||
|
||||
|
||||
def test_copy_to_clipboard_stops_after_verified_copy() -> None:
|
||||
"""Stops iterating once _read_clipboard confirms the text landed."""
|
||||
mock_first = MagicMock()
|
||||
|
|
|
|||
|
|
@ -24,46 +24,62 @@ class TestCommandRegistry:
|
|||
assert registry.get_command_name("hello") is None
|
||||
assert registry.get_command_name("") is None
|
||||
|
||||
def test_find_command_returns_command_when_alias_matches(self) -> None:
|
||||
def test_parse_command_returns_command_when_alias_matches(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
cmd = registry.find_command("/help")
|
||||
assert cmd is not None
|
||||
result = registry.parse_command("/help")
|
||||
assert result is not None
|
||||
cmd_name, cmd, cmd_args = result
|
||||
assert cmd_name == "help"
|
||||
assert cmd.handler == "_show_help"
|
||||
assert isinstance(cmd, Command)
|
||||
assert cmd_args == ""
|
||||
|
||||
def test_find_command_returns_none_when_no_match(self) -> None:
|
||||
def test_parse_command_returns_none_when_no_match(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
assert registry.find_command("/nonexistent") is None
|
||||
assert registry.parse_command("/nonexistent") is None
|
||||
|
||||
def test_find_command_uses_get_command_name(self) -> None:
|
||||
"""find_command and get_command_name stay in sync for same input."""
|
||||
def test_parse_command_uses_get_command_name(self) -> None:
|
||||
"""parse_command and get_command_name stay in sync for same input."""
|
||||
registry = CommandRegistry()
|
||||
for alias in ["/help", "/config", "/clear", "/exit"]:
|
||||
cmd_name = registry.get_command_name(alias)
|
||||
cmd = registry.find_command(alias)
|
||||
result = registry.parse_command(alias)
|
||||
if cmd_name is None:
|
||||
assert cmd is None
|
||||
assert result is None
|
||||
else:
|
||||
assert cmd is not None
|
||||
assert cmd_name in registry.commands
|
||||
assert registry.commands[cmd_name] is cmd
|
||||
assert result is not None
|
||||
found_name, found_cmd, _ = result
|
||||
assert found_name == cmd_name
|
||||
assert registry.commands[cmd_name] is found_cmd
|
||||
|
||||
def test_excluded_commands_not_in_registry(self) -> None:
|
||||
registry = CommandRegistry(excluded_commands=["exit"])
|
||||
assert registry.get_command_name("/exit") is None
|
||||
assert registry.find_command("/exit") is None
|
||||
assert registry.parse_command("/exit") is None
|
||||
assert registry.get_command_name("/help") == "help"
|
||||
|
||||
def test_resume_command_registration(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
assert registry.get_command_name("/resume") == "resume"
|
||||
assert registry.get_command_name("/continue") == "resume"
|
||||
cmd = registry.find_command("/resume")
|
||||
assert cmd is not None
|
||||
result = registry.parse_command("/resume")
|
||||
assert result is not None
|
||||
_, cmd, _ = result
|
||||
assert cmd.handler == "_show_session_picker"
|
||||
|
||||
def test_parse_command_keeps_args_for_no_arg_commands(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
result = registry.parse_command("/help extra")
|
||||
assert result == ("help", registry.commands["help"], "extra")
|
||||
|
||||
def test_parse_command_keeps_args_for_argument_commands(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
result = registry.parse_command("/mcp filesystem")
|
||||
assert result == ("mcp", registry.commands["mcp"], "filesystem")
|
||||
|
||||
def test_data_retention_command_registration(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
cmd = registry.find_command("/data-retention")
|
||||
assert cmd is not None
|
||||
result = registry.parse_command("/data-retention")
|
||||
assert result is not None
|
||||
_, cmd, _ = result
|
||||
assert cmd.handler == "_show_data_retention"
|
||||
|
|
|
|||
156
tests/cli/test_mcp_app.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from vibe.cli.textual_ui.widgets.mcp_app import MCPApp, collect_mcp_tool_index
|
||||
from vibe.core.config import MCPStdio
|
||||
from vibe.core.tools.base import InvokeContext
|
||||
from vibe.core.tools.mcp.tools import MCPTool, MCPToolResult, _OpenArgs
|
||||
from vibe.core.types import ToolStreamEvent
|
||||
|
||||
|
||||
def _make_tool_cls(
|
||||
*,
|
||||
is_mcp: bool,
|
||||
description: str = "",
|
||||
server_name: str | None = None,
|
||||
remote_name: str = "tool",
|
||||
) -> type:
|
||||
if not is_mcp:
|
||||
return type("FakeTool", (), {"description": description})
|
||||
|
||||
async def _run(
|
||||
self: Any, args: _OpenArgs, ctx: InvokeContext | None = None
|
||||
) -> AsyncGenerator[ToolStreamEvent | MCPToolResult, None]:
|
||||
yield MCPToolResult(ok=True, server="", tool="", text=None)
|
||||
|
||||
return type(
|
||||
"FakeMCPTool",
|
||||
(MCPTool,),
|
||||
{
|
||||
"description": description,
|
||||
"_server_name": server_name,
|
||||
"_remote_name": remote_name,
|
||||
"run": _run,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _make_tool_manager(
|
||||
all_tools: dict[str, type], available_tools: dict[str, type] | None = None
|
||||
) -> MagicMock:
|
||||
mgr = MagicMock()
|
||||
mgr.registered_tools = all_tools
|
||||
mgr.available_tools = available_tools if available_tools is not None else all_tools
|
||||
return mgr
|
||||
|
||||
|
||||
class TestCollectMcpToolIndex:
|
||||
def test_non_mcp_tools_are_excluded(self) -> None:
|
||||
servers = [MCPStdio(name="srv", transport="stdio", command="cmd")]
|
||||
all_tools = {
|
||||
"srv_tool": _make_tool_cls(is_mcp=True, server_name="srv"),
|
||||
"bash": _make_tool_cls(is_mcp=False),
|
||||
}
|
||||
mgr = _make_tool_manager(all_tools)
|
||||
|
||||
index = collect_mcp_tool_index(servers, mgr)
|
||||
|
||||
assert "bash" not in str(index.server_tools)
|
||||
assert len(index.server_tools["srv"]) == 1
|
||||
|
||||
def test_counts_match_available_vs_all(self) -> None:
|
||||
servers = [MCPStdio(name="srv", transport="stdio", command="cmd")]
|
||||
tool_a = _make_tool_cls(is_mcp=True, server_name="srv", remote_name="tool_a")
|
||||
tool_b = _make_tool_cls(is_mcp=True, server_name="srv", remote_name="tool_b")
|
||||
all_tools = {"srv_tool_a": tool_a, "srv_tool_b": tool_b}
|
||||
available = {"srv_tool_a": tool_a}
|
||||
mgr = _make_tool_manager(all_tools, available)
|
||||
|
||||
index = collect_mcp_tool_index(servers, mgr)
|
||||
|
||||
assert len(index.server_tools["srv"]) == 2
|
||||
enabled = sum(
|
||||
1 for t, _ in index.server_tools["srv"] if t in index.enabled_tools
|
||||
)
|
||||
assert enabled == 1
|
||||
|
||||
def test_tool_with_no_matching_server_is_skipped(self) -> None:
|
||||
servers = [MCPStdio(name="srv", transport="stdio", command="cmd")]
|
||||
all_tools = {"other_tool": _make_tool_cls(is_mcp=True, server_name="other")}
|
||||
mgr = _make_tool_manager(all_tools)
|
||||
|
||||
index = collect_mcp_tool_index(servers, mgr)
|
||||
|
||||
assert index.server_tools == {}
|
||||
|
||||
def test_empty_servers_returns_empty(self) -> None:
|
||||
mgr = _make_tool_manager({
|
||||
"srv_tool": _make_tool_cls(is_mcp=True, server_name="srv")
|
||||
})
|
||||
index = collect_mcp_tool_index([], mgr)
|
||||
assert index.server_tools == {}
|
||||
|
||||
|
||||
class TestMCPAppInit:
|
||||
def test_viewing_server_none_when_no_initial_server(self) -> None:
|
||||
mgr = _make_tool_manager({})
|
||||
app = MCPApp(mcp_servers=[], tool_manager=mgr)
|
||||
assert app._viewing_server is None
|
||||
|
||||
def test_initial_server_stripped_and_stored(self) -> None:
|
||||
servers = [MCPStdio(name="srv", transport="stdio", command="cmd")]
|
||||
mgr = _make_tool_manager({})
|
||||
app = MCPApp(mcp_servers=servers, tool_manager=mgr, initial_server=" srv ")
|
||||
assert app._viewing_server == "srv"
|
||||
|
||||
def test_widget_id_is_mcp_app(self) -> None:
|
||||
mgr = _make_tool_manager({})
|
||||
app = MCPApp(mcp_servers=[], tool_manager=mgr)
|
||||
assert app.id == "mcp-app"
|
||||
|
||||
def test_refresh_view_unknown_server_falls_back_to_overview(self) -> None:
|
||||
servers = [MCPStdio(name="srv", transport="stdio", command="cmd")]
|
||||
mgr = _make_tool_manager({})
|
||||
app = MCPApp(mcp_servers=servers, tool_manager=mgr)
|
||||
app.query_one = MagicMock()
|
||||
app._refresh_view("nonexistent")
|
||||
assert app._viewing_server is None
|
||||
|
||||
def test_refresh_view_known_server_sets_viewing_server(self) -> None:
|
||||
servers = [MCPStdio(name="srv", transport="stdio", command="cmd")]
|
||||
mgr = _make_tool_manager({})
|
||||
app = MCPApp(mcp_servers=servers, tool_manager=mgr)
|
||||
app.query_one = MagicMock()
|
||||
app._refresh_view("srv")
|
||||
assert app._viewing_server == "srv"
|
||||
|
||||
def test_refresh_view_none_clears_viewing_server(self) -> None:
|
||||
servers = [MCPStdio(name="srv", transport="stdio", command="cmd")]
|
||||
mgr = _make_tool_manager({})
|
||||
app = MCPApp(mcp_servers=servers, tool_manager=mgr)
|
||||
app._viewing_server = "srv"
|
||||
app.query_one = MagicMock()
|
||||
app._refresh_view(None)
|
||||
assert app._viewing_server is None
|
||||
|
||||
def test_action_back_calls_refresh_view_none(self) -> None:
|
||||
servers = [MCPStdio(name="srv", transport="stdio", command="cmd")]
|
||||
mgr = _make_tool_manager({})
|
||||
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.action_back()
|
||||
assert render_calls == [None]
|
||||
|
||||
def test_action_back_noop_when_in_overview(self) -> None:
|
||||
mgr = _make_tool_manager({})
|
||||
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.action_back()
|
||||
assert render_calls == []
|
||||
|
|
@ -37,4 +37,4 @@ async def test_ui_clipboard_notification_does_not_crash_on_markup_text(
|
|||
assert notifications
|
||||
notification = notifications[-1]
|
||||
assert notification.markup is False
|
||||
assert "copied to clipboard" in notification.message
|
||||
assert "Selection copied to clipboard" in notification.message
|
||||
|
|
|
|||
44
tests/cli/textual_ui/test_debug_console_resize.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""Test that _LogView.render_line handles width mismatches during resize."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.geometry import Size
|
||||
from textual.strip import Strip
|
||||
|
||||
from vibe.cli.textual_ui.widgets.debug_console import _LogView
|
||||
|
||||
|
||||
class _LogViewTestApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
self._log_view = _LogView(
|
||||
load_page=lambda: None, has_more=lambda: False, id="test-log-view"
|
||||
)
|
||||
yield self._log_view
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_render_line_no_keyerror_on_width_mismatch():
|
||||
"""render_line must not raise KeyError when wrap width != cached width."""
|
||||
app = _LogViewTestApp()
|
||||
async with app.run_test(size=(80, 24)) as pilot:
|
||||
log_view = app._log_view
|
||||
|
||||
long_line = "A" * 200
|
||||
log_view.write_line(long_line)
|
||||
await pilot.pause()
|
||||
|
||||
assert log_view._total_visual == 3
|
||||
assert log_view._cached_width == 80
|
||||
|
||||
# At width 120, wrapping produces 2 lines, but _wrap_prefix says 3.
|
||||
new_size = Size(120, 24)
|
||||
log_view._render_line_cache.clear()
|
||||
with patch.object(
|
||||
type(log_view), "size", new_callable=PropertyMock, return_value=new_size
|
||||
):
|
||||
result = log_view.render_line(2)
|
||||
assert isinstance(result, Strip)
|
||||
132
tests/cli/textual_ui/test_manual_command_output_cap.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import build_test_agent_loop, build_test_vibe_app
|
||||
from tests.mock.utils import mock_llm_chunk
|
||||
from tests.stubs.fake_backend import FakeBackend
|
||||
from vibe.cli.textual_ui.app import VibeApp
|
||||
from vibe.cli.textual_ui.widgets.chat_input.container import ChatInputContainer
|
||||
from vibe.cli.textual_ui.widgets.messages import BashOutputMessage
|
||||
from vibe.core.types import Role
|
||||
|
||||
|
||||
class TestCapOutput:
|
||||
"""Unit tests for VibeApp._cap_output."""
|
||||
|
||||
def test_short_text_unchanged(self) -> None:
|
||||
assert VibeApp._cap_output("hello", 100) == "hello"
|
||||
|
||||
def test_exact_limit_unchanged(self) -> None:
|
||||
text = "a" * 50
|
||||
assert VibeApp._cap_output(text, 50) == text
|
||||
|
||||
def test_over_limit_truncated(self) -> None:
|
||||
text = "a" * 100
|
||||
result = VibeApp._cap_output(text, 50)
|
||||
assert result == "a" * 50 + "\n... [truncated]"
|
||||
|
||||
def test_empty_string_unchanged(self) -> None:
|
||||
assert VibeApp._cap_output("", 10) == ""
|
||||
|
||||
|
||||
class TestFormatManualCommandContext:
|
||||
"""Unit tests for VibeApp._format_manual_command_context with output capping."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self) -> VibeApp:
|
||||
return build_test_vibe_app()
|
||||
|
||||
def test_stdout_capped_in_context(self, app: VibeApp) -> None:
|
||||
limit = app._get_bash_max_output_bytes()
|
||||
big_stdout = "x" * (limit + 5000)
|
||||
|
||||
result = app._format_manual_command_context(
|
||||
command="cat big.log", cwd="/tmp", stdout=big_stdout, exit_code=0
|
||||
)
|
||||
|
||||
assert "[truncated]" in result
|
||||
# The raw oversized content must not appear in the formatted output
|
||||
assert big_stdout not in result
|
||||
|
||||
def test_stderr_capped_in_context(self, app: VibeApp) -> None:
|
||||
limit = app._get_bash_max_output_bytes()
|
||||
big_stderr = "E" * (limit + 1000)
|
||||
|
||||
result = app._format_manual_command_context(
|
||||
command="make build", cwd="/tmp", stderr=big_stderr, exit_code=1
|
||||
)
|
||||
|
||||
assert "[truncated]" in result
|
||||
assert big_stderr not in result
|
||||
|
||||
def test_small_output_not_truncated(self, app: VibeApp) -> None:
|
||||
result = app._format_manual_command_context(
|
||||
command="echo hi", cwd="/tmp", stdout="hi\n", exit_code=0
|
||||
)
|
||||
|
||||
assert "[truncated]" not in result
|
||||
assert "hi" in result
|
||||
|
||||
def test_both_stdout_and_stderr_capped_independently(self, app: VibeApp) -> None:
|
||||
limit = app._get_bash_max_output_bytes()
|
||||
big_stdout = "O" * (limit + 100)
|
||||
big_stderr = "E" * (limit + 100)
|
||||
|
||||
result = app._format_manual_command_context(
|
||||
command="cmd", cwd="/tmp", stdout=big_stdout, stderr=big_stderr, exit_code=1
|
||||
)
|
||||
|
||||
# Both should be truncated
|
||||
assert result.count("[truncated]") == 2
|
||||
|
||||
|
||||
class TestGetBashMaxOutputBytes:
|
||||
"""Test that _get_bash_max_output_bytes reads from the tool config."""
|
||||
|
||||
def test_returns_default_value(self) -> None:
|
||||
from vibe.core.tools.builtins.bash import BashToolConfig
|
||||
|
||||
app = build_test_vibe_app()
|
||||
result = app._get_bash_max_output_bytes()
|
||||
assert result == BashToolConfig().max_output_bytes
|
||||
|
||||
def test_returns_positive_int(self) -> None:
|
||||
app = build_test_vibe_app()
|
||||
assert app._get_bash_max_output_bytes() > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_large_bang_command_output_is_capped_in_history() -> None:
|
||||
"""Integration test: !command output injected into history respects the cap."""
|
||||
backend = FakeBackend(mock_llm_chunk(content="ok"))
|
||||
app = build_test_vibe_app(agent_loop=build_test_agent_loop(backend=backend))
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
limit = app._get_bash_max_output_bytes()
|
||||
# Generate output larger than the cap.
|
||||
# seq lines average ~4 chars for small numbers but grow; use
|
||||
# a generous count to guarantee we exceed the byte limit.
|
||||
repeat = (limit // 2) + 100
|
||||
cmd = f"!seq 1 {repeat}"
|
||||
|
||||
chat_input = app.query_one(ChatInputContainer)
|
||||
chat_input.value = cmd
|
||||
await pilot.press("enter")
|
||||
|
||||
deadline = time.monotonic() + 2.0
|
||||
while time.monotonic() < deadline:
|
||||
if next(iter(app.query(BashOutputMessage)), None):
|
||||
break
|
||||
await pilot.pause(0.05)
|
||||
|
||||
injected = app.agent_loop.messages[-1]
|
||||
assert injected.role == Role.user
|
||||
assert injected.injected is True
|
||||
assert injected.content is not None
|
||||
assert "[truncated]" in injected.content
|
||||
# The injected content should be bounded; allow generous margin for
|
||||
# formatting overhead but ensure it's not the full raw output.
|
||||
assert len(injected.content) < limit * 3
|
||||
|
|
@ -73,6 +73,12 @@ def config_dir(
|
|||
config_file.write_text(tomli_w.dumps(get_base_config()), encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", config_dir)
|
||||
|
||||
# Re-evaluate PLAN agent overrides so the allowlist uses the monkeypatched path
|
||||
from vibe.core.agents.models import PLAN, _plan_overrides
|
||||
|
||||
object.__setattr__(PLAN, "overrides", _plan_overrides())
|
||||
|
||||
return config_dir
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -78,3 +78,29 @@ class TestAgentManager:
|
|||
|
||||
assert agent.name == "default"
|
||||
assert agent.agent_type == AgentType.AGENT
|
||||
|
||||
def test_initial_agent_rejects_subagent(self) -> None:
|
||||
"""Test that creating AgentManager with a subagent as initial_agent raises."""
|
||||
config = build_test_vibe_config(
|
||||
include_project_context=False, include_prompt_detail=False
|
||||
)
|
||||
with pytest.raises(ValueError, match="cannot be used as the primary agent"):
|
||||
AgentManager(lambda: config, initial_agent="explore")
|
||||
|
||||
def test_initial_agent_accepts_subagent_when_allowed(self) -> None:
|
||||
"""Test that allow_subagent=True permits subagent as initial_agent."""
|
||||
config = build_test_vibe_config(
|
||||
include_project_context=False, include_prompt_detail=False
|
||||
)
|
||||
manager = AgentManager(
|
||||
lambda: config, initial_agent="explore", allow_subagent=True
|
||||
)
|
||||
assert manager.active_profile.name == "explore"
|
||||
|
||||
def test_initial_agent_accepts_agent_type(self) -> None:
|
||||
"""Test that creating AgentManager with an agent-type agent works."""
|
||||
config = build_test_vibe_config(
|
||||
include_project_context=False, include_prompt_detail=False
|
||||
)
|
||||
manager = AgentManager(lambda: config, initial_agent="plan")
|
||||
assert manager.active_profile.name == "plan"
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ from __future__ import annotations
|
|||
|
||||
import pytest
|
||||
|
||||
from vibe.core.config import OtelExporterConfig, ProviderConfig, VibeConfig
|
||||
from vibe.core.config import OtelSpanExporterConfig, ProviderConfig, VibeConfig
|
||||
from vibe.core.types import Backend
|
||||
|
||||
|
||||
class TestOtelExporterConfig:
|
||||
class TestOtelSpanExporterConfig:
|
||||
def test_derives_endpoint_from_mistral_provider(
|
||||
self, vibe_config: VibeConfig, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
|
@ -22,7 +22,7 @@ class TestOtelExporterConfig:
|
|||
]
|
||||
}
|
||||
)
|
||||
result = config.otel_exporter_config
|
||||
result = config.otel_span_exporter_config
|
||||
assert result is not None
|
||||
assert result.endpoint == "https://customer.mistral.ai/telemetry/v1/traces"
|
||||
assert result.headers == {"Authorization": "Bearer sk-test"}
|
||||
|
|
@ -49,7 +49,7 @@ class TestOtelExporterConfig:
|
|||
]
|
||||
}
|
||||
)
|
||||
result = config.otel_exporter_config
|
||||
result = config.otel_span_exporter_config
|
||||
assert result is not None
|
||||
assert result.endpoint == "https://eu.mistral.ai/telemetry/v1/traces"
|
||||
assert result.headers == {"Authorization": "Bearer sk-eu"}
|
||||
|
|
@ -67,7 +67,7 @@ class TestOtelExporterConfig:
|
|||
]
|
||||
}
|
||||
)
|
||||
result = config.otel_exporter_config
|
||||
result = config.otel_span_exporter_config
|
||||
assert result is not None
|
||||
assert result.endpoint == "https://api.mistral.ai/telemetry/v1/traces"
|
||||
assert result.headers == {"Authorization": "Bearer sk-fallback"}
|
||||
|
|
@ -76,7 +76,7 @@ class TestOtelExporterConfig:
|
|||
self, vibe_config: VibeConfig, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "sk-default")
|
||||
result = vibe_config.otel_exporter_config
|
||||
result = vibe_config.otel_span_exporter_config
|
||||
assert result is not None
|
||||
assert result.endpoint == "https://api.mistral.ai/telemetry/v1/traces"
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ class TestOtelExporterConfig:
|
|||
) -> None:
|
||||
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
|
||||
with caplog.at_level("WARNING"):
|
||||
assert vibe_config.otel_exporter_config is None
|
||||
assert vibe_config.otel_span_exporter_config is None
|
||||
assert "OTEL tracing enabled but MISTRAL_API_KEY is not set" in caplog.text
|
||||
|
||||
def test_custom_api_key_env_var(
|
||||
|
|
@ -108,20 +108,33 @@ class TestOtelExporterConfig:
|
|||
]
|
||||
}
|
||||
)
|
||||
result = config.otel_exporter_config
|
||||
result = config.otel_span_exporter_config
|
||||
assert result is not None
|
||||
assert result.endpoint == "https://onprem.corp.com/telemetry/v1/traces"
|
||||
assert result.headers == {"Authorization": "Bearer sk-custom"}
|
||||
|
||||
def test_explicit_otel_endpoint_bypasses_provider_derivation(
|
||||
def test_explicit_otel_endpoint_appends_default_traces_path(
|
||||
self, vibe_config: VibeConfig
|
||||
) -> None:
|
||||
config = vibe_config.model_copy(
|
||||
update={"otel_endpoint": "https://my-collector:4318/v1/traces"}
|
||||
update={"otel_endpoint": "https://my-collector:4318"}
|
||||
)
|
||||
result = config.otel_exporter_config
|
||||
result = config.otel_span_exporter_config
|
||||
assert result is not None
|
||||
assert result == OtelExporterConfig(
|
||||
assert result == OtelSpanExporterConfig(
|
||||
endpoint="https://my-collector:4318/v1/traces"
|
||||
)
|
||||
assert result.headers is None
|
||||
|
||||
def test_explicit_otel_endpoint_preserves_path_prefix(
|
||||
self, vibe_config: VibeConfig
|
||||
) -> None:
|
||||
config = vibe_config.model_copy(
|
||||
update={"otel_endpoint": "https://my-collector:4318/api/public/otel"}
|
||||
)
|
||||
result = config.otel_span_exporter_config
|
||||
assert result is not None
|
||||
assert result == OtelSpanExporterConfig(
|
||||
endpoint="https://my-collector:4318/api/public/otel/v1/traces"
|
||||
)
|
||||
assert result.headers is None
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@ from unittest.mock import MagicMock, patch
|
|||
|
||||
import pytest
|
||||
|
||||
from vibe.core.logger import StructuredLogFormatter, apply_logging_config
|
||||
from vibe.core.logger import (
|
||||
StructuredLogFormatter,
|
||||
apply_logging_config,
|
||||
decode_log_message,
|
||||
encode_log_message,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -278,3 +283,63 @@ class TestApplyLoggingConfig:
|
|||
handler = test_logger.handlers[-1]
|
||||
assert isinstance(handler, RotatingFileHandler)
|
||||
assert handler.maxBytes == 5242880
|
||||
|
||||
|
||||
class TestDecodeLogMessage:
|
||||
def test_plain_message_unchanged(self) -> None:
|
||||
assert decode_log_message("Hello world") == "Hello world"
|
||||
|
||||
def test_decodes_escaped_newline(self) -> None:
|
||||
assert decode_log_message("hello\\nworld") == "hello\nworld"
|
||||
|
||||
def test_decodes_escaped_backslash(self) -> None:
|
||||
assert decode_log_message("C:\\\\path") == "C:\\path"
|
||||
|
||||
def test_decodes_escaped_backslash_before_n(self) -> None:
|
||||
# This is the bug case: C:\new encoded as C:\\new must decode back to C:\new
|
||||
assert decode_log_message("C:\\\\new") == "C:\\new"
|
||||
|
||||
def test_roundtrip_with_newlines(self) -> None:
|
||||
original = "line one\nline two\nline three"
|
||||
encoded = encode_log_message(original)
|
||||
assert decode_log_message(encoded) == original
|
||||
|
||||
def test_roundtrip_with_backslashes(self) -> None:
|
||||
original = "C:\\Users\\test\\file.txt"
|
||||
encoded = encode_log_message(original)
|
||||
assert decode_log_message(encoded) == original
|
||||
|
||||
def test_roundtrip_with_backslash_n(self) -> None:
|
||||
original = "C:\\new folder\\notes.txt"
|
||||
encoded = encode_log_message(original)
|
||||
assert decode_log_message(encoded) == original
|
||||
|
||||
def test_roundtrip_mixed(self) -> None:
|
||||
original = "path: C:\\new\nand a newline"
|
||||
encoded = encode_log_message(original)
|
||||
assert decode_log_message(encoded) == original
|
||||
|
||||
def test_exception_encoding_escapes_backslashes(self) -> None:
|
||||
formatter = StructuredLogFormatter()
|
||||
try:
|
||||
raise ValueError("error in C:\\new\\path")
|
||||
except ValueError:
|
||||
import sys
|
||||
|
||||
exc_info = sys.exc_info()
|
||||
|
||||
record = logging.LogRecord(
|
||||
name="test",
|
||||
level=logging.ERROR,
|
||||
pathname="test.py",
|
||||
lineno=1,
|
||||
msg="fail",
|
||||
args=(),
|
||||
exc_info=exc_info,
|
||||
)
|
||||
|
||||
output = formatter.format(record)
|
||||
|
||||
assert "\n" not in output
|
||||
# The backslashes in the exception should be escaped
|
||||
assert "C:\\\\new\\\\path" in output
|
||||
|
|
|
|||
430
tests/core/test_log_reader.py
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
from dataclasses import FrozenInstanceError
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.log_reader import LogEntry, LogReader
|
||||
|
||||
|
||||
def _wait_for(condition: Callable[[], bool], timeout: float = 3.0) -> bool:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if condition():
|
||||
return True
|
||||
time.sleep(0.05)
|
||||
return False
|
||||
|
||||
|
||||
class TestLogEntry:
|
||||
def test_log_entry_is_frozen(self) -> None:
|
||||
entry = LogEntry(
|
||||
timestamp=datetime.now(),
|
||||
ppid=1,
|
||||
pid=123,
|
||||
level="INFO",
|
||||
message="test",
|
||||
raw_line="raw",
|
||||
line_number=1,
|
||||
)
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
entry.message = "modified" # type: ignore[misc]
|
||||
|
||||
|
||||
class TestLogReaderParsing:
|
||||
@pytest.fixture
|
||||
def log_file(self, tmp_path: Path) -> Path:
|
||||
return tmp_path / "test.log"
|
||||
|
||||
def test_parses_valid_info_log(self, log_file: Path) -> None:
|
||||
log_file.write_text(
|
||||
"2026-02-08T10:30:45.123000+00:00 1234 5678 INFO Test message\n"
|
||||
)
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
assert len(result.entries) == 1
|
||||
assert result.entries[0].level == "INFO"
|
||||
assert result.entries[0].message == "Test message"
|
||||
|
||||
def test_parses_valid_error_log(self, log_file: Path) -> None:
|
||||
log_file.write_text(
|
||||
"2026-02-08T10:30:45.123000+00:00 1234 5678 ERROR Error message\n"
|
||||
)
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
assert result.entries[0].level == "ERROR"
|
||||
|
||||
def test_parses_log_with_ppid_pid(self, log_file: Path) -> None:
|
||||
log_file.write_text("2026-02-08T10:30:45.123000+00:00 1111 2222 DEBUG msg\n")
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
assert result.entries[0].ppid == 1111
|
||||
assert result.entries[0].pid == 2222
|
||||
|
||||
def test_skips_invalid_log_lines(self, log_file: Path) -> None:
|
||||
log_file.write_text(
|
||||
"invalid line\n2026-02-08T10:30:45.123000+00:00 1 2 INFO valid\n"
|
||||
)
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
assert len(result.entries) == 1
|
||||
assert result.entries[0].message == "valid"
|
||||
|
||||
def test_skips_multiline_continuations(self, log_file: Path) -> None:
|
||||
log_file.write_text(
|
||||
"2026-02-08T10:30:45.123000+00:00 1 2 INFO msg\n continuation\n"
|
||||
)
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
assert len(result.entries) == 1
|
||||
|
||||
def test_handles_empty_file(self, log_file: Path) -> None:
|
||||
log_file.write_text("")
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
assert result.entries == []
|
||||
|
||||
def test_handles_missing_file(self, tmp_path: Path) -> None:
|
||||
missing = tmp_path / "nonexistent.log"
|
||||
reader = LogReader(log_file=missing)
|
||||
result = reader.get_logs()
|
||||
assert result.entries == []
|
||||
|
||||
def test_skips_line_with_invalid_timestamp(self, log_file: Path) -> None:
|
||||
log_file.write_text("not-a-timestamp 1 2 INFO message\n")
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
assert result.entries == []
|
||||
|
||||
def test_skips_line_with_invalid_level(self, log_file: Path) -> None:
|
||||
log_file.write_text("2026-02-08T10:30:45.123000+00:00 1 2 UNKNOWN message\n")
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
assert result.entries == []
|
||||
|
||||
def test_skips_line_missing_pid(self, log_file: Path) -> None:
|
||||
log_file.write_text("2026-02-08T10:30:45.123000+00:00 1 INFO message\n")
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
assert result.entries == []
|
||||
|
||||
def test_skips_empty_lines(self, log_file: Path) -> None:
|
||||
log_file.write_text("\n\n2026-02-08T10:30:45.123000+00:00 1 2 INFO valid\n\n")
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
assert len(result.entries) == 1
|
||||
|
||||
def test_handles_extremely_long_lines(self, log_file: Path) -> None:
|
||||
long_message = "x" * (128 * 1024) # 128KB message
|
||||
log_file.write_text(
|
||||
f"2026-02-08T10:30:45.123000+00:00 1 2 INFO {long_message}\n"
|
||||
f"2026-02-08T10:30:46.123000+00:00 1 2 INFO short\n"
|
||||
)
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
assert len(result.entries) == 2
|
||||
assert result.entries[0].message == "short"
|
||||
assert result.entries[1].message == long_message
|
||||
|
||||
|
||||
class TestLogReaderMassiveLogs:
|
||||
@pytest.fixture
|
||||
def log_file(self, tmp_path: Path) -> Path:
|
||||
return tmp_path / "test.log"
|
||||
|
||||
def test_limit_prevents_reading_entire_large_file(self, log_file: Path) -> None:
|
||||
num_lines = 10_000
|
||||
lines = [
|
||||
f"2026-02-08T10:30:{i % 60:02d}.{i:06d}+00:00 1 2 INFO Message {i}\n"
|
||||
for i in range(num_lines)
|
||||
]
|
||||
log_file.write_text("".join(lines))
|
||||
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs(limit=10)
|
||||
|
||||
assert len(result.entries) == 10
|
||||
assert result.has_more is True
|
||||
|
||||
def test_handles_file_with_mostly_invalid_lines(self, log_file: Path) -> None:
|
||||
invalid_lines = ["garbage line\n"] * 1000
|
||||
valid_line = "2026-02-08T10:30:45.123000+00:00 1 2 INFO valid entry\n"
|
||||
log_file.write_text("".join(invalid_lines) + valid_line)
|
||||
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs(limit=100)
|
||||
|
||||
assert len(result.entries) == 1
|
||||
assert result.entries[0].message == "valid entry"
|
||||
|
||||
def test_handles_binary_garbage_in_file(self, log_file: Path) -> None:
|
||||
binary_garbage = bytes(range(256))
|
||||
valid_line = b"2026-02-08T10:30:45.123000+00:00 1 2 INFO valid after binary\n"
|
||||
log_file.write_bytes(binary_garbage + b"\n" + valid_line)
|
||||
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
|
||||
assert len(result.entries) == 1
|
||||
assert result.entries[0].message == "valid after binary"
|
||||
|
||||
def test_handles_null_bytes_in_lines(self, log_file: Path) -> None:
|
||||
line_with_nulls = (
|
||||
"2026-02-08T10:30:45.123000+00:00 1 2 INFO msg\x00with\x00nulls\n"
|
||||
)
|
||||
valid_line = "2026-02-08T10:30:46.123000+00:00 1 2 INFO clean message\n"
|
||||
log_file.write_text(line_with_nulls + valid_line)
|
||||
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
|
||||
assert len(result.entries) == 2
|
||||
assert result.entries[0].message == "clean message"
|
||||
assert result.entries[1].message == "msg\x00with\x00nulls"
|
||||
|
||||
def test_handles_massive_single_line_without_newline(self, log_file: Path) -> None:
|
||||
massive_line = "x" * (1024 * 1024)
|
||||
log_file.write_text(massive_line)
|
||||
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs()
|
||||
|
||||
assert result.entries == []
|
||||
|
||||
|
||||
class TestLogReaderPagination:
|
||||
@pytest.fixture
|
||||
def log_file_with_entries(self, tmp_path: Path) -> Path:
|
||||
log_file = tmp_path / "test.log"
|
||||
lines = [
|
||||
f"2026-02-08T10:30:{i:02d}.000000+00:00 1 2 INFO Message {i}\n"
|
||||
for i in range(10)
|
||||
]
|
||||
log_file.write_text("".join(lines))
|
||||
return log_file
|
||||
|
||||
def test_returns_logs_newest_first(self, log_file_with_entries: Path) -> None:
|
||||
reader = LogReader(log_file=log_file_with_entries)
|
||||
result = reader.get_logs()
|
||||
assert "Message 9" in result.entries[0].message
|
||||
|
||||
def test_limit_restricts_results(self, log_file_with_entries: Path) -> None:
|
||||
reader = LogReader(log_file=log_file_with_entries)
|
||||
result = reader.get_logs(limit=3)
|
||||
assert len(result.entries) == 3
|
||||
|
||||
def test_has_more_false_when_exhausted(self, log_file_with_entries: Path) -> None:
|
||||
reader = LogReader(log_file=log_file_with_entries)
|
||||
result = reader.get_logs(limit=100)
|
||||
assert result.has_more is False
|
||||
|
||||
def test_cursor_continues_from_previous_position(
|
||||
self, log_file_with_entries: Path
|
||||
) -> None:
|
||||
reader = LogReader(log_file=log_file_with_entries)
|
||||
page1 = reader.get_logs(limit=3)
|
||||
assert len(page1.entries) == 3
|
||||
assert page1.has_more is True
|
||||
assert page1.cursor is not None
|
||||
|
||||
page2 = reader.get_logs(limit=3, offset=page1.cursor)
|
||||
assert len(page2.entries) == 3
|
||||
assert page2.has_more is True
|
||||
assert page2.entries[0].message != page1.entries[-1].message
|
||||
|
||||
|
||||
class TestLogReaderWatcher:
|
||||
@pytest.fixture
|
||||
def log_reader(self, tmp_path: Path) -> Generator[LogReader, None, None]:
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("")
|
||||
reader = LogReader(log_file=log_file)
|
||||
yield reader
|
||||
reader.shutdown()
|
||||
|
||||
def test_start_watching_sets_is_watching(self, log_reader: LogReader) -> None:
|
||||
log_reader.start_watching()
|
||||
assert log_reader.is_watching is True
|
||||
|
||||
def test_stop_watching_clears_is_watching(self, log_reader: LogReader) -> None:
|
||||
log_reader.start_watching()
|
||||
log_reader.stop_watching()
|
||||
assert log_reader.is_watching is False
|
||||
|
||||
def test_consumer_receives_new_entries(self, tmp_path: Path) -> None:
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("")
|
||||
received: list[LogEntry] = []
|
||||
reader = LogReader(log_file=log_file, consumer=received.append)
|
||||
try:
|
||||
reader.start_watching()
|
||||
|
||||
with log_file.open("a") as f:
|
||||
f.write("2026-02-08T10:30:45.123000+00:00 1 2 INFO New entry\n")
|
||||
|
||||
assert _wait_for(lambda: len(received) >= 1)
|
||||
assert received[0].message == "New entry"
|
||||
finally:
|
||||
reader.shutdown()
|
||||
|
||||
def test_toggle_watching_on_off(self, log_reader: LogReader) -> None:
|
||||
log_reader.start_watching()
|
||||
assert log_reader.is_watching is True
|
||||
log_reader.stop_watching()
|
||||
assert log_reader.is_watching is False
|
||||
log_reader.start_watching()
|
||||
assert log_reader.is_watching is True
|
||||
|
||||
def test_set_consumer_updates_callback(self, tmp_path: Path) -> None:
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("")
|
||||
received: list[LogEntry] = []
|
||||
reader = LogReader(log_file=log_file)
|
||||
try:
|
||||
reader.set_consumer(received.append)
|
||||
reader.start_watching()
|
||||
|
||||
with log_file.open("a") as f:
|
||||
f.write("2026-02-08T10:30:45.123000+00:00 1 2 INFO Entry\n")
|
||||
|
||||
assert _wait_for(lambda: len(received) >= 1)
|
||||
finally:
|
||||
reader.shutdown()
|
||||
|
||||
def test_consumer_can_call_get_logs_without_deadlock(self, tmp_path: Path) -> None:
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("")
|
||||
reader = LogReader(log_file=log_file, poll_interval=0.05)
|
||||
callback_completed = threading.Event()
|
||||
callback_errors: list[Exception] = []
|
||||
|
||||
def consumer(_: LogEntry) -> None:
|
||||
try:
|
||||
reader.get_logs(limit=1)
|
||||
except Exception as exc:
|
||||
callback_errors.append(exc)
|
||||
finally:
|
||||
callback_completed.set()
|
||||
|
||||
reader.set_consumer(consumer)
|
||||
try:
|
||||
reader.start_watching()
|
||||
with log_file.open("a") as f:
|
||||
f.write("2026-02-08T10:30:45.123000+00:00 1 2 INFO Entry\n")
|
||||
|
||||
assert callback_completed.wait(timeout=1.0)
|
||||
assert callback_errors == []
|
||||
finally:
|
||||
reader.shutdown()
|
||||
|
||||
|
||||
class TestLogReaderCleanup:
|
||||
def test_shutdown_stops_watching(self, tmp_path: Path) -> None:
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("")
|
||||
reader = LogReader(log_file=log_file)
|
||||
reader.start_watching()
|
||||
assert reader.is_watching is True
|
||||
reader.shutdown()
|
||||
assert reader.is_watching is False
|
||||
|
||||
|
||||
class TestLogReaderLineNumbers:
|
||||
def test_line_numbers_relative_from_end(self, tmp_path: Path) -> None:
|
||||
log_file = tmp_path / "test.log"
|
||||
lines = [
|
||||
f"2026-02-08T10:30:{i:02d}.000000+00:00 1 2 INFO Message {i}\n"
|
||||
for i in range(5)
|
||||
]
|
||||
log_file.write_text("".join(lines))
|
||||
|
||||
reader = LogReader(log_file=log_file)
|
||||
result = reader.get_logs(limit=5)
|
||||
|
||||
assert result.entries[0].line_number == 1 # Newest
|
||||
assert result.entries[0].message == "Message 4"
|
||||
assert result.entries[1].line_number == 2 # Second newest
|
||||
assert result.entries[1].message == "Message 3"
|
||||
|
||||
def test_live_entries_have_zero_line_number(self, tmp_path: Path) -> None:
|
||||
log_file = tmp_path / "test.log"
|
||||
log_file.write_text("")
|
||||
received: list[LogEntry] = []
|
||||
reader = LogReader(log_file=log_file, consumer=received.append)
|
||||
try:
|
||||
reader.start_watching()
|
||||
|
||||
with log_file.open("a") as f:
|
||||
f.write("2026-02-08T10:30:45.123000+00:00 1 2 INFO Live entry\n")
|
||||
|
||||
assert _wait_for(lambda: len(received) >= 1)
|
||||
assert received[0].line_number == 0
|
||||
assert received[0].message == "Live entry"
|
||||
finally:
|
||||
reader.shutdown()
|
||||
|
||||
|
||||
class TestLogReaderCursorDrift:
|
||||
def test_cursor_stable_with_new_logs(self, tmp_path: Path) -> None:
|
||||
log_file = tmp_path / "test.log"
|
||||
lines = [
|
||||
f"2026-02-08T10:30:{i:02d}.000000+00:00 1 2 INFO Message {i}\n"
|
||||
for i in range(10)
|
||||
]
|
||||
log_file.write_text("".join(lines))
|
||||
|
||||
received: list[LogEntry] = []
|
||||
reader = LogReader(log_file=log_file, consumer=received.append)
|
||||
|
||||
try:
|
||||
page1 = reader.get_logs(limit=3)
|
||||
assert page1.entries[0].message == "Message 9"
|
||||
assert page1.cursor == 3
|
||||
|
||||
reader.start_watching()
|
||||
with log_file.open("a") as f:
|
||||
f.write("2026-02-08T10:30:50.000000+00:00 1 2 INFO New message 1\n")
|
||||
f.write("2026-02-08T10:30:51.000000+00:00 1 2 INFO New message 2\n")
|
||||
|
||||
assert _wait_for(lambda: len(received) >= 2)
|
||||
|
||||
page2 = reader.get_logs(limit=3, offset=page1.cursor)
|
||||
assert page2.entries[0].message == "Message 6"
|
||||
assert page2.entries[1].message == "Message 5"
|
||||
assert page2.entries[2].message == "Message 4"
|
||||
|
||||
finally:
|
||||
reader.shutdown()
|
||||
|
||||
def test_stop_watching_resets_counter(self, tmp_path: Path) -> None:
|
||||
log_file = tmp_path / "test.log"
|
||||
lines = [
|
||||
f"2026-02-08T10:30:{i:02d}.000000+00:00 1 2 INFO Message {i}\n"
|
||||
for i in range(5)
|
||||
]
|
||||
log_file.write_text("".join(lines))
|
||||
|
||||
received: list[LogEntry] = []
|
||||
reader = LogReader(log_file=log_file, consumer=received.append)
|
||||
|
||||
try:
|
||||
reader.start_watching()
|
||||
|
||||
with log_file.open("a") as f:
|
||||
f.write("2026-02-08T10:30:50.000000+00:00 1 2 INFO New message\n")
|
||||
|
||||
assert _wait_for(lambda: len(received) >= 1)
|
||||
|
||||
reader.stop_watching()
|
||||
|
||||
result = reader.get_logs(limit=10)
|
||||
assert len(result.entries) == 6
|
||||
assert result.entries[0].message == "New message"
|
||||
|
||||
finally:
|
||||
reader.shutdown()
|
||||
|
|
@ -225,6 +225,18 @@ class TestNuageClientGetChatAssistantUrl:
|
|||
url = await nuage.get_chat_assistant_url("exec-123")
|
||||
assert url == "https://chat.example.com/thread/123"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_chat_assistant_url_none(
|
||||
self, nuage: NuageClient, mock_client: MagicMock
|
||||
) -> None:
|
||||
mock_response = MagicMock()
|
||||
mock_response.is_success = True
|
||||
mock_response.json.return_value = {"result": {"chat_url": None}}
|
||||
mock_client.post = AsyncMock(return_value=mock_response)
|
||||
|
||||
url = await nuage.get_chat_assistant_url("exec-123")
|
||||
assert url is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_chat_assistant_url_failure(
|
||||
self, nuage: NuageClient, mock_client: MagicMock
|
||||
|
|
|
|||
|
|
@ -382,6 +382,31 @@ class TestTeleportServiceExecute:
|
|||
assert isinstance(events[4], TeleportAuthCompleteEvent)
|
||||
assert isinstance(events[-1], TeleportCompleteEvent)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_raises_when_chat_url_is_none(
|
||||
self,
|
||||
service: TeleportService,
|
||||
git_info: GitRepoInfo,
|
||||
mock_github_connected: MagicMock,
|
||||
) -> None:
|
||||
service._git.get_info = AsyncMock(return_value=git_info)
|
||||
service._git.is_commit_pushed = AsyncMock(return_value=True)
|
||||
|
||||
mock_nuage = MagicMock()
|
||||
mock_nuage.start_workflow = AsyncMock(return_value="exec-123")
|
||||
mock_nuage.get_github_integration = AsyncMock(
|
||||
return_value=mock_github_connected
|
||||
)
|
||||
mock_nuage.get_chat_assistant_url = AsyncMock(return_value=None)
|
||||
service._nuage = mock_nuage
|
||||
|
||||
session = TeleportSession()
|
||||
gen = service.execute("test prompt", session)
|
||||
|
||||
with pytest.raises(ServiceTeleportError, match="not available"):
|
||||
async for _ in gen:
|
||||
pass
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_uses_default_prompt_when_none(
|
||||
self,
|
||||
|
|
|
|||
50
tests/core/test_watcher.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.autocompletion.file_indexer.watcher import WatchController
|
||||
|
||||
|
||||
class TestWatchControllerIsWatching:
|
||||
@pytest.fixture
|
||||
def watch_dir(self, tmp_path: Path) -> Path:
|
||||
return tmp_path / "watch"
|
||||
|
||||
@pytest.fixture
|
||||
def watcher(self) -> Generator[WatchController, None, None]:
|
||||
changes: list = []
|
||||
controller = WatchController(on_changes=lambda root, c: changes.extend(c))
|
||||
yield controller
|
||||
controller.stop()
|
||||
|
||||
def test_is_watching_false_initially(self, watcher: WatchController) -> None:
|
||||
assert watcher.is_watching is False
|
||||
|
||||
def test_is_watching_true_after_start(
|
||||
self, watcher: WatchController, watch_dir: Path
|
||||
) -> None:
|
||||
watch_dir.mkdir()
|
||||
watcher.start(watch_dir)
|
||||
assert watcher.is_watching is True
|
||||
|
||||
def test_is_watching_false_after_stop(
|
||||
self, watcher: WatchController, watch_dir: Path
|
||||
) -> None:
|
||||
watch_dir.mkdir()
|
||||
watcher.start(watch_dir)
|
||||
assert watcher.is_watching is True
|
||||
watcher.stop()
|
||||
assert watcher.is_watching is False
|
||||
|
||||
def test_is_watching_true_after_restart(
|
||||
self, watcher: WatchController, watch_dir: Path
|
||||
) -> None:
|
||||
watch_dir.mkdir()
|
||||
watcher.start(watch_dir)
|
||||
watcher.stop()
|
||||
assert watcher.is_watching is False
|
||||
watcher.start(watch_dir)
|
||||
assert watcher.is_watching is True
|
||||
|
|
@ -34,7 +34,7 @@ def _setup_manager(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[
|
|||
|
||||
|
||||
def _make_read_file() -> ReadFile:
|
||||
return ReadFile(config=ReadFileToolConfig(), state=ReadFileState())
|
||||
return ReadFile(config_getter=lambda: ReadFileToolConfig(), state=ReadFileState())
|
||||
|
||||
|
||||
class TestGetResultExtra:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
<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: #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">DebugConsoleSnapshotApp</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="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="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-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">devstral-latest</text><text class="terminal-r1" x="622.2" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-r3" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)"> for more information</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</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-r4" 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-r4" 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-r4" 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-r4" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r4" 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-r4" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r4" 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-r4" 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-r4" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r4" 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: 13 KiB |
|
|
@ -0,0 +1,204 @@
|
|||
<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: #9a9b99 }
|
||||
.terminal-r3 { fill: #868887 }
|
||||
.terminal-r4 { fill: #68a0b3 }
|
||||
.terminal-r5 { fill: #d0b344 }
|
||||
.terminal-r6 { fill: #cc555a }
|
||||
.terminal-r7 { fill: #cc555a;font-weight: bold }
|
||||
.terminal-r8 { fill: #ff8205;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">DebugConsoleSnapshotApp</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-r2" x="878.4" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">│</text><text class="terminal-r1" x="902.8" y="20" textLength="183" clip-path="url(#terminal-line-0)">Debug Console  </text><text class="terminal-r3" x="1085.8" y="20" textLength="207.4" clip-path="url(#terminal-line-0)">(ctrl+\ to close)</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r2" x="878.4" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">│</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r2" x="878.4" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">│</text><text class="terminal-r3" x="902.8" y="68.8" textLength="231.8" clip-path="url(#terminal-line-2)">2026-02-21 10:28:51</text><text class="terminal-r3" x="1146.8" y="68.8" textLength="97.6" clip-path="url(#terminal-line-2)">DEBUG   </text><text class="terminal-r1" x="1244.4" y="68.8" textLength="207.4" clip-path="url(#terminal-line-2)"> Initializing    </text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r2" x="878.4" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">│</text><text class="terminal-r1" x="902.8" y="93.2" textLength="170.8" clip-path="url(#terminal-line-3)">model registry</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
</text><text class="terminal-r2" x="878.4" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">│</text><text class="terminal-r3" x="902.8" y="117.6" textLength="231.8" clip-path="url(#terminal-line-4)">2026-02-21 10:28:51</text><text class="terminal-r4" x="1146.8" y="117.6" textLength="97.6" clip-path="url(#terminal-line-4)">INFO    </text><text class="terminal-r1" x="1244.4" y="117.6" textLength="207.4" clip-path="url(#terminal-line-4)"> Server started  </text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
|
||||
</text><text class="terminal-r2" x="878.4" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">│</text><text class="terminal-r1" x="902.8" y="142" textLength="146.4" clip-path="url(#terminal-line-5)">on port 8080</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
|
||||
</text><text class="terminal-r2" x="878.4" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">│</text><text class="terminal-r3" x="902.8" y="166.4" textLength="231.8" clip-path="url(#terminal-line-6)">2026-02-21 10:28:51</text><text class="terminal-r4" x="1146.8" y="166.4" textLength="97.6" clip-path="url(#terminal-line-6)">INFO    </text><text class="terminal-r1" x="1244.4" y="166.4" textLength="207.4" clip-path="url(#terminal-line-6)"> Loading         </text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
|
||||
</text><text class="terminal-r2" x="878.4" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">│</text><text class="terminal-r1" x="902.8" y="190.8" textLength="488" clip-path="url(#terminal-line-7)">configuration from /etc/vibe/config.yaml</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
|
||||
</text><text class="terminal-r2" x="878.4" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">│</text><text class="terminal-r3" x="902.8" y="215.2" textLength="231.8" clip-path="url(#terminal-line-8)">2026-02-21 10:28:51</text><text class="terminal-r5" x="1146.8" y="215.2" textLength="97.6" clip-path="url(#terminal-line-8)">WARNING </text><text class="terminal-r1" x="1244.4" y="215.2" textLength="207.4" clip-path="url(#terminal-line-8)"> Cache miss for  </text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
|
||||
</text><text class="terminal-r2" x="878.4" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">│</text><text class="terminal-r1" x="902.8" y="239.6" textLength="134.2" clip-path="url(#terminal-line-9)">key user:42</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
|
||||
</text><text class="terminal-r2" x="878.4" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">│</text><text class="terminal-r3" x="902.8" y="264" textLength="231.8" clip-path="url(#terminal-line-10)">2026-02-21 10:28:51</text><text class="terminal-r3" x="1146.8" y="264" textLength="97.6" clip-path="url(#terminal-line-10)">DEBUG   </text><text class="terminal-r1" x="1244.4" y="264" textLength="207.4" clip-path="url(#terminal-line-10)"> Processing      </text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
|
||||
</text><text class="terminal-r2" x="878.4" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">│</text><text class="terminal-r1" x="902.8" y="288.4" textLength="317.2" clip-path="url(#terminal-line-11)">request GET /api/v1/models</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
|
||||
</text><text class="terminal-r2" x="878.4" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">│</text><text class="terminal-r3" x="902.8" y="312.8" textLength="231.8" clip-path="url(#terminal-line-12)">2026-02-21 10:28:51</text><text class="terminal-r4" x="1146.8" y="312.8" textLength="97.6" clip-path="url(#terminal-line-12)">INFO    </text><text class="terminal-r1" x="1244.4" y="312.8" textLength="207.4" clip-path="url(#terminal-line-12)"> Request         </text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||
</text><text class="terminal-r2" x="878.4" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">│</text><text class="terminal-r1" x="902.8" y="337.2" textLength="207.4" clip-path="url(#terminal-line-13)">completed in 45ms</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||
</text><text class="terminal-r2" x="878.4" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">│</text><text class="terminal-r3" x="902.8" y="361.6" textLength="231.8" clip-path="url(#terminal-line-14)">2026-02-21 10:28:51</text><text class="terminal-r6" x="1146.8" y="361.6" textLength="97.6" clip-path="url(#terminal-line-14)">ERROR   </text><text class="terminal-r1" x="1244.4" y="361.6" textLength="207.4" clip-path="url(#terminal-line-14)"> Connection      </text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||
</text><text class="terminal-r2" x="878.4" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">│</text><text class="terminal-r1" x="902.8" y="386" textLength="329.4" clip-path="url(#terminal-line-15)">refused to upstream service</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||
</text><text class="terminal-r2" x="878.4" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">│</text><text class="terminal-r3" x="902.8" y="410.4" textLength="231.8" clip-path="url(#terminal-line-16)">2026-02-21 10:28:51</text><text class="terminal-r4" x="1146.8" y="410.4" textLength="97.6" clip-path="url(#terminal-line-16)">INFO    </text><text class="terminal-r1" x="1244.4" y="410.4" textLength="207.4" clip-path="url(#terminal-line-16)"> Retrying        </text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||
</text><text class="terminal-r2" x="878.4" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">│</text><text class="terminal-r1" x="902.8" y="434.8" textLength="268.4" clip-path="url(#terminal-line-17)">connection attempt 1/3</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r2" x="878.4" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">│</text><text class="terminal-r3" x="902.8" y="459.2" textLength="231.8" clip-path="url(#terminal-line-18)">2026-02-21 10:28:51</text><text class="terminal-r5" x="1146.8" y="459.2" textLength="97.6" clip-path="url(#terminal-line-18)">WARNING </text><text class="terminal-r1" x="1244.4" y="459.2" textLength="207.4" clip-path="url(#terminal-line-18)"> Rate limit      </text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r2" x="878.4" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">│</text><text class="terminal-r1" x="902.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">approaching for client api-key-abc</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r2" x="878.4" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">│</text><text class="terminal-r3" x="902.8" y="508" textLength="231.8" clip-path="url(#terminal-line-20)">2026-02-21 10:28:52</text><text class="terminal-r4" x="1146.8" y="508" textLength="97.6" clip-path="url(#terminal-line-20)">INFO    </text><text class="terminal-r1" x="1244.4" y="508" textLength="207.4" clip-path="url(#terminal-line-20)"> Health check    </text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r2" x="878.4" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">│</text><text class="terminal-r1" x="902.8" y="532.4" textLength="73.2" clip-path="url(#terminal-line-21)">passed</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r2" x="878.4" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">│</text><text class="terminal-r3" x="902.8" y="556.8" textLength="231.8" clip-path="url(#terminal-line-22)">2026-02-21 10:28:53</text><text class="terminal-r7" x="1146.8" y="556.8" textLength="97.6" clip-path="url(#terminal-line-22)">CRITICAL</text><text class="terminal-r1" x="1244.4" y="556.8" textLength="207.4" clip-path="url(#terminal-line-22)"> Out of memory   </text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r2" x="878.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">│</text><text class="terminal-r1" x="902.8" y="581.2" textLength="170.8" clip-path="url(#terminal-line-23)">error detected</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r8" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-r4" x="439.2" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">devstral-latest</text><text class="terminal-r1" x="622.2" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)"> · [Subscription] Pro</text><text class="terminal-r2" x="878.4" 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-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r2" x="878.4" 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-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-r4" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)"> for more information</text><text class="terminal-r2" x="878.4" 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-r2" x="878.4" 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-r2" x="878.4" 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-r2" x="878.4" 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-r2" x="0" y="752" textLength="890.6" 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-r2" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r8" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">></text><text class="terminal-r2" x="866.2" y="776.4" textLength="24.4" 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-r2" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r2" x="866.2" y="800.8" textLength="24.4" 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-r2" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r2" x="866.2" y="825.2" textLength="24.4" 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-r2" x="0" y="849.6" textLength="890.6" 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-r2" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r2" x="671" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0% of 200k tokens</text><text class="terminal-r2" x="878.4" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">│</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 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: #9a9b99 }
|
||||
.terminal-r3 { fill: #868887 }
|
||||
.terminal-r4 { fill: #68a0b3 }
|
||||
.terminal-r5 { fill: #d0b344 }
|
||||
.terminal-r6 { fill: #cc555a }
|
||||
.terminal-r7 { fill: #ff8205;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">DebugConsoleSnapshotApp</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-r2" x="878.4" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">│</text><text class="terminal-r1" x="902.8" y="20" textLength="183" clip-path="url(#terminal-line-0)">Debug Console  </text><text class="terminal-r3" x="1085.8" y="20" textLength="207.4" clip-path="url(#terminal-line-0)">(ctrl+\ to close)</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r2" x="878.4" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">│</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r2" x="878.4" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">│</text><text class="terminal-r3" x="902.8" y="68.8" textLength="231.8" clip-path="url(#terminal-line-2)">2026-02-21 10:28:51</text><text class="terminal-r3" x="1146.8" y="68.8" textLength="97.6" clip-path="url(#terminal-line-2)">DEBUG   </text><text class="terminal-r1" x="1244.4" y="68.8" textLength="207.4" clip-path="url(#terminal-line-2)"> Initializing    </text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r2" x="878.4" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">│</text><text class="terminal-r1" x="902.8" y="93.2" textLength="170.8" clip-path="url(#terminal-line-3)">model registry</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
</text><text class="terminal-r2" x="878.4" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">│</text><text class="terminal-r3" x="902.8" y="117.6" textLength="231.8" clip-path="url(#terminal-line-4)">2026-02-21 10:28:51</text><text class="terminal-r4" x="1146.8" y="117.6" textLength="97.6" clip-path="url(#terminal-line-4)">INFO    </text><text class="terminal-r1" x="1244.4" y="117.6" textLength="207.4" clip-path="url(#terminal-line-4)"> Server started  </text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
|
||||
</text><text class="terminal-r2" x="878.4" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">│</text><text class="terminal-r1" x="902.8" y="142" textLength="146.4" clip-path="url(#terminal-line-5)">on port 8080</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
|
||||
</text><text class="terminal-r2" x="878.4" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">│</text><text class="terminal-r3" x="902.8" y="166.4" textLength="231.8" clip-path="url(#terminal-line-6)">2026-02-21 10:28:51</text><text class="terminal-r4" x="1146.8" y="166.4" textLength="97.6" clip-path="url(#terminal-line-6)">INFO    </text><text class="terminal-r1" x="1244.4" y="166.4" textLength="207.4" clip-path="url(#terminal-line-6)"> Loading         </text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
|
||||
</text><text class="terminal-r2" x="878.4" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">│</text><text class="terminal-r1" x="902.8" y="190.8" textLength="488" clip-path="url(#terminal-line-7)">configuration from /etc/vibe/config.yaml</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
|
||||
</text><text class="terminal-r2" x="878.4" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">│</text><text class="terminal-r3" x="902.8" y="215.2" textLength="231.8" clip-path="url(#terminal-line-8)">2026-02-21 10:28:51</text><text class="terminal-r5" x="1146.8" y="215.2" textLength="97.6" clip-path="url(#terminal-line-8)">WARNING </text><text class="terminal-r1" x="1244.4" y="215.2" textLength="207.4" clip-path="url(#terminal-line-8)"> Cache miss for  </text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
|
||||
</text><text class="terminal-r2" x="878.4" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">│</text><text class="terminal-r1" x="902.8" y="239.6" textLength="134.2" clip-path="url(#terminal-line-9)">key user:42</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
|
||||
</text><text class="terminal-r2" x="878.4" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">│</text><text class="terminal-r3" x="902.8" y="264" textLength="231.8" clip-path="url(#terminal-line-10)">2026-02-21 10:28:51</text><text class="terminal-r3" x="1146.8" y="264" textLength="97.6" clip-path="url(#terminal-line-10)">DEBUG   </text><text class="terminal-r1" x="1244.4" y="264" textLength="207.4" clip-path="url(#terminal-line-10)"> Processing      </text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
|
||||
</text><text class="terminal-r2" x="878.4" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">│</text><text class="terminal-r1" x="902.8" y="288.4" textLength="317.2" clip-path="url(#terminal-line-11)">request GET /api/v1/models</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
|
||||
</text><text class="terminal-r2" x="878.4" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">│</text><text class="terminal-r3" x="902.8" y="312.8" textLength="231.8" clip-path="url(#terminal-line-12)">2026-02-21 10:28:51</text><text class="terminal-r4" x="1146.8" y="312.8" textLength="97.6" clip-path="url(#terminal-line-12)">INFO    </text><text class="terminal-r1" x="1244.4" y="312.8" textLength="207.4" clip-path="url(#terminal-line-12)"> Request         </text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||
</text><text class="terminal-r2" x="878.4" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">│</text><text class="terminal-r1" x="902.8" y="337.2" textLength="207.4" clip-path="url(#terminal-line-13)">completed in 45ms</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||
</text><text class="terminal-r2" x="878.4" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">│</text><text class="terminal-r3" x="902.8" y="361.6" textLength="231.8" clip-path="url(#terminal-line-14)">2026-02-21 10:28:51</text><text class="terminal-r6" x="1146.8" y="361.6" textLength="97.6" clip-path="url(#terminal-line-14)">ERROR   </text><text class="terminal-r1" x="1244.4" y="361.6" textLength="207.4" clip-path="url(#terminal-line-14)"> Connection      </text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||
</text><text class="terminal-r2" x="878.4" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">│</text><text class="terminal-r1" x="902.8" y="386" textLength="329.4" clip-path="url(#terminal-line-15)">refused to upstream service</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||
</text><text class="terminal-r2" x="878.4" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">│</text><text class="terminal-r3" x="902.8" y="410.4" textLength="231.8" clip-path="url(#terminal-line-16)">2026-02-21 10:28:51</text><text class="terminal-r4" x="1146.8" y="410.4" textLength="97.6" clip-path="url(#terminal-line-16)">INFO    </text><text class="terminal-r1" x="1244.4" y="410.4" textLength="207.4" clip-path="url(#terminal-line-16)"> Retrying        </text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||
</text><text class="terminal-r2" x="878.4" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">│</text><text class="terminal-r1" x="902.8" y="434.8" textLength="268.4" clip-path="url(#terminal-line-17)">connection attempt 1/3</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r2" x="878.4" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">│</text><text class="terminal-r3" x="902.8" y="459.2" textLength="231.8" clip-path="url(#terminal-line-18)">2026-02-21 10:28:51</text><text class="terminal-r5" x="1146.8" y="459.2" textLength="97.6" clip-path="url(#terminal-line-18)">WARNING </text><text class="terminal-r1" x="1244.4" y="459.2" textLength="207.4" clip-path="url(#terminal-line-18)"> Rate limit      </text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r2" x="878.4" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">│</text><text class="terminal-r1" x="902.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">approaching for client api-key-abc</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r2" x="878.4" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">│</text><text class="terminal-r3" x="902.8" y="508" textLength="231.8" clip-path="url(#terminal-line-20)">2026-02-21 10:28:52</text><text class="terminal-r4" x="1146.8" y="508" textLength="97.6" clip-path="url(#terminal-line-20)">INFO    </text><text class="terminal-r1" x="1244.4" y="508" textLength="207.4" clip-path="url(#terminal-line-20)"> Health check    </text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r2" x="878.4" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">│</text><text class="terminal-r1" x="902.8" y="532.4" textLength="73.2" clip-path="url(#terminal-line-21)">passed</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r2" x="878.4" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">│</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r2" x="878.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">│</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r7" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-r4" x="439.2" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">devstral-latest</text><text class="terminal-r1" x="622.2" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)"> · [Subscription] Pro</text><text class="terminal-r2" x="878.4" 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-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r2" x="878.4" 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-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-r4" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)"> for more information</text><text class="terminal-r2" x="878.4" 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-r2" x="878.4" 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-r2" x="878.4" 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-r2" x="878.4" 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-r2" x="0" y="752" textLength="890.6" 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-r2" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r7" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">></text><text class="terminal-r2" x="866.2" y="776.4" textLength="24.4" 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-r2" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r2" x="866.2" y="800.8" textLength="24.4" 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-r2" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">│</text><text class="terminal-r2" x="866.2" y="825.2" textLength="24.4" 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-r2" x="0" y="849.6" textLength="890.6" 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-r2" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r2" x="671" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0% of 200k tokens</text><text class="terminal-r2" x="878.4" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">│</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 22 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">SnapshotTestAppWithMcpServers</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="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"/>
|
||||
<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="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="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-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="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: 15 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">SnapshotTestAppWithBrokenMcpServer</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="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"/>
|
||||
<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="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-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="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="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: 15 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">SnapshotTestAppWithMcpServers</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="757.9" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="146.4" y="757.9" width="378.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="524.6" y="757.9" width="902.8" 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="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="483.6" textLength="183" clip-path="url(#terminal-line-19)">devstral-latest</text><text class="terminal-r1" x="622.2" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="508" textLength="414.8" clip-path="url(#terminal-line-20)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type </text><text class="terminal-r3" x="231.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/help</text><text class="terminal-r1" x="292.8" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)"> for more information</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</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="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">┃</text><text class="terminal-r2" x="24.4" y="605.6" textLength="48.8" clip-path="url(#terminal-line-24)">/mcp</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="24.4" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">⎣</text><text class="terminal-r1" x="48.8" y="630" textLength="256.2" clip-path="url(#terminal-line-25)">MCP servers opened...</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-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="1464" 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-r6" x="24.4" y="727.6" textLength="268.4" clip-path="url(#terminal-line-29)">MCP Server: filesystem</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-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="109.8" clip-path="url(#terminal-line-31)">fake_tool</text><text class="terminal-r7" x="146.4" y="776.4" textLength="378.2" clip-path="url(#terminal-line-31)">  -  A fake tool for filesystem</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 |
|
|
@ -0,0 +1,201 @@
|
|||
<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 }
|
||||
</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">SnapshotTestAppWithMcpServers</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="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="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="508" textLength="146.4" clip-path="url(#terminal-line-20)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="508" textLength="122" clip-path="url(#terminal-line-20)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="508" textLength="183" clip-path="url(#terminal-line-20)">devstral-latest</text><text class="terminal-r1" x="622.2" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="532.4" textLength="414.8" clip-path="url(#terminal-line-21)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r1" x="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="556.8" textLength="61" clip-path="url(#terminal-line-22)">Type </text><text class="terminal-r3" x="231.8" y="556.8" textLength="61" clip-path="url(#terminal-line-22)">/help</text><text class="terminal-r1" x="292.8" y="556.8" textLength="256.2" clip-path="url(#terminal-line-22)"> for more information</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</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="630" textLength="12.2" clip-path="url(#terminal-line-25)">┃</text><text class="terminal-r2" x="24.4" y="630" textLength="48.8" clip-path="url(#terminal-line-25)">/mcp</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r5" x="24.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">⎣</text><text class="terminal-r1" x="48.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">MCP servers opened...</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="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">⎣</text><text class="terminal-r1" x="48.8" y="678.8" textLength="231.8" clip-path="url(#terminal-line-27)">MCP servers closed.</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-r5" 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-r5" 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-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="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: 14 KiB |
|
|
@ -0,0 +1,201 @@
|
|||
<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 }
|
||||
</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">SnapshotTestAppNoMcpServers</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="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="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="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="532.4" textLength="146.4" clip-path="url(#terminal-line-21)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="532.4" textLength="122" clip-path="url(#terminal-line-21)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="532.4" textLength="183" clip-path="url(#terminal-line-21)">devstral-latest</text><text class="terminal-r1" x="622.2" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r1" x="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="556.8" textLength="414.8" clip-path="url(#terminal-line-22)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r1" x="0" y="581.2" textLength="134.2" clip-path="url(#terminal-line-23)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="581.2" textLength="61" clip-path="url(#terminal-line-23)">Type </text><text class="terminal-r3" x="231.8" y="581.2" textLength="61" clip-path="url(#terminal-line-23)">/help</text><text class="terminal-r1" x="292.8" y="581.2" textLength="256.2" clip-path="url(#terminal-line-23)"> for more information</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-r4" x="0" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">┃</text><text class="terminal-r2" x="24.4" y="654.4" textLength="48.8" clip-path="url(#terminal-line-26)">/mcp</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="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">⎣</text><text class="terminal-r1" x="48.8" y="678.8" textLength="317.2" clip-path="url(#terminal-line-27)">No MCP servers configured.</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-r5" 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-r5" 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-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="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: 13 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">SnapshotTestAppWithMcpServers</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="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"/>
|
||||
<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="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="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="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="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: 15 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">SnapshotTestAppWithMcpServers</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="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"/>
|
||||
<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="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="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="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="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: 15 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">SnapshotTestAppWithMcpServers</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="757.9" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="146.4" y="757.9" width="378.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="524.6" y="757.9" width="902.8" 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="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="483.6" textLength="183" clip-path="url(#terminal-line-19)">devstral-latest</text><text class="terminal-r1" x="622.2" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="508" textLength="414.8" clip-path="url(#terminal-line-20)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type </text><text class="terminal-r3" x="231.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/help</text><text class="terminal-r1" x="292.8" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)"> for more information</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</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="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">┃</text><text class="terminal-r2" x="24.4" y="605.6" textLength="183" clip-path="url(#terminal-line-24)">/mcp filesystem</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="24.4" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">⎣</text><text class="terminal-r1" x="48.8" y="630" textLength="256.2" clip-path="url(#terminal-line-25)">MCP servers opened...</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-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="1464" 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-r6" x="24.4" y="727.6" textLength="268.4" clip-path="url(#terminal-line-29)">MCP Server: filesystem</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-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="109.8" clip-path="url(#terminal-line-31)">fake_tool</text><text class="terminal-r7" x="146.4" y="776.4" textLength="378.2" clip-path="url(#terminal-line-31)">  -  A fake tool for filesystem</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 |
|
|
@ -186,7 +186,7 @@
|
|||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="605.6" textLength="414.8" clip-path="url(#terminal-line-24)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="630" textLength="61" clip-path="url(#terminal-line-25)">Type </text><text class="terminal-r3" x="231.8" y="630" textLength="61" clip-path="url(#terminal-line-25)">/help</text><text class="terminal-r1" x="292.8" y="630" textLength="256.2" clip-path="url(#terminal-line-25)"> for more information</text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r4" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">⎣</text><text class="terminal-r1" x="48.8" y="678.8" textLength="280.6" clip-path="url(#terminal-line-27)">Configuration reloaded.</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r4" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">⎣</text><text class="terminal-r1" x="48.8" y="678.8" textLength="780.8" clip-path="url(#terminal-line-27)">Configuration reloaded (includes agent instructions and skills).</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||
</text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r4" x="0" y="752" textLength="1220" clip-path="url(#terminal-line-30)">┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐</text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -1,8 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _pin_timezone(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("TZ", "UTC")
|
||||
time.tzset()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _pin_banner_version(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
|
|
|
|||
90
tests/snapshots/test_ui_snapshot_debug_console.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from textual.pilot import Pilot
|
||||
|
||||
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp
|
||||
from tests.snapshots.snap_compare import SnapCompare
|
||||
from vibe.core.log_reader import LogReader
|
||||
|
||||
_SAMPLE_LOGS = "\n".join([
|
||||
"2026-02-21T10:28:51.100000+00:00 1234 5678 DEBUG Initializing model registry",
|
||||
"2026-02-21T10:28:51.200000+00:00 1234 5678 INFO Server started on port 8080",
|
||||
"2026-02-21T10:28:51.300000+00:00 1234 5678 INFO Loading configuration from /etc/vibe/config.yaml",
|
||||
"2026-02-21T10:28:51.400000+00:00 1234 5678 WARNING Cache miss for key user:42",
|
||||
"2026-02-21T10:28:51.500000+00:00 1234 5678 DEBUG Processing request GET /api/v1/models",
|
||||
"2026-02-21T10:28:51.600000+00:00 1234 5678 INFO Request completed in 45ms",
|
||||
"2026-02-21T10:28:51.700000+00:00 1234 5678 ERROR Connection refused to upstream service",
|
||||
"2026-02-21T10:28:51.800000+00:00 1234 5678 INFO Retrying connection attempt 1/3",
|
||||
"2026-02-21T10:28:51.900000+00:00 1234 5678 WARNING Rate limit approaching for client api-key-abc",
|
||||
"2026-02-21T10:28:52.000000+00:00 1234 5678 INFO Health check passed",
|
||||
])
|
||||
|
||||
_APPENDED_LOG = (
|
||||
"2026-02-21T10:28:53.000000+00:00 1234 5678 CRITICAL Out of memory error detected"
|
||||
)
|
||||
|
||||
# Module-level temp dir so it's available when import_app instantiates the class
|
||||
_TMP_DIR = Path(tempfile.mkdtemp())
|
||||
_LOG_FILE = _TMP_DIR / "test.log"
|
||||
_LOG_FILE.write_text(_SAMPLE_LOGS + "\n")
|
||||
atexit.register(shutil.rmtree, str(_TMP_DIR), True)
|
||||
|
||||
|
||||
class DebugConsoleSnapshotApp(BaseSnapshotTestApp):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._log_reader = LogReader(log_file=_LOG_FILE, poll_interval=0.1)
|
||||
|
||||
|
||||
def test_snapshot_debug_console_open(snap_compare: SnapCompare) -> None:
|
||||
"""Test that the debug console opens and shows log entries."""
|
||||
_LOG_FILE.write_text(_SAMPLE_LOGS + "\n")
|
||||
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await pilot.pause(0.1)
|
||||
await pilot.press("ctrl+backslash")
|
||||
await pilot.pause(0.4)
|
||||
|
||||
assert snap_compare(
|
||||
DebugConsoleSnapshotApp(), terminal_size=(120, 36), run_before=run_before
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_debug_console_live_append(snap_compare: SnapCompare) -> None:
|
||||
"""Test that appending a log line to the file shows the new entry."""
|
||||
_LOG_FILE.write_text(_SAMPLE_LOGS + "\n")
|
||||
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await pilot.pause(0.1)
|
||||
await pilot.press("ctrl+backslash")
|
||||
await pilot.pause(0.4)
|
||||
with _LOG_FILE.open("a") as f:
|
||||
f.write(_APPENDED_LOG + "\n")
|
||||
f.flush()
|
||||
await pilot.pause(0.5)
|
||||
await pilot.pause()
|
||||
|
||||
assert snap_compare(
|
||||
DebugConsoleSnapshotApp(), terminal_size=(120, 36), run_before=run_before
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_debug_console_close(snap_compare: SnapCompare) -> None:
|
||||
"""Test that closing the debug console restores the normal UI."""
|
||||
_LOG_FILE.write_text(_SAMPLE_LOGS + "\n")
|
||||
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await pilot.pause(0.1)
|
||||
await pilot.press("ctrl+backslash")
|
||||
await pilot.pause(0.4)
|
||||
await pilot.press("ctrl+backslash")
|
||||
await pilot.pause(0.2)
|
||||
|
||||
assert snap_compare(
|
||||
DebugConsoleSnapshotApp(), terminal_size=(120, 36), run_before=run_before
|
||||
)
|
||||
159
tests/snapshots/test_ui_snapshot_mcp_command.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
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_mcp_registry import (
|
||||
FakeMCPRegistry,
|
||||
FakeMCPRegistryWithBrokenServer,
|
||||
)
|
||||
from vibe.core.config import MCPHttp, MCPStdio
|
||||
|
||||
_MCP_PATCH = "vibe.core.agent_loop.MCPRegistry"
|
||||
|
||||
|
||||
class SnapshotTestAppNoMcpServers(BaseSnapshotTestApp):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(config=default_config())
|
||||
|
||||
|
||||
class SnapshotTestAppWithBrokenMcpServer(BaseSnapshotTestApp):
|
||||
def __init__(self) -> None:
|
||||
config = default_config()
|
||||
config.mcp_servers = [
|
||||
MCPStdio(name="filesystem", transport="stdio", command="npx"),
|
||||
MCPStdio(
|
||||
name="broken-server", transport="stdio", command="nonexistent-cmd"
|
||||
),
|
||||
MCPHttp(name="search", transport="http", url="http://localhost:8080"),
|
||||
]
|
||||
super().__init__(config=config)
|
||||
|
||||
|
||||
class SnapshotTestAppWithMcpServers(BaseSnapshotTestApp):
|
||||
def __init__(self) -> None:
|
||||
config = default_config()
|
||||
config.mcp_servers = [
|
||||
MCPStdio(name="filesystem", transport="stdio", command="npx"),
|
||||
MCPHttp(name="search", transport="http", url="http://localhost:8080"),
|
||||
]
|
||||
super().__init__(config=config)
|
||||
|
||||
|
||||
async def _run_mcp_command(pilot: Pilot, command: str) -> None:
|
||||
await pilot.pause(0.1)
|
||||
await pilot.press(*command)
|
||||
await pilot.press("enter")
|
||||
await pilot.pause(0.1)
|
||||
pilot.app.set_focus(None)
|
||||
await pilot.pause(0.1)
|
||||
|
||||
|
||||
def test_snapshot_mcp_no_servers(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:SnapshotTestAppNoMcpServers",
|
||||
terminal_size=(120, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_mcp_broken_server(snap_compare: SnapCompare) -> None:
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await _run_mcp_command(pilot, "/mcp")
|
||||
|
||||
with patch(_MCP_PATCH, FakeMCPRegistryWithBrokenServer):
|
||||
assert snap_compare(
|
||||
"test_ui_snapshot_mcp_command.py:SnapshotTestAppWithBrokenMcpServer",
|
||||
terminal_size=(120, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_mcp_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:SnapshotTestAppWithMcpServers",
|
||||
terminal_size=(120, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_mcp_overview_navigate_down(snap_compare: SnapCompare) -> None:
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await _run_mcp_command(pilot, "/mcp")
|
||||
await pilot.press("down")
|
||||
await pilot.pause(0.1)
|
||||
|
||||
with patch(_MCP_PATCH, FakeMCPRegistry):
|
||||
assert snap_compare(
|
||||
"test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers",
|
||||
terminal_size=(120, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_mcp_enter_drills_into_server(snap_compare: SnapCompare) -> None:
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await _run_mcp_command(pilot, "/mcp")
|
||||
await pilot.press("enter")
|
||||
await pilot.pause(0.1)
|
||||
await pilot.press("down")
|
||||
await pilot.pause(0.1)
|
||||
await pilot.press("enter")
|
||||
|
||||
with patch(_MCP_PATCH, FakeMCPRegistry):
|
||||
assert snap_compare(
|
||||
"test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers",
|
||||
terminal_size=(120, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_mcp_server_arg(snap_compare: SnapCompare) -> None:
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await _run_mcp_command(pilot, "/mcp filesystem")
|
||||
await pilot.pause(0.1)
|
||||
|
||||
with patch(_MCP_PATCH, FakeMCPRegistry):
|
||||
assert snap_compare(
|
||||
"test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers",
|
||||
terminal_size=(120, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_mcp_backspace_returns_to_overview(snap_compare: SnapCompare) -> None:
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await _run_mcp_command(pilot, "/mcp filesystem")
|
||||
await pilot.press("backspace")
|
||||
await pilot.pause(0.1)
|
||||
|
||||
with patch(_MCP_PATCH, FakeMCPRegistry):
|
||||
assert snap_compare(
|
||||
"test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers",
|
||||
terminal_size=(120, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_mcp_escape_closes(snap_compare: SnapCompare) -> None:
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await _run_mcp_command(pilot, "/mcp")
|
||||
await pilot.press("escape")
|
||||
await pilot.pause(0.2)
|
||||
|
||||
with patch(_MCP_PATCH, FakeMCPRegistry):
|
||||
assert snap_compare(
|
||||
"test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers",
|
||||
terminal_size=(120, 36),
|
||||
run_before=run_before,
|
||||
)
|
||||
50
tests/stubs/fake_mcp_registry.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from vibe.core.tools.mcp import MCPRegistry
|
||||
from vibe.core.tools.mcp.tools import (
|
||||
RemoteTool,
|
||||
create_mcp_http_proxy_tool_class,
|
||||
create_mcp_stdio_proxy_tool_class,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vibe.core.config import MCPServer
|
||||
from vibe.core.tools.base import BaseTool
|
||||
|
||||
|
||||
_BROKEN_SERVER_NAME = "broken-server"
|
||||
|
||||
|
||||
class FakeMCPRegistry(MCPRegistry):
|
||||
def get_tools(self, servers: list[MCPServer]) -> dict[str, type[BaseTool]]:
|
||||
result: dict[str, type[BaseTool]] = {}
|
||||
for srv in servers:
|
||||
key = self._server_key(srv)
|
||||
if key not in self._cache:
|
||||
remote = RemoteTool(
|
||||
name="fake_tool", description=f"A fake tool for {srv.name}"
|
||||
)
|
||||
match srv.transport:
|
||||
case "stdio":
|
||||
proxy = create_mcp_stdio_proxy_tool_class(
|
||||
command=["fake-cmd"], remote=remote, alias=srv.name
|
||||
)
|
||||
case "http" | "streamable-http":
|
||||
proxy = create_mcp_http_proxy_tool_class(
|
||||
url="http://fake-mcp-server", remote=remote, alias=srv.name
|
||||
)
|
||||
case _:
|
||||
raise ValueError(
|
||||
f"FakeMCPRegistry: unsupported transport {srv.transport!r}"
|
||||
)
|
||||
self._cache[key] = {proxy.get_name(): proxy}
|
||||
result.update(self._cache[key])
|
||||
return result
|
||||
|
||||
|
||||
class FakeMCPRegistryWithBrokenServer(FakeMCPRegistry):
|
||||
def get_tools(self, servers: list[MCPServer]) -> dict[str, type[BaseTool]]:
|
||||
working = [s for s in servers if s.name != _BROKEN_SERVER_NAME]
|
||||
return super().get_tools(working)
|
||||
111
tests/test_agent_override_resolve_permission.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from tests.conftest import build_test_agent_loop, build_test_vibe_config
|
||||
from vibe.core.agents.models import BuiltinAgentName
|
||||
from vibe.core.paths import PLANS_DIR
|
||||
from vibe.core.tools.base import ToolPermission
|
||||
|
||||
|
||||
class TestPlanAgentWriteFileResolvePermission:
|
||||
"""Plan agent sets write_file to NEVER with allowlist=[plans/*].
|
||||
resolve_permission must use this, not the base config.
|
||||
"""
|
||||
|
||||
def test_write_file_to_non_plan_path_denied_in_plan_mode(self) -> None:
|
||||
config = build_test_vibe_config()
|
||||
agent = build_test_agent_loop(config=config, agent_name=BuiltinAgentName.PLAN)
|
||||
|
||||
tool = agent.tool_manager.get("write_file")
|
||||
from vibe.core.tools.builtins.write_file import WriteFileArgs
|
||||
|
||||
args = WriteFileArgs(path="/some/random/file.py", content="hello")
|
||||
|
||||
ctx = tool.resolve_permission(args)
|
||||
|
||||
# With plan agent override: permission should be NEVER
|
||||
# (unless the path matches the plans allowlist)
|
||||
assert ctx is not None
|
||||
assert ctx.permission == ToolPermission.NEVER
|
||||
|
||||
def test_write_file_to_plan_path_allowed_in_plan_mode(self) -> None:
|
||||
config = build_test_vibe_config()
|
||||
agent = build_test_agent_loop(config=config, agent_name=BuiltinAgentName.PLAN)
|
||||
|
||||
tool = agent.tool_manager.get("write_file")
|
||||
from vibe.core.tools.builtins.write_file import WriteFileArgs
|
||||
|
||||
plan_path = str(PLANS_DIR.path / "my-plan.md")
|
||||
args = WriteFileArgs(path=plan_path, content="# Plan")
|
||||
|
||||
ctx = tool.resolve_permission(args)
|
||||
|
||||
# Plan path is in the allowlist, so should be ALWAYS
|
||||
assert ctx is not None
|
||||
assert ctx.permission == ToolPermission.ALWAYS
|
||||
|
||||
def test_search_replace_to_non_plan_path_denied_in_plan_mode(self) -> None:
|
||||
config = build_test_vibe_config()
|
||||
agent = build_test_agent_loop(config=config, agent_name=BuiltinAgentName.PLAN)
|
||||
|
||||
tool = agent.tool_manager.get("search_replace")
|
||||
from vibe.core.tools.builtins.search_replace import SearchReplaceArgs
|
||||
|
||||
args = SearchReplaceArgs(
|
||||
file_path="/some/file.py", content="<<<< SEARCH\na\n====\nb\n>>>> REPLACE"
|
||||
)
|
||||
|
||||
ctx = tool.resolve_permission(args)
|
||||
|
||||
assert ctx is not None
|
||||
assert ctx.permission == ToolPermission.NEVER
|
||||
|
||||
|
||||
class TestAcceptEditsAgentResolvePermission:
|
||||
"""Accept-edits agent sets write_file/search_replace to ALWAYS.
|
||||
resolve_permission must reflect this.
|
||||
"""
|
||||
|
||||
def test_write_file_always_in_accept_edits_mode(self) -> None:
|
||||
config = build_test_vibe_config()
|
||||
agent = build_test_agent_loop(
|
||||
config=config, agent_name=BuiltinAgentName.ACCEPT_EDITS
|
||||
)
|
||||
|
||||
tool = agent.tool_manager.get("write_file")
|
||||
from vibe.core.tools.builtins.write_file import WriteFileArgs
|
||||
|
||||
# Use a workdir-relative path; outside-workdir always requires ASK
|
||||
# regardless of agent permission.
|
||||
args = WriteFileArgs(path="file.py", content="hello")
|
||||
|
||||
ctx = tool.resolve_permission(args)
|
||||
|
||||
# Inside workdir, no allowlist/denylist/sensitive match → None,
|
||||
# so the caller falls through to config permission (ALWAYS).
|
||||
assert ctx is None
|
||||
|
||||
|
||||
class TestAgentOverrideNotLeakedAcrossSwitches:
|
||||
"""Switching agents must change what resolve_permission returns."""
|
||||
|
||||
def test_switch_from_plan_to_default_restores_write_permission(self) -> None:
|
||||
config = build_test_vibe_config()
|
||||
agent = build_test_agent_loop(config=config, agent_name=BuiltinAgentName.PLAN)
|
||||
|
||||
tool = agent.tool_manager.get("write_file")
|
||||
from vibe.core.tools.builtins.write_file import WriteFileArgs
|
||||
|
||||
args = WriteFileArgs(path="/some/file.py", content="hello")
|
||||
|
||||
# In plan mode: should be NEVER
|
||||
ctx_plan = tool.resolve_permission(args)
|
||||
assert ctx_plan is not None
|
||||
assert ctx_plan.permission == ToolPermission.NEVER
|
||||
|
||||
# Switch to default
|
||||
agent.agent_manager.switch_profile(BuiltinAgentName.DEFAULT)
|
||||
|
||||
# In default mode: should NOT be NEVER
|
||||
ctx_default = tool.resolve_permission(args)
|
||||
assert ctx_default is not None
|
||||
assert ctx_default.permission != ToolPermission.NEVER
|
||||
|
|
@ -613,8 +613,8 @@ async def test_parallel_tool_calls_with_approval_callback(
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parallel_approvals_can_run_concurrently() -> None:
|
||||
"""The core does not serialize approval callbacks — that is a CLI-layer concern.
|
||||
Parallel tool calls may invoke the approval callback concurrently.
|
||||
"""Approval callbacks are serialized by _approval_lock so that an 'always allow'
|
||||
grant from the first call is visible to subsequent parallel calls.
|
||||
"""
|
||||
concurrency = 0
|
||||
max_concurrency = 0
|
||||
|
|
@ -642,7 +642,7 @@ async def test_parallel_approvals_can_run_concurrently() -> None:
|
|||
|
||||
await act_and_collect_events(agent_loop, "Go")
|
||||
|
||||
assert max_concurrency > 1
|
||||
assert max_concurrency == 1
|
||||
assert agent_loop.stats.tool_calls_agreed == 3
|
||||
assert agent_loop.stats.tool_calls_succeeded == 3
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests import TESTS_ROOT
|
||||
from tests.conftest import build_test_agent_loop, build_test_vibe_config
|
||||
from tests.stubs.fake_backend import FakeBackend
|
||||
from vibe.core.agents.manager import AgentManager
|
||||
|
|
@ -656,3 +658,22 @@ class TestAgentLoopInitialization:
|
|||
f"System message should contain custom prompt content. "
|
||||
f"Expected '{custom_prompt_content}' to be in system message."
|
||||
)
|
||||
|
||||
|
||||
class TestActConsumersUseAclosing:
|
||||
def test_no_bare_async_for_over_act(self) -> None:
|
||||
vibe_pkg = TESTS_ROOT.parent / "vibe"
|
||||
violations: list[str] = []
|
||||
for path in vibe_pkg.rglob("*.py"):
|
||||
tree = ast.parse(path.read_text(), filename=str(path))
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.AsyncFor):
|
||||
continue
|
||||
match node.iter:
|
||||
case ast.Call(func=ast.Attribute(attr="act")):
|
||||
violations.append(f"{path}:{node.lineno}")
|
||||
|
||||
assert not violations, (
|
||||
"Bare `async for ... in .act()` found — wrap in "
|
||||
"contextlib.aclosing(). See issue #569.\n" + "\n".join(violations)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -61,14 +61,15 @@ def test_history_manager_filters_invalid_and_duplicated_entries(tmp_path: Path)
|
|||
assert reloaded.get_previous(current_input="") is None
|
||||
|
||||
|
||||
def test_history_manager_filters_commands(tmp_path: Path) -> None:
|
||||
def test_history_manager_stores_slash_prefixed_entries(tmp_path: Path) -> None:
|
||||
history_file = tmp_path / "history.jsonl"
|
||||
manager = HistoryManager(history_file, max_entries=5)
|
||||
manager.add("first")
|
||||
manager.add("/skip")
|
||||
manager.add("/tool_call arg1 arg2")
|
||||
|
||||
reloaded = HistoryManager(history_file)
|
||||
|
||||
assert reloaded.get_previous(current_input="") == "/tool_call arg1 arg2"
|
||||
assert reloaded.get_previous(current_input="") == "first"
|
||||
assert reloaded.get_previous(current_input="") is None
|
||||
|
||||
|
|
|
|||
198
tests/test_install_script.py
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import stat
|
||||
import subprocess
|
||||
from textwrap import dedent
|
||||
|
||||
INSTALL_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "install.sh"
|
||||
|
||||
_FAKE_VIBE_SCRIPT = """#!/usr/bin/env bash
|
||||
exit 0
|
||||
"""
|
||||
|
||||
_FAKE_UV_SCRIPT = """#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
tool_bin_dir="${UV_TOOL_BIN_DIR:?UV_TOOL_BIN_DIR must be set}"
|
||||
|
||||
case "$*" in
|
||||
"--version")
|
||||
echo "uv 0.test"
|
||||
;;
|
||||
"tool dir --bin")
|
||||
echo "$tool_bin_dir"
|
||||
;;
|
||||
"tool install mistral-vibe"|"tool upgrade mistral-vibe")
|
||||
mkdir -p "$tool_bin_dir"
|
||||
cat >"$tool_bin_dir/vibe" <<'VIBE'
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
VIBE
|
||||
chmod +x "$tool_bin_dir/vibe"
|
||||
cat >"$tool_bin_dir/vibe-acp" <<'VIBE_ACP'
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
VIBE_ACP
|
||||
chmod +x "$tool_bin_dir/vibe-acp"
|
||||
;;
|
||||
*)
|
||||
echo "unexpected uv invocation: $*" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
"""
|
||||
|
||||
|
||||
def _write_executable(path: Path, content: str) -> None:
|
||||
path.write_text(dedent(content))
|
||||
path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||
|
||||
|
||||
def _write_fake_uv(path: Path) -> None:
|
||||
_write_executable(path, _FAKE_UV_SCRIPT)
|
||||
|
||||
|
||||
def _write_fake_vibe(path: Path) -> None:
|
||||
_write_executable(path, _FAKE_VIBE_SCRIPT)
|
||||
|
||||
|
||||
def _write_fake_uv_installer(payload_path: Path) -> None:
|
||||
payload_path.write_text(
|
||||
"#!/bin/sh\n"
|
||||
"set -eu\n"
|
||||
"\n"
|
||||
'mkdir -p "$HOME/.local/bin"\n'
|
||||
"cat >\"$HOME/.local/bin/uv\" <<'UV'\n"
|
||||
f"{_FAKE_UV_SCRIPT}"
|
||||
"UV\n"
|
||||
'chmod +x "$HOME/.local/bin/uv"\n'
|
||||
)
|
||||
|
||||
|
||||
def _write_fake_curl(path: Path, payload_path: Path) -> None:
|
||||
_write_executable(
|
||||
path,
|
||||
f"""\
|
||||
#!/usr/bin/env bash
|
||||
cat {shlex.quote(str(payload_path))}
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
def _run_install_script(
|
||||
home: Path, path_entries: list[Path], extra_env: dict[str, str]
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
env = {
|
||||
"HOME": str(home),
|
||||
"PATH": ":".join([*(str(entry) for entry in path_entries), "/usr/bin", "/bin"]),
|
||||
"TERM": "dumb",
|
||||
**extra_env,
|
||||
}
|
||||
|
||||
return subprocess.run(
|
||||
["bash", str(INSTALL_SCRIPT)],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
cwd=INSTALL_SCRIPT.parent.parent,
|
||||
env=env,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
def test_install_reports_missing_path_for_uv_tool_bin(tmp_path: Path) -> None:
|
||||
home = tmp_path / "home"
|
||||
fake_bin = tmp_path / "fake-bin"
|
||||
home.mkdir()
|
||||
fake_bin.mkdir()
|
||||
|
||||
installer_payload = tmp_path / "fake-uv-installer.sh"
|
||||
_write_fake_uv_installer(installer_payload)
|
||||
_write_fake_curl(fake_bin / "curl", installer_payload)
|
||||
|
||||
uv_bin_dir = home / ".local" / "bin"
|
||||
result = _run_install_script(home, [fake_bin], {"UV_TOOL_BIN_DIR": str(uv_bin_dir)})
|
||||
|
||||
assert result.returncode == 1
|
||||
assert (
|
||||
"Your PATH does not include the folder that contains 'vibe'." in result.stderr
|
||||
)
|
||||
assert f'export PATH="{uv_bin_dir}:$PATH"' in result.stderr
|
||||
assert (
|
||||
result.stderr.count(
|
||||
"Add this directory to your shell profile, then restart your terminal:"
|
||||
)
|
||||
== 1
|
||||
)
|
||||
assert (
|
||||
"uv was installed but not found in PATH for this session" not in result.stdout
|
||||
)
|
||||
|
||||
|
||||
def test_install_succeeds_when_uv_bin_dir_is_already_on_path(tmp_path: Path) -> None:
|
||||
home = tmp_path / "home"
|
||||
fake_bin = tmp_path / "fake-bin"
|
||||
home.mkdir()
|
||||
fake_bin.mkdir()
|
||||
_write_fake_uv(fake_bin / "uv")
|
||||
|
||||
result = _run_install_script(home, [fake_bin], {"UV_TOOL_BIN_DIR": str(fake_bin)})
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "Installation completed successfully!" in result.stdout
|
||||
assert (fake_bin / "vibe").exists()
|
||||
assert (fake_bin / "vibe-acp").exists()
|
||||
|
||||
|
||||
def test_install_fails_when_vibe_not_in_uv_tool_dir(tmp_path: Path) -> None:
|
||||
"""Covers the fallback error when uv tool dir doesn't contain a vibe binary."""
|
||||
home = tmp_path / "home"
|
||||
fake_bin = tmp_path / "fake-bin"
|
||||
home.mkdir()
|
||||
fake_bin.mkdir()
|
||||
|
||||
# Create a fake uv that does NOT produce a vibe binary on install
|
||||
_write_executable(
|
||||
fake_bin / "uv",
|
||||
"""\
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
tool_bin_dir="${UV_TOOL_BIN_DIR:?UV_TOOL_BIN_DIR must be set}"
|
||||
case "$*" in
|
||||
"--version") echo "uv 0.test" ;;
|
||||
"tool dir --bin") echo "$tool_bin_dir" ;;
|
||||
"tool install mistral-vibe") mkdir -p "$tool_bin_dir" ;;
|
||||
*) echo "unexpected: $*" >&2; exit 1 ;;
|
||||
esac
|
||||
""",
|
||||
)
|
||||
|
||||
uv_tool_bin = tmp_path / "uv-tools"
|
||||
uv_tool_bin.mkdir()
|
||||
result = _run_install_script(
|
||||
home, [fake_bin], {"UV_TOOL_BIN_DIR": str(uv_tool_bin)}
|
||||
)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "uv did not expose a 'vibe' executable" in result.stderr
|
||||
assert "Your PATH does not include" not in result.stderr
|
||||
|
||||
|
||||
def test_update_succeeds_when_vibe_is_already_on_path(tmp_path: Path) -> None:
|
||||
home = tmp_path / "home"
|
||||
fake_bin = tmp_path / "fake-bin"
|
||||
home.mkdir()
|
||||
fake_bin.mkdir()
|
||||
_write_fake_uv(fake_bin / "uv")
|
||||
_write_fake_vibe(fake_bin / "vibe")
|
||||
|
||||
result = _run_install_script(home, [fake_bin], {"UV_TOOL_BIN_DIR": str(fake_bin)})
|
||||
|
||||
assert result.returncode == 0
|
||||
assert "Updating mistral-vibe from GitHub repository using uv..." in result.stdout
|
||||
assert (
|
||||
"Installing mistral-vibe from GitHub repository using uv..."
|
||||
not in result.stdout
|
||||
)
|
||||
|
|
@ -17,7 +17,7 @@ from tests.conftest import build_test_agent_loop, build_test_vibe_config
|
|||
from tests.mock.utils import mock_llm_chunk
|
||||
from tests.stubs.fake_backend import FakeBackend
|
||||
from vibe.core import tracing
|
||||
from vibe.core.config import OtelExporterConfig
|
||||
from vibe.core.config import OtelSpanExporterConfig
|
||||
from vibe.core.tools.base import BaseToolConfig, ToolPermission
|
||||
from vibe.core.tracing import agent_span, setup_tracing, tool_span
|
||||
from vibe.core.types import BaseEvent, FunctionCall, ToolCall
|
||||
|
|
@ -54,7 +54,7 @@ class TestSetupTracing:
|
|||
mock_set.assert_not_called()
|
||||
|
||||
def test_noop_when_exporter_config_is_none(self) -> None:
|
||||
config = MagicMock(enable_otel=True, otel_exporter_config=None)
|
||||
config = MagicMock(enable_otel=True, otel_span_exporter_config=None)
|
||||
with patch("vibe.core.tracing.trace.set_tracer_provider") as mock_set:
|
||||
setup_tracing(config)
|
||||
mock_set.assert_not_called()
|
||||
|
|
@ -62,7 +62,7 @@ class TestSetupTracing:
|
|||
def test_configures_provider_from_exporter_config(self) -> None:
|
||||
config = MagicMock(
|
||||
enable_otel=True,
|
||||
otel_exporter_config=OtelExporterConfig(
|
||||
otel_span_exporter_config=OtelSpanExporterConfig(
|
||||
endpoint="https://customer.mistral.ai/telemetry/v1/traces",
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
),
|
||||
|
|
@ -86,7 +86,7 @@ class TestSetupTracing:
|
|||
def test_custom_endpoint_has_no_auth_headers(self) -> None:
|
||||
config = MagicMock(
|
||||
enable_otel=True,
|
||||
otel_exporter_config=OtelExporterConfig(
|
||||
otel_span_exporter_config=OtelSpanExporterConfig(
|
||||
endpoint="https://my-collector:4318/v1/traces"
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from vibe.core.types import ToolCallEvent, ToolResultEvent
|
|||
@pytest.fixture
|
||||
def tool():
|
||||
config = AskUserQuestionConfig()
|
||||
return AskUserQuestion(config=config, state=BaseToolState())
|
||||
return AskUserQuestion(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from vibe.core.tools.permissions import PermissionContext
|
|||
def bash(tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
config = BashToolConfig()
|
||||
return Bash(config=config, state=BaseToolState())
|
||||
return Bash(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -39,7 +39,7 @@ async def test_fails_cat_command_with_missing_file(bash):
|
|||
async def test_uses_effective_workdir(tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
config = BashToolConfig()
|
||||
bash_tool = Bash(config=config, state=BaseToolState())
|
||||
bash_tool = Bash(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
result = await collect_result(bash_tool.run(BashArgs(command="pwd")))
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ async def test_handles_timeout(bash):
|
|||
@pytest.mark.asyncio
|
||||
async def test_truncates_output_to_max_bytes(bash):
|
||||
config = BashToolConfig(max_output_bytes=5)
|
||||
bash_tool = Bash(config=config, state=BaseToolState())
|
||||
bash_tool = Bash(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
result = await collect_result(
|
||||
bash_tool.run(BashArgs(command="printf 'abcdefghij'"))
|
||||
|
|
@ -78,7 +78,7 @@ async def test_decodes_non_utf8_bytes(bash):
|
|||
|
||||
|
||||
def test_find_not_in_default_allowlist():
|
||||
bash_tool = Bash(config=BashToolConfig(), state=BaseToolState())
|
||||
bash_tool = Bash(config_getter=lambda: BashToolConfig(), state=BaseToolState())
|
||||
# find -exec runs arbitrary commands; must not be allowlisted by default
|
||||
permission = bash_tool.resolve_permission(BashArgs(command="find . -exec id \\;"))
|
||||
assert (
|
||||
|
|
@ -89,7 +89,7 @@ def test_find_not_in_default_allowlist():
|
|||
|
||||
def test_resolve_permission():
|
||||
config = BashToolConfig(allowlist=["echo", "pwd"], denylist=["rm"])
|
||||
bash_tool = Bash(config=config, state=BaseToolState())
|
||||
bash_tool = Bash(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
allowlisted = bash_tool.resolve_permission(BashArgs(command="echo hi"))
|
||||
denylisted = bash_tool.resolve_permission(BashArgs(command="rm -rf /tmp"))
|
||||
|
|
@ -111,7 +111,7 @@ class TestResolvePermissionWindowsSyntax:
|
|||
|
||||
def _make_bash(self, **kwargs) -> Bash:
|
||||
config = BashToolConfig(**kwargs)
|
||||
return Bash(config=config, state=BaseToolState())
|
||||
return Bash(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
def test_dir_with_windows_flags_allowlisted(self):
|
||||
bash_tool = self._make_bash(allowlist=["dir"])
|
||||
|
|
@ -215,7 +215,7 @@ class TestDenylistWordBoundary:
|
|||
|
||||
def _make_bash(self, **kwargs) -> Bash:
|
||||
config = BashToolConfig(**kwargs)
|
||||
return Bash(config=config, state=BaseToolState())
|
||||
return Bash(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
def test_vi_blocks_vi_exact(self):
|
||||
bash_tool = self._make_bash(denylist=["vi"])
|
||||
|
|
|
|||
|
|
@ -56,7 +56,9 @@ def _default_profile() -> AgentProfile:
|
|||
|
||||
@pytest.fixture
|
||||
def tool() -> ExitPlanMode:
|
||||
return ExitPlanMode(config=ExitPlanModeConfig(), state=BaseToolState())
|
||||
return ExitPlanMode(
|
||||
config_getter=lambda: ExitPlanModeConfig(), state=BaseToolState()
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class TestBashGranularPermissions:
|
|||
|
||||
def _bash(self, **kwargs):
|
||||
config = BashToolConfig(**kwargs)
|
||||
return Bash(config=config, state=BaseToolState())
|
||||
return Bash(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
def test_allowlisted_command_always(self):
|
||||
bash = self._bash()
|
||||
|
|
@ -264,7 +264,7 @@ class TestReadFileGranularPermissions:
|
|||
|
||||
def _read_file(self, **kwargs):
|
||||
config = ReadFileToolConfig(**kwargs)
|
||||
return ReadFile(config=config, state=ReadFileState())
|
||||
return ReadFile(config_getter=lambda: config, state=ReadFileState())
|
||||
|
||||
def test_in_workdir_normal_file_returns_none(self):
|
||||
(self.workdir / "test.py").touch()
|
||||
|
|
@ -346,7 +346,7 @@ class TestWriteFileGranularPermissions:
|
|||
|
||||
def _write_file(self):
|
||||
config = WriteFileConfig()
|
||||
return WriteFile(config=config, state=BaseToolState())
|
||||
return WriteFile(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
def test_in_workdir_returns_none(self):
|
||||
tool = self._write_file()
|
||||
|
|
@ -379,7 +379,7 @@ class TestSearchReplaceGranularPermissions:
|
|||
|
||||
def test_outside_workdir_returns_permission_context(self):
|
||||
config = SearchReplaceConfig()
|
||||
tool = SearchReplace(config=config, state=BaseToolState())
|
||||
tool = SearchReplace(config_getter=lambda: config, state=BaseToolState())
|
||||
result = tool.resolve_permission(
|
||||
SearchReplaceArgs(file_path="/tmp/file.py", content="x")
|
||||
)
|
||||
|
|
@ -395,7 +395,7 @@ class TestGrepGranularPermissions:
|
|||
|
||||
def _grep(self):
|
||||
config = GrepToolConfig()
|
||||
return Grep(config=config, state=BaseToolState())
|
||||
return Grep(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
def test_in_workdir_normal_path_returns_none(self):
|
||||
tool = self._grep()
|
||||
|
|
@ -442,7 +442,7 @@ class TestApprovalFlowSimulation:
|
|||
session_pattern="mkdir *",
|
||||
)
|
||||
]
|
||||
bash = Bash(config=BashToolConfig(), state=BaseToolState())
|
||||
bash = Bash(config_getter=lambda: BashToolConfig(), state=BaseToolState())
|
||||
result = bash.resolve_permission(BashArgs(command="mkdir another_dir"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
uncovered = [
|
||||
|
|
@ -460,7 +460,7 @@ class TestApprovalFlowSimulation:
|
|||
session_pattern="mkdir *",
|
||||
)
|
||||
]
|
||||
bash = Bash(config=BashToolConfig(), state=BaseToolState())
|
||||
bash = Bash(config_getter=lambda: BashToolConfig(), state=BaseToolState())
|
||||
result = bash.resolve_permission(BashArgs(command="npm install"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
uncovered = [
|
||||
|
|
@ -472,7 +472,7 @@ class TestApprovalFlowSimulation:
|
|||
assert uncovered[0].session_pattern == "npm install *"
|
||||
|
||||
def test_outside_dir_approved_covers_subsequent(self):
|
||||
bash = Bash(config=BashToolConfig(), state=BaseToolState())
|
||||
bash = Bash(config_getter=lambda: BashToolConfig(), state=BaseToolState())
|
||||
result = bash.resolve_permission(BashArgs(command="mkdir /tmp/newdir"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
outside_rps = [
|
||||
|
|
@ -509,7 +509,7 @@ class TestApprovalFlowSimulation:
|
|||
session_pattern="rm *",
|
||||
)
|
||||
]
|
||||
bash = Bash(config=BashToolConfig(), state=BaseToolState())
|
||||
bash = Bash(config_getter=lambda: BashToolConfig(), state=BaseToolState())
|
||||
result = bash.resolve_permission(BashArgs(command="rm -rf /tmp/something"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
cmd_perms = [
|
||||
|
|
@ -529,7 +529,7 @@ class TestApprovalFlowSimulation:
|
|||
session_pattern="sudo apt install foo",
|
||||
)
|
||||
]
|
||||
bash = Bash(config=BashToolConfig(), state=BaseToolState())
|
||||
bash = Bash(config_getter=lambda: BashToolConfig(), state=BaseToolState())
|
||||
result = bash.resolve_permission(BashArgs(command="sudo apt install bar"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
cmd_perms = [
|
||||
|
|
@ -575,7 +575,7 @@ class TestApprovalFlowSimulation:
|
|||
|
||||
class TestWebFetchPermissions:
|
||||
def _make_webfetch(self) -> WebFetch:
|
||||
return WebFetch(config=WebFetchConfig(), state=BaseToolState())
|
||||
return WebFetch(config_getter=lambda: WebFetchConfig(), state=BaseToolState())
|
||||
|
||||
def test_returns_url_pattern_with_domain(self):
|
||||
wf = self._make_webfetch()
|
||||
|
|
@ -671,7 +671,7 @@ class TestWebFetchPermissions:
|
|||
|
||||
def test_config_permission_always_honored(self):
|
||||
wf = WebFetch(
|
||||
config=WebFetchConfig(permission=ToolPermission.ALWAYS),
|
||||
config_getter=lambda: WebFetchConfig(permission=ToolPermission.ALWAYS),
|
||||
state=BaseToolState(),
|
||||
)
|
||||
result = wf.resolve_permission(WebFetchArgs(url="https://example.com"))
|
||||
|
|
@ -680,7 +680,7 @@ class TestWebFetchPermissions:
|
|||
|
||||
def test_config_permission_never_honored(self):
|
||||
wf = WebFetch(
|
||||
config=WebFetchConfig(permission=ToolPermission.NEVER),
|
||||
config_getter=lambda: WebFetchConfig(permission=ToolPermission.NEVER),
|
||||
state=BaseToolState(),
|
||||
)
|
||||
result = wf.resolve_permission(WebFetchArgs(url="https://example.com"))
|
||||
|
|
@ -689,7 +689,8 @@ class TestWebFetchPermissions:
|
|||
|
||||
def test_config_permission_ask_falls_through_to_domain(self):
|
||||
wf = WebFetch(
|
||||
config=WebFetchConfig(permission=ToolPermission.ASK), state=BaseToolState()
|
||||
config_getter=lambda: WebFetchConfig(permission=ToolPermission.ASK),
|
||||
state=BaseToolState(),
|
||||
)
|
||||
result = wf.resolve_permission(WebFetchArgs(url="https://example.com"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from vibe.core.tools.builtins.grep import Grep, GrepArgs, GrepBackend, GrepToolC
|
|||
def grep(tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
config = GrepToolConfig()
|
||||
return Grep(config=config, state=BaseToolState())
|
||||
return Grep(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -28,7 +28,7 @@ def grep_gnu_only(tmp_path, monkeypatch):
|
|||
|
||||
monkeypatch.setattr("shutil.which", mock_which)
|
||||
config = GrepToolConfig()
|
||||
return Grep(config=config, state=BaseToolState())
|
||||
return Grep(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
|
||||
def test_detects_ripgrep_when_available(grep):
|
||||
|
|
@ -137,7 +137,7 @@ async def test_truncates_to_max_matches(grep, tmp_path):
|
|||
async def test_truncates_to_max_output_bytes(grep, tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
config = GrepToolConfig(max_output_bytes=100)
|
||||
grep_tool = Grep(config=config, state=BaseToolState())
|
||||
grep_tool = Grep(config_getter=lambda: config, state=BaseToolState())
|
||||
(tmp_path / "test.py").write_text("\n".join("x" * 100 for _ in range(10)))
|
||||
|
||||
result = await collect_result(grep_tool.run(GrepArgs(pattern="x")))
|
||||
|
|
@ -189,7 +189,7 @@ async def test_ignores_comments_in_vibeignore(grep, tmp_path):
|
|||
async def test_uses_effective_workdir(tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
config = GrepToolConfig()
|
||||
grep_tool = Grep(config=config, state=BaseToolState())
|
||||
grep_tool = Grep(config_getter=lambda: config, state=BaseToolState())
|
||||
(tmp_path / "test.py").write_text("match\n")
|
||||
|
||||
result = await collect_result(grep_tool.run(GrepArgs(pattern="match", path=".")))
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class SimpleTool(BaseTool[SimpleArgs, SimpleResult, BaseToolConfig, BaseToolStat
|
|||
|
||||
@pytest.fixture
|
||||
def simple_tool() -> SimpleTool:
|
||||
return SimpleTool(config=BaseToolConfig(), state=BaseToolState())
|
||||
return SimpleTool(config_getter=lambda: BaseToolConfig(), state=BaseToolState())
|
||||
|
||||
|
||||
class TestInvokeContext:
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ def _make_ctx(skill_manager: SkillManager | None = None) -> InvokeContext:
|
|||
|
||||
@pytest.fixture
|
||||
def skill_tool() -> Skill:
|
||||
return Skill(config=SkillToolConfig(), state=BaseToolState())
|
||||
return Skill(config_getter=lambda: SkillToolConfig(), state=BaseToolState())
|
||||
|
||||
|
||||
class TestSkillRun:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from vibe.core.types import AssistantEvent, LLMMessage, Role
|
|||
|
||||
@pytest.fixture
|
||||
def task_tool() -> Task:
|
||||
return Task(config=TaskToolConfig(), state=BaseToolState())
|
||||
return Task(config_getter=lambda: TaskToolConfig(), state=BaseToolState())
|
||||
|
||||
|
||||
class TestTaskArgs:
|
||||
|
|
@ -91,7 +91,7 @@ class TestTaskToolResolvePermission:
|
|||
|
||||
def test_denylist_takes_precedence(self) -> None:
|
||||
config = TaskToolConfig(allowlist=["explore"], denylist=["explore"])
|
||||
tool = Task(config=config, state=BaseToolState())
|
||||
tool = Task(config_getter=lambda: config, state=BaseToolState())
|
||||
args = TaskArgs(task="do something", agent="explore")
|
||||
result = tool.resolve_permission(args)
|
||||
assert isinstance(result, PermissionContext)
|
||||
|
|
@ -99,7 +99,7 @@ class TestTaskToolResolvePermission:
|
|||
|
||||
def test_glob_pattern_in_allowlist(self) -> None:
|
||||
config = TaskToolConfig(allowlist=["exp*"])
|
||||
tool = Task(config=config, state=BaseToolState())
|
||||
tool = Task(config_getter=lambda: config, state=BaseToolState())
|
||||
args = TaskArgs(task="do something", agent="explore")
|
||||
result = tool.resolve_permission(args)
|
||||
assert isinstance(result, PermissionContext)
|
||||
|
|
@ -107,7 +107,7 @@ class TestTaskToolResolvePermission:
|
|||
|
||||
def test_glob_pattern_in_denylist(self) -> None:
|
||||
config = TaskToolConfig(denylist=["danger*"])
|
||||
tool = Task(config=config, state=BaseToolState())
|
||||
tool = Task(config_getter=lambda: config, state=BaseToolState())
|
||||
args = TaskArgs(task="do something", agent="dangerous_agent")
|
||||
result = tool.resolve_permission(args)
|
||||
assert isinstance(result, PermissionContext)
|
||||
|
|
@ -115,7 +115,7 @@ class TestTaskToolResolvePermission:
|
|||
|
||||
def test_empty_lists_returns_none(self) -> None:
|
||||
config = TaskToolConfig(allowlist=[], denylist=[])
|
||||
tool = Task(config=config, state=BaseToolState())
|
||||
tool = Task(config_getter=lambda: config, state=BaseToolState())
|
||||
args = TaskArgs(task="do something", agent="explore")
|
||||
result = tool.resolve_permission(args)
|
||||
assert result is None
|
||||
|
|
|
|||
|
|
@ -5,9 +5,13 @@ import time
|
|||
import pytest
|
||||
from textual.widgets import Static
|
||||
|
||||
from tests.conftest import build_test_agent_loop, build_test_vibe_app
|
||||
from tests.mock.utils import mock_llm_chunk
|
||||
from tests.stubs.fake_backend import FakeBackend
|
||||
from vibe.cli.textual_ui.app import VibeApp
|
||||
from vibe.cli.textual_ui.widgets.chat_input.container import ChatInputContainer
|
||||
from vibe.cli.textual_ui.widgets.messages import BashOutputMessage, ErrorMessage
|
||||
from vibe.core.types import Role
|
||||
|
||||
|
||||
async def _wait_for_bash_output_message(
|
||||
|
|
@ -118,3 +122,38 @@ async def test_ui_handles_non_utf8_stderr(vibe_app: VibeApp) -> None:
|
|||
output_widget = message.query_one(".bash-output", Static)
|
||||
assert str(output_widget.render()) == "<EFBFBD><EFBFBD>"
|
||||
assert_no_command_error(vibe_app)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_sends_manual_command_output_to_next_agent_turn() -> None:
|
||||
backend = FakeBackend(mock_llm_chunk(content="I saw it."))
|
||||
vibe_app = build_test_vibe_app(agent_loop=build_test_agent_loop(backend=backend))
|
||||
|
||||
async with vibe_app.run_test() as pilot:
|
||||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||
chat_input.value = "!echo hello"
|
||||
|
||||
await pilot.press("enter")
|
||||
await _wait_for_bash_output_message(vibe_app, pilot)
|
||||
|
||||
injected_message = vibe_app.agent_loop.messages[-1]
|
||||
assert injected_message.role == Role.user
|
||||
assert injected_message.injected is True
|
||||
assert injected_message.content is not None
|
||||
assert "Manual `!` command result from the user." in injected_message.content
|
||||
assert "Command: `echo hello`" in injected_message.content
|
||||
assert "Exit code: 0" in injected_message.content
|
||||
assert "Stdout:\n```text\nhello\n```" in injected_message.content
|
||||
|
||||
chat_input.value = "what did the command print?"
|
||||
await pilot.press("enter")
|
||||
await pilot.app.workers.wait_for_complete()
|
||||
|
||||
assert len(backend.requests_messages) == 1
|
||||
user_messages = [
|
||||
msg for msg in backend.requests_messages[0] if msg.role == Role.user
|
||||
]
|
||||
assert len(user_messages) >= 2
|
||||
assert user_messages[-2].content == injected_message.content
|
||||
assert user_messages[-2].injected is True
|
||||
assert user_messages[-1].content == "what did the command print?"
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ from vibe.core.tools.builtins.webfetch import WebFetch, WebFetchArgs, WebFetchCo
|
|||
@pytest.fixture
|
||||
def webfetch():
|
||||
config = WebFetchConfig()
|
||||
return WebFetch(config=config, state=BaseToolState())
|
||||
return WebFetch(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def webfetch_small():
|
||||
config = WebFetchConfig(max_content_bytes=100)
|
||||
return WebFetch(config=config, state=BaseToolState())
|
||||
return WebFetch(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -32,6 +32,7 @@ async def test_bare_domain_gets_https(webfetch):
|
|||
result = await collect_result(webfetch.run(WebFetchArgs(url="example.com")))
|
||||
assert result.url == "https://example.com"
|
||||
assert result.content == "ok"
|
||||
assert result.was_truncated is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -167,6 +168,7 @@ async def test_truncates_to_max_bytes_with_disclaimer(webfetch_small):
|
|||
)
|
||||
assert result.content.startswith("a" * 100)
|
||||
assert "[Content truncated due to size limit]" in result.content
|
||||
assert result.was_truncated is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -189,6 +191,7 @@ async def test_truncates_html_with_disclaimer(webfetch_small):
|
|||
assert "## first title" in result.content
|
||||
assert "## second title" not in result.content
|
||||
assert "[Content truncated due to size limit]" in result.content
|
||||
assert result.was_truncated is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ def _make_response(
|
|||
def websearch(monkeypatch):
|
||||
monkeypatch.setenv("MISTRAL_API_KEY", "test-key")
|
||||
config = WebSearchConfig()
|
||||
return WebSearch(config=config, state=BaseToolState())
|
||||
return WebSearch(config_getter=lambda: config, state=BaseToolState())
|
||||
|
||||
|
||||
def test_parse_text_chunks(websearch):
|
||||
|
|
@ -104,7 +104,7 @@ def test_parse_skips_non_message_entries(websearch):
|
|||
async def test_run_missing_api_key(monkeypatch):
|
||||
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
|
||||
config = WebSearchConfig()
|
||||
ws = WebSearch(config=config, state=BaseToolState())
|
||||
ws = WebSearch(config_getter=lambda: config, state=BaseToolState())
|
||||
with pytest.raises(ToolError, match="MISTRAL_API_KEY"):
|
||||
await collect_result(ws.run(WebSearchArgs(query="test")))
|
||||
|
||||
|
|
|
|||