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>
3
.gitignore
vendored
@@ -199,3 +199,6 @@ result-*
|
||||
# Tests run the agent in the playground, we don't need to keep the session files
|
||||
tests/playground/*
|
||||
.
|
||||
|
||||
# Profiler HTML reports (generated by vibe.cli.profiler)
|
||||
*-profile.html
|
||||
|
||||
2
.vscode/launch.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.7.3",
|
||||
"version": "2.7.4",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ACP Server",
|
||||
|
||||
30
CHANGELOG.md
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.7.4] - 2026-04-09
|
||||
|
||||
### Added
|
||||
|
||||
- Console View for enhanced debugging and monitoring
|
||||
- `/mcp` command to display MCP servers and their status
|
||||
- Manual command output forwarding to agent context
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved web_fetch content truncation for better readability
|
||||
- Lazily load heavy dependencies to improve startup time
|
||||
- Optimized folder parsing at startup using scandir
|
||||
- Include file name in search_replace result display
|
||||
|
||||
### Fixed
|
||||
|
||||
- Stale configurations from subagent switch
|
||||
- ValueError on OTEL context detach in agent_span
|
||||
- Clipboard toast preview replaced with fixed text
|
||||
- Only agents with type "agent" are loadable with --agent flag
|
||||
- Made chat_url nullable in ChatAssistantPublicData
|
||||
- Normalized OTEL span exporter endpoint
|
||||
- Removed redundant permission prompts for parallel tool calls needing the same permission
|
||||
- Removed bottom margin issue in UI
|
||||
- Never crash before ACP server starts
|
||||
- Use skill in recent commands via the up-arrow navigation
|
||||
- Fixed loading order issues in vibe initialization
|
||||
|
||||
|
||||
## [2.7.3] - 2026-04-03
|
||||
|
||||
### Added
|
||||
|
||||
@@ -88,6 +88,8 @@ Example:
|
||||
LOG_LEVEL=DEBUG uv run vibe
|
||||
```
|
||||
|
||||
You can also view logs in real-time within the application by pressing `Ctrl+\` to open the debug console.
|
||||
|
||||
### Running Tests
|
||||
|
||||
Run all tests:
|
||||
@@ -108,6 +110,30 @@ Run a specific test file:
|
||||
uv run pytest tests/test_agent_tool_call.py
|
||||
```
|
||||
|
||||
### Profiling
|
||||
|
||||
Vibe ships a lightweight profiler module (`vibe.cli.profiler`) that wraps [pyinstrument](https://github.com/joerick/pyinstrument). It silently no-ops when pyinstrument is not installed or the `VIBE_PROFILE` env var is unset, so instrumentation can stay in the code with zero overhead in normal use.
|
||||
|
||||
**1. Add profiling calls** around the code you want to measure:
|
||||
|
||||
```python
|
||||
from vibe.cli import profiler
|
||||
|
||||
profiler.start("startup")
|
||||
# ... code to measure ...
|
||||
profiler.stop_and_print()
|
||||
```
|
||||
|
||||
Only one profiler can run at a time. The label passed to `start()` is used to name the output file. Each call to `stop_and_print` writes an HTML report (`<label>-profile.html`) and prints a text summary to stderr.
|
||||
|
||||
**2. Run with profiling enabled:**
|
||||
|
||||
```bash
|
||||
VIBE_PROFILE=1 uv run vibe
|
||||
```
|
||||
|
||||
Without `VIBE_PROFILE=1`, all `start` / `stop_and_print` calls are no-ops.
|
||||
|
||||
### Linting and Type Checking
|
||||
|
||||
#### Ruff (Linting and Formatting)
|
||||
|
||||
@@ -218,6 +218,7 @@ Simply run `vibe` to enter the interactive chat loop.
|
||||
- **External Editor**: Press `Ctrl+G` to edit your current input in an external editor.
|
||||
- **Tool Output Toggle**: Press `Ctrl+O` to toggle the tool output view.
|
||||
- **Todo View Toggle**: Press `Ctrl+T` to toggle the todo list view.
|
||||
- **Debug Console**: Press `Ctrl+\` to toggle the debug console.
|
||||
- **Auto-Approve Toggle**: Press `Shift+Tab` to toggle auto-approve mode on/off.
|
||||
|
||||
You can start Vibe with a prompt using the following command:
|
||||
@@ -636,6 +637,7 @@ Mistral Vibe can be used in text editors and IDEs that support [Agent Client Pro
|
||||
|
||||
Use of Vibe is subject to our [Privacy Policy](https://legal.mistral.ai/terms/privacy-policy) and may include the collection and processing of data related to your use of the service, such as usage data, to operate, maintain, and improve Vibe. You can disable telemetry in your `config.toml` by setting `enable_telemetry = false`.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2025 Mistral AI
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "mistral-vibe"
|
||||
name = "Mistral Vibe"
|
||||
description = "Mistral's open-source coding assistant"
|
||||
version = "2.7.3"
|
||||
version = "2.7.4"
|
||||
schema_version = 1
|
||||
authors = ["Mistral AI"]
|
||||
repository = "https://github.com/mistralai/mistral-vibe"
|
||||
@@ -11,25 +11,25 @@ name = "Mistral Vibe"
|
||||
icon = "./icons/mistral_vibe.svg"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.darwin-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.7.3/vibe-acp-darwin-aarch64-2.7.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.7.4/vibe-acp-darwin-aarch64-2.7.4.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.darwin-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.7.3/vibe-acp-darwin-x86_64-2.7.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.7.4/vibe-acp-darwin-x86_64-2.7.4.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.linux-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.7.3/vibe-acp-linux-aarch64-2.7.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.7.4/vibe-acp-linux-aarch64-2.7.4.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.linux-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.7.3/vibe-acp-linux-x86_64-2.7.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.7.4/vibe-acp-linux-x86_64-2.7.4.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.windows-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.7.3/vibe-acp-windows-aarch64-2.7.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.7.4/vibe-acp-windows-aarch64-2.7.4.zip"
|
||||
cmd = "./vibe-acp.exe"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.windows-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.7.3/vibe-acp-windows-x86_64-2.7.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.7.4/vibe-acp-windows-x86_64-2.7.4.zip"
|
||||
cmd = "./vibe-acp.exe"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "mistral-vibe"
|
||||
version = "2.7.3"
|
||||
version = "2.7.4"
|
||||
description = "Minimal CLI coding agent by Mistral"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
@@ -97,6 +97,7 @@ required-version = ">=0.8.0"
|
||||
dev = [
|
||||
"debugpy>=1.8.19",
|
||||
"pre-commit>=4.2.0",
|
||||
"pyinstrument>=5.1.2",
|
||||
"pyright>=1.1.403",
|
||||
"pytest>=8.3.5",
|
||||
"pytest-asyncio>=1.2.0",
|
||||
@@ -175,7 +176,7 @@ max-branches = 15
|
||||
max-locals = 15
|
||||
max-args = 9
|
||||
max-returns = 6
|
||||
max-nested-blocks = 4
|
||||
max-nested-blocks = 5
|
||||
|
||||
[tool.vulture]
|
||||
ignore_decorators = ["@*"]
|
||||
|
||||
@@ -12,6 +12,8 @@ YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
ORIGINAL_PATH="${PATH}"
|
||||
|
||||
function error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1" >&2
|
||||
}
|
||||
@@ -28,6 +30,24 @@ function warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
function find_command_in_path() {
|
||||
local cmd="$1"
|
||||
local path_value="$2"
|
||||
PATH="$path_value" command -v "$cmd" 2>/dev/null || true
|
||||
}
|
||||
|
||||
function print_missing_path_instructions() {
|
||||
local executable_name="$1"
|
||||
local executable_path="$2"
|
||||
local bin_dir
|
||||
bin_dir=$(dirname "$executable_path")
|
||||
|
||||
warning "Installation completed, and '$executable_name' was installed at: $executable_path"
|
||||
error "Your PATH does not include the folder that contains '$executable_name'."
|
||||
error "Add this directory to your shell profile, then restart your terminal:"
|
||||
error " export PATH=\"$bin_dir:\$PATH\""
|
||||
}
|
||||
|
||||
function check_platform() {
|
||||
local platform=$(uname -s)
|
||||
|
||||
@@ -45,7 +65,7 @@ function check_platform() {
|
||||
}
|
||||
|
||||
function check_uv_installed() {
|
||||
if command -v uv &> /dev/null; then
|
||||
if command -v uv >/dev/null 2>&1; then
|
||||
info "uv is already installed: $(uv --version)"
|
||||
UV_INSTALLED=true
|
||||
else
|
||||
@@ -79,12 +99,21 @@ function install_uv() {
|
||||
}
|
||||
|
||||
function check_vibe_installed() {
|
||||
if command -v vibe &> /dev/null; then
|
||||
if [[ -n "$(find_command_in_path vibe "$ORIGINAL_PATH")" ]]; then
|
||||
info "vibe is already installed"
|
||||
VIBE_INSTALLED=true
|
||||
else
|
||||
VIBE_INSTALLED=false
|
||||
return
|
||||
fi
|
||||
|
||||
local uv_bin_dir
|
||||
uv_bin_dir=$(uv tool dir --bin 2>/dev/null || true)
|
||||
if [[ -n "$uv_bin_dir" && -x "$uv_bin_dir/vibe" ]]; then
|
||||
info "vibe is already installed (off PATH) at $uv_bin_dir/vibe"
|
||||
VIBE_INSTALLED=true
|
||||
return
|
||||
fi
|
||||
|
||||
VIBE_INSTALLED=false
|
||||
}
|
||||
|
||||
function install_vibe() {
|
||||
@@ -132,7 +161,7 @@ function main() {
|
||||
update_vibe
|
||||
fi
|
||||
|
||||
if command -v vibe &> /dev/null; then
|
||||
if [[ -n "$(find_command_in_path vibe "$ORIGINAL_PATH")" ]]; then
|
||||
success "Installation completed successfully!"
|
||||
echo
|
||||
echo "You can now run vibe with:"
|
||||
@@ -141,8 +170,20 @@ function main() {
|
||||
echo "Or for ACP mode:"
|
||||
echo " vibe-acp"
|
||||
else
|
||||
error "Installation completed but 'vibe' command not found"
|
||||
error "Please check your installation and PATH settings"
|
||||
local UV_BIN_DIR
|
||||
local VIBE_BIN_PATH=""
|
||||
UV_BIN_DIR=$(uv tool dir --bin 2>/dev/null || true)
|
||||
if [[ -n "$UV_BIN_DIR" && -x "$UV_BIN_DIR/vibe" ]]; then
|
||||
VIBE_BIN_PATH="$UV_BIN_DIR/vibe"
|
||||
fi
|
||||
|
||||
if [[ -n "$VIBE_BIN_PATH" ]]; then
|
||||
print_missing_path_instructions "vibe" "$VIBE_BIN_PATH"
|
||||
else
|
||||
error "Installation completed but 'vibe' command not found"
|
||||
error "uv did not expose a 'vibe' executable in the expected tools directory."
|
||||
error "Please check your installation and PATH settings"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -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")))
|
||||
|
||||
|
||||
46
uv.lock
generated
@@ -3,7 +3,7 @@ revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-03-27T13:28:45.258057Z"
|
||||
exclude-newer = "2026-04-02T14:58:56.644057Z"
|
||||
exclude-newer-span = "P7D"
|
||||
|
||||
[options.exclude-newer-package]
|
||||
@@ -787,7 +787,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mistral-vibe"
|
||||
version = "2.7.3"
|
||||
version = "2.7.4"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "agent-client-protocol" },
|
||||
@@ -834,6 +834,7 @@ build = [
|
||||
dev = [
|
||||
{ name = "debugpy" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pyinstrument" },
|
||||
{ name = "pyright" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
@@ -893,6 +894,7 @@ build = [
|
||||
dev = [
|
||||
{ name = "debugpy", specifier = ">=1.8.19" },
|
||||
{ name = "pre-commit", specifier = ">=4.2.0" },
|
||||
{ name = "pyinstrument", specifier = ">=5.1.2" },
|
||||
{ name = "pyright", specifier = ">=1.1.403" },
|
||||
{ name = "pytest", specifier = ">=8.3.5" },
|
||||
{ name = "pytest-asyncio", specifier = ">=1.2.0" },
|
||||
@@ -1327,6 +1329,46 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/34/3b/1efef5ff4d4d150f646b873e963437c0b800cb375a37df01fefab149f4d9/pyinstaller_hooks_contrib-2026.2-py3-none-any.whl", hash = "sha256:fc29f0481b58adf78ce9c1d9cf135fe96f38c708f74b2aa0670ef93e59578ab9", size = 453939, upload-time = "2026-03-02T23:06:59.469Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstrument"
|
||||
version = "5.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/32/7f/d3c4ef7c43f3294bd5a475dfa6f295a9fee5243c292d5c8122044fa83bcb/pyinstrument-5.1.2.tar.gz", hash = "sha256:af149d672da9493fa37334a1cc68f7b80c3e6cb9fd99b9e426c447db5c650bf0", size = 266889, upload-time = "2026-01-04T18:38:58.464Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/d9/8fa5571ddd21b2b7189bd8b0bb4e90be1659a54dda5af51c7f6bf2b5666f/pyinstrument-5.1.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2519865d4bf58936f2506c1c46a82d29a20f3239aa50c941df1ca9618c7da5f0", size = 131419, upload-time = "2026-01-04T18:37:46.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/50/0512adb83cadfeaa1a215dc9784defff5043c5aa052d15015e3d8013af75/pyinstrument-5.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:059442106b8b5de29ae5ac1bdc20d044fed4da534b8caba434b6ffb119037bf5", size = 124446, upload-time = "2026-01-04T18:37:48.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/78/c45f0b668fb3c8c0d32058a451a8e1d34737cd7586387982185e12df1977/pyinstrument-5.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd51f2d54fc39a4cfd73ba6be27cd0187123132ce3f445b639bff5e1b23d7e26", size = 149694, upload-time = "2026-01-04T18:37:49.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/4d/2ca3ca9906ce6e05070f431c54d54ccbaf57a980cfa58032d35b0b0ac1f8/pyinstrument-5.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12af1e83795b6c640d657d339014dd1ff718b182dec736d7d1f1d8a97534eb53", size = 148461, upload-time = "2026-01-04T18:37:51.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/d2/bfe84a4326172ef68655b65b49fd041eeb94c8e59ee47258589b8b79dd3b/pyinstrument-5.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2565513658e742c5eb691a779cb29d19d01bc9ee951d0eb76482e9f343c38c2e", size = 148560, upload-time = "2026-01-04T18:37:52.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/00/db7f5def351e869230b0165828c4edacbf3fdda8d66aff30dd73a62082c2/pyinstrument-5.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5afd0ba788a1d112da49fb77966918e01df1f9e7d62e72894d82f7acb0996c2d", size = 148178, upload-time = "2026-01-04T18:37:54.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/bc/aea3329576e20b987d205027b8e6442ece845d681b9f9d8682d5404f81f3/pyinstrument-5.1.2-cp312-cp312-win32.whl", hash = "sha256:554077b031b278593cb2301f0057be771ea62a729878c69aaf29fcdfb7b71281", size = 125927, upload-time = "2026-01-04T18:37:55.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/e2/d928434ec3a840478e95fd0d73b0dfc0b8060a07b06f4b45e9df30444e9a/pyinstrument-5.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:55a905384ba43efc924b8863aa6cfd276f029e4aa70c4a0e3b7389e27b191e45", size = 126675, upload-time = "2026-01-04T18:37:57.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/8e/b9aea969eec67c129652000446384d550a0df45c297adc9fd74da2f8482c/pyinstrument-5.1.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7b8bab2334bf1d4c9e92d61db574300b914b594588a6b6dd67c45450152dfc29", size = 131418, upload-time = "2026-01-04T18:37:58.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/62/76418eb29b5591f3e5500369a6777ce928135c3aa6ccdb0c861a9c6ca93b/pyinstrument-5.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:13dcc138a61298ef4994b7aebff509d2c06db89dfd6e2021f0b9cd96aaa44ec3", size = 124448, upload-time = "2026-01-04T18:37:59.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/73/874bccc04bcf6f4babc3de1a9568e209e7e40998563974f5030b0fb4d3e0/pyinstrument-5.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8abd4a7ffa2e7f9e00039a5e549e8eebc80d7ca8d43f0fb51a50ff2b117ce4a", size = 149853, upload-time = "2026-01-04T18:38:01.405Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/85/268446c4388d77ff4abdeaff202356e1527b3ff9576f5587443a24980bec/pyinstrument-5.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb3a05108edebc30f31e2c69c904576042f1158b2513ab80adc08f7848a7a8f0", size = 148641, upload-time = "2026-01-04T18:38:03.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/15/4f8dea3381483e68d00582a9b823a21a088acfe77a847a7991a1a8feed76/pyinstrument-5.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f70d588b53f3f35829d1d1ddfa05e07fcebf1434b3b1509d542ca317d8e9a2a5", size = 148674, upload-time = "2026-01-04T18:38:04.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/61/72c180454b6511d5b90166f8828e1bab3b45d0489952a1fe48c5c585233d/pyinstrument-5.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b007327e0d6a6a01d5064883dd27c19996f044ce7488d507826fee7884e6a32e", size = 148315, upload-time = "2026-01-04T18:38:06.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/f0/4c27cebddf22a8840bd8b419366bb321ce41f921ca1893e309c932ab28bf/pyinstrument-5.1.2-cp313-cp313-win32.whl", hash = "sha256:9ba0e6b17a7e86c3dc02d208e4c25506e8f914d9964ae89449f1f37f0b70abc0", size = 125926, upload-time = "2026-01-04T18:38:07.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/20/6b1bee88ddef065b0df3a3ba4ba60ed8a9ca443d5cded7152a8a9750914f/pyinstrument-5.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:660d7fc486a839814db0b2f716bc13d8b99b9c780aaeb47f74a70a34adc02a7b", size = 126678, upload-time = "2026-01-04T18:38:08.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/0f/7d5154c92904bdf25be067a7fe4cad4ba48919f16ccbb51bb953d9ae1a20/pyinstrument-5.1.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0baed297beee2bb9897e737bbd89e3b9d45a2fbbea9f1ad4e809007d780a9b1e", size = 131388, upload-time = "2026-01-04T18:38:10.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/28/bf83231a3f951e11b4dfaf160e1eeba1ce29377eab30e3d2eb6ee22ff3ba/pyinstrument-5.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ebb910a32a45bde6c3fc30c578efc28a54517990e11e94b5e48a0d5479728568", size = 124456, upload-time = "2026-01-04T18:38:11.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/98/762cf10896d907268629e1db08a48f128984a53e8d92b99ea96f862597e5/pyinstrument-5.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad403c157f9c6dba7f731a6fca5bfcd8ca2701a39bcc717dcc6e0b10055ffc4", size = 149594, upload-time = "2026-01-04T18:38:13.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/1b/48580e16e623d89af58b89c552c95a2ae65f70a1f4fab1d97879f34791db/pyinstrument-5.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f456cabdb95fd343c798a7f2a56688b028f981522e283c5f59bd59195b66df5", size = 148339, upload-time = "2026-01-04T18:38:14.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/7e/38157a8a6ec67789d8ee109fd09877ea3340df44e1a7add8f249e30a8ade/pyinstrument-5.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4e9c4dcc1f2c4a0cd6b576e3604abc37496a7868243c9a1443ad3b9db69d590f", size = 148485, upload-time = "2026-01-04T18:38:16.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/34/31ee72b19cfc48a82801024b5d653f07982154a11381a3ae65bbfdbf2c7b/pyinstrument-5.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:acf93b128328c6d80fdb85431068ac17508f0f7845e89505b0ea6130dead5ca6", size = 148106, upload-time = "2026-01-04T18:38:17.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/b4/7ab20243187262d66ab062778b1ccac4ca55090752f32a83f603f4e5e3a2/pyinstrument-5.1.2-cp314-cp314-win32.whl", hash = "sha256:9c7f0167903ecff8b1d744f7e37b2bd4918e05a69cca724cb112f5ed59d1e41b", size = 126593, upload-time = "2026-01-04T18:38:18.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/a0/db6a8ae3182546227f5a043b1be29b8d5f98bf973e20d922981ef206de85/pyinstrument-5.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:ce3f6b1f9a2b5d74819ecc07d631eadececf915f551474a75ad65ac580ec5a0e", size = 127358, upload-time = "2026-01-04T18:38:20.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/d2/719f439972b3f80e35fb5b1bcd888c3218d60dbc91957b99ffafd7ac9221/pyinstrument-5.1.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:af8651b239049accbeecd389d35823233f649446f76f47fd005316b05d08cef2", size = 132317, upload-time = "2026-01-04T18:38:21.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/1c/0ebfef69ae926665fae635424c5647411235c3689c9a9ad69fd68de6cae2/pyinstrument-5.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c6082f1c3e43e1d22834e91ba8975f0080186df4018a04b4dd29f9623c59df1d", size = 124917, upload-time = "2026-01-04T18:38:23.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/ee/5599f769f515a0f1c97443edc7394fe2b9829bf39f404c046499c1a62378/pyinstrument-5.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c031eb066ddc16425e1e2f56aad5c1ce1e27b2432a70329e5385b85e812decee", size = 157407, upload-time = "2026-01-04T18:38:24.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/40/32aa865252288caef301237488ee309bd6701125888bf453d23ab764e357/pyinstrument-5.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f447ec391cad30667ba412dce41607aaa20d4a2496a7ab867e0c199f0fe3ae3d", size = 155068, upload-time = "2026-01-04T18:38:26.112Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/68/0b56a1540fe1c357dfcda82d4f5b52c87fada5962cbf18703ea39ccbbe69/pyinstrument-5.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:50299bddfc1fe0039898f895b10ef12f9db08acffb4d85326fad589cda24d2ee", size = 155186, upload-time = "2026-01-04T18:38:27.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/48/7ef84abfc3e41148cf993095214f104e75ecff585e94c6e8be001e672573/pyinstrument-5.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a193ff08825ece115ececa136832acb14c491c77ab1e6b6a361905df8753d5c6", size = 153979, upload-time = "2026-01-04T18:38:29.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/cf/a28ad117d58b33c1d74bcdfbbcf1603b67346883800ac7d510cff8d3bcee/pyinstrument-5.1.2-cp314-cp314t-win32.whl", hash = "sha256:de887ba19e1057bd2d86e6584f17788516a890ae6fe1b7eed9927873f416b4d8", size = 127267, upload-time = "2026-01-04T18:38:30.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/97/03635143a12a5d941f545548b00f8ac39d35565321a2effb4154ed267338/pyinstrument-5.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:b6a71f5e7f53c86c9b476b30cf19509463a63581ef17ddbd8680fee37ae509db", size = 128164, upload-time = "2026-01-04T18:38:32.281Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.11.0"
|
||||
|
||||
@@ -3,4 +3,4 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
|
||||
VIBE_ROOT = Path(__file__).parent
|
||||
__version__ = "2.7.3"
|
||||
__version__ = "2.7.4"
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import aclosing
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
@@ -445,14 +446,14 @@ class VibeAcpAgentLoop(AcpAgent):
|
||||
await self.client.session_update(session_id=session_id, update=update)
|
||||
|
||||
elif msg.role == Role.assistant:
|
||||
if text_update := create_assistant_message_replay(msg):
|
||||
await self.client.session_update(
|
||||
session_id=session_id, update=text_update
|
||||
)
|
||||
if reasoning_update := create_reasoning_replay(msg):
|
||||
await self.client.session_update(
|
||||
session_id=session_id, update=reasoning_update
|
||||
)
|
||||
if text_update := create_assistant_message_replay(msg):
|
||||
await self.client.session_update(
|
||||
session_id=session_id, update=text_update
|
||||
)
|
||||
await self._replay_tool_calls(session_id, msg)
|
||||
|
||||
elif msg.role == Role.tool:
|
||||
@@ -885,61 +886,64 @@ class VibeAcpAgentLoop(AcpAgent):
|
||||
) -> AsyncGenerator[SessionUpdate]:
|
||||
rendered_prompt = render_path_prompt(prompt, base_dir=Path.cwd())
|
||||
|
||||
async for event in session.agent_loop.act(
|
||||
rendered_prompt, client_message_id=client_message_id
|
||||
):
|
||||
if isinstance(event, AssistantEvent):
|
||||
yield AgentMessageChunk(
|
||||
session_update="agent_message_chunk",
|
||||
content=TextContentBlock(type="text", text=event.content),
|
||||
message_id=event.message_id,
|
||||
)
|
||||
|
||||
elif isinstance(event, ReasoningEvent):
|
||||
yield AgentThoughtChunk(
|
||||
session_update="agent_thought_chunk",
|
||||
content=TextContentBlock(type="text", text=event.content),
|
||||
message_id=event.message_id,
|
||||
)
|
||||
|
||||
elif isinstance(event, ToolCallEvent):
|
||||
if issubclass(event.tool_class, BaseAcpTool):
|
||||
event.tool_class.update_tool_state(
|
||||
tool_manager=session.agent_loop.tool_manager,
|
||||
client=self.client,
|
||||
session_id=session.id,
|
||||
tool_call_id=event.tool_call_id,
|
||||
async with aclosing(
|
||||
session.agent_loop.act(rendered_prompt, client_message_id=client_message_id)
|
||||
) as events:
|
||||
async for event in events:
|
||||
if isinstance(event, AssistantEvent):
|
||||
yield AgentMessageChunk(
|
||||
session_update="agent_message_chunk",
|
||||
content=TextContentBlock(type="text", text=event.content),
|
||||
message_id=event.message_id,
|
||||
)
|
||||
|
||||
session_update = tool_call_session_update(event)
|
||||
if session_update:
|
||||
yield session_update
|
||||
elif isinstance(event, ReasoningEvent):
|
||||
yield AgentThoughtChunk(
|
||||
session_update="agent_thought_chunk",
|
||||
content=TextContentBlock(type="text", text=event.content),
|
||||
message_id=event.message_id,
|
||||
)
|
||||
|
||||
elif isinstance(event, ToolResultEvent):
|
||||
session_update = tool_result_session_update(event)
|
||||
if session_update:
|
||||
yield session_update
|
||||
|
||||
elif isinstance(event, ToolStreamEvent):
|
||||
yield ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content",
|
||||
content=TextContentBlock(type="text", text=event.message),
|
||||
elif isinstance(event, ToolCallEvent):
|
||||
if issubclass(event.tool_class, BaseAcpTool):
|
||||
event.tool_class.update_tool_state(
|
||||
tool_manager=session.agent_loop.tool_manager,
|
||||
client=self.client,
|
||||
session_id=session.id,
|
||||
tool_call_id=event.tool_call_id,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
elif isinstance(event, CompactStartEvent):
|
||||
yield create_compact_start_session_update(event)
|
||||
session_update = tool_call_session_update(event)
|
||||
if session_update:
|
||||
yield session_update
|
||||
|
||||
elif isinstance(event, CompactEndEvent):
|
||||
yield create_compact_end_session_update(event)
|
||||
elif isinstance(event, ToolResultEvent):
|
||||
session_update = tool_result_session_update(event)
|
||||
if session_update:
|
||||
yield session_update
|
||||
|
||||
elif isinstance(event, AgentProfileChangedEvent):
|
||||
pass
|
||||
elif isinstance(event, ToolStreamEvent):
|
||||
yield ToolCallProgress(
|
||||
session_update="tool_call_update",
|
||||
tool_call_id=event.tool_call_id,
|
||||
content=[
|
||||
ContentToolCallContent(
|
||||
type="content",
|
||||
content=TextContentBlock(
|
||||
type="text", text=event.message
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
elif isinstance(event, CompactStartEvent):
|
||||
yield create_compact_start_session_update(event)
|
||||
|
||||
elif isinstance(event, CompactEndEvent):
|
||||
yield create_compact_end_session_update(event)
|
||||
|
||||
elif isinstance(event, AgentProfileChangedEvent):
|
||||
pass
|
||||
|
||||
@override
|
||||
async def close_session(
|
||||
|
||||
@@ -8,7 +8,7 @@ import sys
|
||||
import tomli_w
|
||||
|
||||
from vibe import __version__
|
||||
from vibe.core.config import MissingAPIKeyError, VibeConfig
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.config.harness_files import (
|
||||
get_harness_files_manager,
|
||||
init_harness_files_manager,
|
||||
@@ -92,8 +92,8 @@ def main() -> None:
|
||||
try:
|
||||
config = VibeConfig.load()
|
||||
setup_tracing(config)
|
||||
except MissingAPIKeyError:
|
||||
pass # tracing disabled, but server can still handle the error properly in new_session
|
||||
except Exception:
|
||||
pass # tracing disabled
|
||||
|
||||
run_acp_server()
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ import subprocess
|
||||
import pyperclip
|
||||
from textual.app import App
|
||||
|
||||
_PREVIEW_MAX_LENGTH = 40
|
||||
|
||||
|
||||
def _copy_osc52(text: str) -> None:
|
||||
encoded = base64.b64encode(text.encode("utf-8")).decode("ascii")
|
||||
@@ -117,13 +115,6 @@ def _copy_to_clipboard(text: str) -> None:
|
||||
raise RuntimeError("All clipboard strategies failed")
|
||||
|
||||
|
||||
def _shorten_preview(texts: list[str]) -> str:
|
||||
dense_text = "⏎".join(texts).replace("\n", "⏎")
|
||||
if len(dense_text) > _PREVIEW_MAX_LENGTH:
|
||||
return f"{dense_text[: _PREVIEW_MAX_LENGTH - 1]}…"
|
||||
return dense_text
|
||||
|
||||
|
||||
def _get_selected_texts(app: App) -> list[str]:
|
||||
selected_texts = []
|
||||
|
||||
@@ -156,7 +147,7 @@ def copy_selection_to_clipboard(app: App, show_toast: bool = True) -> str | None
|
||||
_copy_to_clipboard(combined_text)
|
||||
if show_toast:
|
||||
app.notify(
|
||||
f'"{_shorten_preview(selected_texts)}" copied to clipboard',
|
||||
"Selection copied to clipboard",
|
||||
severity="information",
|
||||
timeout=2,
|
||||
markup=False,
|
||||
|
||||
@@ -36,7 +36,7 @@ class CommandRegistry:
|
||||
),
|
||||
"reload": Command(
|
||||
aliases=frozenset(["/reload"]),
|
||||
description="Reload configuration from disk",
|
||||
description="Reload configuration, agent instructions, and skills from disk",
|
||||
handler="_reload_config",
|
||||
),
|
||||
"clear": Command(
|
||||
@@ -49,6 +49,11 @@ class CommandRegistry:
|
||||
description="Show path to current interaction log file",
|
||||
handler="_show_log_path",
|
||||
),
|
||||
"debug": Command(
|
||||
aliases=frozenset(["/debug"]),
|
||||
description="Toggle debug console",
|
||||
handler="action_toggle_debug_console",
|
||||
),
|
||||
"compact": Command(
|
||||
aliases=frozenset(["/compact"]),
|
||||
description="Compact conversation history by summarizing",
|
||||
@@ -85,6 +90,11 @@ class CommandRegistry:
|
||||
description="Browse and resume past sessions",
|
||||
handler="_show_session_picker",
|
||||
),
|
||||
"mcp": Command(
|
||||
aliases=frozenset(["/mcp"]),
|
||||
description="Display available MCP servers. Pass the name of a server to list its tools",
|
||||
handler="_show_mcp",
|
||||
),
|
||||
"voice": Command(
|
||||
aliases=frozenset(["/voice"]),
|
||||
description="Configure voice settings",
|
||||
@@ -120,13 +130,23 @@ class CommandRegistry:
|
||||
for alias in cmd.aliases:
|
||||
self._alias_map[alias] = cmd_name
|
||||
|
||||
def find_command(self, user_input: str) -> Command | None:
|
||||
cmd_name = self.get_command_name(user_input)
|
||||
return self.commands.get(cmd_name) if cmd_name else None
|
||||
|
||||
def get_command_name(self, user_input: str) -> str | None:
|
||||
return self._alias_map.get(user_input.lower().strip())
|
||||
|
||||
def parse_command(self, user_input: str) -> tuple[str, Command, str] | None:
|
||||
parts = user_input.strip().split(None, 1)
|
||||
if not parts:
|
||||
return None
|
||||
|
||||
cmd_word = parts[0]
|
||||
cmd_args = parts[1] if len(parts) > 1 else ""
|
||||
cmd_name = self.get_command_name(cmd_word)
|
||||
if cmd_name is None:
|
||||
return None
|
||||
|
||||
command = self.commands[cmd_name]
|
||||
return cmd_name, command, cmd_args
|
||||
|
||||
def get_help_text(self) -> str:
|
||||
lines: list[str] = [
|
||||
"### Keyboard Shortcuts",
|
||||
|
||||
@@ -44,7 +44,7 @@ class HistoryManager:
|
||||
|
||||
def add(self, text: str) -> None:
|
||||
text = text.strip()
|
||||
if not text or text.startswith("/"):
|
||||
if not text:
|
||||
return
|
||||
|
||||
if self._entries and self._entries[-1] == text:
|
||||
|
||||
81
vibe/cli/profiler.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""General-purpose profiler for measuring any section of the application.
|
||||
|
||||
Wraps pyinstrument (dev-only dependency). Silently no-ops when not installed.
|
||||
Activated by the VIBE_PROFILE=1 environment variable.
|
||||
|
||||
Usage:
|
||||
from vibe.cli import profiler
|
||||
|
||||
profiler.start("startup")
|
||||
# ... code to profile ...
|
||||
profiler.stop_and_print()
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pyinstrument import Profiler
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class _State:
|
||||
profiler: Profiler | None = None
|
||||
label: str = "default"
|
||||
|
||||
|
||||
_state = _State()
|
||||
|
||||
|
||||
def is_enabled() -> bool:
|
||||
"""Return True when profiling is activated via environment variable."""
|
||||
return bool(os.environ.get("VIBE_PROFILE"))
|
||||
|
||||
|
||||
def start(label: str = "default") -> None:
|
||||
"""Start profiling. The label is used to name the output file.
|
||||
|
||||
No-op if pyinstrument is missing or env var unset.
|
||||
"""
|
||||
if not is_enabled():
|
||||
return
|
||||
try:
|
||||
from pyinstrument import Profiler
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
if _state.profiler is not None:
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Profiler already running; stop it before starting a new one.", stacklevel=2
|
||||
)
|
||||
return
|
||||
|
||||
_state.label = label
|
||||
_state.profiler = Profiler()
|
||||
_state.profiler.start()
|
||||
|
||||
|
||||
def stop_and_print() -> None:
|
||||
"""Stop profiling, write an HTML report, and print a text summary to stderr."""
|
||||
if _state.profiler is None:
|
||||
return
|
||||
_state.profiler.stop()
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
output_path = Path(f"{_state.label}-profile.html")
|
||||
output_path.write_text(_state.profiler.output_html(), encoding="utf-8")
|
||||
print(
|
||||
f"\n[profiler:{_state.label}] Saved HTML profile to {output_path.resolve()}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(_state.profiler.output_text(color=True), file=sys.stderr)
|
||||
|
||||
_state.profiler = None
|
||||
_state.label = "default"
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import aclosing
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum, auto
|
||||
import gc
|
||||
@@ -56,9 +57,11 @@ from vibe.cli.textual_ui.widgets.chat_input.text_area import ChatTextArea
|
||||
from vibe.cli.textual_ui.widgets.compact import CompactMessage
|
||||
from vibe.cli.textual_ui.widgets.config_app import ConfigApp
|
||||
from vibe.cli.textual_ui.widgets.context_progress import ContextProgress, TokenState
|
||||
from vibe.cli.textual_ui.widgets.debug_console import DebugConsole
|
||||
from vibe.cli.textual_ui.widgets.feedback_bar import FeedbackBar
|
||||
from vibe.cli.textual_ui.widgets.load_more import HistoryLoadMoreRequested
|
||||
from vibe.cli.textual_ui.widgets.loading import LoadingWidget, paused_timer
|
||||
from vibe.cli.textual_ui.widgets.mcp_app import MCPApp
|
||||
from vibe.cli.textual_ui.widgets.messages import (
|
||||
BashOutputMessage,
|
||||
ErrorMessage,
|
||||
@@ -112,6 +115,7 @@ from vibe.core.audio_recorder import AudioRecorder
|
||||
from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.data_retention import DATA_RETENTION_MESSAGE
|
||||
from vibe.core.log_reader import LogReader
|
||||
from vibe.core.logger import logger
|
||||
from vibe.core.paths import HISTORY_FILE
|
||||
from vibe.core.rewind import RewindError
|
||||
@@ -171,6 +175,7 @@ class BottomApp(StrEnum):
|
||||
Approval = auto()
|
||||
Config = auto()
|
||||
Input = auto()
|
||||
MCP = auto()
|
||||
ModelPicker = auto()
|
||||
ProxySetup = auto()
|
||||
Question = auto()
|
||||
@@ -184,7 +189,7 @@ class ChatScroll(VerticalScroll):
|
||||
|
||||
@property
|
||||
def is_at_bottom(self) -> bool:
|
||||
return self.scroll_target_y >= (self.max_scroll_y - 3)
|
||||
return self.scroll_target_y >= self.max_scroll_y
|
||||
|
||||
_reanchor_pending: bool = False
|
||||
_scrolling_down: bool = False
|
||||
@@ -281,6 +286,7 @@ class VibeApp(App): # noqa: PLR0904
|
||||
Binding(
|
||||
"shift+down", "scroll_chat_down", "Scroll Down", show=False, priority=True
|
||||
),
|
||||
Binding("ctrl+backslash", "toggle_debug_console", "Debug Console", show=False),
|
||||
Binding("alt+up", "rewind_prev", "Rewind Previous", show=False, priority=True),
|
||||
Binding("ctrl+p", "rewind_prev", "Rewind Previous", show=False, priority=True),
|
||||
Binding("alt+down", "rewind_next", "Rewind Next", show=False, priority=True),
|
||||
@@ -354,6 +360,8 @@ class VibeApp(App): # noqa: PLR0904
|
||||
self._cached_messages_area: Widget | None = None
|
||||
self._cached_chat: ChatScroll | None = None
|
||||
self._cached_loading_area: Widget | None = None
|
||||
self._log_reader = LogReader()
|
||||
self._debug_console: DebugConsole | None = None
|
||||
self._switch_agent_generation = 0
|
||||
self._plan_info: PlanInfo | None = None
|
||||
self._narrator_manager: NarratorManagerPort = (
|
||||
@@ -663,6 +671,10 @@ class VibeApp(App): # noqa: PLR0904
|
||||
) -> None:
|
||||
await self._switch_to_input_app()
|
||||
|
||||
async def on_mcpapp_mcpclosed(self, _message: MCPApp.MCPClosed) -> None:
|
||||
await self._mount_and_scroll(UserCommandMessage("MCP servers closed."))
|
||||
await self._switch_to_input_app()
|
||||
|
||||
async def on_proxy_setup_app_proxy_setup_closed(
|
||||
self, message: ProxySetupApp.ProxySetupClosed
|
||||
) -> None:
|
||||
@@ -700,17 +712,17 @@ class VibeApp(App): # noqa: PLR0904
|
||||
await widget.remove()
|
||||
|
||||
async def _handle_command(self, user_input: str) -> bool:
|
||||
if command := self.commands.find_command(user_input):
|
||||
if cmd_name := self.commands.get_command_name(user_input):
|
||||
self.agent_loop.telemetry_client.send_slash_command_used(
|
||||
cmd_name, "builtin"
|
||||
)
|
||||
if resolved := self.commands.parse_command(user_input):
|
||||
cmd_name, command, cmd_args = resolved
|
||||
self.agent_loop.telemetry_client.send_slash_command_used(
|
||||
cmd_name, "builtin"
|
||||
)
|
||||
await self._mount_and_scroll(UserMessage(user_input))
|
||||
handler = getattr(self, command.handler)
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
await handler()
|
||||
await handler(cmd_args=cmd_args)
|
||||
else:
|
||||
handler()
|
||||
handler(cmd_args=cmd_args)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -781,17 +793,103 @@ class VibeApp(App): # noqa: PLR0904
|
||||
await self._mount_and_scroll(
|
||||
BashOutputMessage(command, str(Path.cwd()), output, exit_code)
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
await self.agent_loop.inject_user_context(
|
||||
self._format_manual_command_context(
|
||||
command=command,
|
||||
cwd=str(Path.cwd()),
|
||||
exit_code=exit_code,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
)
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
stdout = (
|
||||
error.stdout.decode("utf-8", errors="replace")
|
||||
if isinstance(error.stdout, bytes)
|
||||
else (error.stdout or "")
|
||||
)
|
||||
stderr = (
|
||||
error.stderr.decode("utf-8", errors="replace")
|
||||
if isinstance(error.stderr, bytes)
|
||||
else (error.stderr or "")
|
||||
)
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
"Command timed out after 30 seconds",
|
||||
collapsed=self._tools_collapsed,
|
||||
)
|
||||
)
|
||||
await self.agent_loop.inject_user_context(
|
||||
self._format_manual_command_context(
|
||||
command=command,
|
||||
cwd=str(Path.cwd()),
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
status="timed out after 30 seconds",
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(f"Command failed: {e}", collapsed=self._tools_collapsed)
|
||||
)
|
||||
await self.agent_loop.inject_user_context(
|
||||
self._format_manual_command_context(
|
||||
command=command,
|
||||
cwd=str(Path.cwd()),
|
||||
status=f"failed before completion: {e}",
|
||||
)
|
||||
)
|
||||
|
||||
def _get_bash_max_output_bytes(self) -> int:
|
||||
from vibe.core.tools.builtins.bash import BashToolConfig
|
||||
|
||||
config = self.agent_loop.tool_manager.get_tool_config("bash")
|
||||
if isinstance(config, BashToolConfig):
|
||||
return config.max_output_bytes
|
||||
return BashToolConfig().max_output_bytes
|
||||
|
||||
@staticmethod
|
||||
def _cap_output(text: str, limit: int) -> str:
|
||||
if len(text) <= limit:
|
||||
return text
|
||||
return text[:limit] + "\n... [truncated]"
|
||||
|
||||
def _format_manual_command_context(
|
||||
self,
|
||||
*,
|
||||
command: str,
|
||||
cwd: str,
|
||||
stdout: str = "",
|
||||
stderr: str = "",
|
||||
exit_code: int | None = None,
|
||||
status: str | None = None,
|
||||
) -> str:
|
||||
limit = self._get_bash_max_output_bytes()
|
||||
stdout = self._cap_output(stdout, limit)
|
||||
stderr = self._cap_output(stderr, limit)
|
||||
|
||||
sections = [
|
||||
"Manual `!` command result from the user. Use this as context only.",
|
||||
f"Command: `{command}`",
|
||||
f"Working directory: `{cwd}`",
|
||||
]
|
||||
|
||||
if status is not None:
|
||||
sections.append(f"Status: {status}")
|
||||
|
||||
if exit_code is not None:
|
||||
sections.append(f"Exit code: {exit_code}")
|
||||
|
||||
if stdout:
|
||||
sections.append(f"Stdout:\n```text\n{stdout.rstrip()}\n```")
|
||||
|
||||
if stderr:
|
||||
sections.append(f"Stderr:\n```text\n{stderr.rstrip()}\n```")
|
||||
|
||||
if not stdout and not stderr:
|
||||
sections.append("Output:\n```text\n(no output)\n```")
|
||||
|
||||
return "\n\n".join(sections)
|
||||
|
||||
async def _handle_user_message(self, message: str) -> None:
|
||||
if self._remote_manager.is_active:
|
||||
@@ -967,20 +1065,21 @@ class VibeApp(App): # noqa: PLR0904
|
||||
rendered_prompt = render_path_prompt(prompt, base_dir=Path.cwd())
|
||||
self._narrator_manager.cancel()
|
||||
self._narrator_manager.on_turn_start(rendered_prompt)
|
||||
async for event in self.agent_loop.act(rendered_prompt):
|
||||
self._narrator_manager.on_turn_event(event)
|
||||
if isinstance(event, WaitingForInputEvent):
|
||||
await self._remove_loading_widget()
|
||||
if self._remote_manager.is_active:
|
||||
await self._handle_remote_waiting_input(event)
|
||||
elif self._loading_widget is None and is_progress_event(event):
|
||||
await self._ensure_loading_widget()
|
||||
if self.event_handler:
|
||||
await self.event_handler.handle_event(
|
||||
event,
|
||||
loading_active=self._loading_widget is not None,
|
||||
loading_widget=self._loading_widget,
|
||||
)
|
||||
async with aclosing(self.agent_loop.act(rendered_prompt)) as events:
|
||||
async for event in events:
|
||||
self._narrator_manager.on_turn_event(event)
|
||||
if isinstance(event, WaitingForInputEvent):
|
||||
await self._remove_loading_widget()
|
||||
if self._remote_manager.is_active:
|
||||
await self._handle_remote_waiting_input(event)
|
||||
elif self._loading_widget is None and is_progress_event(event):
|
||||
await self._ensure_loading_widget()
|
||||
if self.event_handler:
|
||||
await self.event_handler.handle_event(
|
||||
event,
|
||||
loading_active=self._loading_widget is not None,
|
||||
loading_widget=self._loading_widget,
|
||||
)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
await self._handle_turn_error()
|
||||
@@ -1020,7 +1119,7 @@ class VibeApp(App): # noqa: PLR0904
|
||||
return "Rate limits exceeded. Please wait a moment before trying again, or upgrade to Pro for higher rate limits and uninterrupted access."
|
||||
return "Rate limits exceeded. Please wait a moment before trying again."
|
||||
|
||||
async def _teleport_command(self) -> None:
|
||||
async def _teleport_command(self, **kwargs: Any) -> None:
|
||||
await self._handle_teleport_command(show_message=False)
|
||||
|
||||
async def _handle_teleport_command(
|
||||
@@ -1172,11 +1271,39 @@ class VibeApp(App): # noqa: PLR0904
|
||||
|
||||
self._interrupt_requested = False
|
||||
|
||||
async def _show_help(self) -> None:
|
||||
async def _show_help(self, **kwargs: Any) -> None:
|
||||
help_text = self.commands.get_help_text()
|
||||
await self._mount_and_scroll(UserCommandMessage(help_text))
|
||||
|
||||
async def _show_status(self) -> None:
|
||||
async def _show_mcp(self, cmd_args: str = "", **kwargs: Any) -> None:
|
||||
mcp_servers = self.config.mcp_servers
|
||||
if not mcp_servers:
|
||||
await self._mount_and_scroll(
|
||||
UserCommandMessage("No MCP servers configured.")
|
||||
)
|
||||
return
|
||||
if self._current_bottom_app == BottomApp.MCP:
|
||||
return
|
||||
name = cmd_args.strip()
|
||||
if name and not any(s.name == name for s in mcp_servers):
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
f"Unknown MCP server: {name}. Known servers: "
|
||||
+ ", ".join(s.name for s in mcp_servers),
|
||||
collapsed=self._tools_collapsed,
|
||||
)
|
||||
)
|
||||
return
|
||||
await self._mount_and_scroll(UserCommandMessage("MCP servers opened..."))
|
||||
await self._switch_from_input(
|
||||
MCPApp(
|
||||
mcp_servers=mcp_servers,
|
||||
tool_manager=self.agent_loop.tool_manager,
|
||||
initial_server=name,
|
||||
)
|
||||
)
|
||||
|
||||
async def _show_status(self, **kwargs: Any) -> None:
|
||||
stats = self.agent_loop.stats
|
||||
status_text = f"""## Agent Statistics
|
||||
|
||||
@@ -1189,27 +1316,27 @@ class VibeApp(App): # noqa: PLR0904
|
||||
"""
|
||||
await self._mount_and_scroll(UserCommandMessage(status_text))
|
||||
|
||||
async def _show_config(self) -> None:
|
||||
async def _show_config(self, **kwargs: Any) -> None:
|
||||
"""Switch to the configuration app in the bottom panel."""
|
||||
if self._current_bottom_app == BottomApp.Config:
|
||||
return
|
||||
await self._switch_to_config_app()
|
||||
|
||||
async def _show_model(self) -> None:
|
||||
async def _show_model(self, **kwargs: Any) -> None:
|
||||
"""Switch to the model picker in the bottom panel."""
|
||||
if self._current_bottom_app == BottomApp.ModelPicker:
|
||||
return
|
||||
await self._switch_to_model_picker_app()
|
||||
|
||||
async def _show_proxy_setup(self) -> None:
|
||||
async def _show_proxy_setup(self, **kwargs: Any) -> None:
|
||||
if self._current_bottom_app == BottomApp.ProxySetup:
|
||||
return
|
||||
await self._switch_to_proxy_setup_app()
|
||||
|
||||
async def _show_data_retention(self) -> None:
|
||||
async def _show_data_retention(self, **kwargs: Any) -> None:
|
||||
await self._mount_and_scroll(UserCommandMessage(DATA_RETENTION_MESSAGE))
|
||||
|
||||
async def _show_session_picker(self) -> None:
|
||||
async def _show_session_picker(self, **kwargs: Any) -> None:
|
||||
cwd = str(Path.cwd())
|
||||
local_sessions = (
|
||||
list_local_resume_sessions(self.config, cwd)
|
||||
@@ -1405,7 +1532,7 @@ class VibeApp(App): # noqa: PLR0904
|
||||
def loading_widget(self) -> LoadingWidget | None:
|
||||
return self._loading_widget
|
||||
|
||||
async def _reload_config(self) -> None:
|
||||
async def _reload_config(self, **kwargs: Any) -> None:
|
||||
try:
|
||||
self._reset_ui_state()
|
||||
await self._load_more.hide()
|
||||
@@ -1422,7 +1549,11 @@ class VibeApp(App): # noqa: PLR0904
|
||||
self.agent_loop.mcp_registry,
|
||||
plan_title(self._plan_info),
|
||||
)
|
||||
await self._mount_and_scroll(UserCommandMessage("Configuration reloaded."))
|
||||
await self._mount_and_scroll(
|
||||
UserCommandMessage(
|
||||
"Configuration reloaded (includes agent instructions and skills)."
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
@@ -1430,7 +1561,7 @@ class VibeApp(App): # noqa: PLR0904
|
||||
)
|
||||
)
|
||||
|
||||
async def _install_lean(self) -> None:
|
||||
async def _install_lean(self, **kwargs: Any) -> None:
|
||||
current = list(self.agent_loop.base_config.installed_agents)
|
||||
if "lean" in current:
|
||||
await self._mount_and_scroll(
|
||||
@@ -1440,7 +1571,7 @@ class VibeApp(App): # noqa: PLR0904
|
||||
VibeConfig.save_updates({"installed_agents": sorted([*current, "lean"])})
|
||||
await self._reload_config()
|
||||
|
||||
async def _uninstall_lean(self) -> None:
|
||||
async def _uninstall_lean(self, **kwargs: Any) -> None:
|
||||
current = list(self.agent_loop.base_config.installed_agents)
|
||||
if "lean" not in current:
|
||||
await self._mount_and_scroll(
|
||||
@@ -1452,7 +1583,7 @@ class VibeApp(App): # noqa: PLR0904
|
||||
})
|
||||
await self._reload_config()
|
||||
|
||||
async def _clear_history(self) -> None:
|
||||
async def _clear_history(self, **kwargs: Any) -> None:
|
||||
try:
|
||||
self._reset_ui_state()
|
||||
if self._remote_manager.is_active:
|
||||
@@ -1482,7 +1613,7 @@ class VibeApp(App): # noqa: PLR0904
|
||||
)
|
||||
)
|
||||
|
||||
async def _show_log_path(self) -> None:
|
||||
async def _show_log_path(self, **kwargs: Any) -> None:
|
||||
if not self.agent_loop.session_logger.enabled:
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
@@ -1506,7 +1637,7 @@ class VibeApp(App): # noqa: PLR0904
|
||||
)
|
||||
)
|
||||
|
||||
async def _compact_history(self) -> None:
|
||||
async def _compact_history(self, **kwargs: Any) -> None:
|
||||
if self._agent_running:
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
@@ -1570,11 +1701,12 @@ class VibeApp(App): # noqa: PLR0904
|
||||
return None
|
||||
return short_session_id(self.agent_loop.session_logger.session_id)
|
||||
|
||||
async def _exit_app(self) -> None:
|
||||
async def _exit_app(self, **kwargs: Any) -> None:
|
||||
self._log_reader.shutdown()
|
||||
await self._narrator_manager.close()
|
||||
self.exit(result=self._get_session_resume_info())
|
||||
|
||||
async def _setup_terminal(self) -> None:
|
||||
async def _setup_terminal(self, **kwargs: Any) -> None:
|
||||
result = setup_terminal()
|
||||
|
||||
if result.success:
|
||||
@@ -1612,7 +1744,7 @@ class VibeApp(App): # noqa: PLR0904
|
||||
telemetry_client=self.agent_loop.telemetry_client,
|
||||
)
|
||||
|
||||
async def _show_voice_settings(self) -> None:
|
||||
async def _show_voice_settings(self, **kwargs: Any) -> None:
|
||||
if self._current_bottom_app == BottomApp.Voice:
|
||||
return
|
||||
await self._switch_to_voice_app()
|
||||
@@ -1720,6 +1852,8 @@ class VibeApp(App): # noqa: PLR0904
|
||||
self.query_one(QuestionApp).focus()
|
||||
case BottomApp.SessionPicker:
|
||||
self.query_one(SessionPickerApp).focus()
|
||||
case BottomApp.MCP:
|
||||
self.query_one(MCPApp).focus()
|
||||
case BottomApp.Rewind:
|
||||
self.query_one(RewindApp).focus()
|
||||
case BottomApp.Voice:
|
||||
@@ -1794,7 +1928,7 @@ class VibeApp(App): # noqa: PLR0904
|
||||
if isinstance(child, UserMessage) and child.message_index is not None
|
||||
]
|
||||
|
||||
def _start_rewind_mode(self) -> None:
|
||||
def _start_rewind_mode(self, **kwargs: Any) -> None:
|
||||
self.action_rewind_prev()
|
||||
|
||||
def action_rewind_prev(self) -> None:
|
||||
@@ -1987,6 +2121,15 @@ class VibeApp(App): # noqa: PLR0904
|
||||
self.agent_loop.telemetry_client.send_user_cancelled_action("interrupt_agent")
|
||||
self.run_worker(self._interrupt_agent_loop(), exclusive=False)
|
||||
|
||||
def _handle_bottom_app_close_escape(
|
||||
self, widget_type: type[MCPApp] | type[ProxySetupApp]
|
||||
) -> None:
|
||||
try:
|
||||
self.query_one(widget_type).action_close()
|
||||
except Exception:
|
||||
pass
|
||||
self._last_escape_time = None
|
||||
|
||||
def action_interrupt(self) -> None: # noqa: PLR0911
|
||||
if self._voice_manager.transcribe_state != TranscribeState.IDLE:
|
||||
self._voice_manager.cancel_recording()
|
||||
@@ -2002,13 +2145,12 @@ class VibeApp(App): # noqa: PLR0904
|
||||
self._handle_voice_app_escape()
|
||||
return
|
||||
|
||||
if self._current_bottom_app == BottomApp.MCP:
|
||||
self._handle_bottom_app_close_escape(MCPApp)
|
||||
return
|
||||
|
||||
if self._current_bottom_app == BottomApp.ProxySetup:
|
||||
try:
|
||||
proxy_setup_app = self.query_one(ProxySetupApp)
|
||||
proxy_setup_app.action_close()
|
||||
except Exception:
|
||||
pass
|
||||
self._last_escape_time = None
|
||||
self._handle_bottom_app_close_escape(ProxySetupApp)
|
||||
return
|
||||
|
||||
if self._current_bottom_app == BottomApp.Approval:
|
||||
@@ -2170,6 +2312,14 @@ class VibeApp(App): # noqa: PLR0904
|
||||
|
||||
self.call_after_refresh(schedule_switch)
|
||||
|
||||
async def action_toggle_debug_console(self) -> None:
|
||||
if self._debug_console is not None:
|
||||
await self._debug_console.remove()
|
||||
self._debug_console = None
|
||||
else:
|
||||
self._debug_console = DebugConsole(log_reader=self._log_reader)
|
||||
await self.mount(self._debug_console)
|
||||
|
||||
def action_clear_quit(self) -> None:
|
||||
input_widgets = self.query(ChatInputContainer)
|
||||
if input_widgets:
|
||||
@@ -2185,6 +2335,7 @@ class VibeApp(App): # noqa: PLR0904
|
||||
self._agent_task.cancel()
|
||||
self._remote_manager.cancel_stream_task()
|
||||
|
||||
self._log_reader.shutdown()
|
||||
self._narrator_manager.cancel()
|
||||
self.exit(result=self._get_session_resume_info())
|
||||
|
||||
|
||||
@@ -84,10 +84,17 @@ TextArea > .text-area--cursor {
|
||||
}
|
||||
|
||||
#completion-popup {
|
||||
width: 100%;
|
||||
padding: 1;
|
||||
padding-left: 3;
|
||||
overlay: screen;
|
||||
constrain: inside inside;
|
||||
display: none;
|
||||
width: auto;
|
||||
max-width: 80;
|
||||
height: auto;
|
||||
max-height: 12;
|
||||
padding: 0 1;
|
||||
color: ansi_default;
|
||||
background: $surface;
|
||||
border: solid ansi_bright_black;
|
||||
}
|
||||
|
||||
#input-box {
|
||||
@@ -679,7 +686,7 @@ StatusMessage {
|
||||
|
||||
}
|
||||
|
||||
#config-app, #voice-app {
|
||||
#config-app, #voice-app, #mcp-app {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
@@ -688,7 +695,7 @@ StatusMessage {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#config-content, #voice-content {
|
||||
#config-content, #voice-content, #mcp-content {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
@@ -699,13 +706,13 @@ StatusMessage {
|
||||
color: ansi_blue;
|
||||
}
|
||||
|
||||
#config-options {
|
||||
#config-options, #mcp-options {
|
||||
width: 100%;
|
||||
max-height: 50vh;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#config-options:focus {
|
||||
#config-options:focus, #mcp-options:focus {
|
||||
border: none;
|
||||
}
|
||||
|
||||
@@ -1089,6 +1096,35 @@ NarratorStatus {
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
/* Debug Console */
|
||||
|
||||
#debug-console {
|
||||
dock: right;
|
||||
width: 40%;
|
||||
max-width: 80;
|
||||
min-width: 40;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
border-left: solid ansi_bright_black;
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
#debug-console-header {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
padding: 0 0 1 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#debug-console-log {
|
||||
width: 100%;
|
||||
height: 1fr;
|
||||
background: transparent;
|
||||
overflow-x: hidden;
|
||||
scrollbar-size: 1 1;
|
||||
}
|
||||
|
||||
#modelpicker-app {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
@@ -33,15 +33,12 @@ class CompletionPopup(Static):
|
||||
text.append(description, style=description_style)
|
||||
|
||||
self.update(text)
|
||||
self.show()
|
||||
self.styles.display = "block"
|
||||
|
||||
def hide(self) -> None:
|
||||
self.update("")
|
||||
self.styles.display = "none"
|
||||
|
||||
def show(self) -> None:
|
||||
self.styles.display = "block"
|
||||
|
||||
def _display_label(self, label: str) -> str:
|
||||
if label.startswith("@"):
|
||||
return label[1:]
|
||||
|
||||
@@ -72,7 +72,6 @@ class ChatInputContainer(Vertical):
|
||||
self,
|
||||
),
|
||||
])
|
||||
self._completion_popup: CompletionPopup | None = None
|
||||
self._body: ChatInputBody | None = None
|
||||
|
||||
def _get_slash_entries(self) -> list[tuple[str, str]]:
|
||||
@@ -86,8 +85,7 @@ class ChatInputContainer(Vertical):
|
||||
return sorted(entries)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
self._completion_popup = CompletionPopup()
|
||||
yield self._completion_popup
|
||||
yield CompletionPopup()
|
||||
|
||||
border_class = self._get_border_class()
|
||||
with Vertical(id=self.ID_INPUT_BOX, classes=border_class) as input_box:
|
||||
@@ -138,12 +136,32 @@ class ChatInputContainer(Vertical):
|
||||
def render_completion_suggestions(
|
||||
self, suggestions: list[tuple[str, str]], selected_index: int
|
||||
) -> None:
|
||||
if self._completion_popup:
|
||||
self._completion_popup.update_suggestions(suggestions, selected_index)
|
||||
try:
|
||||
popup = self.query_one(CompletionPopup)
|
||||
except Exception:
|
||||
return
|
||||
popup.update_suggestions(suggestions, selected_index)
|
||||
self._position_popup(popup, len(suggestions))
|
||||
|
||||
def clear_completion_suggestions(self) -> None:
|
||||
if self._completion_popup:
|
||||
self._completion_popup.hide()
|
||||
try:
|
||||
popup = self.query_one(CompletionPopup)
|
||||
except Exception:
|
||||
return
|
||||
popup.hide()
|
||||
|
||||
def _position_popup(self, popup: CompletionPopup, line_count: int) -> None:
|
||||
widget = self.input_widget
|
||||
if not widget:
|
||||
return
|
||||
cursor = widget.cursor_screen_offset
|
||||
my_region = self.region
|
||||
# Place popup bottom edge just above the cursor row
|
||||
popup_height = line_count + 2 # +2 for solid border
|
||||
popup.styles.offset = (
|
||||
cursor.x - my_region.x,
|
||||
cursor.y - popup_height - my_region.y,
|
||||
)
|
||||
|
||||
def _format_insertion(self, replacement: str, suffix: str) -> str:
|
||||
"""Format the insertion text with appropriate spacing.
|
||||
|
||||
244
vibe/cli/textual_ui/widgets/debug_console.py
Normal file
@@ -0,0 +1,244 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import bisect
|
||||
from collections.abc import Callable
|
||||
|
||||
from rich.markup import escape
|
||||
from rich.text import Text
|
||||
from textual import events
|
||||
from textual.app import ComposeResult
|
||||
from textual.cache import LRUCache
|
||||
from textual.containers import Vertical
|
||||
from textual.geometry import Size
|
||||
from textual.scroll_view import ScrollView
|
||||
from textual.strip import Strip
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.core.log_reader import LogEntry, LogReader
|
||||
from vibe.core.logger import decode_log_message
|
||||
|
||||
LOG_LEVEL_COLORS: dict[str, str] = {
|
||||
"DEBUG": "dim",
|
||||
"INFO": "cyan",
|
||||
"WARNING": "yellow",
|
||||
"ERROR": "red",
|
||||
"CRITICAL": "bold red",
|
||||
}
|
||||
|
||||
DEFAULT_LOG_PAGE_SIZE = 30
|
||||
|
||||
|
||||
class _LogView(ScrollView, can_focus=True):
|
||||
def __init__(
|
||||
self,
|
||||
load_page: Callable[[], None],
|
||||
has_more: Callable[[], bool],
|
||||
*,
|
||||
id: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(id=id)
|
||||
self._lines: list[str] = []
|
||||
self._wrap_counts: list[int] = []
|
||||
self._wrap_prefix: list[int] = [0]
|
||||
self._total_visual: int = 0
|
||||
self._cached_width: int = 0
|
||||
self._render_line_cache: LRUCache[int, Strip] = LRUCache(1024)
|
||||
self._load_page = load_page
|
||||
self._has_more = has_more
|
||||
|
||||
def _wrap_markup(self, markup: str) -> int:
|
||||
"""Return the number of visual lines this markup produces at current width."""
|
||||
width = self._cached_width
|
||||
if width <= 0:
|
||||
return 1
|
||||
|
||||
text = Text.from_markup(markup, style=self.rich_style)
|
||||
|
||||
return len(text.wrap(self.app.console, width))
|
||||
|
||||
def _recompute_prefix(self) -> None:
|
||||
self._wrap_prefix = [0]
|
||||
for count in self._wrap_counts:
|
||||
self._wrap_prefix.append(self._wrap_prefix[-1] + count)
|
||||
self._total_visual = self._wrap_prefix[-1]
|
||||
|
||||
def _reflow(self) -> None:
|
||||
"""Re-wrap all lines at current widget width."""
|
||||
width = self.size.width
|
||||
if width <= 0:
|
||||
return
|
||||
self._cached_width = width
|
||||
self._render_line_cache.clear()
|
||||
self._wrap_counts = [self._wrap_markup(m) for m in self._lines]
|
||||
self._recompute_prefix()
|
||||
self.virtual_size = Size(width, self._total_visual)
|
||||
|
||||
def write_line(self, markup: str, scroll_end: bool | None = None) -> None:
|
||||
at_bottom = self.is_vertical_scroll_end
|
||||
width = self._cached_width or self.size.width
|
||||
self._cached_width = width
|
||||
|
||||
self._lines.append(markup)
|
||||
count = self._wrap_markup(markup)
|
||||
self._wrap_counts.append(count)
|
||||
self._wrap_prefix.append(self._wrap_prefix[-1] + count)
|
||||
self._total_visual += count
|
||||
self.virtual_size = Size(width, self._total_visual)
|
||||
|
||||
if scroll_end or (scroll_end is None and at_bottom):
|
||||
self.scroll_end(animate=False, immediate=True, x_axis=False)
|
||||
|
||||
def prepend_lines(self, markups: list[str]) -> None:
|
||||
if not markups:
|
||||
return
|
||||
width = self._cached_width or self.size.width
|
||||
self._cached_width = width
|
||||
|
||||
new_counts = [self._wrap_markup(m) for m in markups]
|
||||
new_visual = sum(new_counts)
|
||||
|
||||
self._lines[0:0] = markups
|
||||
self._wrap_counts[0:0] = new_counts
|
||||
self._recompute_prefix()
|
||||
self._render_line_cache.clear()
|
||||
self.virtual_size = Size(width, self._total_visual)
|
||||
self.scroll_to(y=self.scroll_y + new_visual, animate=False, immediate=True)
|
||||
|
||||
def render_line(self, y: int) -> Strip:
|
||||
_, scroll_y = self.scroll_offset
|
||||
abs_y = scroll_y + y
|
||||
width = self.size.width
|
||||
wrap_width = self._cached_width or width
|
||||
rich_style = self.rich_style
|
||||
|
||||
if abs_y >= self._total_visual:
|
||||
return Strip.blank(width, rich_style)
|
||||
if abs_y in self._render_line_cache:
|
||||
return self._render_line_cache[abs_y]
|
||||
|
||||
logical_idx = bisect.bisect_right(self._wrap_prefix, abs_y) - 1
|
||||
text = Text.from_markup(self._lines[logical_idx], style=rich_style)
|
||||
wrapped = text.wrap(self.app.console, wrap_width)
|
||||
|
||||
base = self._wrap_prefix[logical_idx]
|
||||
for i, line_text in enumerate(wrapped):
|
||||
strip = Strip(line_text.render(self.app.console), line_text.cell_len)
|
||||
strip = strip.crop_extend(0, width, rich_style)
|
||||
self._render_line_cache[base + i] = strip
|
||||
|
||||
try:
|
||||
return self._render_line_cache[abs_y]
|
||||
except KeyError:
|
||||
return Strip.blank(width, rich_style)
|
||||
|
||||
def notify_style_update(self) -> None:
|
||||
super().notify_style_update()
|
||||
self._render_line_cache.clear()
|
||||
|
||||
def on_resize(self, event: events.Resize) -> None:
|
||||
if event.size.width != self._cached_width:
|
||||
self._reflow()
|
||||
|
||||
def on_click(self, event: events.Click) -> None:
|
||||
_, scroll_y = self.scroll_offset
|
||||
visual_y = scroll_y + event.y
|
||||
logical_idx = bisect.bisect_right(self._wrap_prefix, visual_y) - 1
|
||||
if 0 <= logical_idx < len(self._lines):
|
||||
plain = Text.from_markup(self._lines[logical_idx]).plain
|
||||
self.app.copy_to_clipboard(plain)
|
||||
self.app.notify("Copied to clipboard", timeout=2.0)
|
||||
|
||||
def _try_load_previous(self) -> None:
|
||||
if not self._has_more() or self.scroll_y > 0:
|
||||
return
|
||||
self._load_page()
|
||||
|
||||
def _on_mouse_scroll_up(self, event: events.MouseScrollUp) -> None:
|
||||
super()._on_mouse_scroll_up(event)
|
||||
self._try_load_previous()
|
||||
|
||||
def action_scroll_up(self) -> None:
|
||||
super().action_scroll_up()
|
||||
self._try_load_previous()
|
||||
|
||||
def action_page_up(self) -> None:
|
||||
super().action_page_up()
|
||||
self._try_load_previous()
|
||||
|
||||
def action_scroll_home(self) -> None:
|
||||
super().action_scroll_home()
|
||||
self._try_load_previous()
|
||||
|
||||
|
||||
class DebugConsole(Vertical):
|
||||
def __init__(
|
||||
self, log_reader: LogReader, page_size: int = DEFAULT_LOG_PAGE_SIZE
|
||||
) -> None:
|
||||
super().__init__(id="debug-console")
|
||||
self._log_reader = log_reader
|
||||
self._log_view: _LogView | None = None
|
||||
self._cursor: int | None = None
|
||||
self._has_more: bool = True
|
||||
self._page_size = page_size
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static(
|
||||
"Debug Console [dim](ctrl+\\ to close)[/dim]", id="debug-console-header"
|
||||
)
|
||||
self._log_view = _LogView(
|
||||
load_page=self._load_page,
|
||||
has_more=lambda: self._has_more and self._cursor is not None,
|
||||
id="debug-console-log",
|
||||
)
|
||||
yield self._log_view
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._fill_viewport()
|
||||
self._log_reader.set_consumer(self._on_log_entry)
|
||||
self._log_reader.start_watching()
|
||||
|
||||
def on_unmount(self) -> None:
|
||||
self._log_reader.set_consumer(None)
|
||||
self._log_reader.stop_watching()
|
||||
|
||||
def _load_page(self) -> None:
|
||||
if self._log_view is None:
|
||||
return
|
||||
result = self._log_reader.get_logs(
|
||||
limit=self._page_size, offset=self._cursor or 0
|
||||
)
|
||||
self._cursor = result.cursor
|
||||
self._has_more = result.has_more
|
||||
markups = [self._format_entry(e) for e in reversed(result.entries)]
|
||||
self._log_view.prepend_lines(markups)
|
||||
|
||||
def _fill_viewport(self) -> None:
|
||||
"""Load enough logs to fill the viewport, then scroll to the bottom."""
|
||||
if self._log_view is None or not self._has_more:
|
||||
return
|
||||
self.call_after_refresh(self._check_and_fill)
|
||||
|
||||
def _check_and_fill(self) -> None:
|
||||
if self._log_view is None or not self._has_more:
|
||||
return
|
||||
if self._log_view.virtual_size.height <= self._log_view.size.height:
|
||||
self._load_page()
|
||||
self._fill_viewport()
|
||||
else:
|
||||
self._log_view.scroll_end(animate=False)
|
||||
|
||||
def _on_log_entry(self, entry: LogEntry) -> None:
|
||||
self.app.call_from_thread(self._append_log_entry, entry)
|
||||
|
||||
def _append_log_entry(self, entry: LogEntry) -> None:
|
||||
if self._log_view is None:
|
||||
return
|
||||
self._log_view.write_line(self._format_entry(entry))
|
||||
|
||||
@staticmethod
|
||||
def _format_entry(entry: LogEntry) -> str:
|
||||
color = LOG_LEVEL_COLORS.get(entry.level, "dim")
|
||||
ts = entry.timestamp.astimezone().strftime("%Y-%m-%d %H:%M:%S")
|
||||
message = decode_log_message(entry.message)
|
||||
safe_message = escape(message)
|
||||
return f"[dim]{ts}[/dim] [{color}]{entry.level:<8}[/{color}] {safe_message}"
|
||||
157
vibe/cli/textual_ui/widgets/mcp_app.py
Normal file
@@ -0,0 +1,157 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple
|
||||
|
||||
from rich.text import Text
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding, BindingType
|
||||
from textual.containers import Container, Vertical
|
||||
from textual.events import DescendantBlur
|
||||
from textual.message import Message
|
||||
from textual.widgets import OptionList
|
||||
from textual.widgets.option_list import Option
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.core.tools.mcp.tools import MCPTool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vibe.core.config import MCPServer
|
||||
from vibe.core.tools.manager import ToolManager
|
||||
|
||||
|
||||
class MCPToolIndex(NamedTuple):
|
||||
server_tools: dict[str, list[tuple[str, type[MCPTool]]]]
|
||||
enabled_tools: dict[str, type[Any]]
|
||||
|
||||
|
||||
def collect_mcp_tool_index(
|
||||
mcp_servers: Sequence[MCPServer], tool_manager: ToolManager
|
||||
) -> MCPToolIndex:
|
||||
registered = tool_manager.registered_tools
|
||||
available = tool_manager.available_tools
|
||||
configured_servers = {server.name for server in mcp_servers}
|
||||
server_tools: dict[str, list[tuple[str, type[MCPTool]]]] = {}
|
||||
|
||||
for tool_name, cls in registered.items():
|
||||
if not issubclass(cls, MCPTool):
|
||||
continue
|
||||
server_name = cls.get_server_name()
|
||||
if server_name is None or server_name not in configured_servers:
|
||||
continue
|
||||
server_tools.setdefault(server_name, []).append((tool_name, cls))
|
||||
|
||||
return MCPToolIndex(server_tools, enabled_tools=available)
|
||||
|
||||
|
||||
class MCPApp(Container):
|
||||
can_focus_children = True
|
||||
BINDINGS: ClassVar[list[BindingType]] = [
|
||||
Binding("escape", "close", "Close", show=False),
|
||||
Binding("backspace", "back", "Back", show=False),
|
||||
]
|
||||
|
||||
class MCPClosed(Message):
|
||||
pass
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mcp_servers: Sequence[MCPServer],
|
||||
tool_manager: ToolManager,
|
||||
initial_server: str = "",
|
||||
) -> None:
|
||||
super().__init__(id="mcp-app")
|
||||
self._mcp_servers = mcp_servers
|
||||
self._tool_manager = tool_manager
|
||||
self._index = collect_mcp_tool_index(mcp_servers, tool_manager)
|
||||
self._viewing_server: str | None = initial_server.strip() or None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="mcp-content"):
|
||||
yield NoMarkupStatic("", id="mcp-title", classes="settings-title")
|
||||
yield NoMarkupStatic("")
|
||||
yield OptionList(id="mcp-options")
|
||||
yield NoMarkupStatic("")
|
||||
yield NoMarkupStatic("", id="mcp-help", classes="settings-help")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._refresh_view(self._viewing_server)
|
||||
self.query_one(OptionList).focus()
|
||||
|
||||
def on_descendant_blur(self, _event: DescendantBlur) -> None:
|
||||
self.query_one(OptionList).focus()
|
||||
|
||||
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
||||
option_id = event.option.id or ""
|
||||
if option_id.startswith("server:"):
|
||||
self._refresh_view(option_id.removeprefix("server:"))
|
||||
|
||||
def action_back(self) -> None:
|
||||
if self._viewing_server is not None:
|
||||
self._refresh_view(None)
|
||||
|
||||
def action_close(self) -> None:
|
||||
self.post_message(self.MCPClosed())
|
||||
|
||||
def _refresh_view(self, server_name: str | None) -> None:
|
||||
index = self._index
|
||||
option_list = self.query_one(OptionList)
|
||||
option_list.clear_options()
|
||||
|
||||
server_names = {s.name for s in self._mcp_servers}
|
||||
if server_name is None or server_name not in server_names:
|
||||
self._viewing_server = None
|
||||
self.query_one("#mcp-title", NoMarkupStatic).update("MCP Servers")
|
||||
self.query_one("#mcp-help", NoMarkupStatic).update(
|
||||
"↑↓ Navigate Enter Show tools Esc Close"
|
||||
)
|
||||
for srv in self._mcp_servers:
|
||||
tools = index.server_tools.get(srv.name, [])
|
||||
enabled = sum(1 for t, _ in tools if t in index.enabled_tools)
|
||||
status = _server_status(enabled)
|
||||
label = Text(no_wrap=True)
|
||||
label.append(srv.name)
|
||||
label.append(f" [{srv.transport}] {status}")
|
||||
option_list.add_option(Option(label, id=f"server:{srv.name}"))
|
||||
if self._mcp_servers:
|
||||
option_list.highlighted = 0
|
||||
return
|
||||
|
||||
self._viewing_server = server_name
|
||||
self.query_one("#mcp-title", NoMarkupStatic).update(
|
||||
f"MCP Server: {server_name}"
|
||||
)
|
||||
self.query_one("#mcp-help", NoMarkupStatic).update(
|
||||
"↑↓ Navigate Backspace Back Esc Close"
|
||||
)
|
||||
enabled_tools = [
|
||||
(tool_name, cls)
|
||||
for tool_name, cls in sorted(
|
||||
index.server_tools.get(server_name, []), key=lambda t: t[0]
|
||||
)
|
||||
if tool_name in index.enabled_tools
|
||||
]
|
||||
if not enabled_tools:
|
||||
option_list.add_option(
|
||||
Option("No enabled tools for this server", disabled=True)
|
||||
)
|
||||
return
|
||||
for tool_name, cls in enabled_tools:
|
||||
remote_name = cls.get_remote_name()
|
||||
raw_desc = (
|
||||
(cls.description or "").removeprefix(f"[{server_name}] ").split("\n")[0]
|
||||
)
|
||||
label = Text(no_wrap=True)
|
||||
label.append(remote_name, style="bold")
|
||||
if raw_desc:
|
||||
label.append(f" - {raw_desc}")
|
||||
option_list.add_option(Option(label, id=f"tool:{tool_name}"))
|
||||
if enabled_tools:
|
||||
option_list.highlighted = 0
|
||||
|
||||
|
||||
def _server_status(enabled: int) -> str:
|
||||
if enabled == 0:
|
||||
return "unavailable"
|
||||
noun = "tool" if enabled == 1 else "tools"
|
||||
return f"{enabled} {noun} enabled"
|
||||
@@ -1,5 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["run_programmatic"]
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from vibe.core.programmatic import run_programmatic
|
||||
if TYPE_CHECKING:
|
||||
from vibe.core.programmatic import run_programmatic as run_programmatic
|
||||
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
if name == "run_programmatic":
|
||||
from vibe.core.programmatic import run_programmatic
|
||||
|
||||
return run_programmatic
|
||||
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
@@ -159,11 +159,14 @@ class AgentLoop:
|
||||
backend: BackendLike | None = None,
|
||||
enable_streaming: bool = False,
|
||||
entrypoint_metadata: EntrypointMetadata | None = None,
|
||||
is_subagent: bool = False,
|
||||
) -> None:
|
||||
self._base_config = config
|
||||
self.mcp_registry = MCPRegistry()
|
||||
self.agent_manager = AgentManager(
|
||||
lambda: self._base_config, initial_agent=agent_name
|
||||
lambda: self._base_config,
|
||||
initial_agent=agent_name,
|
||||
allow_subagent=is_subagent,
|
||||
)
|
||||
self.tool_manager = ToolManager(
|
||||
lambda: self.config, mcp_registry=self.mcp_registry
|
||||
@@ -209,6 +212,7 @@ class AgentLoop:
|
||||
self._is_user_prompt_call: bool = False
|
||||
|
||||
self._session_rules: list[ApprovedRule] = []
|
||||
self._approval_lock = asyncio.Lock()
|
||||
|
||||
self.telemetry_client = TelemetryClient(
|
||||
config_getter=lambda: self.config, session_id_getter=lambda: self.session_id
|
||||
@@ -267,7 +271,6 @@ class AgentLoop:
|
||||
self.config.tools[tool_name] = {}
|
||||
|
||||
self.config.tools[tool_name]["permission"] = permission.value
|
||||
self.tool_manager.invalidate_tool(tool_name)
|
||||
|
||||
def add_session_rule(self, rule: ApprovedRule) -> None:
|
||||
self._session_rules.append(rule)
|
||||
@@ -350,6 +353,10 @@ class AgentLoop:
|
||||
self.agent_profile,
|
||||
)
|
||||
|
||||
async def inject_user_context(self, content: str) -> None:
|
||||
self.messages.append(LLMMessage(role=Role.user, content=content, injected=True))
|
||||
await self._save_messages()
|
||||
|
||||
async def act(
|
||||
self, msg: str, client_message_id: str | None = None
|
||||
) -> AsyncGenerator[BaseEvent, None]:
|
||||
@@ -1019,40 +1026,41 @@ class AgentLoop:
|
||||
approval_type=ToolPermission.ALWAYS,
|
||||
)
|
||||
|
||||
tool_name = tool.get_name()
|
||||
ctx = tool.resolve_permission(args)
|
||||
async with self._approval_lock:
|
||||
tool_name = tool.get_name()
|
||||
ctx = tool.resolve_permission(args)
|
||||
|
||||
if ctx is None:
|
||||
config_perm = self.tool_manager.get_tool_config(tool_name).permission
|
||||
ctx = PermissionContext(permission=config_perm)
|
||||
if ctx is None:
|
||||
config_perm = self.tool_manager.get_tool_config(tool_name).permission
|
||||
ctx = PermissionContext(permission=config_perm)
|
||||
|
||||
match ctx.permission:
|
||||
case ToolPermission.ALWAYS:
|
||||
return ToolDecision(
|
||||
verdict=ToolExecutionResponse.EXECUTE,
|
||||
approval_type=ToolPermission.ALWAYS,
|
||||
)
|
||||
case ToolPermission.NEVER:
|
||||
return ToolDecision(
|
||||
verdict=ToolExecutionResponse.SKIP,
|
||||
approval_type=ToolPermission.NEVER,
|
||||
feedback=ctx.reason
|
||||
or f"Tool '{tool_name}' is permanently disabled",
|
||||
)
|
||||
case _:
|
||||
uncovered = [
|
||||
rp
|
||||
for rp in ctx.required_permissions
|
||||
if not self._is_permission_covered(tool_name, rp)
|
||||
]
|
||||
if ctx.required_permissions and not uncovered:
|
||||
match ctx.permission:
|
||||
case ToolPermission.ALWAYS:
|
||||
return ToolDecision(
|
||||
verdict=ToolExecutionResponse.EXECUTE,
|
||||
approval_type=ToolPermission.ALWAYS,
|
||||
)
|
||||
return await self._ask_approval(
|
||||
tool_name, args, tool_call_id, uncovered
|
||||
)
|
||||
case ToolPermission.NEVER:
|
||||
return ToolDecision(
|
||||
verdict=ToolExecutionResponse.SKIP,
|
||||
approval_type=ToolPermission.NEVER,
|
||||
feedback=ctx.reason
|
||||
or f"Tool '{tool_name}' is permanently disabled",
|
||||
)
|
||||
case _:
|
||||
uncovered = [
|
||||
rp
|
||||
for rp in ctx.required_permissions
|
||||
if not self._is_permission_covered(tool_name, rp)
|
||||
]
|
||||
if ctx.required_permissions and not uncovered:
|
||||
return ToolDecision(
|
||||
verdict=ToolExecutionResponse.EXECUTE,
|
||||
approval_type=ToolPermission.ALWAYS,
|
||||
)
|
||||
return await self._ask_approval(
|
||||
tool_name, args, tool_call_id, uncovered
|
||||
)
|
||||
|
||||
async def _ask_approval(
|
||||
self,
|
||||
|
||||
@@ -23,6 +23,7 @@ class AgentManager:
|
||||
self,
|
||||
config_getter: Callable[[], VibeConfig],
|
||||
initial_agent: str = BuiltinAgentName.DEFAULT,
|
||||
allow_subagent: bool = False,
|
||||
) -> None:
|
||||
self._config_getter = config_getter
|
||||
self._search_paths = self._compute_search_paths(self._config)
|
||||
@@ -39,9 +40,18 @@ class AgentManager:
|
||||
" ".join(str(p) for p in self._search_paths),
|
||||
)
|
||||
|
||||
self.active_profile = self._available.get(
|
||||
initial_agent, self._available[BuiltinAgentName.DEFAULT]
|
||||
)
|
||||
profile = self._available.get(initial_agent)
|
||||
if (
|
||||
not allow_subagent
|
||||
and profile is not None
|
||||
and profile.agent_type != AgentType.AGENT
|
||||
):
|
||||
raise ValueError(
|
||||
f"Agent '{initial_agent}' is a {profile.agent_type} and cannot be used"
|
||||
f" as the primary agent. Only agents of type 'agent' can be selected"
|
||||
f" with --agent."
|
||||
)
|
||||
self.active_profile = profile or self._available[BuiltinAgentName.DEFAULT]
|
||||
self._cached_config: VibeConfig | None = None
|
||||
|
||||
@property
|
||||
|
||||
@@ -177,7 +177,7 @@ LEAN = AgentProfile(
|
||||
}
|
||||
],
|
||||
"compaction_model": {
|
||||
"name": "mistral-vibe-cli-latest",
|
||||
"name": "mistral-small-latest",
|
||||
"provider": "mistral-testing",
|
||||
"alias": "devstral-compact",
|
||||
"temperature": 0.2,
|
||||
|
||||
@@ -41,6 +41,10 @@ class WatchController:
|
||||
thread.start()
|
||||
ready_event.wait(timeout=0.5)
|
||||
|
||||
@property
|
||||
def is_watching(self) -> bool:
|
||||
return self._thread is not None and self._thread.is_alive()
|
||||
|
||||
def stop(self) -> None:
|
||||
thread = self._thread
|
||||
if self._stop_event:
|
||||
|
||||
@@ -15,7 +15,7 @@ from vibe.core.config._settings import (
|
||||
MissingAPIKeyError,
|
||||
MissingPromptFileError,
|
||||
ModelConfig,
|
||||
OtelExporterConfig,
|
||||
OtelSpanExporterConfig,
|
||||
ProjectContextConfig,
|
||||
ProviderConfig,
|
||||
SessionLoggingConfig,
|
||||
@@ -45,7 +45,7 @@ __all__ = [
|
||||
"MissingAPIKeyError",
|
||||
"MissingPromptFileError",
|
||||
"ModelConfig",
|
||||
"OtelExporterConfig",
|
||||
"OtelSpanExporterConfig",
|
||||
"ProjectContextConfig",
|
||||
"ProviderConfig",
|
||||
"SessionLoggingConfig",
|
||||
|
||||
@@ -11,6 +11,9 @@ from typing import Annotated, Any, Literal
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from dotenv import dotenv_values
|
||||
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
||||
DEFAULT_TRACES_EXPORT_PATH,
|
||||
)
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from pydantic.fields import FieldInfo
|
||||
from pydantic_core import to_jsonable_python
|
||||
@@ -291,7 +294,7 @@ class TTSModelConfig(BaseModel):
|
||||
_default_alias_to_name = model_validator(mode="before")(_default_alias_to_name)
|
||||
|
||||
|
||||
class OtelExporterConfig(BaseModel):
|
||||
class OtelSpanExporterConfig(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
endpoint: str
|
||||
@@ -299,7 +302,7 @@ class OtelExporterConfig(BaseModel):
|
||||
|
||||
|
||||
DEFAULT_MISTRAL_API_ENV_KEY = "MISTRAL_API_KEY"
|
||||
MISTRAL_OTEL_TRACES_PATH = "/telemetry/v1/traces"
|
||||
MISTRAL_OTEL_PATH = "/telemetry"
|
||||
_DEFAULT_MISTRAL_SERVER_URL = "https://api.mistral.ai"
|
||||
|
||||
DEFAULT_PROVIDERS = [
|
||||
@@ -523,12 +526,17 @@ class VibeConfig(BaseSettings):
|
||||
return os.getenv(self.nuage_api_key_env_var, "")
|
||||
|
||||
@property
|
||||
def otel_exporter_config(self) -> OtelExporterConfig | None:
|
||||
def otel_span_exporter_config(self) -> OtelSpanExporterConfig | None:
|
||||
# When otel_endpoint is set explicitly, authentication is the user's responsibility
|
||||
# (via OTEL_EXPORTER_OTLP_* env vars), so headers are left empty.
|
||||
# Otherwise endpoint and API key are derived from the first MISTRAL provider.
|
||||
traces_export_path = DEFAULT_TRACES_EXPORT_PATH.lstrip("/")
|
||||
if self.otel_endpoint:
|
||||
return OtelExporterConfig(endpoint=self.otel_endpoint)
|
||||
return OtelSpanExporterConfig(
|
||||
endpoint=urljoin(
|
||||
f"{self.otel_endpoint.rstrip('/')}/", traces_export_path
|
||||
)
|
||||
)
|
||||
|
||||
provider = next(
|
||||
(p for p in self.providers if p.backend == Backend.MISTRAL), None
|
||||
@@ -542,7 +550,8 @@ class VibeConfig(BaseSettings):
|
||||
api_key_env = DEFAULT_MISTRAL_API_ENV_KEY
|
||||
|
||||
endpoint = urljoin(
|
||||
server_url or _DEFAULT_MISTRAL_SERVER_URL, MISTRAL_OTEL_TRACES_PATH
|
||||
f"{urljoin(server_url or _DEFAULT_MISTRAL_SERVER_URL, MISTRAL_OTEL_PATH).rstrip('/')}/",
|
||||
traces_export_path,
|
||||
)
|
||||
|
||||
if not (api_key := os.getenv(api_key_env)):
|
||||
@@ -551,7 +560,7 @@ class VibeConfig(BaseSettings):
|
||||
)
|
||||
return None
|
||||
|
||||
return OtelExporterConfig(
|
||||
return OtelSpanExporterConfig(
|
||||
endpoint=endpoint, headers={"Authorization": f"Bearer {api_key}"}
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import httpx
|
||||
from vibe.core.llm.backend.anthropic import AnthropicAdapter
|
||||
from vibe.core.llm.backend.base import APIAdapter, PreparedRequest
|
||||
from vibe.core.llm.backend.reasoning_adapter import ReasoningAdapter
|
||||
from vibe.core.llm.backend.vertex import VertexAnthropicAdapter
|
||||
from vibe.core.llm.exceptions import BackendErrorBuilder
|
||||
from vibe.core.llm.message_utils import merge_consecutive_user_messages
|
||||
from vibe.core.types import (
|
||||
@@ -160,14 +159,27 @@ class OpenAIAdapter(APIAdapter):
|
||||
return LLMChunk(message=message, usage=usage)
|
||||
|
||||
|
||||
ADAPTERS: dict[str, APIAdapter] = {
|
||||
_ADAPTERS: dict[str, APIAdapter] = {
|
||||
"openai": OpenAIAdapter(),
|
||||
"anthropic": AnthropicAdapter(),
|
||||
"vertex-anthropic": VertexAnthropicAdapter(),
|
||||
"reasoning": ReasoningAdapter(),
|
||||
}
|
||||
|
||||
|
||||
def _get_adapter(api_style: str) -> APIAdapter:
|
||||
"""Loads the appropriate adapter for the given API style,
|
||||
lazily if the adapter is not already loaded.
|
||||
"""
|
||||
if api_style not in _ADAPTERS:
|
||||
if api_style == "vertex-anthropic":
|
||||
from vibe.core.llm.backend.vertex import VertexAnthropicAdapter
|
||||
|
||||
_ADAPTERS["vertex-anthropic"] = VertexAnthropicAdapter()
|
||||
else:
|
||||
raise KeyError(api_style)
|
||||
return _ADAPTERS[api_style]
|
||||
|
||||
|
||||
class GenericBackend:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -232,7 +244,7 @@ class GenericBackend:
|
||||
)
|
||||
|
||||
api_style = getattr(self._provider, "api_style", "openai")
|
||||
adapter = ADAPTERS[api_style]
|
||||
adapter = _get_adapter(api_style)
|
||||
|
||||
req = adapter.prepare_request(
|
||||
model_name=model.name,
|
||||
@@ -300,7 +312,7 @@ class GenericBackend:
|
||||
)
|
||||
|
||||
api_style = getattr(self._provider, "api_style", "openai")
|
||||
adapter = ADAPTERS[api_style]
|
||||
adapter = _get_adapter(api_style)
|
||||
|
||||
req = adapter.prepare_request(
|
||||
model_name=model.name,
|
||||
|
||||
@@ -32,10 +32,7 @@ from mistralai.client.models import (
|
||||
from mistralai.client.utils.retries import BackoffStrategy, RetryConfig
|
||||
|
||||
from vibe.core.llm.exceptions import BackendErrorBuilder
|
||||
from vibe.core.llm.message_utils import (
|
||||
merge_consecutive_user_messages,
|
||||
strip_reasoning as strip_reasoning_message,
|
||||
)
|
||||
from vibe.core.llm.message_utils import merge_consecutive_user_messages
|
||||
from vibe.core.types import (
|
||||
AvailableTool,
|
||||
Content,
|
||||
@@ -171,9 +168,6 @@ class MistralMapper:
|
||||
for tool_call in tool_calls
|
||||
]
|
||||
|
||||
def strip_reasoning(self, msg: LLMMessage) -> LLMMessage:
|
||||
return strip_reasoning_message(msg)
|
||||
|
||||
|
||||
ReasoningEffortValue = Literal["none", "high"]
|
||||
|
||||
@@ -271,10 +265,6 @@ class MistralBackend:
|
||||
reasoning_effort = _THINKING_TO_REASONING_EFFORT.get(model.thinking)
|
||||
if reasoning_effort is not None:
|
||||
temperature = 1.0
|
||||
else:
|
||||
merged_messages = [
|
||||
strip_reasoning_message(msg) for msg in merged_messages
|
||||
]
|
||||
|
||||
response = await self._get_client().chat.complete_async(
|
||||
model=model.name,
|
||||
@@ -355,10 +345,6 @@ class MistralBackend:
|
||||
reasoning_effort = _THINKING_TO_REASONING_EFFORT.get(model.thinking)
|
||||
if reasoning_effort is not None:
|
||||
temperature = 1.0
|
||||
else:
|
||||
merged_messages = [
|
||||
strip_reasoning_message(msg) for msg in merged_messages
|
||||
]
|
||||
|
||||
stream = await self._get_client().chat.stream_async(
|
||||
model=model.name,
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Any, ClassVar
|
||||
|
||||
from vibe.core.config import ProviderConfig
|
||||
from vibe.core.llm.backend.base import APIAdapter, PreparedRequest
|
||||
from vibe.core.llm.message_utils import merge_consecutive_user_messages, strip_reasoning
|
||||
from vibe.core.llm.message_utils import merge_consecutive_user_messages
|
||||
from vibe.core.types import (
|
||||
AvailableTool,
|
||||
FunctionCall,
|
||||
@@ -122,8 +122,6 @@ class ReasoningAdapter(APIAdapter):
|
||||
thinking: str = "off",
|
||||
) -> PreparedRequest:
|
||||
merged_messages = merge_consecutive_user_messages(messages)
|
||||
if thinking == "off":
|
||||
merged_messages = [strip_reasoning(msg) for msg in merged_messages]
|
||||
converted_messages = [self._convert_message(msg) for msg in merged_messages]
|
||||
|
||||
payload = self._build_payload(
|
||||
|
||||
@@ -5,14 +5,6 @@ from collections.abc import Sequence
|
||||
from vibe.core.types import LLMMessage, Role
|
||||
|
||||
|
||||
def strip_reasoning(msg: LLMMessage) -> LLMMessage:
|
||||
if msg.role != Role.assistant or not msg.reasoning_content:
|
||||
return msg
|
||||
return msg.model_copy(
|
||||
update={"reasoning_content": None, "reasoning_signature": None}
|
||||
)
|
||||
|
||||
|
||||
def merge_consecutive_user_messages(messages: Sequence[LLMMessage]) -> list[LLMMessage]:
|
||||
"""Merge consecutive user messages into a single message.
|
||||
|
||||
|
||||
224
vibe/core/log_reader.py
Normal file
@@ -0,0 +1,224 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import re
|
||||
import threading
|
||||
|
||||
from vibe.core.logger import logger
|
||||
from vibe.core.paths import LOG_FILE
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LogEntry:
|
||||
timestamp: datetime
|
||||
ppid: int
|
||||
pid: int
|
||||
level: str
|
||||
message: str
|
||||
raw_line: str
|
||||
line_number: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PaginatedLogs:
|
||||
entries: list[LogEntry]
|
||||
has_more: bool
|
||||
cursor: int | None
|
||||
|
||||
|
||||
LogConsumer = Callable[[LogEntry], None]
|
||||
|
||||
# Format: timestamp ppid pid level message [exception]
|
||||
# Timestamp is ISO 8601 from datetime.isoformat(), e.g. 2026-02-21T10:28:51.529718+00:00
|
||||
DEFAULT_LOG_PATTERN = re.compile(
|
||||
r"^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+(?:[+-]\d{2}:\d{2})?)\s+"
|
||||
r"(\d+)\s+(\d+)\s+"
|
||||
r"(DEBUG|INFO|WARNING|ERROR|CRITICAL)\s+"
|
||||
r"(.+)$"
|
||||
)
|
||||
|
||||
LOG_POLL_INTERVAL = 0.5
|
||||
|
||||
|
||||
class LogReader:
|
||||
def __init__(
|
||||
self,
|
||||
log_file: Path | None = None,
|
||||
consumer: LogConsumer | None = None,
|
||||
log_pattern: re.Pattern[str] = DEFAULT_LOG_PATTERN,
|
||||
poll_interval: float = LOG_POLL_INTERVAL,
|
||||
) -> None:
|
||||
self._log_file = log_file if log_file is not None else LOG_FILE.path
|
||||
self._consumer = consumer
|
||||
self._log_pattern = log_pattern
|
||||
self._lock = threading.Lock()
|
||||
self._last_position: int = 0
|
||||
self._new_lines_count: int = 0
|
||||
self._stop_event: threading.Event | None = None
|
||||
self._thread: threading.Thread | None = None
|
||||
self._poll_interval = poll_interval
|
||||
|
||||
@property
|
||||
def log_file(self) -> Path:
|
||||
return self._log_file
|
||||
|
||||
@property
|
||||
def is_watching(self) -> bool:
|
||||
return self._thread is not None and self._thread.is_alive()
|
||||
|
||||
def get_logs(self, limit: int = 100, offset: int = 0) -> PaginatedLogs:
|
||||
if not self._log_file.exists():
|
||||
return PaginatedLogs(entries=[], has_more=False, cursor=None)
|
||||
|
||||
entries: list[LogEntry] = []
|
||||
|
||||
for line, relative_position in self._read_lines_backward(start_position=offset):
|
||||
if entry := self._parse_line(line, relative_position):
|
||||
entries.append(entry)
|
||||
if len(entries) >= limit:
|
||||
return PaginatedLogs(
|
||||
entries=entries, has_more=True, cursor=offset + len(entries)
|
||||
)
|
||||
|
||||
return PaginatedLogs(entries=entries, has_more=False, cursor=None)
|
||||
|
||||
def _read_lines_backward(self, start_position: int) -> Iterator[tuple[str, int]]:
|
||||
if not self._log_file.exists():
|
||||
return
|
||||
|
||||
with self._lock:
|
||||
# Snapshot once: file end-position is also captured once (seek EOF below),
|
||||
# so new lines appended by the poll thread after this point are invisible
|
||||
# to this iteration and the skip count stays consistent.
|
||||
adjusted_skip = start_position + self._new_lines_count
|
||||
chunk_size = 8192
|
||||
relative_position = 0
|
||||
skipped = 0
|
||||
|
||||
with self._log_file.open("rb") as f:
|
||||
f.seek(0, 2)
|
||||
position = f.tell()
|
||||
remainder = b""
|
||||
|
||||
while position > 0:
|
||||
read_size = min(chunk_size, position)
|
||||
position -= read_size
|
||||
f.seek(position)
|
||||
chunk = f.read(read_size) + remainder
|
||||
|
||||
lines = chunk.split(b"\n")
|
||||
remainder = lines[0]
|
||||
|
||||
for line in reversed(lines[1:]):
|
||||
if not line:
|
||||
continue
|
||||
if skipped < adjusted_skip:
|
||||
skipped += 1
|
||||
continue
|
||||
relative_position += 1
|
||||
yield line.decode("utf-8", errors="replace"), relative_position
|
||||
|
||||
if remainder:
|
||||
if skipped < adjusted_skip:
|
||||
return
|
||||
relative_position += 1
|
||||
yield remainder.decode("utf-8", errors="replace"), relative_position
|
||||
|
||||
def set_consumer(self, consumer: LogConsumer | None) -> None:
|
||||
self._consumer = consumer
|
||||
|
||||
def start_watching(self) -> None:
|
||||
if self._thread and self._thread.is_alive():
|
||||
return
|
||||
|
||||
with self._lock:
|
||||
self._last_position = (
|
||||
self._log_file.stat().st_size if self._log_file.exists() else 0
|
||||
)
|
||||
|
||||
self._stop_event = threading.Event()
|
||||
self._thread = threading.Thread(
|
||||
target=self._poll_log_loop, name="log-reader-poll", daemon=True
|
||||
)
|
||||
self._thread.start()
|
||||
|
||||
def stop_watching(self) -> None:
|
||||
if self._stop_event:
|
||||
self._stop_event.set()
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(timeout=1)
|
||||
self._thread = None
|
||||
self._stop_event = None
|
||||
with self._lock:
|
||||
self._new_lines_count = 0
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self.stop_watching()
|
||||
|
||||
def _poll_log_loop(self) -> None:
|
||||
stop_event = self._stop_event
|
||||
if stop_event is None:
|
||||
return
|
||||
while not stop_event.is_set():
|
||||
stop_event.wait(self._poll_interval)
|
||||
if stop_event.is_set():
|
||||
break
|
||||
self._process_new_content()
|
||||
|
||||
def _process_new_content(self) -> None:
|
||||
consumer = self._consumer
|
||||
if consumer is None:
|
||||
return
|
||||
|
||||
lines: list[str]
|
||||
with self._lock:
|
||||
if not self._log_file.exists():
|
||||
self._last_position = 0
|
||||
return
|
||||
|
||||
current_size = self._log_file.stat().st_size
|
||||
|
||||
if current_size < self._last_position:
|
||||
self._last_position = 0
|
||||
self._new_lines_count = 0
|
||||
|
||||
if current_size == self._last_position:
|
||||
return
|
||||
|
||||
with self._log_file.open("r") as f:
|
||||
f.seek(self._last_position)
|
||||
new_content = f.read()
|
||||
self._last_position = f.tell()
|
||||
|
||||
lines = new_content.splitlines()
|
||||
self._new_lines_count += len(lines)
|
||||
|
||||
for line in lines:
|
||||
if entry := self._parse_line(line, 0):
|
||||
consumer(entry)
|
||||
|
||||
def _parse_line(self, line: str, line_number: int) -> LogEntry | None:
|
||||
try:
|
||||
match = self._log_pattern.match(line)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
timestamp_str, ppid_str, pid_str, level, message = match.groups()
|
||||
|
||||
timestamp = datetime.fromisoformat(timestamp_str)
|
||||
|
||||
return LogEntry(
|
||||
timestamp=timestamp,
|
||||
ppid=int(ppid_str),
|
||||
pid=int(pid_str),
|
||||
level=level,
|
||||
message=message,
|
||||
raw_line=line,
|
||||
line_number=line_number,
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("Failed to parse log line: %s", line[:100])
|
||||
return None
|
||||
@@ -4,6 +4,7 @@ from datetime import UTC, datetime
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
import re
|
||||
|
||||
from vibe.core.paths import LOG_DIR, LOG_FILE
|
||||
|
||||
@@ -18,17 +19,27 @@ class StructuredLogFormatter(logging.Formatter):
|
||||
ppid = os.getppid()
|
||||
pid = os.getpid()
|
||||
level = record.levelname
|
||||
message = record.getMessage().replace("\\", "\\\\").replace("\n", "\\n")
|
||||
message = encode_log_message(record.getMessage())
|
||||
|
||||
line = f"{timestamp} {ppid} {pid} {level} {message}"
|
||||
|
||||
if record.exc_info:
|
||||
exc_text = self.formatException(record.exc_info).replace("\n", "\\n")
|
||||
exc_text = encode_log_message(self.formatException(record.exc_info))
|
||||
line = f"{line} {exc_text}"
|
||||
|
||||
return line
|
||||
|
||||
|
||||
def encode_log_message(message: str) -> str:
|
||||
return message.replace("\\", "\\\\").replace("\n", "\\n")
|
||||
|
||||
|
||||
def decode_log_message(encoded: str) -> str:
|
||||
return re.sub(
|
||||
r"\\(.)", lambda m: "\n" if m.group(1) == "n" else m.group(1), encoded
|
||||
)
|
||||
|
||||
|
||||
def apply_logging_config(target_logger: logging.Logger) -> None:
|
||||
LOG_DIR.path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@@ -23,37 +23,48 @@ _MAX_DIRS = 2000
|
||||
|
||||
def _collect_config_dirs_at(
|
||||
path: Path,
|
||||
entries: set[str],
|
||||
entry_names: set[str],
|
||||
tools: list[Path],
|
||||
skills: list[Path],
|
||||
agents: list[Path],
|
||||
) -> None:
|
||||
"""Check a single directory for .vibe/ and .agents/ config subdirs."""
|
||||
if _VIBE_DIR in entries:
|
||||
if _VIBE_DIR in entry_names:
|
||||
if (candidate := path / _TOOLS_SUBDIR).is_dir():
|
||||
tools.append(candidate)
|
||||
if (candidate := path / _VIBE_SKILLS_SUBDIR).is_dir():
|
||||
skills.append(candidate)
|
||||
if (candidate := path / _AGENTS_SUBDIR).is_dir():
|
||||
agents.append(candidate)
|
||||
if _AGENTS_DIR in entries:
|
||||
if _AGENTS_DIR in entry_names:
|
||||
if (candidate := path / _AGENTS_SKILLS_SUBDIR).is_dir():
|
||||
skills.append(candidate)
|
||||
|
||||
|
||||
def _iter_child_dirs(path: Path, entries: set[str]) -> list[Path]:
|
||||
"""Return sorted child directories to descend into, skipping ignored and dot-dirs."""
|
||||
def _scandir_entries(path: Path) -> tuple[set[str], list[Path]]:
|
||||
"""Scan a directory, returning entry names and sorted child directories to descend into.
|
||||
|
||||
Uses ``os.scandir`` so that ``DirEntry.is_dir()`` leverages the dirent
|
||||
d_type field and avoids a separate ``stat`` syscall on most filesystems.
|
||||
"""
|
||||
try:
|
||||
entries = list(os.scandir(path))
|
||||
except OSError:
|
||||
return set(), []
|
||||
|
||||
entry_names = {e.name for e in entries}
|
||||
children: list[Path] = []
|
||||
for name in sorted(entries):
|
||||
for entry in entries:
|
||||
name = entry.name
|
||||
if name in WALK_SKIP_DIR_NAMES or name.startswith("."):
|
||||
continue
|
||||
child = path / name
|
||||
try:
|
||||
if child.is_dir():
|
||||
children.append(child)
|
||||
if entry.is_dir():
|
||||
children.append(path / name)
|
||||
except OSError:
|
||||
continue
|
||||
return children
|
||||
children.sort()
|
||||
return entry_names, children
|
||||
|
||||
|
||||
@cache
|
||||
@@ -77,17 +88,16 @@ def walk_local_config_dirs_all(
|
||||
current, depth = queue.popleft()
|
||||
visited += 1
|
||||
|
||||
try:
|
||||
entries = set(os.listdir(current))
|
||||
except OSError:
|
||||
entry_names, children = _scandir_entries(current)
|
||||
if not entry_names:
|
||||
continue
|
||||
|
||||
_collect_config_dirs_at(current, entries, tools_dirs, skills_dirs, agents_dirs)
|
||||
_collect_config_dirs_at(
|
||||
current, entry_names, tools_dirs, skills_dirs, agents_dirs
|
||||
)
|
||||
|
||||
if depth < WALK_MAX_DEPTH:
|
||||
queue.extend(
|
||||
(child, depth + 1) for child in _iter_child_dirs(current, entries)
|
||||
)
|
||||
queue.extend((child, depth + 1) for child in children)
|
||||
|
||||
if visited >= _MAX_DIRS:
|
||||
logger.warning(
|
||||
@@ -116,18 +126,15 @@ def has_config_dirs_nearby(
|
||||
current, depth = queue.popleft()
|
||||
visited += 1
|
||||
|
||||
try:
|
||||
entries = set(os.listdir(current))
|
||||
except OSError:
|
||||
entry_names, children = _scandir_entries(current)
|
||||
if not entry_names:
|
||||
continue
|
||||
|
||||
_collect_config_dirs_at(current, entries, found, found, found)
|
||||
_collect_config_dirs_at(current, entry_names, found, found, found)
|
||||
if found:
|
||||
return True
|
||||
|
||||
if depth < max_depth:
|
||||
queue.extend(
|
||||
(child, depth + 1) for child in _iter_child_dirs(current, entries)
|
||||
)
|
||||
queue.extend((child, depth + 1) for child in children)
|
||||
|
||||
return False
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import aclosing
|
||||
|
||||
from vibe import __version__
|
||||
from vibe.core.agent_loop import AgentLoop, TeleportError
|
||||
@@ -79,13 +80,14 @@ def run_programmatic(
|
||||
)
|
||||
formatter.on_event(next_event)
|
||||
else:
|
||||
async for event in agent_loop.act(prompt):
|
||||
formatter.on_event(event)
|
||||
if (
|
||||
isinstance(event, AssistantEvent)
|
||||
and event.stopped_by_middleware
|
||||
):
|
||||
raise ConversationLimitException(event.content)
|
||||
async with aclosing(agent_loop.act(prompt)) as events:
|
||||
async for event in events:
|
||||
formatter.on_event(event)
|
||||
if (
|
||||
isinstance(event, AssistantEvent)
|
||||
and event.stopped_by_middleware
|
||||
):
|
||||
raise ConversationLimitException(event.content)
|
||||
|
||||
return formatter.finalize()
|
||||
finally:
|
||||
|
||||
@@ -82,7 +82,7 @@ class GitHubPublicData(BaseModel):
|
||||
|
||||
|
||||
class ChatAssistantPublicData(BaseModel):
|
||||
chat_url: str
|
||||
chat_url: str | None = None
|
||||
|
||||
|
||||
class GetChatAssistantIntegrationResponse(BaseModel):
|
||||
@@ -195,7 +195,7 @@ class NuageClient:
|
||||
await asyncio.sleep(min(interval, remaining))
|
||||
raise ServiceTeleportError("GitHub connection timed out")
|
||||
|
||||
async def get_chat_assistant_url(self, execution_id: str) -> str:
|
||||
async def get_chat_assistant_url(self, execution_id: str) -> str | None:
|
||||
response = await self._http_client.post(
|
||||
f"{self._base_url}/v1/workflows/executions/{execution_id}/updates",
|
||||
headers=self._headers(),
|
||||
|
||||
@@ -193,6 +193,9 @@ class TeleportService:
|
||||
yield TeleportFetchingUrlEvent()
|
||||
chat_url = await self._nuage_client.get_chat_assistant_url(execution_id)
|
||||
|
||||
if not chat_url:
|
||||
raise ServiceTeleportError("Chat assistant URL is not available yet")
|
||||
|
||||
yield TeleportCompleteEvent(url=chat_url)
|
||||
|
||||
async def _push_or_fail(self) -> None:
|
||||
|
||||