Co-authored-by: Alexis Tacnet <alexis@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com>
Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@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: p.vezia <166131032+le-codeur-rapide@users.noreply.github.com>
Co-authored-by: Hiba Chaabnia <Hiba-Chaabnia@users.noreply.github.com>
Co-authored-by: Nikhil Bhima <nikhilbhima@users.noreply.github.com>
Co-authored-by: Nkipohcs <Nkipohcs@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-06-04 18:26:35 +02:00 committed by GitHub
parent ad0d5c9520
commit 3f8487f761
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
197 changed files with 10819 additions and 2830 deletions

View file

@ -29,13 +29,16 @@ jobs:
{"runner": "windows-2022", "os": "windows", "arch": "x86_64"} {"runner": "windows-2022", "os": "windows", "arch": "x86_64"}
] ]
}' }'
# TODO: Re-enable macOS standalone artifact smoke tests once the PyInstaller
# binaries are Developer ID signed and notarized. The direct GitHub zip
# download path gets Gatekeeper-assessed via quarantine and currently rejects
# ad-hoc signed binaries, even though vibe-acp continues to work when bundled
# through the VS Code extension.
smoke_matrix='{ smoke_matrix='{
"include": [ "include": [
{"runner": "ubuntu-24.04", "os": "linux", "arch": "x86_64"}, {"runner": "ubuntu-24.04", "os": "linux", "arch": "x86_64"},
{"runner": "ubuntu-24.04", "os": "linux", "arch": "x86_64", "tag": "old-glibc", "container": "almalinux:8"}, {"runner": "ubuntu-24.04", "os": "linux", "arch": "x86_64", "tag": "old-glibc", "container": "almalinux:8"},
{"runner": "ubuntu-24.04-arm", "os": "linux", "arch": "aarch64"}, {"runner": "ubuntu-24.04-arm", "os": "linux", "arch": "aarch64"},
{"runner": "macos-15", "os": "darwin", "arch": "aarch64"},
{"runner": "macos-15-intel", "os": "darwin", "arch": "x86_64"},
{"runner": "windows-latest", "os": "windows", "arch": "x86_64"} {"runner": "windows-latest", "os": "windows", "arch": "x86_64"}
] ]
}' }'
@ -84,27 +87,39 @@ jobs:
- name: Sync dependencies - name: Sync dependencies
run: uv sync --no-dev --group build run: uv sync --no-dev --group build
- name: Build with PyInstaller - name: Build ACP with PyInstaller
run: uv run --no-dev --group build pyinstaller vibe-acp.spec run: uv run --no-dev --group build pyinstaller vibe-acp.spec
- name: Build CLI with PyInstaller
run: uv run --no-dev --group build pyinstaller vibe.spec
- name: Clear executable stack on bundled libraries - name: Clear executable stack on bundled libraries
if: ${{ matrix.os == 'linux' }} if: ${{ matrix.os == 'linux' }}
run: | run: |
find dist/vibe-acp-dir/_internal -name '*.so*' -type f -print0 \ find dist/vibe-acp-dir/_internal -name '*.so*' -type f -print0 \
| xargs -0 -I{} patchelf --clear-execstack {} | xargs -0 -I{} patchelf --clear-execstack {}
patchelf --clear-execstack dist/vibe-acp-dir/vibe-acp || true patchelf --clear-execstack dist/vibe-acp-dir/vibe-acp || true
find dist/vibe-dir/_internal -name '*.so*' -type f -print0 \
| xargs -0 -I{} patchelf --clear-execstack {}
patchelf --clear-execstack dist/vibe-dir/vibe || true
- name: Get package version - name: Get package version
id: get_version id: get_version
shell: bash shell: bash
run: echo "version=$(uv version | cut -d' ' -f2)" >> $GITHUB_OUTPUT run: echo "version=$(uv version | cut -d' ' -f2)" >> $GITHUB_OUTPUT
- name: Upload binary as artifact - name: Upload ACP binary as artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with: with:
name: vibe-acp-${{ matrix.os }}-${{ matrix.arch }}-${{ steps.get_version.outputs.version }} name: vibe-acp-${{ matrix.os }}-${{ matrix.arch }}-${{ steps.get_version.outputs.version }}
path: dist/vibe-acp-dir/ path: dist/vibe-acp-dir/
- name: Upload CLI binary as artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: vibe-${{ matrix.os }}-${{ matrix.arch }}-${{ steps.get_version.outputs.version }}
path: dist/vibe-dir/
nix-build: nix-build:
needs: configure needs: configure
name: "Nix build and upload: ${{ matrix.os }}-${{ matrix.arch }}" name: "Nix build and upload: ${{ matrix.os }}-${{ matrix.arch }}"
@ -159,16 +174,23 @@ jobs:
run: yum install -y python3.11 python3.11-pip run: yum install -y python3.11 python3.11-pip
- name: Install smoke test deps - name: Install smoke test deps
run: ${{ matrix.container && 'python3.11' || 'python' }} -m pip install agent-client-protocol==0.9.0 run: ${{ matrix.container && 'python3.11' || 'python' }} -m pip install agent-client-protocol==0.10.1
- name: Download artifact - name: Download ACP artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with: with:
pattern: vibe-acp-${{ matrix.os }}-${{ matrix.arch }}-* pattern: vibe-acp-${{ matrix.os }}-${{ matrix.arch }}-*
merge-multiple: true merge-multiple: true
path: dist/vibe-acp-dir path: dist/vibe-acp-dir
- name: Run smoke tests - name: Download CLI artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
pattern: vibe-${{ matrix.os }}-${{ matrix.arch }}-*
merge-multiple: true
path: dist/vibe-dir
- name: Run ACP smoke tests
run: ${{ matrix.container && 'python3.11' || 'python' }} tests/acp/smoke_binary.py dist/vibe-acp-dir run: ${{ matrix.container && 'python3.11' || 'python' }} tests/acp/smoke_binary.py dist/vibe-acp-dir
attach-to-release: attach-to-release:
@ -191,6 +213,9 @@ jobs:
if [ -f "$dir/vibe-acp" ] || [ -f "$dir/vibe-acp.exe" ]; then if [ -f "$dir/vibe-acp" ] || [ -f "$dir/vibe-acp.exe" ]; then
chmod -f +x "$dir/vibe-acp" 2>/dev/null || true chmod -f +x "$dir/vibe-acp" 2>/dev/null || true
(cd "$dir" && zip -r "../../release-assets/${name}.zip" .) (cd "$dir" && zip -r "../../release-assets/${name}.zip" .)
elif [ -f "$dir/vibe" ] || [ -f "$dir/vibe.exe" ]; then
chmod -f +x "$dir/vibe" 2>/dev/null || true
(cd "$dir" && zip -r "../../release-assets/${name}.zip" .)
fi fi
done done

View file

@ -20,7 +20,7 @@ repos:
- id: pyright - id: pyright
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.5 rev: v0.15.13
hooks: hooks:
- id: ruff-check - id: ruff-check
args: [--fix, --unsafe-fixes] args: [--fix, --unsafe-fixes]

View file

@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.14.0] - 2026-06-04
### Added
- Image attachments via `@`-mentions in the TUI for vision-capable models
- Session deletion, exposed over ACP as the `session/delete` extension method
- Browser sign-in now shows a copyable fallback URL when the browser does not open, so SSH and remote sessions can complete onboarding
- Toast notification when a `SKILL.md` file fails to parse instead of silently dropping the skill
- Trust prompt now proposes the git repository ancestor as a trust target
- `EnvironmentLayer` in the layered configuration, populated from `VIBE_`-prefixed environment variables
### Changed
- New tool-call format for `read` and the file `edit` tool
- `write_file` is now create-only and refuses to overwrite existing files
- Bumped `agent-client-protocol` to 0.10.1
### Fixed
- LLM calls now retry on network errors and timeouts (connection, read, write, remote protocol, timeout)
- Approval modal now sizes itself so the bottom of long tool-call payloads stays visible above the options block
- Banner connector count and `/mcp` panel ordering now reflect which connectors are actually usable and the user's enable/disable choices
- `disabled_tools` from runtime is merged with the TOML configuration instead of replacing it, and is enforced inside `ToolManager.get()`
- `shift+backspace` and `shift+delete` now work in the chat input
- Crash in the ACP `todo` plan-update handler when the model's tool call failed validation
### Removed
- Recursive search for nested harness files
## [2.13.0] - 2026-05-29 ## [2.13.0] - 2026-05-29
### Added ### Added

View file

@ -93,7 +93,7 @@ pip install mistral-vibe
- **Interactive Chat**: A conversational AI agent that understands your requests and breaks down complex tasks. - **Interactive Chat**: A conversational AI agent that understands your requests and breaks down complex tasks.
- **Powerful Toolset**: A suite of tools for file manipulation, code searching, version control, and command execution, right from the chat prompt. - **Powerful Toolset**: A suite of tools for file manipulation, code searching, version control, and command execution, right from the chat prompt.
- Read, write, and patch files (`read_file`, `write_file`, `search_replace`). - Read, write, and patch files (`read`, `write_file`, `edit`).
- Execute shell commands in a stateful terminal (`bash`). - Execute shell commands in a stateful terminal (`bash`).
- Recursively search code with `grep` (with `ripgrep` support). - Recursively search code with `grep` (with `ripgrep` support).
- Manage a `todo` list to track the agent's work. - Manage a `todo` list to track the agent's work.
@ -102,6 +102,7 @@ pip install mistral-vibe
- **Project-Aware Context**: Vibe automatically scans your project's file structure and Git status to provide relevant context to the agent, improving its understanding of your codebase. - **Project-Aware Context**: Vibe automatically scans your project's file structure and Git status to provide relevant context to the agent, improving its understanding of your codebase.
- **Advanced CLI Experience**: Built with modern libraries for a smooth and efficient workflow. - **Advanced CLI Experience**: Built with modern libraries for a smooth and efficient workflow.
- Autocompletion for slash commands (`/`) and file paths (`@`). - Autocompletion for slash commands (`/`) and file paths (`@`).
- Image attachments via `@` mentions — `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp` files are sent to vision-capable models (e.g. Mistral Medium 3.5) as native multimodal content.
- Persistent command history. - Persistent command history.
- Beautiful Themes. - Beautiful Themes.
- **Highly Configurable**: Customize models, providers, tool permissions, and UI preferences through a simple `config.toml` file. - **Highly Configurable**: Customize models, providers, tool permissions, and UI preferences through a simple `config.toml` file.
@ -113,8 +114,8 @@ pip install mistral-vibe
Vibe comes with several built-in agent profiles, each designed for different use cases: Vibe comes with several built-in agent profiles, each designed for different use cases:
- **`default`**: Standard agent that requires approval for tool executions. Best for general use. - **`default`**: Standard agent that requires approval for tool executions. Best for general use.
- **`plan`**: Read-only agent for exploration and planning. Auto-approves safe tools like `grep` and `read_file`. - **`plan`**: Read-only agent for exploration and planning. Auto-approves safe tools like `grep` and `read`.
- **`accept-edits`**: Auto-approves file edits only (`write_file`, `search_replace`). Useful for code refactoring. - **`accept-edits`**: Auto-approves file edits only (`write_file`, `edit`). Useful for code refactoring.
- **`auto-approve`**: Auto-approves all tool executions. Use with caution. - **`auto-approve`**: Auto-approves all tool executions. Use with caution.
Use the `--agent` flag to select a different agent: Use the `--agent` flag to select a different agent:
@ -356,7 +357,7 @@ license: MIT
compatibility: Python 3.12+ compatibility: Python 3.12+
user-invocable: true user-invocable: true
allowed-tools: allowed-tools:
- read_file - read
- grep - grep
- ask_user_question - ask_user_question
--- ---
@ -491,13 +492,13 @@ active_model = "mistral-medium-3.5"
system_prompt_id = "redteam" system_prompt_id = "redteam"
# Disable some tools for this agent # Disable some tools for this agent
disabled_tools = ["search_replace", "write_file"] disabled_tools = ["edit", "write_file"]
# Override tool permissions for this agent # Override tool permissions for this agent
[tools.bash] [tools.bash]
permission = "always" permission = "always"
[tools.read_file] [tools.read]
permission = "always" permission = "always"
``` ```

View file

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

View file

@ -1,6 +1,6 @@
[project] [project]
name = "mistral-vibe" name = "mistral-vibe"
version = "2.13.0" version = "2.14.0"
description = "Minimal CLI coding agent by Mistral" description = "Minimal CLI coding agent by Mistral"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
@ -27,7 +27,7 @@ classifiers = [
"Topic :: Utilities", "Topic :: Utilities",
] ]
dependencies = [ dependencies = [
"agent-client-protocol==0.9.0", "agent-client-protocol==0.10.1",
"annotated-types==0.7.0", "annotated-types==0.7.0",
"anyio==4.13.0", "anyio==4.13.0",
"attrs==26.1.0", "attrs==26.1.0",
@ -49,6 +49,7 @@ dependencies = [
"httpcore==1.0.9", "httpcore==1.0.9",
"httpx==0.28.1", "httpx==0.28.1",
"httpx-sse==0.4.3", "httpx-sse==0.4.3",
"humanize==4.15.0",
"idna==3.13", "idna==3.13",
"importlib-metadata==8.7.1", "importlib-metadata==8.7.1",
"jaraco-classes==3.4.0", "jaraco-classes==3.4.0",
@ -147,7 +148,6 @@ vibe-acp = "vibe.acp.entrypoint:main"
[tool.uv] [tool.uv]
exclude-newer = "7 days" exclude-newer = "7 days"
exclude-newer-package = { agent-client-protocol = false, mistralai = false, textual = false }
package = true package = true
required-version = ">=0.8.0" required-version = ">=0.8.0"
@ -165,7 +165,7 @@ dev = [
"pytest-timeout>=2.4.0", "pytest-timeout>=2.4.0",
"pytest-xdist>=3.8.0", "pytest-xdist>=3.8.0",
"respx>=0.22.0", "respx>=0.22.0",
"ruff>=0.14.5", "ruff>=0.15.13",
"tomlkit>=0.13.0", "tomlkit>=0.13.0",
"twine>=5.0.0", "twine>=5.0.0",
"typos>=1.34.0", "typos>=1.34.0",
@ -174,65 +174,13 @@ dev = [
build = [ build = [
"altgraph==0.17.5", "altgraph==0.17.5",
"anyio==4.13.0",
"certifi==2026.4.22",
"cffi==2.0.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'",
"cfgv==3.5.0",
"charset-normalizer==3.4.7",
"colorama==0.4.6 ; sys_platform == 'win32'",
"cryptography==47.0.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform == 'linux'",
"distlib==0.4.0",
"docutils==0.22.4",
"execnet==2.1.2",
"filelock==3.29.0",
"h11==0.16.0",
"httpcore==1.0.9",
"httpx==0.28.1",
"id==1.6.1",
"identify==2.6.19",
"idna==3.13",
"iniconfig==2.3.0",
"jaraco-classes==3.4.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x'",
"jaraco-context==6.1.2 ; platform_machine != 'ppc64le' and platform_machine != 's390x'",
"jaraco-functools==4.4.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x'",
"jeepney==0.9.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform == 'linux'",
"jinja2==3.1.6",
"keyring==25.7.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x'",
"linkify-it-py==2.1.0",
"macholib==1.16.4 ; sys_platform == 'darwin'", "macholib==1.16.4 ; sys_platform == 'darwin'",
"markdown-it-py==4.0.0",
"markupsafe==3.0.3",
"mdit-py-plugins==0.5.0",
"mdurl==0.1.2",
"more-itertools==11.0.2 ; platform_machine != 'ppc64le' and platform_machine != 's390x'",
"nh3==0.3.5",
"nodeenv==1.10.0",
"packaging==26.2", "packaging==26.2",
"pefile==2024.8.26 ; sys_platform == 'win32'", "pefile==2024.8.26 ; sys_platform == 'win32'",
"platformdirs==4.9.6",
"pluggy==1.6.0",
"pycparser==3.0 ; implementation_name != 'PyPy' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'",
"pygments==2.20.0",
"pyinstaller==6.20.0", "pyinstaller==6.20.0",
"pyinstaller-hooks-contrib==2026.4", "pyinstaller-hooks-contrib==2026.4",
"pyinstrument==5.1.2",
"python-discovery==1.2.2",
"pywin32-ctypes==0.2.3 ; sys_platform == 'win32'", "pywin32-ctypes==0.2.3 ; sys_platform == 'win32'",
"pyyaml==6.0.3",
"readme-renderer==44.0",
"requests==2.33.1",
"requests-toolbelt==1.0.0",
"rfc3986==2.0.0",
"rich==15.0.0",
"secretstorage==3.5.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform == 'linux'",
"setuptools==82.0.1", "setuptools==82.0.1",
"syrupy==4.8.0",
"textual==8.2.7",
"truststore==0.10.4",
"typing-extensions==4.15.0",
"uc-micro-py==2.0.0",
"urllib3==2.6.3",
"virtualenv==21.2.4",
] ]
[tool.pyright] [tool.pyright]

View file

@ -101,6 +101,13 @@ def vibe_home_grep_ask(tmp_path: Path) -> Path:
return _create_vibe_home_dir(tmp_path, {"tools": {"grep": {"permission": "ask"}}}) return _create_vibe_home_dir(tmp_path, {"tools": {"grep": {"permission": "ask"}}})
@pytest.fixture
def acp_project_dir(tmp_path: Path) -> Path:
project_dir = tmp_path / "project"
project_dir.mkdir()
return project_dir
class JsonRpcRequest(BaseModel): class JsonRpcRequest(BaseModel):
jsonrpc: str = "2.0" jsonrpc: str = "2.0"
id: int | str id: int | str
@ -358,7 +365,10 @@ def parse_conversation(message_texts: list[str]) -> list[JsonRpcMessage]:
return parsed_messages return parsed_messages
async def initialize_session(acp_agent_loop_process: asyncio.subprocess.Process) -> str: async def initialize_session(
acp_agent_loop_process: asyncio.subprocess.Process,
session_cwd: Path = PLAYGROUND_DIR,
) -> str:
await send_json_rpc( await send_json_rpc(
acp_agent_loop_process, acp_agent_loop_process,
InitializeJsonRpcRequest(id=1, params=InitializeRequest(protocol_version=1)), InitializeJsonRpcRequest(id=1, params=InitializeRequest(protocol_version=1)),
@ -371,7 +381,7 @@ async def initialize_session(acp_agent_loop_process: asyncio.subprocess.Process)
await send_json_rpc( await send_json_rpc(
acp_agent_loop_process, acp_agent_loop_process,
NewSessionJsonRpcRequest( NewSessionJsonRpcRequest(
id=2, params=NewSessionRequest(cwd=str(PLAYGROUND_DIR), mcp_servers=[]) id=2, params=NewSessionRequest(cwd=str(session_cwd), mcp_servers=[])
), ),
) )
session_response = await read_response_for_id(acp_agent_loop_process, expected_id=2) session_response = await read_response_for_id(acp_agent_loop_process, expected_id=2)
@ -551,9 +561,9 @@ class TestSessionUpdates:
async def start_session_with_request_permission( async def start_session_with_request_permission(
process: asyncio.subprocess.Process, prompt: str process: asyncio.subprocess.Process, prompt: str, cwd: Path = PLAYGROUND_DIR
) -> RequestPermissionJsonRpcRequest: ) -> RequestPermissionJsonRpcRequest:
session_id = await initialize_session(process) session_id = await initialize_session(process, session_cwd=cwd)
await send_json_rpc( await send_json_rpc(
process, process,
PromptJsonRpcRequest( PromptJsonRpcRequest(
@ -580,7 +590,7 @@ async def start_session_with_request_permission(
class TestToolCallStructure: class TestToolCallStructure:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_tool_call_request_permission_structure( async def test_tool_call_request_permission_structure(
self, vibe_home_grep_ask: Path self, vibe_home_grep_ask: Path, acp_project_dir: Path
) -> None: ) -> None:
custom_results = [ custom_results = [
mock_llm_chunk( mock_llm_chunk(
@ -600,7 +610,7 @@ class TestToolCallStructure:
async for process in get_acp_agent_loop_process( async for process in get_acp_agent_loop_process(
mock_env=mock_env, vibe_home=vibe_home_grep_ask mock_env=mock_env, vibe_home=vibe_home_grep_ask
): ):
session_id = await initialize_session(process) session_id = await initialize_session(process, session_cwd=acp_project_dir)
await send_json_rpc( await send_json_rpc(
process, process,
PromptJsonRpcRequest( PromptJsonRpcRequest(
@ -638,8 +648,9 @@ class TestToolCallStructure:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_tool_call_update_approved_structure( async def test_tool_call_update_approved_structure(
self, vibe_home_grep_ask: Path self, vibe_home_grep_ask: Path, acp_project_dir: Path
) -> None: ) -> None:
(acp_project_dir / "fixture.txt").write_text("auth\n", encoding="utf-8")
custom_results = [ custom_results = [
mock_llm_chunk( mock_llm_chunk(
tool_calls=[ tool_calls=[
@ -661,7 +672,9 @@ class TestToolCallStructure:
mock_env=mock_env, vibe_home=vibe_home_grep_ask mock_env=mock_env, vibe_home=vibe_home_grep_ask
): ):
permission_request = await start_session_with_request_permission( permission_request = await start_session_with_request_permission(
process, "Search for files containing the pattern 'auth'" process,
"Search for files containing the pattern 'auth'",
cwd=acp_project_dir,
) )
assert permission_request.params is not None assert permission_request.params is not None
selected_option_id = ToolOption.ALLOW_ONCE selected_option_id = ToolOption.ALLOW_ONCE
@ -698,7 +711,7 @@ class TestToolCallStructure:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_tool_call_update_rejected_structure( async def test_tool_call_update_rejected_structure(
self, vibe_home_grep_ask: Path self, vibe_home_grep_ask: Path, acp_project_dir: Path
) -> None: ) -> None:
custom_results = [ custom_results = [
mock_llm_chunk( mock_llm_chunk(
@ -723,7 +736,9 @@ class TestToolCallStructure:
mock_env=mock_env, vibe_home=vibe_home_grep_ask mock_env=mock_env, vibe_home=vibe_home_grep_ask
): ):
permission_request = await start_session_with_request_permission( permission_request = await start_session_with_request_permission(
process, "Search for files containing the pattern 'auth'" process,
"Search for files containing the pattern 'auth'",
cwd=acp_project_dir,
) )
assert permission_request.params is not None assert permission_request.params is not None
selected_option_id = ToolOption.REJECT_ONCE selected_option_id = ToolOption.REJECT_ONCE
@ -760,7 +775,7 @@ class TestToolCallStructure:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_permission_options_include_granular_labels_for_bash( async def test_permission_options_include_granular_labels_for_bash(
self, vibe_home_dir: Path self, vibe_home_dir: Path, acp_project_dir: Path
) -> None: ) -> None:
"""Bash 'npm install foo' should produce granular labels in permission options.""" """Bash 'npm install foo' should produce granular labels in permission options."""
custom_results = [ custom_results = [
@ -782,7 +797,7 @@ class TestToolCallStructure:
mock_env=mock_env, vibe_home=vibe_home_dir mock_env=mock_env, vibe_home=vibe_home_dir
): ):
permission_request = await start_session_with_request_permission( permission_request = await start_session_with_request_permission(
process, "Run npm install foo" process, "Run npm install foo", cwd=acp_project_dir
) )
assert permission_request.params is not None assert permission_request.params is not None
@ -799,7 +814,7 @@ class TestToolCallStructure:
@pytest.mark.skip(reason="Long running tool call updates are not implemented yet") @pytest.mark.skip(reason="Long running tool call updates are not implemented yet")
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_tool_call_in_progress_update_structure( async def test_tool_call_in_progress_update_structure(
self, vibe_home_grep_ask: Path self, vibe_home_grep_ask: Path, acp_project_dir: Path
) -> None: ) -> None:
custom_results = [ custom_results = [
mock_llm_chunk( mock_llm_chunk(
@ -821,7 +836,7 @@ class TestToolCallStructure:
async for process in get_acp_agent_loop_process( async for process in get_acp_agent_loop_process(
mock_env=mock_env, vibe_home=vibe_home_grep_ask mock_env=mock_env, vibe_home=vibe_home_grep_ask
): ):
session_id = await initialize_session(process) session_id = await initialize_session(process, session_cwd=acp_project_dir)
await send_json_rpc( await send_json_rpc(
process, process,
PromptJsonRpcRequest( PromptJsonRpcRequest(
@ -856,7 +871,7 @@ class TestToolCallStructure:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_tool_call_result_update_failure_structure( async def test_tool_call_result_update_failure_structure(
self, vibe_home_grep_ask: Path self, vibe_home_grep_ask: Path, acp_project_dir: Path
) -> None: ) -> None:
custom_results = [ custom_results = [
mock_llm_chunk( mock_llm_chunk(
@ -883,6 +898,7 @@ class TestToolCallStructure:
permission_request = await start_session_with_request_permission( permission_request = await start_session_with_request_permission(
process, process,
"Search for files containing the pattern 'auth' in /nonexistent", "Search for files containing the pattern 'auth' in /nonexistent",
cwd=acp_project_dir,
) )
assert permission_request.params is not None assert permission_request.params is not None
selected_option_id = ToolOption.ALLOW_ONCE selected_option_id = ToolOption.ALLOW_ONCE
@ -934,8 +950,7 @@ class TestCancellationStructure:
ToolCall( ToolCall(
function=FunctionCall( function=FunctionCall(
name="write_file", name="write_file",
arguments='{"path":"test.txt","content":"hello, world!"' arguments='{"path":"test.txt","content":"hello, world!"}',
',"overwrite":false}',
), ),
type="function", type="function",
index=0, index=0,

View file

@ -0,0 +1,35 @@
from __future__ import annotations
from tests.conftest import build_test_vibe_config
from vibe.acp.acp_agent_loop import (
NON_INTERACTIVE_DISABLED_TOOLS,
_merge_non_interactive_disabled_tools,
)
def test_merge_preserves_toml_disabled_tools():
config = build_test_vibe_config(disabled_tools=["task"])
_merge_non_interactive_disabled_tools(config)
assert "task" in config.disabled_tools
for tool in NON_INTERACTIVE_DISABLED_TOOLS:
assert tool in config.disabled_tools
def test_merge_deduplicates():
config = build_test_vibe_config(disabled_tools=["ask_user_question", "task"])
_merge_non_interactive_disabled_tools(config)
assert config.disabled_tools.count("ask_user_question") == 1
assert "task" in config.disabled_tools
assert "exit_plan_mode" in config.disabled_tools
def test_merge_on_empty_disabled_tools():
config = build_test_vibe_config()
_merge_non_interactive_disabled_tools(config)
assert set(config.disabled_tools) == set(NON_INTERACTIVE_DISABLED_TOOLS)

View file

@ -6,23 +6,12 @@ from acp import ReadTextFileResponse
import pytest import pytest
from tests.mock.utils import collect_result from tests.mock.utils import collect_result
from vibe.acp.tools.builtins.search_replace import AcpSearchReplaceState, SearchReplace from vibe.acp.tools.builtins.edit import AcpEditState, Edit
from vibe.core.tools.base import ToolError from vibe.core.tools.base import ToolError
from vibe.core.tools.builtins.search_replace import ( from vibe.core.tools.builtins.edit import EditArgs, EditConfig, EditResult
SearchReplaceArgs,
SearchReplaceConfig,
SearchReplaceResult,
)
from vibe.core.types import ToolCallEvent, ToolResultEvent from vibe.core.types import ToolCallEvent, ToolResultEvent
def _make_block(search: str, replace: str) -> str:
head = "<" * 7 + " SEARCH"
sep = "=" * 7
tail = ">" * 7 + " REPLACE"
return f"{head}\n{search}\n{sep}\n{replace}\n{tail}"
class MockClient: class MockClient:
def __init__( def __init__(
self, self,
@ -82,52 +71,45 @@ def mock_client() -> MockClient:
@pytest.fixture @pytest.fixture
def acp_search_replace_tool( def acp_edit_tool(
mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> SearchReplace: ) -> Edit:
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
config = SearchReplaceConfig() config = EditConfig()
state = AcpSearchReplaceState.model_construct( state = AcpEditState.model_construct(
client=mock_client, session_id="test_session_123" client=mock_client, session_id="test_session_123"
) )
return SearchReplace(config_getter=lambda: config, state=state) return Edit(config_getter=lambda: config, state=state)
class TestAcpSearchReplaceBasic: class TestAcpEditBasic:
def test_get_name(self) -> None: def test_get_name(self) -> None:
assert SearchReplace.get_name() == "search_replace" assert Edit.get_name() == "edit"
class TestAcpSearchReplaceExecution: class TestAcpEditExecution:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_run_success( async def test_run_success(
self, self, acp_edit_tool: Edit, mock_client: MockClient, tmp_path: Path
acp_search_replace_tool: SearchReplace,
mock_client: MockClient,
tmp_path: Path,
) -> None: ) -> None:
test_file = tmp_path / "test_file.txt" test_file = tmp_path / "test_file.txt"
test_file.write_text("original line 1\noriginal line 2\noriginal line 3") test_file.write_text("original line 1\noriginal line 2\noriginal line 3")
search_replace_content = ( args = EditArgs(
"<<<<<<< SEARCH\noriginal line 2\n=======\nmodified line 2\n>>>>>>> REPLACE" file_path=str(test_file),
old_string="original line 2",
new_string="modified line 2",
) )
args = SearchReplaceArgs( result = await collect_result(acp_edit_tool.run(args))
file_path=str(test_file), content=search_replace_content
)
result = await collect_result(acp_search_replace_tool.run(args))
assert isinstance(result, SearchReplaceResult) assert isinstance(result, EditResult)
assert result.file == str(test_file) assert result.file == str(test_file)
assert result.blocks_applied == 1
assert mock_client._read_text_file_called assert mock_client._read_text_file_called
assert mock_client._write_text_file_called assert mock_client._write_text_file_called
# Verify read_text_file was called correctly
read_params = mock_client._last_read_params read_params = mock_client._last_read_params
assert read_params["session_id"] == "test_session_123" assert read_params["session_id"] == "test_session_123"
assert read_params["path"] == str(test_file) assert read_params["path"] == str(test_file)
# Verify write_text_file was called correctly
write_params = mock_client._last_write_params write_params = mock_client._last_write_params
assert write_params["session_id"] == "test_session_123" assert write_params["session_id"] == "test_session_123"
assert write_params["path"] == str(test_file) assert write_params["path"] == str(test_file)
@ -136,32 +118,6 @@ class TestAcpSearchReplaceExecution:
== "original line 1\nmodified line 2\noriginal line 3" == "original line 1\nmodified line 2\noriginal line 3"
) )
@pytest.mark.asyncio
async def test_run_with_backup(
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
config = SearchReplaceConfig(create_backup=True)
tool = SearchReplace(
config_getter=lambda: config,
state=AcpSearchReplaceState.model_construct(
client=mock_client, session_id="test_session"
),
)
test_file = tmp_path / "test_file.txt"
test_file.write_text("original line 1\noriginal line 2\noriginal line 3")
search_replace_content = (
"<<<<<<< SEARCH\noriginal line 1\n=======\nmodified line 1\n>>>>>>> REPLACE"
)
args = SearchReplaceArgs(
file_path=str(test_file), content=search_replace_content
)
result = await collect_result(tool.run(args))
assert result.blocks_applied == 1
assert sum(w["path"].endswith(".bak") for w in mock_client._write_calls) == 1
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("newline", ["\r\n", "\r", "\n"]) @pytest.mark.parametrize("newline", ["\r\n", "\r", "\n"])
async def test_run_preserves_line_endings( async def test_run_preserves_line_endings(
@ -178,18 +134,19 @@ class TestAcpSearchReplaceExecution:
"original line 3", "original line 3",
]) ])
tool = SearchReplace( tool = Edit(
config_getter=lambda: SearchReplaceConfig(), config_getter=lambda: EditConfig(),
state=AcpSearchReplaceState.model_construct( state=AcpEditState.model_construct(
client=mock_client, session_id="test_session" client=mock_client, session_id="test_session"
), ),
) )
test_file = tmp_path / "test_file.txt" test_file = tmp_path / "test_file.txt"
test_file.touch() test_file.touch()
args = SearchReplaceArgs( args = EditArgs(
file_path=str(test_file), file_path=str(test_file),
content=_make_block("original line 2", "modified line 2"), old_string="original line 2",
new_string="modified line 2",
) )
await collect_result(tool.run(args)) await collect_result(tool.run(args))
@ -199,35 +156,6 @@ class TestAcpSearchReplaceExecution:
"original line 3", "original line 3",
]) ])
@pytest.mark.asyncio
async def test_run_backup_preserves_crlf_byte_for_byte(
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
original = "original line 1\r\noriginal line 2\r\noriginal line 3"
mock_client._file_content = original
tool = SearchReplace(
config_getter=lambda: SearchReplaceConfig(create_backup=True),
state=AcpSearchReplaceState.model_construct(
client=mock_client, session_id="test_session"
),
)
test_file = tmp_path / "test_file.txt"
test_file.touch()
args = SearchReplaceArgs(
file_path=str(test_file),
content=_make_block("original line 1", "modified line 1"),
)
await collect_result(tool.run(args))
backup_calls = [
w for w in mock_client._write_calls if w["path"].endswith(".bak")
]
assert len(backup_calls) == 1
assert backup_calls[0]["content"] == original
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_run_read_error( async def test_run_read_error(
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
@ -235,26 +163,20 @@ class TestAcpSearchReplaceExecution:
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
mock_client._read_error = RuntimeError("File not found") mock_client._read_error = RuntimeError("File not found")
tool = SearchReplace( tool = Edit(
config_getter=lambda: SearchReplaceConfig(), config_getter=lambda: EditConfig(),
state=AcpSearchReplaceState.model_construct( state=AcpEditState.model_construct(
client=mock_client, session_id="test_session" client=mock_client, session_id="test_session"
), ),
) )
test_file = tmp_path / "test.txt" test_file = tmp_path / "test.txt"
test_file.touch() test_file.touch()
search_replace_content = "<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE" args = EditArgs(file_path=str(test_file), old_string="old", new_string="new")
args = SearchReplaceArgs(
file_path=str(test_file), content=search_replace_content
)
with pytest.raises(ToolError) as exc_info: with pytest.raises(ToolError) as exc_info:
await collect_result(tool.run(args)) await collect_result(tool.run(args))
assert ( assert str(exc_info.value) == f"Error reading {test_file}: File not found"
str(exc_info.value)
== f"Unexpected error reading {test_file}: File not found"
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_run_write_error( async def test_run_write_error(
@ -264,19 +186,16 @@ class TestAcpSearchReplaceExecution:
mock_client._write_error = RuntimeError("Permission denied") mock_client._write_error = RuntimeError("Permission denied")
test_file = tmp_path / "test.txt" test_file = tmp_path / "test.txt"
test_file.touch() test_file.touch()
mock_client._file_content = "old" # Update mock to return correct content mock_client._file_content = "old"
tool = SearchReplace( tool = Edit(
config_getter=lambda: SearchReplaceConfig(), config_getter=lambda: EditConfig(),
state=AcpSearchReplaceState.model_construct( state=AcpEditState.model_construct(
client=mock_client, session_id="test_session" client=mock_client, session_id="test_session"
), ),
) )
search_replace_content = "<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE" args = EditArgs(file_path=str(test_file), old_string="old", new_string="new")
args = SearchReplaceArgs(
file_path=str(test_file), content=search_replace_content
)
with pytest.raises(ToolError) as exc_info: with pytest.raises(ToolError) as exc_info:
await collect_result(tool.run(args)) await collect_result(tool.run(args))
@ -309,38 +228,30 @@ class TestAcpSearchReplaceExecution:
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
test_file = tmp_path / "test.txt" test_file = tmp_path / "test.txt"
test_file.touch() test_file.touch()
tool = SearchReplace( tool = Edit(
config_getter=lambda: SearchReplaceConfig(), config_getter=lambda: EditConfig(),
state=AcpSearchReplaceState.model_construct( state=AcpEditState.model_construct(client=client, session_id=session_id),
client=client, session_id=session_id
),
) )
search_replace_content = "<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE" args = EditArgs(file_path=str(test_file), old_string="old", new_string="new")
args = SearchReplaceArgs(
file_path=str(test_file), content=search_replace_content
)
with pytest.raises(ToolError) as exc_info: with pytest.raises(ToolError) as exc_info:
await collect_result(tool.run(args)) await collect_result(tool.run(args))
assert str(exc_info.value) == expected_error assert str(exc_info.value) == expected_error
class TestAcpSearchReplaceSessionUpdates: class TestAcpEditSessionUpdates:
def test_tool_call_session_update(self) -> None: def test_tool_call_session_update(self) -> None:
search_replace_content = (
"<<<<<<< SEARCH\nold text\n=======\nnew text\n>>>>>>> REPLACE"
)
event = ToolCallEvent( event = ToolCallEvent(
tool_name="search_replace", tool_name="edit",
tool_call_id="test_call_123", tool_call_id="test_call_123",
args=SearchReplaceArgs( args=EditArgs(
file_path="/tmp/test.txt", content=search_replace_content file_path="/tmp/test.txt", old_string="old text", new_string="new text"
), ),
tool_class=SearchReplace, tool_class=Edit,
) )
update = SearchReplace.tool_call_session_update(event) update = Edit.tool_call_session_update(event)
assert update is not None assert update is not None
assert update.session_update == "tool_call" assert update.session_update == "tool_call"
assert update.tool_call_id == "test_call_123" assert update.tool_call_id == "test_call_123"
@ -362,40 +273,37 @@ class TestAcpSearchReplaceSessionUpdates:
pass pass
event = ToolCallEvent.model_construct( event = ToolCallEvent.model_construct(
tool_name="search_replace", tool_name="edit",
tool_call_id="test_call_123", tool_call_id="test_call_123",
args=InvalidArgs(), # type: ignore[arg-type] args=InvalidArgs(), # type: ignore[arg-type]
tool_class=SearchReplace, tool_class=Edit,
) )
update = SearchReplace.tool_call_session_update(event) update = Edit.tool_call_session_update(event)
assert update is not None assert update is not None
assert update.title == "search_replace" assert update.title == "edit"
def test_tool_result_session_update(self) -> None: def test_tool_result_session_update(self) -> None:
search_replace_content = ( result = EditResult(
"<<<<<<< SEARCH\nold text\n=======\nnew text\n>>>>>>> REPLACE"
)
result = SearchReplaceResult(
file="/tmp/test.txt", file="/tmp/test.txt",
blocks_applied=1, message="The file has been updated successfully.",
lines_changed=1, old_string="old text",
content=search_replace_content, new_string="new text",
warnings=[],
) )
event = ToolResultEvent( event = ToolResultEvent(
tool_name="search_replace", tool_name="edit",
tool_call_id="test_call_123", tool_call_id="test_call_123",
result=result, result=result,
tool_class=SearchReplace, tool_class=Edit,
) )
update = SearchReplace.tool_result_session_update(event) update = Edit.tool_result_session_update(event)
assert update is not None assert update is not None
assert update.session_update == "tool_call_update" assert update.session_update == "tool_call_update"
assert update.tool_call_id == "test_call_123" assert update.tool_call_id == "test_call_123"
assert update.status == "completed" assert update.status == "completed"
assert update.kind == "edit"
assert update.content is not None assert update.content is not None
assert isinstance(update.content, list) assert isinstance(update.content, list)
assert len(update.content) == 1 assert len(update.content) == 1
@ -412,12 +320,12 @@ class TestAcpSearchReplaceSessionUpdates:
pass pass
event = ToolResultEvent.model_construct( event = ToolResultEvent.model_construct(
tool_name="search_replace", tool_name="edit",
tool_call_id="test_call_123", tool_call_id="test_call_123",
result=InvalidResult(), # type: ignore[arg-type] result=InvalidResult(), # type: ignore[arg-type]
tool_class=SearchReplace, tool_class=Edit,
) )
update = SearchReplace.tool_result_session_update(event) update = Edit.tool_result_session_update(event)
assert update is not None assert update is not None
assert update.status == "failed" assert update.status == "failed"

View file

@ -83,7 +83,7 @@ class TestACPForkSession:
id="call-1", id="call-1",
index=0, index=0,
function=FunctionCall( function=FunctionCall(
name="read_file", arguments='{"path":"a.txt"}' name="read", arguments='{"file_path":"a.txt"}'
), ),
) )
], ],

View file

@ -62,7 +62,7 @@ class TestACPInitialize:
), ),
) )
assert response.agent_info == Implementation( assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.13.0" name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.14.0"
) )
assert response.auth_methods is not None assert response.auth_methods is not None
@ -94,7 +94,7 @@ class TestACPInitialize:
), ),
) )
assert response.agent_info == Implementation( assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.13.0" name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.14.0"
) )
assert response.auth_methods is not None assert response.auth_methods is not None

View file

@ -238,8 +238,8 @@ class TestLoadSession:
"id": "call_123", "id": "call_123",
"type": "function", "type": "function",
"function": { "function": {
"name": "read_file", "name": "read",
"arguments": '{"path": "/tmp/test.txt"}', "arguments": '{"file_path": "/tmp/test.txt"}',
}, },
} }
], ],
@ -254,7 +254,7 @@ class TestLoadSession:
u for u in client._session_updates if isinstance(u.update, ToolCallStart) u for u in client._session_updates if isinstance(u.update, ToolCallStart)
] ]
assert len(tool_call_starts) == 1 assert len(tool_call_starts) == 1
assert tool_call_starts[0].update.title == "read_file" assert tool_call_starts[0].update.title == "read"
assert tool_call_starts[0].update.tool_call_id == "call_123" assert tool_call_starts[0].update.tool_call_id == "call_123"
tool_results = [ tool_results = [

View file

@ -6,12 +6,13 @@ from acp import ReadTextFileResponse
import pytest import pytest
from tests.mock.utils import collect_result from tests.mock.utils import collect_result
from vibe.acp.tools.builtins.read_file import AcpReadFileState, ReadFile from vibe.acp.tools.builtins.read import AcpReadState, Read
from vibe.core.tools.base import ToolError from vibe.core.tools.base import ToolError
from vibe.core.tools.builtins.read_file import ( from vibe.core.tools.builtins.read import (
ReadFileArgs, DEFAULT_LINE_LIMIT,
ReadFileResult, ReadArgs,
ReadFileToolConfig, ReadConfig,
ReadResult,
) )
@ -49,7 +50,7 @@ class MockClient:
content = self._file_content content = self._file_content
if line is not None or limit is not None: if line is not None or limit is not None:
lines = content.splitlines(keepends=True) lines = content.splitlines(keepends=True)
start_line = (line or 1) - 1 # Convert to 0-indexed start_line = (line or 1) - 1
end_line = start_line + limit if limit is not None else len(lines) end_line = start_line + limit if limit is not None else len(lines)
lines = lines[start_line:end_line] lines = lines[start_line:end_line]
content = "".join(lines) content = "".join(lines)
@ -66,45 +67,43 @@ def mock_client() -> MockClient:
@pytest.fixture @pytest.fixture
def acp_read_file_tool( def acp_read_tool(
mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> ReadFile: ) -> Read:
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
config = ReadFileToolConfig() config = ReadConfig()
state = AcpReadFileState.model_construct( state = AcpReadState.model_construct(
client=mock_client, # type: ignore[arg-type] client=mock_client, # type: ignore[arg-type]
session_id="test_session_123", session_id="test_session_123",
) )
return ReadFile(config_getter=lambda: config, state=state) return Read(config_getter=lambda: config, state=state)
class TestAcpReadFileBasic: class TestAcpReadBasic:
def test_get_name(self) -> None: def test_get_name(self) -> None:
assert ReadFile.get_name() == "read_file" assert Read.get_name() == "read"
class TestAcpReadFileExecution: class TestAcpReadExecution:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_run_success( async def test_run_success(
self, acp_read_file_tool: ReadFile, mock_client: MockClient, tmp_path: Path self, acp_read_tool: Read, mock_client: MockClient, tmp_path: Path
) -> None: ) -> None:
test_file = tmp_path / "test_file.txt" test_file = tmp_path / "test_file.txt"
test_file.touch() test_file.touch()
args = ReadFileArgs(path=str(test_file)) args = ReadArgs(file_path=str(test_file))
result = await collect_result(acp_read_file_tool.run(args)) result = await collect_result(acp_read_tool.run(args))
assert isinstance(result, ReadFileResult) assert isinstance(result, ReadResult)
assert result.path == str(test_file) assert result.file_path == str(test_file)
assert result.content == "line 1\nline 2\nline 3" assert result.num_lines == 3
assert result.lines_read == 3
assert mock_client._read_text_file_called assert mock_client._read_text_file_called
# Verify read_text_file was called correctly
params = mock_client._last_read_params params = mock_client._last_read_params
assert params["session_id"] == "test_session_123" assert params["session_id"] == "test_session_123"
assert params["path"] == str(test_file) assert params["path"] == str(test_file)
assert params["line"] is None # offset=0 means no line specified assert params["line"] is None
assert params["limit"] is None assert params["limit"] == DEFAULT_LINE_LIMIT + 1
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_run_with_offset( async def test_run_with_offset(
@ -113,21 +112,21 @@ class TestAcpReadFileExecution:
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
test_file = tmp_path / "test_file.txt" test_file = tmp_path / "test_file.txt"
test_file.touch() test_file.touch()
tool = ReadFile( tool = Read(
config_getter=lambda: ReadFileToolConfig(), config_getter=lambda: ReadConfig(),
state=AcpReadFileState.model_construct( state=AcpReadState.model_construct(
client=mock_client, session_id="test_session" client=mock_client, session_id="test_session"
), ),
) )
args = ReadFileArgs(path=str(test_file), offset=1) args = ReadArgs(file_path=str(test_file), offset=2)
result = await collect_result(tool.run(args)) result = await collect_result(tool.run(args))
assert result.lines_read == 2 assert result.num_lines == 2
assert result.content == "line 2\nline 3" assert result.start_line == 2
params = mock_client._last_read_params params = mock_client._last_read_params
assert params["line"] == 2 # offset=1 means line 2 (1-indexed) assert params["line"] == 2
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_run_with_limit( async def test_run_with_limit(
@ -136,21 +135,20 @@ class TestAcpReadFileExecution:
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
test_file = tmp_path / "test_file.txt" test_file = tmp_path / "test_file.txt"
test_file.touch() test_file.touch()
tool = ReadFile( tool = Read(
config_getter=lambda: ReadFileToolConfig(), config_getter=lambda: ReadConfig(),
state=AcpReadFileState.model_construct( state=AcpReadState.model_construct(
client=mock_client, session_id="test_session" client=mock_client, session_id="test_session"
), ),
) )
args = ReadFileArgs(path=str(test_file), limit=2) args = ReadArgs(file_path=str(test_file), limit=2)
result = await collect_result(tool.run(args)) result = await collect_result(tool.run(args))
assert result.lines_read == 2 assert result.num_lines == 2
assert result.content == "line 1\nline 2\n"
params = mock_client._last_read_params params = mock_client._last_read_params
assert params["limit"] == 2 assert params["limit"] == 3
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_run_with_offset_and_limit( async def test_run_with_offset_and_limit(
@ -159,22 +157,22 @@ class TestAcpReadFileExecution:
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
test_file = tmp_path / "test_file.txt" test_file = tmp_path / "test_file.txt"
test_file.touch() test_file.touch()
tool = ReadFile( tool = Read(
config_getter=lambda: ReadFileToolConfig(), config_getter=lambda: ReadConfig(),
state=AcpReadFileState.model_construct( state=AcpReadState.model_construct(
client=mock_client, session_id="test_session" client=mock_client, session_id="test_session"
), ),
) )
args = ReadFileArgs(path=str(test_file), offset=1, limit=1) args = ReadArgs(file_path=str(test_file), offset=2, limit=1)
result = await collect_result(tool.run(args)) result = await collect_result(tool.run(args))
assert result.lines_read == 1 assert result.num_lines == 1
assert result.content == "line 2\n" assert result.start_line == 2
params = mock_client._last_read_params params = mock_client._last_read_params
assert params["line"] == 2 assert params["line"] == 2
assert params["limit"] == 1 assert params["limit"] == 2
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_run_read_error( async def test_run_read_error(
@ -184,14 +182,14 @@ class TestAcpReadFileExecution:
mock_client._read_error = RuntimeError("File not found") mock_client._read_error = RuntimeError("File not found")
test_file = tmp_path / "test.txt" test_file = tmp_path / "test.txt"
test_file.touch() test_file.touch()
tool = ReadFile( tool = Read(
config_getter=lambda: ReadFileToolConfig(), config_getter=lambda: ReadConfig(),
state=AcpReadFileState.model_construct( state=AcpReadState.model_construct(
client=mock_client, session_id="test_session" client=mock_client, session_id="test_session"
), ),
) )
args = ReadFileArgs(path=str(test_file)) args = ReadArgs(file_path=str(test_file))
with pytest.raises(ToolError) as exc_info: with pytest.raises(ToolError) as exc_info:
await collect_result(tool.run(args)) await collect_result(tool.run(args))
@ -204,14 +202,12 @@ class TestAcpReadFileExecution:
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
test_file = tmp_path / "test.txt" test_file = tmp_path / "test.txt"
test_file.touch() test_file.touch()
tool = ReadFile( tool = Read(
config_getter=lambda: ReadFileToolConfig(), config_getter=lambda: ReadConfig(),
state=AcpReadFileState.model_construct( state=AcpReadState.model_construct(client=None, session_id="test_session"),
client=None, session_id="test_session"
),
) )
args = ReadFileArgs(path=str(test_file)) args = ReadArgs(file_path=str(test_file))
with pytest.raises(ToolError) as exc_info: with pytest.raises(ToolError) as exc_info:
await collect_result(tool.run(args)) await collect_result(tool.run(args))
@ -228,12 +224,12 @@ class TestAcpReadFileExecution:
test_file = tmp_path / "test.txt" test_file = tmp_path / "test.txt"
test_file.touch() test_file.touch()
mock_client = MockClient() mock_client = MockClient()
tool = ReadFile( tool = Read(
config_getter=lambda: ReadFileToolConfig(), config_getter=lambda: ReadConfig(),
state=AcpReadFileState.model_construct(client=mock_client, session_id=None), state=AcpReadState.model_construct(client=mock_client, session_id=None),
) )
args = ReadFileArgs(path=str(test_file)) args = ReadArgs(file_path=str(test_file))
with pytest.raises(ToolError) as exc_info: with pytest.raises(ToolError) as exc_info:
await collect_result(tool.run(args)) await collect_result(tool.run(args))

View file

@ -0,0 +1,177 @@
from __future__ import annotations
import json
from pathlib import Path
from unittest.mock import AsyncMock
import pytest
from tests.stubs.fake_client import FakeClient
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.acp.exceptions import InvalidRequestError
from vibe.core.session import last_session_pointer
def _write_saved_session(
session_dir: Path, timestamp: str, session_id: str, cwd: str
) -> Path:
saved_session_dir = session_dir / f"session_{timestamp}_{session_id[:8]}"
saved_session_dir.mkdir()
(saved_session_dir / "messages.jsonl").write_text(
json.dumps({"role": "user", "content": "Hello"}) + "\n", encoding="utf-8"
)
(saved_session_dir / "meta.json").write_text(
json.dumps({
"session_id": session_id,
"start_time": "2024-01-01T12:00:00Z",
"end_time": "2024-01-01T12:05:00Z",
"git_commit": None,
"git_branch": None,
"username": "test-user",
"environment": {"working_directory": cwd},
"title": "Saved session",
}),
encoding="utf-8",
)
return saved_session_dir
class TestSessionDelete:
@pytest.mark.asyncio
async def test_deletes_saved_but_not_loaded_session(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent = acp_agent_with_session_config[0]
session_id = "offline-session-12345678"
session_dir = create_test_session(temp_session_dir, session_id, str(Path.cwd()))
result = await acp_agent.ext_method("session/delete", {"sessionId": session_id})
assert result == {}
assert not session_dir.exists()
response = await acp_agent.list_sessions()
assert response.sessions == []
@pytest.mark.asyncio
async def test_deletes_saved_session_and_clears_last_session_pointer(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent = acp_agent_with_session_config[0]
session_id = "pointer-session-12345678"
session_dir = create_test_session(temp_session_dir, session_id, str(Path.cwd()))
pointer_dir = temp_session_dir / last_session_pointer.POINTER_DIR_NAME
pointer_dir.mkdir()
matching_pointer = pointer_dir / "ttys001"
other_pointer = pointer_dir / "ttys002"
matching_pointer.write_text(f"{session_id}\n", encoding="utf-8")
other_pointer.write_text("other-session\n", encoding="utf-8")
result = await acp_agent.ext_method("session/delete", {"sessionId": session_id})
assert result == {}
assert not session_dir.exists()
assert not matching_pointer.exists()
assert other_pointer.read_text(encoding="utf-8") == "other-session\n"
@pytest.mark.asyncio
async def test_deletes_loaded_saved_session(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
create_test_session,
) -> None:
acp_agent = acp_agent_with_session_config[0]
saved_session_id = "saved-session-12345678"
acp_session_id = saved_session_id[:8]
cwd = str(Path.cwd())
session_dir = create_test_session(temp_session_dir, saved_session_id, cwd)
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=acp_session_id)
session = acp_agent.sessions[acp_session_id]
session.agent_loop.telemetry_client.aclose = AsyncMock()
result = await acp_agent.ext_method(
"session/delete", {"sessionId": saved_session_id}
)
assert result == {}
assert acp_session_id not in acp_agent.sessions
assert not session_dir.exists()
response = await acp_agent.list_sessions()
assert response.sessions == []
@pytest.mark.asyncio
async def test_deletes_live_unsaved_session_without_saved_history(
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
) -> None:
acp_agent = acp_agent_with_session_config[0]
response = await acp_agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
assert response is not None
session = acp_agent.sessions[response.session_id]
session.agent_loop.telemetry_client.aclose = AsyncMock()
assert not session.agent_loop.session_logger.metadata_filepath.exists()
result = await acp_agent.ext_method(
"session/delete", {"sessionId": response.session_id}
)
assert result == {}
assert response.session_id not in acp_agent.sessions
@pytest.mark.asyncio
async def test_raises_on_invalid_params(
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
) -> None:
acp_agent = acp_agent_with_session_config[0]
with pytest.raises(InvalidRequestError):
await acp_agent.ext_method("session/delete", {})
with pytest.raises(InvalidRequestError):
await acp_agent.ext_method("session/delete", {"sessionId": " "})
with pytest.raises(InvalidRequestError):
await acp_agent.ext_method(
"session/delete", {"savedSessionId": "unsupported-session"}
)
@pytest.mark.asyncio
async def test_succeeds_when_session_cannot_be_found(
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
) -> None:
acp_agent = acp_agent_with_session_config[0]
result = await acp_agent.ext_method(
"session/delete", {"sessionId": "missing-session"}
)
assert result == {}
@pytest.mark.asyncio
async def test_requires_exact_saved_session_id_before_deleting(
self,
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
temp_session_dir: Path,
) -> None:
acp_agent = acp_agent_with_session_config[0]
cwd = str(Path.cwd())
collision_dir = _write_saved_session(
temp_session_dir, "20240101_120000", "aaaaaaaa-1111", cwd
)
target_dir = _write_saved_session(
temp_session_dir, "20240101_120500", "aaaaaaaa-2222", cwd
)
result = await acp_agent.ext_method(
"session/delete", {"sessionId": "aaaaaaaa-2222"}
)
assert result == {}
assert collision_dir.exists()
assert not target_dir.exists()

64
tests/acp/test_todo.py Normal file
View file

@ -0,0 +1,64 @@
from __future__ import annotations
from vibe.acp.tools.builtins.todo import Todo
from vibe.core.tools.builtins.todo import TodoItem, TodoPriority, TodoResult, TodoStatus
from vibe.core.types import ToolResultEvent
class TestAcpTodoSessionUpdates:
def test_tool_result_session_update(self) -> None:
result = TodoResult(
message="Updated 2 todos",
todos=[
TodoItem(
id="1",
content="First",
status=TodoStatus.IN_PROGRESS,
priority=TodoPriority.HIGH,
),
TodoItem(id="2", content="Second", status=TodoStatus.PENDING),
],
total_count=2,
)
event = ToolResultEvent(
tool_name="todo",
tool_call_id="test_call_123",
result=result,
tool_class=Todo,
)
update = Todo.tool_result_session_update(event)
assert update is not None
assert update.session_update == "plan"
assert len(update.entries) == 2
assert update.entries[0].content == "First"
assert update.entries[0].status == "in_progress"
assert update.entries[0].priority == "high"
def test_tool_result_session_update_failed_result(self) -> None:
event = ToolResultEvent(
tool_name="todo",
tool_call_id="test_call_123",
error="Todo IDs must be unique",
tool_class=Todo,
)
update = Todo.tool_result_session_update(event)
assert update is not None
assert update.status == "failed"
def test_tool_result_session_update_invalid_result(self) -> None:
class InvalidResult:
pass
event = ToolResultEvent.model_construct(
tool_name="todo",
tool_call_id="test_call_123",
result=InvalidResult(), # type: ignore[arg-type]
tool_class=Todo,
)
update = Todo.tool_result_session_update(event)
assert update is not None
assert update.status == "failed"

View file

@ -2,19 +2,19 @@ from __future__ import annotations
from acp.schema import ToolCallStart from acp.schema import ToolCallStart
from vibe.acp.tools.builtins.read_file import ReadFile from vibe.acp.tools.builtins.read import Read
from vibe.acp.tools.session_update import tool_call_session_update from vibe.acp.tools.session_update import tool_call_session_update
from vibe.core.tools.builtins.read_file import ReadFileArgs from vibe.core.tools.builtins.read import ReadArgs
from vibe.core.types import ToolCallEvent from vibe.core.types import ToolCallEvent
class TestToolCallSessionUpdate: class TestToolCallSessionUpdate:
def _create_event(self) -> ToolCallEvent: def _create_event(self) -> ToolCallEvent:
return ToolCallEvent( return ToolCallEvent(
tool_name="read_file", tool_name="read",
tool_call_id="test_call_123", tool_call_id="test_call_123",
args=ReadFileArgs(path="/tmp/test.txt"), args=ReadArgs(file_path="/tmp/test.txt"),
tool_class=ReadFile, tool_class=Read,
) )
def test_returns_tool_call_start(self) -> None: def test_returns_tool_call_start(self) -> None:
@ -29,10 +29,7 @@ class TestToolCallSessionUpdate:
def test_returns_tool_call_start_for_streaming_event(self) -> None: def test_returns_tool_call_start_for_streaming_event(self) -> None:
event = ToolCallEvent( event = ToolCallEvent(
tool_name="read_file", tool_name="read", tool_call_id="test_call_123", tool_class=Read, args=None
tool_call_id="test_call_123",
tool_class=ReadFile,
args=None,
) )
update = tool_call_session_update(event) update = tool_call_session_update(event)

View file

@ -8,7 +8,7 @@ from acp.schema import ToolCallProgress, ToolCallStart
from pydantic import BaseModel from pydantic import BaseModel
from vibe.acp.tools.builtins.grep import Grep from vibe.acp.tools.builtins.grep import Grep
from vibe.acp.tools.builtins.read_file import ReadFile from vibe.acp.tools.builtins.read import Read
from vibe.acp.tools.builtins.skill import Skill from vibe.acp.tools.builtins.skill import Skill
from vibe.acp.tools.builtins.task import Task from vibe.acp.tools.builtins.task import Task
from vibe.acp.tools.builtins.web_fetch import WebFetch from vibe.acp.tools.builtins.web_fetch import WebFetch
@ -19,7 +19,7 @@ from vibe.acp.tools.session_update import (
tool_result_session_update, tool_result_session_update,
) )
from vibe.core.tools.builtins.grep import GrepArgs, GrepResult from vibe.core.tools.builtins.grep import GrepArgs, GrepResult
from vibe.core.tools.builtins.read_file import ReadFileArgs, ReadFileResult from vibe.core.tools.builtins.read import ReadArgs, ReadResult
from vibe.core.tools.builtins.skill import SkillArgs, SkillResult from vibe.core.tools.builtins.skill import SkillArgs, SkillResult
from vibe.core.tools.builtins.task import TaskArgs, TaskResult from vibe.core.tools.builtins.task import TaskArgs, TaskResult
from vibe.core.tools.builtins.webfetch import WebFetchArgs, WebFetchResult from vibe.core.tools.builtins.webfetch import WebFetchArgs, WebFetchResult
@ -99,10 +99,10 @@ class TestGrepFieldMeta:
assert update.field_meta == {"tool_name": "grep"} assert update.field_meta == {"tool_name": "grep"}
class TestReadFileFieldMeta: class TestReadFieldMeta:
def test_call_location_has_offset_and_limit(self) -> None: def test_call_location_has_offset_and_limit(self) -> None:
event = _call_event( event = _call_event(
"read_file", ReadFile, ReadFileArgs(path="/tmp/f.txt", offset=10, limit=50) "read", Read, ReadArgs(file_path="/tmp/f.txt", offset=10, limit=50)
) )
update = tool_call_session_update(event) update = tool_call_session_update(event)
@ -110,26 +110,26 @@ class TestReadFileFieldMeta:
assert update.locations is not None assert update.locations is not None
loc = update.locations[0] loc = update.locations[0]
assert loc.field_meta == {"type": "file_range", "offset": 10, "limit": 50} assert loc.field_meta == {"type": "file_range", "offset": 10, "limit": 50}
assert update.field_meta == {"tool_name": "read_file"} assert update.field_meta == {"tool_name": "read"}
def test_call_defaults_offset_zero_limit_none(self) -> None: def test_call_defaults_offset_none_limit_default(self) -> None:
event = _call_event("read_file", ReadFile, ReadFileArgs(path="/tmp/f.txt")) event = _call_event("read", Read, ReadArgs(file_path="/tmp/f.txt"))
update = tool_call_session_update(event) update = tool_call_session_update(event)
assert isinstance(update, ToolCallStart) assert isinstance(update, ToolCallStart)
assert update.locations is not None assert update.locations is not None
loc = update.locations[0] loc = update.locations[0]
assert loc.field_meta == {"type": "file_range", "offset": 0, "limit": None} assert loc.field_meta == {"type": "file_range", "offset": None, "limit": 2000}
def test_result_location_has_offset_and_lines_read(self) -> None: def test_result_location_has_start_line_and_num_lines(self) -> None:
result = ReadFileResult( result = ReadResult(
path="/tmp/f.txt", file_path="/tmp/f.txt",
content="line1\nline2\nline3\n", content=" 1→line1\n 2→line2\n 3→line3",
lines_read=3, num_lines=3,
was_truncated=False, start_line=10,
offset=10, total_lines=20,
) )
event = _result_event("read_file", ReadFile, result) event = _result_event("read", Read, result)
update = tool_result_session_update(event) update = tool_result_session_update(event)
assert isinstance(update, ToolCallProgress) assert isinstance(update, ToolCallProgress)
@ -287,9 +287,7 @@ class TestWriteFileFieldMeta:
assert update.locations[0].path == str(Path("out.txt").resolve()) assert update.locations[0].path == str(Path("out.txt").resolve())
def test_result_location_is_resolved_path(self) -> None: def test_result_location_is_resolved_path(self) -> None:
result = WriteFileResult( result = WriteFileResult(path="out.txt", content="hello", bytes_written=5)
path="out.txt", content="hello", bytes_written=5, file_existed=False
)
event = _result_event("write_file", WriteFile, result) event = _result_event("write_file", WriteFile, result)
update = tool_result_session_update(event) update = tool_result_session_update(event)

View file

@ -77,7 +77,6 @@ class TestAcpWriteFileExecution:
assert result.path == str(test_file) assert result.path == str(test_file)
assert result.content == "Hello, world!" assert result.content == "Hello, world!"
assert result.bytes_written == len(b"Hello, world!") assert result.bytes_written == len(b"Hello, world!")
assert result.file_existed is False
assert mock_client._write_text_file_called assert mock_client._write_text_file_called
# Verify write_text_file was called correctly # Verify write_text_file was called correctly
@ -87,7 +86,7 @@ class TestAcpWriteFileExecution:
assert params["content"] == "Hello, world!" assert params["content"] == "Hello, world!"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_run_success_overwrite( async def test_run_existing_file_raises(
self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
@ -100,23 +99,11 @@ class TestAcpWriteFileExecution:
test_file = tmp_path / "existing_file.txt" test_file = tmp_path / "existing_file.txt"
test_file.touch() test_file.touch()
# Simulate existing file by checking in the core tool logic args = WriteFileArgs(path=str(test_file), content="New content")
# The ACP tool doesn't check existence, it's handled by the core tool with pytest.raises(ToolError, match="already exists"):
args = WriteFileArgs(path=str(test_file), content="New content", overwrite=True) await collect_result(tool.run(args))
result = await collect_result(tool.run(args))
assert isinstance(result, WriteFileResult) assert not mock_client._write_text_file_called
assert result.path == str(test_file)
assert result.content == "New content"
assert result.bytes_written == len(b"New content")
assert result.file_existed is True
assert mock_client._write_text_file_called
# Verify write_text_file was called correctly
params = mock_client._last_write_params
assert params["session_id"] == "test_session"
assert params["path"] == str(test_file)
assert params["content"] == "New content"
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("input_newline", ["\r\n", "\r", "\n"]) @pytest.mark.parametrize("input_newline", ["\r\n", "\r", "\n"])
@ -305,9 +292,7 @@ class TestAcpWriteFileSessionUpdates:
assert update.title == "write_file" assert update.title == "write_file"
def test_tool_result_session_update(self) -> None: def test_tool_result_session_update(self) -> None:
result = WriteFileResult( result = WriteFileResult(path="/tmp/test.txt", content="Hello", bytes_written=5)
path="/tmp/test.txt", content="Hello", bytes_written=5, file_existed=False
)
event = ToolResultEvent( event = ToolResultEvent(
tool_name="write_file", tool_name="write_file",

View file

@ -2,6 +2,8 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
import pytest
from vibe.core.autocompletion.path_prompt import ( from vibe.core.autocompletion.path_prompt import (
build_path_prompt_payload, build_path_prompt_payload,
build_title_segments, build_title_segments,
@ -22,6 +24,47 @@ def test_deduplicates_same_file_mentioned_twice(tmp_path: Path) -> None:
assert len(payload.all_resources) == 2 assert len(payload.all_resources) == 2
class TestTildeExpansion:
def test_tilde_path_expands_and_attaches(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("HOME", str(tmp_path))
shot = tmp_path / "shot.png"
shot.write_bytes(b"\x89PNG")
payload = build_path_prompt_payload("look at @~/shot.png", base_dir=tmp_path)
assert len(payload.resources) == 1
resource = payload.resources[0]
assert resource.path == shot.resolve()
assert resource.alias == "~/shot.png"
assert resource.kind == "image"
def test_bare_tilde_attaches_home_as_folder(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("HOME", str(tmp_path))
payload = build_path_prompt_payload("go to @~", base_dir=tmp_path)
assert len(payload.resources) == 1
assert payload.resources[0].path == tmp_path.resolve()
assert payload.resources[0].kind == "folder"
def test_unknown_user_tilde_does_not_crash(self, tmp_path: Path) -> None:
payload = build_path_prompt_payload(
"see @~nonexistentuser1234/x.png", base_dir=tmp_path
)
assert payload.resources == []
def test_tilde_mid_token_without_anchor_stays_text(self, tmp_path: Path) -> None:
payload = build_path_prompt_payload("foo~bar baz", base_dir=tmp_path)
assert payload.resources == []
assert payload.prompt_text == "foo~bar baz"
class TestBuildTitleSegments: class TestBuildTitleSegments:
def test_empty_message(self) -> None: def test_empty_message(self) -> None:
assert build_title_segments("") == [] assert build_title_segments("") == []

View file

@ -0,0 +1,70 @@
from __future__ import annotations
from pathlib import Path
from vibe.core.autocompletion.path_prompt import build_path_prompt_payload
from vibe.core.autocompletion.path_prompt_adapter import (
extract_image_resources,
render_path_prompt,
)
def test_image_extension_is_classified_as_image_kind(tmp_path: Path) -> None:
(tmp_path / "logo.png").write_bytes(b"\x89PNG")
payload = build_path_prompt_payload("look at @logo.png", base_dir=tmp_path)
assert len(payload.resources) == 1
assert payload.resources[0].kind == "image"
assert payload.resources[0].alias == "logo.png"
def test_text_file_remains_kind_file(tmp_path: Path) -> None:
(tmp_path / "notes.md").write_text("hello")
payload = build_path_prompt_payload("read @notes.md", base_dir=tmp_path)
assert payload.resources[0].kind == "file"
def test_image_mentions_omit_resource_link_when_skip_images(tmp_path: Path) -> None:
(tmp_path / "logo.png").write_bytes(b"\x89PNG")
rendered = render_path_prompt(
"look at @logo.png", base_dir=tmp_path, skip_images=True
)
assert "logo.png" in rendered # kept in the prompt text as the @ mention
assert "file://" not in rendered # no resource_link block emitted
def test_image_mentions_emit_resource_link_by_default(tmp_path: Path) -> None:
(tmp_path / "logo.png").write_bytes(b"\x89PNG")
rendered = render_path_prompt("look at @logo.png", base_dir=tmp_path)
assert "logo.png" in rendered
assert (tmp_path / "logo.png").as_uri() in rendered
def test_extract_image_resources_filters_only_images(tmp_path: Path) -> None:
(tmp_path / "logo.png").write_bytes(b"\x89PNG")
(tmp_path / "notes.md").write_text("hi")
(tmp_path / "diagram.webp").write_bytes(b"RIFF")
payload = build_path_prompt_payload(
"see @logo.png @notes.md @diagram.webp", base_dir=tmp_path
)
images = extract_image_resources(payload)
aliases = sorted(r.alias for r in images)
assert aliases == ["diagram.webp", "logo.png"]
def test_case_insensitive_image_extension(tmp_path: Path) -> None:
(tmp_path / "screenshot.PNG").write_bytes(b"\x89PNG")
payload = build_path_prompt_payload("see @screenshot.PNG", base_dir=tmp_path)
assert payload.resources[0].kind == "image"

View file

@ -0,0 +1,68 @@
from __future__ import annotations
import base64
from pathlib import Path
import pytest
from vibe.core.llm.backend._image import (
ImageReadError,
_encode_cached,
to_base64,
to_data_uri,
)
from vibe.core.types import ImageAttachment
PNG_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00" * 16
@pytest.fixture(autouse=True)
def _clear_cache() -> None:
_encode_cached.cache_clear()
def _att(tmp_path: Path, name: str = "shot.png") -> ImageAttachment:
p = tmp_path / name
p.write_bytes(PNG_BYTES)
return ImageAttachment(path=p, alias=name, mime_type="image/png")
def test_repeated_calls_hit_cache_and_skip_disk(tmp_path: Path) -> None:
att = _att(tmp_path)
first = to_base64(att)
second = to_base64(att)
third = to_data_uri(att)
assert first == second == base64.b64encode(PNG_BYTES).decode("ascii")
assert third == f"data:image/png;base64,{first}"
info = _encode_cached.cache_info()
assert info.misses == 1
assert info.hits == 2
def test_cache_invalidates_when_file_mtime_changes(tmp_path: Path) -> None:
att = _att(tmp_path)
to_base64(att)
assert _encode_cached.cache_info().misses == 1
new_bytes = PNG_BYTES + b"\x01"
att.path.write_bytes(new_bytes)
# Force a distinct mtime even on coarse-resolution filesystems.
import os
stat = att.path.stat()
os.utime(att.path, ns=(stat.st_atime_ns, stat.st_mtime_ns + 1_000_000))
refreshed = to_base64(att)
assert refreshed == base64.b64encode(new_bytes).decode("ascii")
assert _encode_cached.cache_info().misses == 2
def test_missing_file_raises_image_read_error(tmp_path: Path) -> None:
att = ImageAttachment(
path=tmp_path / "nope.png", alias="nope.png", mime_type="image/png"
)
with pytest.raises(ImageReadError):
to_data_uri(att)

View file

@ -0,0 +1,146 @@
from __future__ import annotations
import base64
from collections.abc import Sequence
import json
from pathlib import Path
from typing import Any
import pytest
from vibe.core.llm.backend.anthropic import AnthropicAdapter
from vibe.core.llm.backend.base import APIAdapter
from vibe.core.llm.backend.generic import OpenAIAdapter
from vibe.core.llm.backend.mistral import MistralMapper
from vibe.core.llm.backend.openai_responses import OpenAIResponsesAdapter
from vibe.core.llm.backend.reasoning_adapter import ReasoningAdapter
from vibe.core.types import ImageAttachment, LLMMessage, Role
PNG_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00" * 16
EXPECTED_B64 = base64.b64encode(PNG_BYTES).decode("ascii")
EXPECTED_DATA_URI = f"data:image/png;base64,{EXPECTED_B64}"
@pytest.fixture()
def image_attachment(tmp_path: Path) -> ImageAttachment:
path = tmp_path / "shot.png"
path.write_bytes(PNG_BYTES)
return ImageAttachment(path=path, alias="shot.png", mime_type="image/png")
def _user_message(image: ImageAttachment) -> LLMMessage:
return LLMMessage(role=Role.user, content="describe this", images=[image])
class _FakeProvider:
name = "mistral"
reasoning_field_name = "reasoning_content"
def _adapter_payload(
adapter: APIAdapter,
messages: Sequence[LLMMessage],
*,
model: str = "gpt-4o",
**overrides: Any,
) -> dict[str, Any]:
kwargs: dict[str, Any] = {
"model_name": model,
"messages": list(messages),
"temperature": 0.0,
"tools": None,
"max_tokens": None,
"tool_choice": None,
"enable_streaming": False,
"provider": _FakeProvider(),
"api_key": "k",
}
kwargs.update(overrides)
prepared = adapter.prepare_request(**kwargs)
return json.loads(prepared.body.decode("utf-8"))
def test_mistral_mapper_emits_image_url_chunk(
image_attachment: ImageAttachment,
) -> None:
mapper = MistralMapper()
prepared = mapper.prepare_message(_user_message(image_attachment))
dumped = prepared.model_dump()
assert dumped["role"] == "user"
parts = dumped["content"]
assert parts[0] == {"type": "text", "text": "describe this"}
assert parts[1]["type"] == "image_url"
assert parts[1]["image_url"]["url"] == EXPECTED_DATA_URI
def test_openai_adapter_emits_image_url_part(image_attachment: ImageAttachment) -> None:
payload = _adapter_payload(OpenAIAdapter(), [_user_message(image_attachment)])
msg = payload["messages"][0]
assert msg["role"] == "user"
assert msg["content"][0] == {"type": "text", "text": "describe this"}
assert msg["content"][1] == {
"type": "image_url",
"image_url": {"url": EXPECTED_DATA_URI},
}
assert "images" not in msg
def test_openai_responses_adapter_emits_input_image_part(
image_attachment: ImageAttachment,
) -> None:
payload = _adapter_payload(
OpenAIResponsesAdapter(), [_user_message(image_attachment)]
)
user = payload["input"][0]
assert user["role"] == "user"
assert user["content"][0] == {"type": "input_text", "text": "describe this"}
assert user["content"][1] == {"type": "input_image", "image_url": EXPECTED_DATA_URI}
def test_anthropic_adapter_emits_image_source(
image_attachment: ImageAttachment,
) -> None:
payload = _adapter_payload(
AnthropicAdapter(), [_user_message(image_attachment)], model="claude-3-5-sonnet"
)
msg = payload["messages"][0]
assert msg["role"] == "user"
assert msg["content"][0] == {"type": "text", "text": "describe this"}
image_block = msg["content"][1]
assert image_block["type"] == "image"
assert image_block["source"] == {
"type": "base64",
"media_type": "image/png",
"data": EXPECTED_B64,
}
# prepare_request stamps cache_control on the last block of the last user turn
assert image_block["cache_control"] == {"type": "ephemeral"}
def test_reasoning_adapter_emits_image_url_part(
image_attachment: ImageAttachment,
) -> None:
payload = _adapter_payload(ReasoningAdapter(), [_user_message(image_attachment)])
msg = payload["messages"][0]
assert msg["role"] == "user"
assert msg["content"][0] == {"type": "text", "text": "describe this"}
assert msg["content"][1] == {
"type": "image_url",
"image_url": {"url": EXPECTED_DATA_URI},
}
def test_text_only_user_message_keeps_string_content() -> None:
text_msg = LLMMessage(role=Role.user, content="hi")
mistral_prepared = MistralMapper().prepare_message(text_msg)
assert mistral_prepared.model_dump()["content"] == "hi"
anthropic_payload = _adapter_payload(
AnthropicAdapter(), [text_msg], model="claude-3-5-sonnet"
)
# Anthropic emits parts even for text-only; keep as wrapped text part
text_block = anthropic_payload["messages"][0]["content"][0]
assert text_block["type"] == "text"
assert text_block["text"] == "hi"

View file

@ -91,7 +91,8 @@ def _write_tls_material(tmp_path: Path) -> _TLSMaterial:
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Vibe test CA")]) ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Vibe test CA")])
ca_cert = ( ca_cert = (
x509.CertificateBuilder() x509
.CertificateBuilder()
.subject_name(ca_name) .subject_name(ca_name)
.issuer_name(ca_name) .issuer_name(ca_name)
.public_key(ca_key.public_key()) .public_key(ca_key.public_key())
@ -105,7 +106,8 @@ def _write_tls_material(tmp_path: Path) -> _TLSMaterial:
server_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) server_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
server_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "127.0.0.1")]) server_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "127.0.0.1")])
server_cert = ( server_cert = (
x509.CertificateBuilder() x509
.CertificateBuilder()
.subject_name(server_name) .subject_name(server_name)
.issuer_name(ca_cert.subject) .issuer_name(ca_cert.subject)
.public_key(server_key.public_key()) .public_key(server_key.public_key())

View file

@ -44,7 +44,7 @@ class TestBannerInitialState:
banner = Banner( banner = Banner(
config=_make_mock_config(), config=_make_mock_config(),
skill_manager=skill_manager, skill_manager=skill_manager,
connectors_enabled=5, connectors_connected=5,
connectors_total=5, connectors_total=5,
) )
@ -54,7 +54,7 @@ class TestBannerInitialState:
banner._initial_state.mcp_servers_enabled == 0 banner._initial_state.mcp_servers_enabled == 0
) # No MCP servers configured ) # No MCP servers configured
assert banner._initial_state.mcp_servers_total == 0 assert banner._initial_state.mcp_servers_total == 0
assert banner._initial_state.connectors_enabled == 5 assert banner._initial_state.connectors_connected == 5
assert banner._initial_state.connectors_total == 5 assert banner._initial_state.connectors_total == 5
assert banner._initial_state.skills_count == 0 assert banner._initial_state.skills_count == 0
@ -64,7 +64,7 @@ class TestBannerInitialState:
banner = Banner(config=_make_mock_config(), skill_manager=skill_manager) banner = Banner(config=_make_mock_config(), skill_manager=skill_manager)
assert banner._initial_state.connectors_enabled == 0 assert banner._initial_state.connectors_connected == 0
assert banner._initial_state.connectors_total == 0 assert banner._initial_state.connectors_total == 0
def test_banner_shows_thinking_level(self) -> None: def test_banner_shows_thinking_level(self) -> None:
@ -88,7 +88,7 @@ class TestBannerInitialState:
models_count=2, models_count=2,
mcp_servers_enabled=1, mcp_servers_enabled=1,
mcp_servers_total=2, mcp_servers_total=2,
connectors_enabled=3, connectors_connected=3,
connectors_total=3, connectors_total=3,
skills_count=5, skills_count=5,
) )
@ -103,7 +103,7 @@ class TestBannerInitialState:
models_count=2, models_count=2,
mcp_servers_enabled=1, mcp_servers_enabled=1,
mcp_servers_total=2, mcp_servers_total=2,
connectors_enabled=0, connectors_connected=0,
connectors_total=0, connectors_total=0,
skills_count=5, skills_count=5,
) )
@ -222,9 +222,9 @@ class TestBannerConnectorsCount:
banner = Banner( banner = Banner(
config=_make_mock_config(), config=_make_mock_config(),
skill_manager=skill_manager, skill_manager=skill_manager,
connectors_enabled=3, connectors_connected=3,
connectors_total=5, connectors_total=5,
) )
assert banner._initial_state.connectors_enabled == 3 assert banner._initial_state.connectors_connected == 3
assert banner._initial_state.connectors_total == 5 assert banner._initial_state.connectors_total == 5

View file

@ -18,12 +18,15 @@ from tests.browser_sign_in.stubs import (
noop_sleep, noop_sleep,
) )
from vibe.setup.auth import ( from vibe.setup.auth import (
BrowserSignInAttemptStarted,
BrowserSignInError, BrowserSignInError,
BrowserSignInErrorCode, BrowserSignInErrorCode,
BrowserSignInEvent,
BrowserSignInPollResult, BrowserSignInPollResult,
BrowserSignInProcess, BrowserSignInProcess,
BrowserSignInService, BrowserSignInService,
BrowserSignInStatus, BrowserSignInStatus,
BrowserSignInStatusChanged,
) )
TEST_NOW = datetime(2026, 3, 16, tzinfo=UTC) TEST_NOW = datetime(2026, 3, 16, tzinfo=UTC)
@ -69,7 +72,7 @@ def build_test_service(
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_authenticate_returns_api_key_after_pending_poll() -> None: async def test_authenticate_returns_api_key_after_pending_poll() -> None:
opened_urls: list[str] = [] opened_urls: list[str] = []
statuses: list[BrowserSignInStatus] = [] events: list[BrowserSignInEvent] = []
gateway, service = build_test_service( gateway, service = build_test_service(
poll_results=[ poll_results=[
BrowserSignInPollResult(status="pending"), BrowserSignInPollResult(status="pending"),
@ -78,17 +81,23 @@ async def test_authenticate_returns_api_key_after_pending_poll() -> None:
open_browser=lambda url: opened_urls.append(url) or True, open_browser=lambda url: opened_urls.append(url) or True,
) )
api_key = await service.authenticate(status_callback=statuses.append) api_key = await service.authenticate(event_callback=events.append)
code_verifier = gateway.exchange_requests[0].code_verifier code_verifier = gateway.exchange_requests[0].code_verifier
assert gateway.code_challenges == [build_code_challenge(code_verifier)] assert gateway.code_challenges == [build_code_challenge(code_verifier)]
assert api_key == "sk-browser-key" assert api_key == "sk-browser-key"
assert opened_urls == [TEST_SIGN_IN_URL] assert opened_urls == [TEST_SIGN_IN_URL]
assert statuses == [ assert events == [
BrowserSignInStatus.OPENING_BROWSER, BrowserSignInAttemptStarted(
BrowserSignInStatus.WAITING_FOR_BROWSER_SIGN_IN, sign_in_url=TEST_SIGN_IN_URL,
BrowserSignInStatus.EXCHANGING, expires_at=build_sign_in_process(TEST_NOW).expires_at,
BrowserSignInStatus.COMPLETED, ),
BrowserSignInStatusChanged(status=BrowserSignInStatus.OPENING_BROWSER),
BrowserSignInStatusChanged(
status=BrowserSignInStatus.WAITING_FOR_BROWSER_SIGN_IN
),
BrowserSignInStatusChanged(status=BrowserSignInStatus.EXCHANGING),
BrowserSignInStatusChanged(status=BrowserSignInStatus.COMPLETED),
] ]
assert gateway.polled_urls == [TEST_POLL_URL, TEST_POLL_URL] assert gateway.polled_urls == [TEST_POLL_URL, TEST_POLL_URL]
assert gateway.exchange_requests[0].exchange_token == "exchange-1" assert gateway.exchange_requests[0].exchange_token == "exchange-1"
@ -115,7 +124,6 @@ async def test_start_attempt_returns_attempt_without_opening_browser() -> None:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_complete_attempt_returns_api_key_without_opening_browser() -> None: async def test_complete_attempt_returns_api_key_without_opening_browser() -> None:
opened_urls: list[str] = [] opened_urls: list[str] = []
statuses: list[BrowserSignInStatus] = []
gateway, service = build_test_service( gateway, service = build_test_service(
poll_results=[ poll_results=[
BrowserSignInPollResult(status="pending"), BrowserSignInPollResult(status="pending"),
@ -125,15 +133,10 @@ async def test_complete_attempt_returns_api_key_without_opening_browser() -> Non
) )
attempt = await service.start_attempt() attempt = await service.start_attempt()
api_key = await service.complete_attempt(attempt, status_callback=statuses.append) api_key = await service.complete_attempt(attempt)
assert api_key == "sk-browser-key" assert api_key == "sk-browser-key"
assert opened_urls == [] assert opened_urls == []
assert statuses == [
BrowserSignInStatus.WAITING_FOR_BROWSER_SIGN_IN,
BrowserSignInStatus.EXCHANGING,
BrowserSignInStatus.COMPLETED,
]
assert gateway.polled_urls == [TEST_POLL_URL, TEST_POLL_URL] assert gateway.polled_urls == [TEST_POLL_URL, TEST_POLL_URL]
assert gateway.exchange_requests[0].exchange_token == "exchange-1" assert gateway.exchange_requests[0].exchange_token == "exchange-1"
@ -241,10 +244,19 @@ async def test_authenticate_raises_on_unknown_poll_state() -> None:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_authenticate_raises_when_browser_cannot_be_opened() -> None: async def test_authenticate_raises_when_browser_cannot_be_opened() -> None:
events: list[BrowserSignInEvent] = []
_, service = build_test_service(poll_results=[], open_browser=lambda _: False) _, service = build_test_service(poll_results=[], open_browser=lambda _: False)
with pytest.raises(BrowserSignInError, match="open browser"): with pytest.raises(BrowserSignInError, match="open browser"):
await service.authenticate() await service.authenticate(event_callback=events.append)
assert events == [
BrowserSignInAttemptStarted(
sign_in_url=TEST_SIGN_IN_URL,
expires_at=build_sign_in_process(TEST_NOW).expires_at,
),
BrowserSignInStatusChanged(status=BrowserSignInStatus.OPENING_BROWSER),
]
@pytest.mark.asyncio @pytest.mark.asyncio

View file

@ -12,6 +12,7 @@ from vibe.cli.plan_offer.decide_plan_offer import (
WhoAmIPlanType, WhoAmIPlanType,
decide_plan_offer, decide_plan_offer,
plan_offer_cta, plan_offer_cta,
plan_title,
resolve_api_key_for_plan, resolve_api_key_for_plan,
) )
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIResponse from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIResponse
@ -263,3 +264,20 @@ def test_teleport_eligibility_depends_on_chat_plan_and_current_key(
response: WhoAmIResponse, expected: bool response: WhoAmIResponse, expected: bool
) -> None: ) -> None:
assert PlanInfo.from_response(response).is_teleport_eligible() is expected assert PlanInfo.from_response(response).is_teleport_eligible() is expected
@pytest.mark.parametrize(
("payload", "expected_title"),
[
(PlanInfo(plan_type=WhoAmIPlanType.API, plan_name="FREE"), "Free"),
(
PlanInfo(plan_type=WhoAmIPlanType.API, plan_name="Scale plan"),
"[API] Scale plan",
),
],
ids=["free-api-plan", "paid-api-plan"],
)
def test_plan_title_uses_current_api_plan_labels(
payload: PlanInfo, expected_title: str
) -> None:
assert plan_title(payload) == expected_title

416
tests/cli/smoke_binary.py Normal file
View file

@ -0,0 +1,416 @@
#!/usr/bin/env python3
"""Smoke tests for the built vibe binary.
Usage: python tests/cli/smoke_binary.py <binary-dir>
Tests:
1. --version exits successfully
2. Normal interactive launch starts far enough to load the main Textual app
3. --setup starts far enough to load bundled setup/Textual assets
4. Programmatic mode without an API key fails with the expected config error
5. Runtime data files are present in the bundle
6. The relocated bundle can be launched from PATH
7. (Linux) No ELF binaries require executable stack (GNU_STACK RWE)
"""
from __future__ import annotations
import os
from pathlib import Path
import platform
import shutil
import struct
import subprocess
import sys
import tempfile
import time
from typing import NoReturn
def _fail(msg: str) -> NoReturn:
print(f"FAIL: {msg}", file=sys.stderr)
sys.exit(1)
def _isolated_env(vibe_home: Path) -> dict[str, str]:
env = os.environ.copy()
env["VIBE_HOME"] = str(vibe_home)
env["TERM"] = env.get("TERM") or "xterm-256color"
env.pop("MISTRAL_API_KEY", None)
env.pop("VIBE_MISTRAL_API_KEY", None)
return env
def test_version(binary: Path) -> None:
result = subprocess.run(
[str(binary), "--version"], capture_output=True, text=True, timeout=30
)
if result.returncode != 0:
_fail(
f"--version exited with code {result.returncode}\nstderr: {result.stderr}"
)
print(f"PASS: --version -> {result.stdout.strip()}")
def test_interactive_launch_loads_bundled_assets(binary: Path) -> None:
if platform.system() == "Windows":
# Windows does not provide the Unix pty module used to drive Textual here.
print("SKIP: interactive pty smoke test (Windows)")
return
import pty
import select
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
workdir = tmp_path / "workdir"
workdir.mkdir()
master_fd, slave_fd = pty.openpty()
env = _isolated_env(tmp_path / "home")
env["MISTRAL_API_KEY"] = "smoke-test-mock-key"
proc = subprocess.Popen(
[str(binary), "--trust", "--workdir", str(workdir)],
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
env=env,
text=False,
)
os.close(slave_fd)
output = bytearray()
deadline = time.monotonic() + 30
try:
while time.monotonic() < deadline:
if proc.poll() is not None:
break
readable, _, _ = select.select([master_fd], [], [], 0.2)
if not readable:
continue
chunk = os.read(master_fd, 4096)
if not chunk:
break
output.extend(chunk)
decoded = output.decode("utf-8", errors="replace")
if "Traceback" in decoded or "StylesheetError" in decoded:
_fail(decoded)
# This pins the Textual app title used as the main-app smoke marker.
if "\x1b]0;Vibe\x07" in decoded and len(output) > 4096:
print("PASS: interactive launch loaded bundled UI assets")
return
finally:
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=5)
os.close(master_fd)
_fail("interactive launch did not render expected UI output before timeout")
def test_setup_loads_bundled_assets(binary: Path) -> None:
if platform.system() == "Windows":
# Windows does not provide the Unix pty module used to drive Textual here.
print("SKIP: --setup pty smoke test (Windows)")
return
import pty
import select
with tempfile.TemporaryDirectory() as tmp:
workdir = Path(tmp) / "workdir"
workdir.mkdir()
master_fd, slave_fd = pty.openpty()
proc = subprocess.Popen(
[str(binary), "--setup", "--workdir", str(workdir)],
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
env=_isolated_env(Path(tmp) / "home"),
text=False,
)
os.close(slave_fd)
output = bytearray()
deadline = time.monotonic() + 30
try:
while time.monotonic() < deadline:
if proc.poll() is not None:
break
readable, _, _ = select.select([master_fd], [], [], 0.2)
if not readable:
continue
chunk = os.read(master_fd, 4096)
if not chunk:
break
output.extend(chunk)
if b"Traceback" in output or b"StylesheetError" in output:
_fail(output.decode("utf-8", errors="replace"))
if b"Mistral" in output or b"API" in output or b"Welcome" in output:
print("PASS: --setup loaded bundled UI assets")
return
finally:
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=5)
os.close(master_fd)
_fail("--setup did not render expected setup UI text before timeout")
def test_programmatic_missing_api_key(binary: Path) -> None:
with tempfile.TemporaryDirectory() as tmp:
result = subprocess.run(
[str(binary), "-p", "hello"],
capture_output=True,
env=_isolated_env(Path(tmp) / ".vibe"),
text=True,
timeout=30,
)
if result.returncode == 0:
_fail("programmatic mode without API key unexpectedly succeeded")
output = f"{result.stdout}\n{result.stderr}"
if "Traceback" in output:
_fail(f"programmatic mode raised a traceback:\n{output}")
# This pins the user-facing guidance for the missing API key path.
if "Set the environment variable" not in output:
_fail(f"missing expected API key guidance:\n{output}")
print("PASS: programmatic mode reports missing API key")
def test_bundled_runtime_files(binary_dir: Path) -> None:
bundle_root = binary_dir / "_internal" / "vibe"
source_root = Path(__file__).resolve().parents[2] / "vibe"
if not bundle_root.is_dir():
_fail(f"bundled vibe package not found at {bundle_root}")
if not source_root.is_dir():
_fail(f"source vibe package not found at {source_root}")
required_exact = [
"whats_new.md",
"cli/textual_ui/app.tcss",
"setup/onboarding/onboarding.tcss",
"setup/trusted_folders/trust_folder_dialog.tcss",
]
missing_exact = [
relative
for relative in required_exact
if not (bundle_root / relative).is_file()
]
if missing_exact:
lines = ["Missing required bundled runtime file(s):"]
lines.extend(f" - vibe/{relative}" for relative in missing_exact)
_fail("\n".join(lines))
mirrored_globs = [
"**/*.tcss",
"**/*.md",
"core/tools/builtins/*.py",
"core/skills/builtins/*.py",
]
for pattern in mirrored_globs:
source_files = {
path.relative_to(source_root)
for path in source_root.glob(pattern)
if path.is_file()
}
bundled_files = {
path.relative_to(bundle_root)
for path in bundle_root.glob(pattern)
if path.is_file()
}
missing = sorted(source_files - bundled_files)
if missing:
lines = [f"Bundle is missing runtime file(s) for pattern {pattern}:"]
lines.extend(f" - vibe/{path}" for path in missing)
_fail("\n".join(lines))
print("PASS: bundled runtime files are present")
def test_installed_bundle_launches(binary_dir: Path, binary_name: str) -> None:
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
install_dir = tmp_path / "install" / "vibe"
workdir = tmp_path / "workdir"
vibe_home = tmp_path / "home"
workdir.mkdir()
shutil.copytree(binary_dir, install_dir)
installed_binary = install_dir / binary_name
if not installed_binary.exists():
_fail(f"installed binary not found at {installed_binary}")
if platform.system() != "Windows":
installed_binary.chmod(0o755)
env = _isolated_env(vibe_home)
env["PATH"] = f"{install_dir}{os.pathsep}{env.get('PATH', '')}"
result = subprocess.run(
[binary_name, "--version"],
capture_output=True,
cwd=workdir,
env=env,
text=True,
timeout=30,
)
if result.returncode != 0:
_fail(
"installed bundle --version exited with code "
f"{result.returncode}\nstderr: {result.stderr}"
)
if "Traceback" in f"{result.stdout}\n{result.stderr}":
_fail(f"installed bundle raised a traceback:\n{result.stdout}\n{result.stderr}")
print(f"PASS: installed bundle launches from PATH -> {result.stdout.strip()}")
_PT_GNU_STACK = 0x6474E551
_PF_X = 0x1
def _has_executable_stack(filepath: Path) -> bool | None:
try:
with filepath.open("rb") as f:
magic = f.read(4)
if magic != b"\x7fELF":
return None
ei_class = f.read(1)[0]
ei_data = f.read(1)[0]
match ei_data:
case 1:
endian = "<"
case 2:
endian = ">"
case _:
return None
if ei_class == 2:
f.seek(32)
(e_phoff,) = struct.unpack(f"{endian}Q", f.read(8))
f.seek(54)
(e_phentsize,) = struct.unpack(f"{endian}H", f.read(2))
(e_phnum,) = struct.unpack(f"{endian}H", f.read(2))
for i in range(e_phnum):
f.seek(e_phoff + i * e_phentsize)
(p_type,) = struct.unpack(f"{endian}I", f.read(4))
(p_flags,) = struct.unpack(f"{endian}I", f.read(4))
if p_type == _PT_GNU_STACK:
return bool(p_flags & _PF_X)
elif ei_class == 1:
f.seek(28)
(e_phoff,) = struct.unpack(f"{endian}I", f.read(4))
f.seek(42)
(e_phentsize,) = struct.unpack(f"{endian}H", f.read(2))
(e_phnum,) = struct.unpack(f"{endian}H", f.read(2))
for i in range(e_phnum):
off = e_phoff + i * e_phentsize
f.seek(off)
(p_type,) = struct.unpack(f"{endian}I", f.read(4))
f.seek(off + 24)
(p_flags,) = struct.unpack(f"{endian}I", f.read(4))
if p_type == _PT_GNU_STACK:
return bool(p_flags & _PF_X)
return False
except (OSError, struct.error):
return None
def test_no_executable_stack(binary_dir: Path, binary_name: str) -> None:
if platform.system() != "Linux":
print("SKIP: executable stack check (not Linux)")
return
internal_dir = binary_dir / "_internal"
if not internal_dir.exists():
_fail(f"_internal directory not found at {internal_dir}")
violations: list[Path] = []
checked = 0
candidates = [binary_dir / binary_name, *internal_dir.rglob("*")]
for filepath in candidates:
if not filepath.is_file():
continue
result = _has_executable_stack(filepath)
if result is None:
continue
checked += 1
if result:
violations.append(filepath)
if violations:
lines = [
f"Found {len(violations)} ELF file(s) with executable stack "
f"(GNU_STACK RWE) out of {checked} checked.",
"",
"These will fail on SELinux-enforcing or hardened kernels:",
]
for violation in violations:
lines.append(f" - {violation.relative_to(binary_dir)}")
lines.append("")
lines.append("Fix: run 'patchelf --clear-execstack' on these files.")
_fail("\n".join(lines))
print(f"PASS: no executable stack in {checked} ELF files")
def main() -> None:
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <binary-dir>")
sys.exit(1)
binary_dir = Path(sys.argv[1])
binary_name = "vibe.exe" if platform.system() == "Windows" else "vibe"
binary = binary_dir / binary_name
if not binary.exists():
_fail(f"binary not found at {binary}")
if platform.system() != "Windows":
binary.chmod(0o755)
print(f"Testing binary: {binary}\n")
test_version(binary)
test_bundled_runtime_files(binary_dir)
test_installed_bundle_launches(binary_dir, binary_name)
test_no_executable_stack(binary_dir, binary_name)
test_interactive_launch_loads_bundled_assets(binary)
test_setup_loads_bundled_assets(binary)
test_programmatic_missing_api_key(binary)
print("\nAll smoke tests passed!")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,193 @@
from __future__ import annotations
from pathlib import Path
from typing import Literal
import pytest
from vibe.cli import entrypoint as entrypoint_mod
from vibe.core.trusted_folders import trusted_folders_manager
from vibe.setup.trusted_folders.trust_folder_dialog import TrustDecision
def _make_git_repo(path: Path) -> None:
git_dir = path / ".git"
git_dir.mkdir()
(git_dir / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8")
def _git_sub_with_agents(
tmp_path: Path, *, agents_at_sub: bool = True
) -> tuple[Path, Path]:
repo = tmp_path / "repo"
repo.mkdir()
_make_git_repo(repo)
sub = repo / "src"
sub.mkdir()
if agents_at_sub:
(sub / "AGENTS.md").write_text("# Agents", encoding="utf-8")
else:
(repo / "AGENTS.md").write_text("# Agents", encoding="utf-8")
return repo, sub
def _patch_ask(
monkeypatch: pytest.MonkeyPatch, *, decision: TrustDecision = TrustDecision.DECLINE
) -> dict[str, object]:
captured: dict[str, object] = {"called": False}
def fake_ask(
cwd: Path,
repo_root: Path | None,
detected_files: list[str],
repo_detected_files: list[str] | None = None,
offer_repo_trust: bool = False,
repo_explicitly_untrusted: bool = False,
) -> TrustDecision:
captured["called"] = True
captured["cwd"] = cwd
captured["repo_root"] = repo_root
captured["detected_files"] = detected_files
captured["repo_detected_files"] = repo_detected_files
captured["offer_repo_trust"] = offer_repo_trust
captured["repo_explicitly_untrusted"] = repo_explicitly_untrusted
return decision
monkeypatch.setattr(entrypoint_mod, "ask_trust_folder", fake_ask)
return captured
@pytest.fixture
def away_from_home(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(Path, "home", classmethod(lambda _cls: tmp_path / "elsewhere"))
@pytest.mark.parametrize(
"setup",
[
pytest.param("repo_already_trusted", id="repo_already_trusted"),
pytest.param("cwd_explicitly_untrusted", id="cwd_explicitly_untrusted"),
],
)
def test_skips_trust_dialog(
setup: Literal["repo_already_trusted", "cwd_explicitly_untrusted"],
tmp_path: Path,
away_from_home: None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
match setup:
case "repo_already_trusted":
repo, cwd = _git_sub_with_agents(tmp_path)
trusted_folders_manager.add_trusted(repo)
case "cwd_explicitly_untrusted":
cwd = tmp_path / "plain"
cwd.mkdir()
(cwd / "AGENTS.md").write_text("# Agents", encoding="utf-8")
trusted_folders_manager.add_untrusted(cwd)
captured = _patch_ask(monkeypatch)
entrypoint_mod.check_and_resolve_trusted_folder(cwd)
assert captured["called"] is False
def test_shows_trust_dialog_when_only_repo_context_files_exist(
tmp_path: Path, away_from_home: None, monkeypatch: pytest.MonkeyPatch
) -> None:
repo, cwd = _git_sub_with_agents(tmp_path, agents_at_sub=False)
captured = _patch_ask(monkeypatch)
entrypoint_mod.check_and_resolve_trusted_folder(cwd)
assert captured["called"] is True
assert captured["detected_files"] == []
assert captured["repo_detected_files"] == ["AGENTS.md"]
assert captured["repo_root"] == repo.resolve()
@pytest.mark.parametrize(
"untrusted_at,offer_repo_trust,repo_explicitly_untrusted",
[
pytest.param(None, True, False, id="repo_undecided"),
pytest.param("repo", False, True, id="repo_explicitly_untrusted"),
pytest.param("parent", True, False, id="parent_untrusted_only"),
],
)
def test_ask_trust_folder_args_in_git_repo(
untrusted_at: str | None,
offer_repo_trust: bool,
repo_explicitly_untrusted: bool,
tmp_path: Path,
away_from_home: None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
if untrusted_at == "parent":
parent = tmp_path / "parent"
parent.mkdir()
repo = parent / "repo"
repo.mkdir()
_make_git_repo(repo)
cwd = repo / "src"
cwd.mkdir()
(cwd / "AGENTS.md").write_text("# Agents", encoding="utf-8")
trusted_folders_manager.add_untrusted(parent)
else:
repo, cwd = _git_sub_with_agents(tmp_path)
if untrusted_at == "repo":
trusted_folders_manager.add_untrusted(repo)
captured = _patch_ask(monkeypatch)
entrypoint_mod.check_and_resolve_trusted_folder(cwd)
assert captured["called"] is True
assert captured["cwd"] == cwd
assert captured["repo_root"] == repo.resolve()
assert captured["detected_files"] == ["AGENTS.md"]
assert captured["repo_detected_files"] == []
assert captured["offer_repo_trust"] is offer_repo_trust
assert captured["repo_explicitly_untrusted"] is repo_explicitly_untrusted
@pytest.mark.parametrize(
("decision", "sub_trusted", "repo_trusted", "sub_explicitly_untrusted"),
[
pytest.param(TrustDecision.TRUST_REPO, True, True, False, id="trust_repo"),
pytest.param(TrustDecision.TRUST_CWD, True, None, False, id="trust_cwd"),
pytest.param(TrustDecision.DECLINE, False, None, True, id="decline"),
],
)
def test_applies_trust_decision(
decision: TrustDecision,
sub_trusted: bool | None,
repo_trusted: bool | None,
sub_explicitly_untrusted: bool,
tmp_path: Path,
away_from_home: None,
monkeypatch: pytest.MonkeyPatch,
) -> None:
repo, sub = _git_sub_with_agents(tmp_path)
_patch_ask(monkeypatch, decision=decision)
entrypoint_mod.check_and_resolve_trusted_folder(sub)
assert trusted_folders_manager.is_trusted(sub) is sub_trusted
assert trusted_folders_manager.is_trusted(repo) is repo_trusted
assert (
trusted_folders_manager.is_explicitly_untrusted(sub) is sub_explicitly_untrusted
)
if decision == TrustDecision.TRUST_CWD:
assert trusted_folders_manager.is_trusted(repo / "other") is None
def test_no_git_repo_offers_no_repo_trust_and_decline_untrusts_cwd(
tmp_path: Path, away_from_home: None, monkeypatch: pytest.MonkeyPatch
) -> None:
cwd = tmp_path / "plain"
cwd.mkdir()
(cwd / "AGENTS.md").write_text("# Agents", encoding="utf-8")
captured = _patch_ask(monkeypatch, decision=TrustDecision.DECLINE)
entrypoint_mod.check_and_resolve_trusted_folder(cwd)
assert captured["called"] is True
assert captured["repo_root"] is None
assert captured["repo_detected_files"] == []
assert captured["offer_repo_trust"] is False
assert trusted_folders_manager.is_explicitly_untrusted(cwd) is True

View file

@ -18,6 +18,7 @@ from vibe.cli.clipboard import (
_read_clipboard, _read_clipboard,
copy_selection_to_clipboard, copy_selection_to_clipboard,
copy_text_to_clipboard, copy_text_to_clipboard,
try_copy_text_to_clipboard,
) )
@ -206,6 +207,32 @@ def test_copy_text_to_clipboard_shows_failure_when_clipboard_unavailable(
) )
@patch("vibe.cli.clipboard._copy_to_clipboard")
def test_try_copy_text_to_clipboard_returns_true_when_copy_succeeds(
mock_copy_to_clipboard: MagicMock,
) -> None:
result = try_copy_text_to_clipboard("assistant text")
assert result is True
mock_copy_to_clipboard.assert_called_once_with("assistant text")
@patch("vibe.cli.clipboard._copy_to_clipboard")
def test_try_copy_text_to_clipboard_returns_false_when_clipboard_unavailable(
mock_copy_to_clipboard: MagicMock,
) -> None:
mock_copy_to_clipboard.side_effect = RuntimeError("All clipboard strategies failed")
result = try_copy_text_to_clipboard("assistant text")
assert result is False
mock_copy_to_clipboard.assert_called_once_with("assistant text")
def test_try_copy_text_to_clipboard_returns_false_for_empty_text() -> None:
assert try_copy_text_to_clipboard("") is False
def test_copy_text_to_clipboard_returns_none_for_empty_text( def test_copy_text_to_clipboard_returns_none_for_empty_text(
mock_app: MagicMock, mock_app: MagicMock,
) -> None: ) -> None:

View file

@ -246,7 +246,7 @@ class TestConnectorMenuOrdering:
) )
ordered = _sort_connector_names_for_menu( ordered = _sort_connector_names_for_menu(
registry.get_connector_names(), registry registry.get_connector_names(), registry, set()
) )
assert ordered == ["alpha", "beta", "zeta"] assert ordered == ["alpha", "beta", "zeta"]
@ -261,11 +261,54 @@ class TestConnectorMenuOrdering:
) )
ordered = _sort_connector_names_for_menu( ordered = _sort_connector_names_for_menu(
registry.get_connector_names(), registry registry.get_connector_names(), registry, set()
) )
assert ordered == ["alpha", "Beta", "Zeta"] assert ordered == ["alpha", "Beta", "Zeta"]
def test_disabled_connectors_sink_to_bottom(self) -> None:
registry = FakeConnectorRegistry(
connectors={
"alpha": [RemoteTool(name="lookup", description="Lookup")],
"beta": [RemoteTool(name="search", description="Search")],
"gamma": [],
}
)
# alpha is enabled+connected, beta is enabled+connected but disabled in
# config, gamma is enabled+disconnected. Expected: enabled-and-connected
# first, then enabled-but-disconnected, then disabled at the bottom.
ordered = _sort_connector_names_for_menu(
registry.get_connector_names(), registry, {"beta"}
)
assert ordered == ["alpha", "gamma", "beta"]
def test_full_ordering_enabled_then_connected_then_alpha(self) -> None:
registry = FakeConnectorRegistry(
connectors={
"d_disabled_connected": [RemoteTool(name="t", description="t")],
"b_enabled_disconnected": [],
"a_enabled_connected": [RemoteTool(name="t", description="t")],
"c_enabled_connected": [RemoteTool(name="t", description="t")],
"e_disabled_disconnected": [],
}
)
ordered = _sort_connector_names_for_menu(
registry.get_connector_names(),
registry,
{"d_disabled_connected", "e_disabled_disconnected"},
)
assert ordered == [
"a_enabled_connected",
"c_enabled_connected",
"b_enabled_disconnected",
"d_disabled_connected",
"e_disabled_disconnected",
]
class TestHelpBarChanges: class TestHelpBarChanges:
"""The help bar should show 'Enter Authenticate' for disconnected connectors.""" """The help bar should show 'Enter Authenticate' for disconnected connectors."""

View file

@ -45,7 +45,7 @@ async def test_ui_displays_messages_when_resuming_session(
id="tool_call_1", id="tool_call_1",
index=0, index=0,
function=FunctionCall( function=FunctionCall(
name="read_file", arguments='{"path": "test.txt"}' name="read", arguments='{"file_path": "test.txt"}'
), ),
) )
], ],
@ -53,7 +53,7 @@ async def test_ui_displays_messages_when_resuming_session(
tool_result_msg = LLMMessage( tool_result_msg = LLMMessage(
role=Role.tool, role=Role.tool,
content="File content here", content="File content here",
name="read_file", name="read",
tool_call_id="tool_call_1", tool_call_id="tool_call_1",
) )
@ -78,12 +78,12 @@ async def test_ui_displays_messages_when_resuming_session(
# Verify tool call message is displayed # Verify tool call message is displayed
tool_call_messages = app.query(ToolCallMessage) tool_call_messages = app.query(ToolCallMessage)
assert len(tool_call_messages) == 1 assert len(tool_call_messages) == 1
assert tool_call_messages[0]._tool_name == "read_file" assert tool_call_messages[0]._tool_name == "read"
# Verify tool result message is displayed # Verify tool result message is displayed
tool_result_messages = app.query(ToolResultMessage) tool_result_messages = app.query(ToolResultMessage)
assert len(tool_result_messages) == 1 assert len(tool_result_messages) == 1
assert tool_result_messages[0].tool_name == "read_file" assert tool_result_messages[0].tool_name == "read"
assert tool_result_messages[0]._content == "File content here" assert tool_result_messages[0]._content == "File content here"

View file

@ -0,0 +1,64 @@
from __future__ import annotations
import pytest
from tests.conftest import build_test_vibe_app
from vibe.cli.textual_ui.widgets.chat_input import ChatInputContainer, ChatTextArea
@pytest.mark.asyncio
async def test_shift_backspace_deletes_character_like_backspace() -> None:
app = build_test_vibe_app()
async with app.run_test() as pilot:
await pilot.pause(0.1)
text_area = app.query_one(ChatTextArea)
text_area.focus()
await pilot.pause(0.1)
await pilot.press("a", "b", "c")
await pilot.pause(0.1)
assert app.query_one(ChatInputContainer).value == "abc"
await pilot.press("shift+backspace")
await pilot.pause(0.1)
assert app.query_one(ChatInputContainer).value == "ab"
@pytest.mark.asyncio
async def test_shift_delete_deletes_character_like_delete() -> None:
app = build_test_vibe_app()
async with app.run_test() as pilot:
await pilot.pause(0.1)
text_area = app.query_one(ChatTextArea)
text_area.focus()
await pilot.pause(0.1)
await pilot.press("a", "b", "c", "left")
await pilot.pause(0.1)
assert app.query_one(ChatInputContainer).value == "abc"
await pilot.press("shift+delete")
await pilot.pause(0.1)
assert app.query_one(ChatInputContainer).value == "ab"
@pytest.mark.asyncio
async def test_shift_backspace_resets_mode_when_empty() -> None:
app = build_test_vibe_app()
async with app.run_test() as pilot:
await pilot.pause(0.1)
text_area = app.query_one(ChatTextArea)
text_area.focus()
text_area.set_mode("!")
await pilot.pause(0.1)
assert text_area.input_mode == "!"
await pilot.press("shift+backspace")
await pilot.pause(0.1)
assert text_area.input_mode == ">"

View file

@ -0,0 +1,149 @@
from __future__ import annotations
from pathlib import Path
import pytest
from tests.conftest import build_test_vibe_app, build_test_vibe_config
from vibe.cli.textual_ui.app import _ImageAttachmentRejection
from vibe.cli.textual_ui.widgets.chat_input.container import ChatInputContainer
from vibe.cli.textual_ui.widgets.messages import ErrorMessage
from vibe.core.autocompletion.path_prompt import build_path_prompt_payload
from vibe.core.config import ModelConfig, ProviderConfig
from vibe.core.types import (
MAX_IMAGE_BYTES,
MAX_IMAGES_PER_MESSAGE,
Backend,
ImageAttachment,
)
PNG_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00" * 16
def _vision_config(*, supports_images: bool = True):
models = [
ModelConfig(
name="mistral-vibe-cli-latest",
provider="mistral",
alias="devstral-latest",
supports_images=supports_images,
)
]
providers = [
ProviderConfig(
name="mistral",
api_base="https://api.mistral.ai/v1",
api_key_env_var="MISTRAL_API_KEY",
backend=Backend.MISTRAL,
)
]
return build_test_vibe_config(
active_model="devstral-latest", models=models, providers=providers
)
@pytest.mark.asyncio
async def test_build_image_attachments_happy_path(tmp_path: Path) -> None:
(tmp_path / "shot.png").write_bytes(PNG_BYTES)
payload = build_path_prompt_payload("look at @shot.png", base_dir=tmp_path)
app = build_test_vibe_app(config=_vision_config())
result = await app._build_image_attachments(payload)
assert isinstance(result, list)
assert len(result) == 1
assert isinstance(result[0], ImageAttachment)
assert result[0].alias == "shot.png"
assert result[0].mime_type == "image/png"
@pytest.mark.asyncio
async def test_build_image_attachments_returns_empty_when_no_images(
tmp_path: Path,
) -> None:
(tmp_path / "notes.md").write_text("hi")
payload = build_path_prompt_payload("read @notes.md", base_dir=tmp_path)
app = build_test_vibe_app(config=_vision_config())
result = await app._build_image_attachments(payload)
assert result == []
@pytest.mark.asyncio
async def test_build_image_attachments_rejects_too_many_images(tmp_path: Path) -> None:
mentions = []
for i in range(MAX_IMAGES_PER_MESSAGE + 1):
name = f"img{i}.png"
(tmp_path / name).write_bytes(PNG_BYTES)
mentions.append(f"@{name}")
payload = build_path_prompt_payload(" ".join(mentions), base_dir=tmp_path)
app = build_test_vibe_app(config=_vision_config())
result = await app._build_image_attachments(payload)
assert isinstance(result, _ImageAttachmentRejection)
assert not result.no_vision
assert "Too many image attachments" in result.message
assert str(MAX_IMAGES_PER_MESSAGE) in result.message
@pytest.mark.asyncio
async def test_build_image_attachments_rejects_non_vision_model(tmp_path: Path) -> None:
(tmp_path / "shot.png").write_bytes(PNG_BYTES)
payload = build_path_prompt_payload("look at @shot.png", base_dir=tmp_path)
app = build_test_vibe_app(config=_vision_config(supports_images=False))
result = await app._build_image_attachments(payload)
assert isinstance(result, _ImageAttachmentRejection)
assert result.no_vision
assert "does not support images" in result.message
assert "devstral-latest" in result.message
@pytest.mark.asyncio
async def test_build_image_attachments_rejects_oversize_image(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
# Patch the cap down so we don't need to write 10MB to disk.
monkeypatch.setattr("vibe.cli.textual_ui.app.MAX_IMAGE_BYTES", 32)
(tmp_path / "shot.png").write_bytes(PNG_BYTES + b"\x00" * 64)
payload = build_path_prompt_payload("look at @shot.png", base_dir=tmp_path)
app = build_test_vibe_app(config=_vision_config())
result = await app._build_image_attachments(payload)
assert isinstance(result, _ImageAttachmentRejection)
assert not result.no_vision
assert "shot.png" in result.message
assert "max" in result.message.lower()
def test_max_image_bytes_default_is_10_mib() -> None:
assert MAX_IMAGE_BYTES == 10 * 1024 * 1024
def test_max_images_per_message_default_is_8() -> None:
assert MAX_IMAGES_PER_MESSAGE == 8
@pytest.mark.asyncio
async def test_submit_restores_input_when_image_validation_fails(
tmp_path: Path,
) -> None:
(tmp_path / "shot.png").write_bytes(PNG_BYTES)
app = build_test_vibe_app(config=_vision_config(supports_images=False))
typed = f"look at @{tmp_path / 'shot.png'}"
async with app.run_test() as pilot:
chat_input = app.query_one(ChatInputContainer)
chat_input.value = typed
await pilot.press("enter")
await pilot.pause()
assert chat_input.value == typed
errors = list(app.query(ErrorMessage))
assert errors
assert all(not e._show_border for e in errors)

View file

@ -0,0 +1,221 @@
from __future__ import annotations
from pathlib import Path
import pytest
from textual import events
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.chat_input.paste_path import (
maybe_prepend_at_for_image_path,
rewrite_bare_image_paths_in_text,
)
from vibe.cli.textual_ui.widgets.chat_input.text_area import ChatTextArea
def test_bare_absolute_image_path_gets_at_prefix(tmp_path: Path) -> None:
img = tmp_path / "shot.png"
img.write_bytes(b"\x89PNG")
rewritten = maybe_prepend_at_for_image_path(str(img))
assert rewritten == f"@{img}"
def test_quoted_image_path_with_spaces_is_unwrapped_and_quoted(tmp_path: Path) -> None:
img = tmp_path / "has space.png"
img.write_bytes(b"\x89PNG")
pasted = f"'{img}'"
rewritten = maybe_prepend_at_for_image_path(pasted)
assert rewritten == f"@'{img}'"
def test_backslash_escaped_image_path_is_unescaped_and_quoted(tmp_path: Path) -> None:
img = tmp_path / "has space.png"
img.write_bytes(b"\x89PNG")
pasted = str(img).replace(" ", "\\ ")
rewritten = maybe_prepend_at_for_image_path(pasted)
assert rewritten == f"@'{img}'"
def test_non_image_file_path_is_left_untouched(tmp_path: Path) -> None:
txt = tmp_path / "readme.md"
txt.write_text("hi")
rewritten = maybe_prepend_at_for_image_path(str(txt))
assert rewritten == str(txt)
def test_missing_image_path_is_left_untouched(tmp_path: Path) -> None:
missing = tmp_path / "nope.png"
rewritten = maybe_prepend_at_for_image_path(str(missing))
assert rewritten == str(missing)
def test_unresolvable_tilde_user_does_not_crash() -> None:
# `~a` raises RuntimeError from Path.expanduser() when user `a` does not
# exist; the rewrite hook must swallow it so every keystroke after `~`
# does not crash the TUI.
assert maybe_prepend_at_for_image_path("~a") == "~a"
assert rewrite_bare_image_paths_in_text("hello ~a world") == "hello ~a world"
def test_multiline_paste_is_left_untouched(tmp_path: Path) -> None:
img = tmp_path / "shot.png"
img.write_bytes(b"\x89PNG")
pasted = f"{img}\nother line"
assert maybe_prepend_at_for_image_path(pasted) == pasted
def test_relative_path_is_left_untouched(tmp_path: Path, monkeypatch) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "shot.png").write_bytes(b"\x89PNG")
assert maybe_prepend_at_for_image_path("shot.png") == "shot.png"
def test_already_at_prefixed_path_is_left_untouched(tmp_path: Path) -> None:
img = tmp_path / "shot.png"
img.write_bytes(b"\x89PNG")
pasted = f"@{img}"
assert maybe_prepend_at_for_image_path(pasted) == pasted
@pytest.mark.asyncio
async def test_paste_event_inserts_at_prefixed_path_into_chat_input(
vibe_app: VibeApp, tmp_path: Path
) -> None:
img = tmp_path / "shot.png"
img.write_bytes(b"\x89PNG")
async with vibe_app.run_test() as pilot:
chat_input = vibe_app.query_one(ChatInputContainer)
text_area = chat_input.query_one(ChatTextArea)
text_area.focus()
text_area.post_message(events.Paste(text=str(img)))
await pilot.pause()
assert chat_input.value == f"@{img}"
@pytest.mark.asyncio
async def test_paste_event_leaves_non_image_paths_untouched(
vibe_app: VibeApp, tmp_path: Path
) -> None:
txt = tmp_path / "notes.md"
txt.write_text("hi")
async with vibe_app.run_test() as pilot:
chat_input = vibe_app.query_one(ChatInputContainer)
text_area = chat_input.query_one(ChatTextArea)
text_area.focus()
text_area.post_message(events.Paste(text=str(txt)))
await pilot.pause()
assert chat_input.value == str(txt)
def test_rewrite_bare_image_paths_handles_bare_path(tmp_path: Path) -> None:
img = tmp_path / "shot.png"
img.write_bytes(b"\x89PNG")
rewritten = rewrite_bare_image_paths_in_text(f"look at {img} please")
assert rewritten == f"look at @{img} please"
def test_rewrite_bare_image_paths_handles_quoted_path(tmp_path: Path) -> None:
img = tmp_path / "has space.png"
img.write_bytes(b"\x89PNG")
rewritten = rewrite_bare_image_paths_in_text(f"look at '{img}'")
assert rewritten == f"look at @'{img}'"
def test_rewrite_bare_image_paths_is_idempotent(tmp_path: Path) -> None:
img = tmp_path / "shot.png"
img.write_bytes(b"\x89PNG")
once = rewrite_bare_image_paths_in_text(f"see {img}")
twice = rewrite_bare_image_paths_in_text(once)
assert once == twice == f"see @{img}"
def test_rewrite_bare_image_paths_skips_non_image(tmp_path: Path) -> None:
txt = tmp_path / "notes.md"
txt.write_text("hi")
rewritten = rewrite_bare_image_paths_in_text(f"see {txt}")
assert rewritten == f"see {txt}"
def test_rewrite_bare_image_paths_fast_path_skips_stat_for_plain_text(
monkeypatch,
) -> None:
from pathlib import Path as _Path
calls = 0
original = _Path.is_file
def _counting_is_file(self):
nonlocal calls
calls += 1
return original(self)
monkeypatch.setattr(_Path, "is_file", _counting_is_file)
rewrite_bare_image_paths_in_text("hello world, nothing path-shaped here")
rewrite_bare_image_paths_in_text("multi\nline\ntext\nwith no slash")
assert calls == 0
@pytest.mark.asyncio
async def test_text_change_hook_rewrites_quoted_image_path(
vibe_app: VibeApp, tmp_path: Path
) -> None:
img = tmp_path / "shot.png"
img.write_bytes(b"\x89PNG")
async with vibe_app.run_test() as pilot:
chat_input = vibe_app.query_one(ChatInputContainer)
text_area = chat_input.query_one(ChatTextArea)
text_area.focus()
# Simulate a non-bracketed-paste insertion (terminal that does
# not emit Paste): set the text directly the way a bulk insert
# would land it.
text_area.text = f"'{img}'"
await pilot.pause()
# No spaces in the path -> the scanner emits an unquoted `@<path>`.
assert chat_input.value == f"@{img}"
@pytest.mark.asyncio
async def test_text_change_hook_rewrites_quoted_image_path_with_spaces(
vibe_app: VibeApp, tmp_path: Path
) -> None:
img = tmp_path / "has space.png"
img.write_bytes(b"\x89PNG")
async with vibe_app.run_test() as pilot:
chat_input = vibe_app.query_one(ChatInputContainer)
text_area = chat_input.query_one(ChatTextArea)
text_area.focus()
text_area.text = f"'{img}'"
await pilot.pause()
assert chat_input.value == f"@'{img}'"

View file

@ -0,0 +1,18 @@
from __future__ import annotations
from vibe.cli.textual_ui.widgets.tool_widgets import _strip_line_numbers
def test_strips_numbered_prefixes() -> None:
content = " 1→first\n 42→second\n 100→third"
assert _strip_line_numbers(content) == "first\nsecond\nthird"
def test_leaves_warning_lines_untouched() -> None:
content = "<vibe_warning>Warning: the file exists but the contents are empty.</vibe_warning>"
assert _strip_line_numbers(content) == content
def test_preserves_arrows_inside_content() -> None:
content = " 1→a → b → c"
assert _strip_line_numbers(content) == "a → b → c"

View file

@ -0,0 +1,78 @@
from __future__ import annotations
from pathlib import Path
from weakref import WeakKeyDictionary
from vibe.cli.textual_ui.widgets.messages import UserMessage
from vibe.cli.textual_ui.windowing.history import build_history_widgets
from vibe.core.types import ImageAttachment, LLMMessage, Role
def _att(path: Path, alias: str) -> ImageAttachment:
path.write_bytes(b"\x89PNG")
return ImageAttachment(path=path, alias=alias, mime_type="image/png")
def test_attachments_footer_singular(tmp_path: Path) -> None:
att = _att(tmp_path / "shot.png", "shot.png")
rendered = UserMessage._format_attachments_footer([att])
assert "attached image:" in rendered
assert '[link="file://' in rendered
assert "]shot.png[/link]" in rendered
def test_attachments_footer_plural(tmp_path: Path) -> None:
a = _att(tmp_path / "a.png", "a.png")
b = _att(tmp_path / "b.png", "b.png")
rendered = UserMessage._format_attachments_footer([a, b])
assert "attached images:" in rendered
assert rendered.count('[link="file://') == 2
assert "a.png" in rendered
assert "b.png" in rendered
def test_attachments_footer_escapes_alias_brackets(tmp_path: Path) -> None:
att = _att(tmp_path / "shot.png", "weird [bracket].png")
rendered = UserMessage._format_attachments_footer([att])
# Rich's escape() turns "[" into "\[".
assert "\\[bracket]" in rendered
def test_resumed_user_message_with_images_renders_footer(tmp_path: Path) -> None:
att = _att(tmp_path / "shot.png", "shot.png")
msg = LLMMessage(role=Role.user, content="look at @shot.png", images=[att])
widgets = build_history_widgets(
[msg],
tool_call_map={},
start_index=0,
tools_collapsed=False,
history_widget_indices=WeakKeyDictionary(),
)
assert len(widgets) == 1
user_widget = widgets[0]
assert isinstance(user_widget, UserMessage)
assert user_widget._images == [att]
def test_resumed_user_message_with_images_only_still_mounts(tmp_path: Path) -> None:
att = _att(tmp_path / "shot.png", "shot.png")
msg = LLMMessage(role=Role.user, content="", images=[att])
widgets = build_history_widgets(
[msg],
tool_call_map={},
start_index=0,
tools_collapsed=False,
history_widget_indices=WeakKeyDictionary(),
)
assert len(widgets) == 1
assert isinstance(widgets[0], UserMessage)

View file

@ -0,0 +1,71 @@
from __future__ import annotations
import hashlib
from pathlib import Path
import pytest
from vibe.core.session.image_snapshot import ImageSnapshotError, snapshot_image
PNG_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00" * 16
def test_snapshot_image_copies_to_attachments_dir(tmp_path: Path) -> None:
src = tmp_path / "screenshot.png"
src.write_bytes(PNG_BYTES)
session_dir = tmp_path / "session"
session_dir.mkdir()
att = snapshot_image(src, alias="screenshot.png", session_dir=session_dir)
digest = hashlib.sha1(PNG_BYTES, usedforsecurity=False).hexdigest()
assert att.path == (session_dir / "attachments" / f"{digest}.png").resolve()
assert att.alias == "screenshot.png"
assert att.mime_type == "image/png"
assert att.path.read_bytes() == PNG_BYTES
def test_snapshot_image_is_idempotent_on_same_bytes(tmp_path: Path) -> None:
src_a = tmp_path / "a.png"
src_b = tmp_path / "b.png"
src_a.write_bytes(PNG_BYTES)
src_b.write_bytes(PNG_BYTES)
session_dir = tmp_path / "session"
att_a = snapshot_image(src_a, alias="a.png", session_dir=session_dir)
att_b = snapshot_image(src_b, alias="b.png", session_dir=session_dir)
assert att_a.path == att_b.path
assert sum(1 for _ in (session_dir / "attachments").iterdir()) == 1
def test_snapshot_image_returns_source_when_session_dir_is_none(tmp_path: Path) -> None:
src = tmp_path / "screenshot.png"
src.write_bytes(PNG_BYTES)
att = snapshot_image(src, alias="screenshot.png", session_dir=None)
assert att.path == src.resolve()
assert att.alias == "screenshot.png"
def test_snapshot_image_rejects_non_image_extension(tmp_path: Path) -> None:
src = tmp_path / "readme.txt"
src.write_bytes(b"hi")
with pytest.raises(ImageSnapshotError):
snapshot_image(src, alias="readme.txt", session_dir=None)
def test_snapshot_image_rejects_missing_file(tmp_path: Path) -> None:
with pytest.raises(ImageSnapshotError):
snapshot_image(tmp_path / "missing.png", alias="missing.png", session_dir=None)
def test_snapshot_image_normalizes_jpg_to_jpeg_mime(tmp_path: Path) -> None:
src = tmp_path / "photo.jpg"
src.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 8)
att = snapshot_image(src, alias="photo.jpg", session_dir=None)
assert att.mime_type == "image/jpeg"

View file

@ -80,6 +80,36 @@ def test_record_ignores_empty_session_id(
assert last_session_pointer.load(session_logging) is None assert last_session_pointer.load(session_logging) is None
def test_clear_matching_removes_matching_pointers_only(
session_logging: SessionLoggingConfig,
) -> None:
pointer_dir = Path(session_logging.save_dir) / last_session_pointer.POINTER_DIR_NAME
pointer_dir.mkdir()
(pointer_dir / "ttys001").write_text("deleted-session\n", encoding="utf-8")
(pointer_dir / "ttys002").write_text("other-session\n", encoding="utf-8")
(pointer_dir / "ttys003").write_text("deleted-session\n", encoding="utf-8")
(pointer_dir / "nested").mkdir()
last_session_pointer.clear_matching(session_logging, "deleted-session")
assert not (pointer_dir / "ttys001").exists()
assert (pointer_dir / "ttys002").read_text(encoding="utf-8") == "other-session\n"
assert not (pointer_dir / "ttys003").exists()
assert (pointer_dir / "nested").is_dir()
def test_clear_matching_skips_when_logging_disabled(tmp_path: Path) -> None:
disabled = SessionLoggingConfig(save_dir=str(tmp_path), enabled=False)
pointer_dir = tmp_path / last_session_pointer.POINTER_DIR_NAME
pointer_dir.mkdir()
pointer_path = pointer_dir / "ttys001"
pointer_path.write_text("deleted-session\n", encoding="utf-8")
last_session_pointer.clear_matching(disabled, "deleted-session")
assert pointer_path.exists()
def test_current_tty_key_returns_none_when_ttyname_is_unavailable( def test_current_tty_key_returns_none_when_ttyname_is_unavailable(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:

View file

@ -344,13 +344,13 @@ class TestProjectPromptsDirsAdditionalDirs:
class TestProjectRootsNestedDedup: class TestProjectRootsNestedDedup:
def test_nested_add_dirs_collapse(self, tmp_path: Path) -> None: def test_nested_add_dirs_preserved(self, tmp_path: Path) -> None:
outer = (tmp_path / "outer").resolve() outer = (tmp_path / "outer").resolve()
inner = outer / "inner" inner = outer / "inner"
inner.mkdir(parents=True) inner.mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(outer, inner)) mgr = HarnessFilesManager(sources=("user",), _additional_dirs=(outer, inner))
assert mgr.project_roots == [outer] assert mgr.project_roots == [outer, inner]
def test_add_dir_containing_cwd_keeps_both( def test_add_dir_containing_cwd_keeps_both(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
@ -380,10 +380,13 @@ class TestProjectRootsNestedDedup:
outer = (tmp_path / "outer").resolve() outer = (tmp_path / "outer").resolve()
inner = outer / "inner" inner = outer / "inner"
inner.mkdir(parents=True) inner.mkdir(parents=True)
skills_dir = inner / ".vibe" / "skills"
skills_dir.mkdir(parents=True)
monkeypatch.chdir(outer) monkeypatch.chdir(outer)
trusted_folders_manager.trust_for_session(outer) trusted_folders_manager.trust_for_session(outer)
mgr = HarnessFilesManager( mgr = HarnessFilesManager(
sources=("user", "project"), _additional_dirs=(inner,) sources=("user", "project"), _additional_dirs=(inner,)
) )
assert mgr.project_roots == [outer] assert mgr.project_roots == [outer, inner]
assert skills_dir in mgr.project_skills_dirs

View file

@ -20,7 +20,7 @@ class TestAgentProfile:
"""Test that EXPLORE agent has expected enabled tools.""" """Test that EXPLORE agent has expected enabled tools."""
enabled_tools = EXPLORE.overrides.get("enabled_tools", []) enabled_tools = EXPLORE.overrides.get("enabled_tools", [])
assert "grep" in enabled_tools assert "grep" in enabled_tools
assert "read_file" in enabled_tools assert "read" in enabled_tools
def test_builtin_agents_contains_explore(self) -> None: def test_builtin_agents_contains_explore(self) -> None:
"""Test that BUILTIN_AGENTS includes explore.""" """Test that BUILTIN_AGENTS includes explore."""

View file

@ -13,6 +13,7 @@ from vibe.core.config.layer import (
TrustNotResolvedError, TrustNotResolvedError,
UntrustedLayerError, UntrustedLayerError,
) )
from vibe.core.config.patch import ConfigPatch
class StubLayer(ConfigLayer[BaseModel]): class StubLayer(ConfigLayer[BaseModel]):
@ -401,7 +402,7 @@ async def test_get_fingerprint_not_implemented() -> None:
async def test_apply_not_implemented() -> None: async def test_apply_not_implemented() -> None:
layer = StubLayer() layer = StubLayer()
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
await layer.apply({"op": "set"}) await layer.apply(ConfigPatch(fingerprint="fp-1"))
@pytest.mark.asyncio @pytest.mark.asyncio

View file

@ -7,6 +7,7 @@ import pytest
from vibe.core.config.layer import ConfigLayer, RawConfig from vibe.core.config.layer import ConfigLayer, RawConfig
from vibe.core.config.orchestrator import ConfigOrchestrator from vibe.core.config.orchestrator import ConfigOrchestrator
from vibe.core.config.patch import ConfigPatch
from vibe.core.config.schema import ConfigSchema, WithReplaceMerge from vibe.core.config.schema import ConfigSchema, WithReplaceMerge
@ -76,7 +77,7 @@ async def test_origin_of_missing_key_returns_none() -> None:
async def test_apply_patch_raises_not_implemented() -> None: async def test_apply_patch_raises_not_implemented() -> None:
orch = await ConfigOrchestrator.create(schema=SimpleSchema, layers=[]) orch = await ConfigOrchestrator.create(schema=SimpleSchema, layers=[])
with pytest.raises(NotImplementedError, match="M2"): with pytest.raises(NotImplementedError, match="M2"):
await orch.apply_patch({}) await orch.apply_patch(ConfigPatch(fingerprint="fp-1"))
@pytest.mark.asyncio @pytest.mark.asyncio

View file

@ -78,7 +78,7 @@ class TestProjectToolsDirs:
mgr = HarnessFilesManager(sources=("user", "project")) mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_tools_dirs == [] assert mgr.project_tools_dirs == []
def test_finds_tools_dirs_recursively( def test_does_not_find_nested_tools_dirs(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
@ -86,19 +86,6 @@ class TestProjectToolsDirs:
(tmp_path / ".vibe" / "tools").mkdir(parents=True) (tmp_path / ".vibe" / "tools").mkdir(parents=True)
(tmp_path / "sub" / ".vibe" / "tools").mkdir(parents=True) (tmp_path / "sub" / ".vibe" / "tools").mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project")) mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_tools_dirs == [
tmp_path / ".vibe" / "tools",
tmp_path / "sub" / ".vibe" / "tools",
]
def test_does_not_descend_into_ignored_dirs(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
(tmp_path / ".git" / ".vibe" / "tools").mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_tools_dirs == [tmp_path / ".vibe" / "tools"] assert mgr.project_tools_dirs == [tmp_path / ".vibe" / "tools"]
@ -147,7 +134,7 @@ class TestProjectAgentsDirs:
mgr = HarnessFilesManager(sources=("user", "project")) mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_agents_dirs == [] assert mgr.project_agents_dirs == []
def test_finds_agents_dirs_recursively( def test_does_not_find_nested_agents_dirs(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
@ -155,19 +142,6 @@ class TestProjectAgentsDirs:
(tmp_path / ".vibe" / "agents").mkdir(parents=True) (tmp_path / ".vibe" / "agents").mkdir(parents=True)
(tmp_path / "sub" / "deep" / ".vibe" / "agents").mkdir(parents=True) (tmp_path / "sub" / "deep" / ".vibe" / "agents").mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project")) mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_agents_dirs == [
tmp_path / ".vibe" / "agents",
tmp_path / "sub" / "deep" / ".vibe" / "agents",
]
def test_does_not_descend_into_ignored_dirs(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
(tmp_path / ".vibe" / "agents").mkdir(parents=True)
(tmp_path / "__pycache__" / ".vibe" / "agents").mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_agents_dirs == [tmp_path / ".vibe" / "agents"] assert mgr.project_agents_dirs == [tmp_path / ".vibe" / "agents"]
@ -544,7 +518,7 @@ class TestProjectSkillsDirs:
mgr = HarnessFilesManager(sources=("user", "project")) mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_skills_dirs == [] assert mgr.project_skills_dirs == []
def test_finds_skills_dirs_recursively_in_trusted_folder( def test_does_not_find_nested_skills_dirs(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
@ -553,20 +527,6 @@ class TestProjectSkillsDirs:
(tmp_path / "sub" / ".agents" / "skills").mkdir(parents=True) (tmp_path / "sub" / ".agents" / "skills").mkdir(parents=True)
(tmp_path / "sub" / "deep" / ".vibe" / "skills").mkdir(parents=True) (tmp_path / "sub" / "deep" / ".vibe" / "skills").mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project")) mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_skills_dirs == [
tmp_path / ".vibe" / "skills",
tmp_path / "sub" / ".agents" / "skills",
tmp_path / "sub" / "deep" / ".vibe" / "skills",
]
def test_does_not_descend_into_ignored_dirs(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
(tmp_path / ".vibe" / "skills").mkdir(parents=True)
(tmp_path / "node_modules" / ".vibe" / "skills").mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_skills_dirs == [tmp_path / ".vibe" / "skills"] assert mgr.project_skills_dirs == [tmp_path / ".vibe" / "skills"]

View file

@ -309,6 +309,32 @@ class TestSetThinking:
assert result["models"][0].get("thinking") is None assert result["models"][0].get("thinking") is None
assert result["models"][1]["thinking"] == "max" assert result["models"][1]["thinking"] == "max"
def test_preserves_supports_images_when_materializing_defaults(
self, config_dir: Path
) -> None:
config_file = config_dir / "config.toml"
data = {"active_model": "mistral-medium-3.5"}
with config_file.open("wb") as f:
tomli_w.dump(data, f)
cfg = VibeConfig.load()
cfg.set_thinking("low")
reloaded = VibeConfig.load()
active = reloaded.get_active_model()
assert active.alias == "mistral-medium-3.5"
assert active.thinking == "low"
assert active.supports_images is True
with config_file.open("rb") as f:
result = tomllib.load(f)
active_entry = result["models"][0]
assert active_entry["supports_images"] is True
assert "temperature" not in active_entry
assert "input_price" not in active_entry
assert "output_price" not in active_entry
assert "auto_compact_threshold" not in active_entry
assert "supports_images" not in result["models"][1]
class TestMigrateLeavesFindInBashAllowlist: class TestMigrateLeavesFindInBashAllowlist:
def test_keeps_find_in_config_file( def test_keeps_find_in_config_file(
@ -659,6 +685,62 @@ class TestMigrateMistralVibeCliLatestDefaults:
assert result["models"][0]["output_price"] == 7.5 assert result["models"][0]["output_price"] == 7.5
assert result["models"][0]["thinking"] == "high" assert result["models"][0]["thinking"] == "high"
def test_backfills_supports_images_on_existing_mistral_medium_entry(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
config_file = tmp_path / "config.toml"
data = {
"active_model": "mistral-medium-3.5",
"models": [
{
"name": "mistral-vibe-cli-latest",
"provider": "mistral",
"alias": "mistral-medium-3.5",
"temperature": 1.0,
"input_price": 1.5,
"output_price": 7.5,
"thinking": "high",
}
],
}
with config_file.open("wb") as f:
tomli_w.dump(data, f)
reset_harness_files_manager()
init_harness_files_manager("user")
VibeConfig._migrate()
with config_file.open("rb") as f:
result = tomllib.load(f)
assert result["models"][0]["supports_images"] is True
def test_preserves_explicit_supports_images_false(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
config_file = tmp_path / "config.toml"
data = {
"models": [
{
"name": "mistral-vibe-cli-latest",
"provider": "mistral",
"alias": "mistral-medium-3.5",
"supports_images": False,
}
]
}
with config_file.open("wb") as f:
tomli_w.dump(data, f)
reset_harness_files_manager()
init_harness_files_manager("user")
VibeConfig._migrate()
with config_file.open("rb") as f:
result = tomllib.load(f)
assert result["models"][0]["supports_images"] is False
class TestAutoCompactThresholdFallback: class TestAutoCompactThresholdFallback:
def test_model_without_explicit_threshold_inherits_global(self) -> None: def test_model_without_explicit_threshold_inherits_global(self) -> None:
@ -1320,3 +1402,105 @@ class TestIsActiveModelMistral:
active_model="llama-local", active_model="llama-local",
) )
assert cfg.is_active_model_mistral() is False assert cfg.is_active_model_mistral() is False
class TestMigrateRenamedTools:
def test_renames_read_file_and_search_replace_keys(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
config_file = tmp_path / "config.toml"
data = {
"tools": {
"read_file": {
"permission": "always",
"allowlist": ["src/**"],
"max_read_bytes": 64000,
},
"search_replace": {
"allowlist": ["src/**"],
"max_content_size": 100000,
"create_backup": True,
},
}
}
with config_file.open("wb") as f:
tomli_w.dump(data, f)
reset_harness_files_manager()
init_harness_files_manager("user")
VibeConfig._migrate()
with config_file.open("rb") as f:
result = tomllib.load(f)
tools = result["tools"]
assert "read_file" not in tools
assert "search_replace" not in tools
assert tools["read"] == {
"permission": "always",
"allowlist": ["src/**"],
"max_read_bytes": 64000,
}
# Common options carry over; edit-incompatible options are dropped.
assert tools["edit"] == {"allowlist": ["src/**"]}
def test_prefers_existing_new_key_and_drops_legacy(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
config_file = tmp_path / "config.toml"
data = {
"tools": {
"read_file": {"permission": "always"},
"read": {"permission": "ask"},
}
}
with config_file.open("wb") as f:
tomli_w.dump(data, f)
reset_harness_files_manager()
init_harness_files_manager("user")
VibeConfig._migrate()
with config_file.open("rb") as f:
result = tomllib.load(f)
assert "read_file" not in result["tools"]
assert result["tools"]["read"] == {"permission": "ask"}
def test_renames_entries_in_enabled_and_disabled_lists(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
config_file = tmp_path / "config.toml"
data = {
"enabled_tools": ["read_file", "grep"],
"disabled_tools": ["search_replace"],
}
with config_file.open("wb") as f:
tomli_w.dump(data, f)
reset_harness_files_manager()
init_harness_files_manager("user")
VibeConfig._migrate()
with config_file.open("rb") as f:
result = tomllib.load(f)
assert result["enabled_tools"] == ["read", "grep"]
assert result["disabled_tools"] == ["edit"]
def test_noop_when_no_legacy_tool_names(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
config_file = tmp_path / "config.toml"
data = {"tools": {"read": {"permission": "always"}}}
with config_file.open("wb") as f:
tomli_w.dump(data, f)
reset_harness_files_manager()
init_harness_files_manager("user")
VibeConfig._migrate()
with config_file.open("rb") as f:
result = tomllib.load(f)
assert result["tools"] == {"read": {"permission": "always"}}

View file

@ -0,0 +1,86 @@
from __future__ import annotations
import os
from pathlib import Path
import stat
import pytest
from tests.mock.utils import collect_result
from vibe.core.tools.base import BaseToolState, ToolError
from vibe.core.tools.builtins.edit import Edit, EditArgs, EditConfig
@pytest.mark.asyncio
async def test_edit_rewrites_with_detected_encoding(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
path = tmp_path / "utf16.txt"
original = "line one café\nline two été\n"
path.write_bytes(original.encode("utf-16"))
tool = Edit(config_getter=lambda: EditConfig(), state=BaseToolState())
await collect_result(
tool.run(
EditArgs(
file_path=str(path),
old_string="line one café",
new_string="LINE ONE CAFÉ",
)
)
)
assert path.read_bytes().startswith(b"\xff\xfe")
assert path.read_text(encoding="utf-16") == "LINE ONE CAFÉ\nline two été\n"
@pytest.mark.asyncio
async def test_edit_preserves_file_mode(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
path = tmp_path / "script.sh"
path.write_text("#!/bin/sh\necho old\n")
os.chmod(path, 0o755)
tool = Edit(config_getter=lambda: EditConfig(), state=BaseToolState())
await collect_result(
tool.run(EditArgs(file_path=str(path), old_string="old", new_string="new"))
)
assert stat.S_IMODE(path.stat().st_mode) == 0o755
@pytest.mark.asyncio
@pytest.mark.parametrize("newline", ["\r\n", "\r", "\n"])
async def test_edit_preserves_line_endings(
newline: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
path = tmp_path / "f.txt"
original = newline.join(["alpha", "beta", "gamma"])
path.write_bytes(original.encode("utf-8"))
tool = Edit(config_getter=lambda: EditConfig(), state=BaseToolState())
await collect_result(
tool.run(EditArgs(file_path=str(path), old_string="beta", new_string="BETA"))
)
assert path.read_bytes() == newline.join(["alpha", "BETA", "gamma"]).encode("utf-8")
@pytest.mark.asyncio
async def test_edit_binary_file_raises_tool_error(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
path = tmp_path / "blob.bin"
# PNG header + some non-text payload.
path.write_bytes(b"\x89PNG\r\n\x1a\n" + bytes(range(256)) * 4)
tool = Edit(config_getter=lambda: EditConfig(), state=BaseToolState())
with pytest.raises(ToolError, match="not valid text"):
await collect_result(
tool.run(EditArgs(file_path=str(path), old_string="PNG", new_string="JPG"))
)

View file

@ -0,0 +1,48 @@
from __future__ import annotations
import os
from unittest.mock import patch
import pytest
from vibe.core.config.layers.environment import EnvironmentLayer
from vibe.core.config.vibe_schema import VibeConfigSchema
@pytest.mark.asyncio
async def test_reads_env_vars() -> None:
env = {
"MISTRAL_API_KEY": "test-key",
"VIBE_ACTIVE_MODEL": "mistral-large",
"VIBE_VIM_KEYBINDINGS": "true",
"VIBE_ENABLE_TELEMETRY": "0",
"VIBE_UNKNOWN_VAR": "ignored",
"VIBE_SESSION_LOGGING__ENABLED": "false",
"VIBE_SESSION_LOGGING__SESSION_PREFIX": "mysession",
"VIBE_API_TIMEOUT": ".12",
}
with patch.dict(os.environ, env, clear=True):
layer = EnvironmentLayer(schema=VibeConfigSchema)
data = await layer.load()
assert data.model_dump() == {
"active_model": "mistral-large",
"vim_keybindings": True,
"enable_telemetry": False,
"session_logging": {"enabled": False, "session_prefix": "mysession"},
"api_timeout": 0.12,
}
assert layer.name == "environment"
@pytest.mark.asyncio
async def test_no_vars_set_returns_empty() -> None:
with patch.dict(os.environ, {}, clear=True):
data = await EnvironmentLayer(schema=VibeConfigSchema).load()
assert data.model_dump() == {}
@pytest.mark.asyncio
async def test_always_trusted() -> None:
assert await EnvironmentLayer(schema=VibeConfigSchema).resolve_trust() is True

View file

@ -0,0 +1,51 @@
from __future__ import annotations
from pathlib import Path
import pytest
from vibe.core.types import ImageAttachment, LLMMessage, Role
@pytest.fixture()
def image_a(tmp_path: Path) -> ImageAttachment:
p = tmp_path / "a.png"
p.write_bytes(b"\x89PNG")
return ImageAttachment(path=p, alias="a.png", mime_type="image/png")
@pytest.fixture()
def image_b(tmp_path: Path) -> ImageAttachment:
p = tmp_path / "b.png"
p.write_bytes(b"\x89PNG")
return ImageAttachment(path=p, alias="b.png", mime_type="image/png")
def _msg(content: str, images: list[ImageAttachment] | None = None) -> LLMMessage:
return LLMMessage(role=Role.assistant, content=content, images=images)
def test_merge_prefers_self_images_when_present(
image_a: ImageAttachment, image_b: ImageAttachment
) -> None:
merged = _msg("hi", images=[image_a]) + _msg(" there", images=[image_b])
assert merged.images == [image_a]
def test_merge_falls_back_to_other_when_self_is_none(image_b: ImageAttachment) -> None:
merged = _msg("hi") + _msg(" there", images=[image_b])
assert merged.images == [image_b]
def test_merge_preserves_explicit_empty_self_images_over_other(
image_b: ImageAttachment,
) -> None:
# An explicit `[]` (intentional clearing) must NOT silently inherit
# `other.images`. Truthy/falsy-based merging would have flipped this.
merged = _msg("hi", images=[]) + _msg(" there", images=[image_b])
assert merged.images == []
def test_merge_yields_none_when_both_sides_are_none() -> None:
merged = _msg("hi") + _msg(" there")
assert merged.images is None

View file

@ -0,0 +1,114 @@
from __future__ import annotations
from pathlib import Path
from vibe.core.paths._local_config_files import LocalConfigDirs, find_local_config_dirs
class TestSubdirs:
def test_finds_config_at_root(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
result = find_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".vibe" / "tools" in result.tools
def test_does_not_descend_into_subdirectories(self, tmp_path: Path) -> None:
(tmp_path / "sub" / ".vibe" / "tools").mkdir(parents=True)
(tmp_path / "a" / "b" / ".vibe" / "skills").mkdir(parents=True)
result = find_local_config_dirs(tmp_path)
assert result.tools == ()
assert result.skills == ()
assert result.agents == ()
assert result.config_dirs == ()
def test_finds_agents_skills_at_root(self, tmp_path: Path) -> None:
(tmp_path / ".agents" / "skills").mkdir(parents=True)
result = find_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".agents" / "skills" in result.skills
def test_finds_all_config_types_at_root(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
(tmp_path / ".vibe" / "skills").mkdir(parents=True)
(tmp_path / ".vibe" / "agents").mkdir(parents=True)
(tmp_path / ".agents" / "skills").mkdir(parents=True)
result = find_local_config_dirs(tmp_path)
resolved = tmp_path.resolve()
assert resolved / ".vibe" / "tools" in result.tools
assert resolved / ".vibe" / "skills" in result.skills
assert resolved / ".vibe" / "agents" in result.agents
assert resolved / ".agents" / "skills" in result.skills
class TestConfigDirs:
def test_finds_vibe_with_tools(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
result = find_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".vibe" in result.config_dirs
def test_finds_vibe_with_skills(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "skills").mkdir(parents=True)
result = find_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".vibe" in result.config_dirs
def test_finds_agents_with_skills(self, tmp_path: Path) -> None:
(tmp_path / ".agents" / "skills").mkdir(parents=True)
result = find_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".agents" in result.config_dirs
def test_ignores_empty_vibe_dir(self, tmp_path: Path) -> None:
(tmp_path / ".vibe").mkdir()
result = find_local_config_dirs(tmp_path)
assert result.config_dirs == ()
def test_ignores_empty_agents_dir(self, tmp_path: Path) -> None:
(tmp_path / ".agents").mkdir()
result = find_local_config_dirs(tmp_path)
assert result.config_dirs == ()
def test_returns_empty_when_empty(self, tmp_path: Path) -> None:
result = find_local_config_dirs(tmp_path)
assert result.config_dirs == ()
def test_finds_vibe_with_prompts(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "prompts").mkdir(parents=True)
result = find_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".vibe" in result.config_dirs
def test_finds_vibe_with_config_toml(self, tmp_path: Path) -> None:
(tmp_path / ".vibe").mkdir()
(tmp_path / ".vibe" / "config.toml").write_text("")
result = find_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".vibe" in result.config_dirs
def test_finds_vibe_and_agents_at_same_root(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "skills").mkdir(parents=True)
(tmp_path / ".agents" / "skills").mkdir(parents=True)
result = find_local_config_dirs(tmp_path)
resolved = tmp_path.resolve()
assert resolved / ".vibe" in result.config_dirs
assert resolved / ".agents" in result.config_dirs
class TestLocalConfigDirsOr:
def test_or_concatenates_each_field(self) -> None:
a = LocalConfigDirs(
config_dirs=(Path("/a/.vibe"),),
tools=(Path("/a/.vibe/tools"),),
skills=(Path("/a/.vibe/skills"),),
agents=(Path("/a/.vibe/agents"),),
)
b = LocalConfigDirs(
config_dirs=(Path("/b/.vibe"),),
tools=(Path("/b/.vibe/tools"),),
skills=(Path("/b/.vibe/skills"),),
agents=(Path("/b/.vibe/agents"),),
)
merged = a | b
assert merged.config_dirs == (Path("/a/.vibe"), Path("/b/.vibe"))
assert merged.tools == (Path("/a/.vibe/tools"), Path("/b/.vibe/tools"))
assert merged.skills == (Path("/a/.vibe/skills"), Path("/b/.vibe/skills"))
assert merged.agents == (Path("/a/.vibe/agents"), Path("/b/.vibe/agents"))
def test_or_with_empty_is_identity(self) -> None:
a = LocalConfigDirs(tools=(Path("/a/.vibe/tools"),))
assert (a | LocalConfigDirs()) == a
assert (LocalConfigDirs() | a) == a

View file

@ -1,195 +0,0 @@
from __future__ import annotations
from pathlib import Path
from vibe.core.paths._local_config_walk import (
_MAX_DIRS,
WALK_MAX_DEPTH,
ConfigWalkResult,
walk_local_config_dirs,
)
class TestWalkTools:
def test_finds_config_at_root(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".vibe" / "tools" in result.tools
def test_finds_config_within_depth_limit(self, tmp_path: Path) -> None:
nested = tmp_path
for i in range(WALK_MAX_DEPTH):
nested = nested / f"level{i}"
(nested / ".vibe" / "skills").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert nested.resolve() / ".vibe" / "skills" in result.skills
def test_does_not_find_config_beyond_depth_limit(self, tmp_path: Path) -> None:
nested = tmp_path
for i in range(WALK_MAX_DEPTH + 1):
nested = nested / f"level{i}"
(nested / ".vibe" / "tools").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert not result.tools
assert not result.skills
assert not result.agents
def test_respects_dir_count_limit(self, tmp_path: Path) -> None:
for i in range(_MAX_DIRS + 10):
(tmp_path / f"dir{i:05d}").mkdir()
(tmp_path / "zzz_last" / ".vibe" / "tools").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert isinstance(result.tools, tuple)
def test_skips_ignored_directories(self, tmp_path: Path) -> None:
(tmp_path / "node_modules" / ".vibe" / "tools").mkdir(parents=True)
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert result.tools == (tmp_path.resolve() / ".vibe" / "tools",)
def test_skips_dot_directories(self, tmp_path: Path) -> None:
(tmp_path / ".hidden" / ".vibe" / "tools").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert not result.tools
def test_preserves_alphabetical_ordering(self, tmp_path: Path) -> None:
(tmp_path / "bbb" / ".vibe" / "tools").mkdir(parents=True)
(tmp_path / "aaa" / ".vibe" / "tools").mkdir(parents=True)
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
resolved = tmp_path.resolve()
assert result.tools == (
resolved / ".vibe" / "tools",
resolved / "aaa" / ".vibe" / "tools",
resolved / "bbb" / ".vibe" / "tools",
)
def test_finds_agents_skills(self, tmp_path: Path) -> None:
(tmp_path / ".agents" / "skills").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".agents" / "skills" in result.skills
def test_finds_all_config_types(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
(tmp_path / ".vibe" / "skills").mkdir(parents=True)
(tmp_path / ".vibe" / "agents").mkdir(parents=True)
(tmp_path / ".agents" / "skills").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
resolved = tmp_path.resolve()
assert resolved / ".vibe" / "tools" in result.tools
assert resolved / ".vibe" / "skills" in result.skills
assert resolved / ".vibe" / "agents" in result.agents
assert resolved / ".agents" / "skills" in result.skills
class TestWalkConfigDirs:
def test_finds_vibe_with_tools(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".vibe" in result.config_dirs
def test_finds_vibe_with_skills(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "skills").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".vibe" in result.config_dirs
def test_finds_agents_with_skills(self, tmp_path: Path) -> None:
(tmp_path / ".agents" / "skills").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".agents" in result.config_dirs
def test_ignores_empty_vibe_dir(self, tmp_path: Path) -> None:
(tmp_path / ".vibe").mkdir()
result = walk_local_config_dirs(tmp_path)
assert result.config_dirs == ()
def test_ignores_empty_agents_dir(self, tmp_path: Path) -> None:
(tmp_path / ".agents").mkdir()
result = walk_local_config_dirs(tmp_path)
assert result.config_dirs == ()
def test_returns_empty_when_empty(self, tmp_path: Path) -> None:
result = walk_local_config_dirs(tmp_path)
assert result.config_dirs == ()
def test_finds_shallow_nested(self, tmp_path: Path) -> None:
(tmp_path / "sub" / ".vibe" / "skills").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert tmp_path.resolve() / "sub" / ".vibe" in result.config_dirs
def test_finds_at_depth_2(self, tmp_path: Path) -> None:
(tmp_path / "a" / "b" / ".agents" / "skills").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert tmp_path.resolve() / "a" / "b" / ".agents" in result.config_dirs
def test_returns_empty_beyond_default_depth(self, tmp_path: Path) -> None:
(tmp_path / "a" / "b" / "c" / "d" / "e" / ".vibe" / "tools").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert result.config_dirs == ()
def test_custom_depth(self, tmp_path: Path) -> None:
(tmp_path / "a" / "b" / "c" / "d" / "e" / ".vibe" / "tools").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path, max_depth=5)
assert (
tmp_path.resolve() / "a" / "b" / "c" / "d" / "e" / ".vibe"
in result.config_dirs
)
def test_finds_match_among_many_dirs(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
for i in range(100):
(tmp_path / f"dir{i}").mkdir()
result = walk_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".vibe" in result.config_dirs
def test_skips_ignored_directories(self, tmp_path: Path) -> None:
(tmp_path / "node_modules" / ".vibe" / "skills").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert result.config_dirs == ()
def test_finds_vibe_with_prompts(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "prompts").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".vibe" in result.config_dirs
def test_finds_vibe_with_config_toml(self, tmp_path: Path) -> None:
(tmp_path / ".vibe").mkdir()
(tmp_path / ".vibe" / "config.toml").write_text("")
result = walk_local_config_dirs(tmp_path)
assert tmp_path.resolve() / ".vibe" in result.config_dirs
def test_finds_multiple_config_dirs(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "skills").mkdir(parents=True)
(tmp_path / ".agents" / "skills").mkdir(parents=True)
(tmp_path / "sub" / ".vibe" / "tools").mkdir(parents=True)
result = walk_local_config_dirs(tmp_path)
resolved = tmp_path.resolve()
assert resolved / ".vibe" in result.config_dirs
assert resolved / ".agents" in result.config_dirs
assert resolved / "sub" / ".vibe" in result.config_dirs
class TestConfigWalkResultOr:
def test_or_concatenates_each_field(self) -> None:
a = ConfigWalkResult(
config_dirs=(Path("/a/.vibe"),),
tools=(Path("/a/.vibe/tools"),),
skills=(Path("/a/.vibe/skills"),),
agents=(Path("/a/.vibe/agents"),),
)
b = ConfigWalkResult(
config_dirs=(Path("/b/.vibe"),),
tools=(Path("/b/.vibe/tools"),),
skills=(Path("/b/.vibe/skills"),),
agents=(Path("/b/.vibe/agents"),),
)
merged = a | b
assert merged.config_dirs == (Path("/a/.vibe"), Path("/b/.vibe"))
assert merged.tools == (Path("/a/.vibe/tools"), Path("/b/.vibe/tools"))
assert merged.skills == (Path("/a/.vibe/skills"), Path("/b/.vibe/skills"))
assert merged.agents == (Path("/a/.vibe/agents"), Path("/b/.vibe/agents"))
def test_or_with_empty_is_identity(self) -> None:
a = ConfigWalkResult(tools=(Path("/a/.vibe/tools"),))
assert (a | ConfigWalkResult()) == a
assert (ConfigWalkResult() | a) == a

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import pytest import pytest
from vibe.core.config.layers.overrides import OverridesLayer from vibe.core.config.layers.overrides import OverridesLayer
from vibe.core.config.patch import ConfigPatch
@pytest.mark.asyncio @pytest.mark.asyncio
@ -23,7 +24,7 @@ async def test_always_trusted() -> None:
async def test_apply_raises_not_implemented() -> None: async def test_apply_raises_not_implemented() -> None:
layer = OverridesLayer(data={}) layer = OverridesLayer(data={})
with pytest.raises(NotImplementedError, match="M2"): with pytest.raises(NotImplementedError, match="M2"):
await layer.apply({"op": "set"}) await layer.apply(ConfigPatch(fingerprint="fp-1"))
@pytest.mark.asyncio @pytest.mark.asyncio

View file

@ -6,6 +6,7 @@ import pytest
from vibe.core.config.layer import UntrustedLayerError from vibe.core.config.layer import UntrustedLayerError
from vibe.core.config.layers.project import ProjectConfigLayer from vibe.core.config.layers.project import ProjectConfigLayer
from vibe.core.config.patch import ConfigPatch
from vibe.core.paths._vibe_home import GlobalPath from vibe.core.paths._vibe_home import GlobalPath
from vibe.core.trusted_folders import trusted_folders_manager from vibe.core.trusted_folders import trusted_folders_manager
@ -66,7 +67,7 @@ async def test_default_name(tmp_working_directory: Path) -> None:
async def test_apply_raises_not_implemented(tmp_working_directory: Path) -> None: async def test_apply_raises_not_implemented(tmp_working_directory: Path) -> None:
layer = ProjectConfigLayer(path=tmp_working_directory) layer = ProjectConfigLayer(path=tmp_working_directory)
with pytest.raises(NotImplementedError, match="M2"): with pytest.raises(NotImplementedError, match="M2"):
await layer.apply({"op": "set"}) await layer.apply(ConfigPatch(fingerprint="fp-1"))
@pytest.mark.asyncio @pytest.mark.asyncio

View file

@ -413,7 +413,6 @@ def test_working_task_promoted_to_real_tool_call_does_not_create_duplicate_row()
"output": { "output": {
"path": "/workspace/hello.py", "path": "/workspace/hello.py",
"bytes_written": 22, "bytes_written": 22,
"file_existed": False,
"content": 'print("Hello, World!")', "content": 'print("Hello, World!")',
}, },
}, },
@ -456,7 +455,6 @@ def test_idle_boundary_waits_for_open_tool_results() -> None:
"output": { "output": {
"path": "/workspace/hello_world.js", "path": "/workspace/hello_world.js",
"bytes_written": 29, "bytes_written": 29,
"file_existed": False,
"content": "console.log('Hello, World!');", "content": "console.log('Hello, World!');",
}, },
}, },
@ -731,7 +729,7 @@ def test_working_completed_with_tool_call_id_emits_error_result() -> None:
"working", "working",
{ {
"title": "Executing write_file", "title": "Executing write_file",
"content": "Error: File exists. Set overwrite=True.", "content": "Error: Permission denied.",
"toolUIState": { "toolUIState": {
"type": "file", "type": "file",
"toolCallId": "call-write-err", "toolCallId": "call-write-err",

View file

@ -1,9 +1,15 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import AsyncGenerator
import httpx import httpx
import pytest import pytest
from vibe.core.utils.retry import _is_retryable_http_error from vibe.core.utils.retry import (
_is_retryable_http_error,
async_generator_retry,
async_retry,
)
def _make_http_status_error(status_code: int) -> httpx.HTTPStatusError: def _make_http_status_error(status_code: int) -> httpx.HTTPStatusError:
@ -15,6 +21,10 @@ def _make_http_status_error(status_code: int) -> httpx.HTTPStatusError:
) )
def _make_request(url: str = "https://example.com") -> httpx.Request:
return httpx.Request("POST", url)
class TestIsRetryableHttpError: class TestIsRetryableHttpError:
@pytest.mark.parametrize("code", [408, 409, 425, 429, 500, 502, 503, 504, 529]) @pytest.mark.parametrize("code", [408, 409, 425, 429, 500, 502, 503, 504, 529])
def test_retryable_codes(self, code: int) -> None: def test_retryable_codes(self, code: int) -> None:
@ -24,8 +34,127 @@ class TestIsRetryableHttpError:
def test_non_retryable_codes(self, code: int) -> None: def test_non_retryable_codes(self, code: int) -> None:
assert _is_retryable_http_error(_make_http_status_error(code)) is False assert _is_retryable_http_error(_make_http_status_error(code)) is False
@pytest.mark.parametrize(
"exc",
[
httpx.ConnectTimeout("connect timed out", request=_make_request()),
httpx.ReadTimeout("read timed out", request=_make_request()),
httpx.WriteTimeout("write timed out", request=_make_request()),
httpx.PoolTimeout("pool timed out", request=_make_request()),
httpx.ConnectError("connection refused", request=_make_request()),
httpx.ReadError("read failed", request=_make_request()),
httpx.WriteError("write failed", request=_make_request()),
httpx.RemoteProtocolError("server disconnected", request=_make_request()),
],
)
def test_retryable_network_errors(self, exc: Exception) -> None:
assert _is_retryable_http_error(exc) is True
def test_non_retryable_request_error(self) -> None:
assert _is_retryable_http_error(httpx.InvalidURL("bad url")) is False
def test_non_http_error_returns_false(self) -> None: def test_non_http_error_returns_false(self) -> None:
assert _is_retryable_http_error(ValueError("not http")) is False assert _is_retryable_http_error(ValueError("not http")) is False
def test_generic_exception_returns_false(self) -> None: def test_generic_exception_returns_false(self) -> None:
assert _is_retryable_http_error(RuntimeError("boom")) is False assert _is_retryable_http_error(RuntimeError("boom")) is False
class TestAsyncRetry:
@pytest.mark.asyncio
async def test_retries_network_error_then_succeeds(self) -> None:
attempts = 0
@async_retry(tries=3, delay_seconds=0.0, backoff_factor=1.0)
async def call() -> str:
nonlocal attempts
attempts += 1
if attempts < 2:
raise httpx.ConnectTimeout("timeout", request=_make_request())
return "ok"
result = await call()
assert result == "ok"
assert attempts == 2
@pytest.mark.asyncio
async def test_does_not_retry_non_retryable(self) -> None:
attempts = 0
@async_retry(tries=3, delay_seconds=0.0, backoff_factor=1.0)
async def call() -> str:
nonlocal attempts
attempts += 1
raise ValueError("nope")
with pytest.raises(ValueError):
await call()
assert attempts == 1
@pytest.mark.asyncio
async def test_exhausts_retries(self) -> None:
attempts = 0
@async_retry(tries=3, delay_seconds=0.0, backoff_factor=1.0)
async def call() -> str:
nonlocal attempts
attempts += 1
raise httpx.ReadTimeout("timeout", request=_make_request())
with pytest.raises(httpx.ReadTimeout):
await call()
assert attempts == 3
class TestAsyncGeneratorRetry:
@pytest.mark.asyncio
async def test_retries_before_first_yield(self) -> None:
attempts = 0
@async_generator_retry(tries=3, delay_seconds=0.0, backoff_factor=1.0)
async def gen() -> AsyncGenerator[int]:
nonlocal attempts
attempts += 1
if attempts < 2:
raise httpx.ConnectError("connect failed", request=_make_request())
yield 1
yield 2
items = [item async for item in gen()]
assert items == [1, 2]
assert attempts == 2
@pytest.mark.asyncio
async def test_does_not_retry_after_first_yield(self) -> None:
attempts = 0
@async_generator_retry(tries=3, delay_seconds=0.0, backoff_factor=1.0)
async def gen() -> AsyncGenerator[int]:
nonlocal attempts
attempts += 1
yield 1
raise httpx.ReadError("midstream", request=_make_request())
items: list[int] = []
with pytest.raises(httpx.ReadError):
async for item in gen():
items.append(item)
assert items == [1]
assert attempts == 1
@pytest.mark.asyncio
async def test_does_not_retry_non_retryable_before_yield(self) -> None:
attempts = 0
@async_generator_retry(tries=3, delay_seconds=0.0, backoff_factor=1.0)
async def gen() -> AsyncGenerator[int]:
nonlocal attempts
attempts += 1
raise ValueError("nope")
yield 0 # pragma: no cover
with pytest.raises(ValueError):
async for _ in gen():
pass
assert attempts == 1

View file

@ -25,23 +25,24 @@ async def _act_and_collect(agent_loop, prompt: str) -> list[BaseEvent]:
def _write_file_tool_call( def _write_file_tool_call(
path: str, content: str, *, call_id: str = "call_1", overwrite: bool = False path: str, content: str, *, call_id: str = "call_1"
) -> ToolCall: ) -> ToolCall:
args = json.dumps({"path": path, "content": content, "overwrite": overwrite}) args = json.dumps({"path": path, "content": content})
return ToolCall( return ToolCall(
id=call_id, index=0, function=FunctionCall(name="write_file", arguments=args) id=call_id, index=0, function=FunctionCall(name="write_file", arguments=args)
) )
def _search_replace_tool_call( def _edit_tool_call(
file_path: str, search: str, replace: str, *, call_id: str = "call_1" file_path: str, old_string: str, new_string: str, *, call_id: str = "call_1"
) -> ToolCall: ) -> ToolCall:
content = f"<<<<<<< SEARCH\n{search}\n=======\n{replace}\n>>>>>>> REPLACE" args = json.dumps({
args = json.dumps({"file_path": file_path, "content": content}) "file_path": file_path,
"old_string": old_string,
"new_string": new_string,
})
return ToolCall( return ToolCall(
id=call_id, id=call_id, index=0, function=FunctionCall(name="edit", arguments=args)
index=0,
function=FunctionCall(name="search_replace", arguments=args),
) )
@ -54,10 +55,10 @@ def _bash_tool_call(command: str, *, call_id: str = "call_1") -> ToolCall:
def _make_agent_loop(backend: FakeBackend): def _make_agent_loop(backend: FakeBackend):
config = build_test_vibe_config( config = build_test_vibe_config(
enabled_tools=["write_file", "search_replace", "bash"], enabled_tools=["write_file", "edit", "bash"],
tools={ tools={
"write_file": {"permission": "always"}, "write_file": {"permission": "always"},
"search_replace": {"permission": "always"}, "edit": {"permission": "always"},
"bash": {"permission": "always"}, "bash": {"permission": "always"},
}, },
system_prompt_id="tests", system_prompt_id="tests",
@ -98,10 +99,10 @@ class TestRewindIntegration:
assert not target.exists() assert not target.exists()
async def test_search_replace_rewind_restores_previous_version( async def test_edit_rewind_restores_previous_version(
self, tmp_working_directory: Path self, tmp_working_directory: Path
) -> None: ) -> None:
"""Edit a pre-existing file with search_replace, rewind restores original.""" """Edit a pre-existing file with edit, rewind restores original."""
target = tmp_working_directory / "config.yaml" target = tmp_working_directory / "config.yaml"
target.write_text("key: original\n", encoding="utf-8") target.write_text("key: original\n", encoding="utf-8")
@ -110,9 +111,7 @@ class TestRewindIntegration:
mock_llm_chunk( mock_llm_chunk(
content="Updating config.", content="Updating config.",
tool_calls=[ tool_calls=[
_search_replace_tool_call( _edit_tool_call(str(target), "key: original", "key: modified")
str(target), "key: original", "key: modified"
)
], ],
) )
], ],
@ -129,11 +128,11 @@ class TestRewindIntegration:
assert target.read_text() == "key: original\n" assert target.read_text() == "key: original\n"
async def test_write_then_search_replace_rewind_to_middle( async def test_write_then_edit_rewind_to_middle(
self, tmp_working_directory: Path self, tmp_working_directory: Path
) -> None: ) -> None:
"""Turn 1 creates a file with write_file, turn 2 patches it with """Turn 1 creates a file with write_file, turn 2 patches it with
search_replace. Rewind to turn 2 restores the turn 1 version. edit. Rewind to turn 2 restores the turn 1 version.
""" """
target = tmp_working_directory / "app.py" target = tmp_working_directory / "app.py"
@ -148,12 +147,12 @@ class TestRewindIntegration:
) )
], ],
[mock_llm_chunk(content="Created.")], [mock_llm_chunk(content="Created.")],
# Turn 2: patch with search_replace # Turn 2: patch with edit
[ [
mock_llm_chunk( mock_llm_chunk(
content="Updating.", content="Updating.",
tool_calls=[ tool_calls=[
_search_replace_tool_call( _edit_tool_call(
str(target), str(target),
" pass", " pass",
' print("hello")', ' print("hello")',
@ -222,9 +221,7 @@ class TestRewindIntegration:
mock_llm_chunk( mock_llm_chunk(
content="v2.", content="v2.",
tool_calls=[ tool_calls=[
_write_file_tool_call( _edit_tool_call(str(target), "v1", "v2", call_id="call_2")
str(target), "v2", call_id="call_2", overwrite=True
)
], ],
) )
], ],
@ -234,9 +231,7 @@ class TestRewindIntegration:
mock_llm_chunk( mock_llm_chunk(
content="v2bis.", content="v2bis.",
tool_calls=[ tool_calls=[
_write_file_tool_call( _edit_tool_call(str(target), "v1", "v2bis", call_id="call_3")
str(target), "v2bis", call_id="call_3", overwrite=True
)
], ],
) )
], ],

View file

@ -1,34 +0,0 @@
from __future__ import annotations
from pathlib import Path
import pytest
from tests.mock.utils import collect_result
from vibe.core.tools.base import BaseToolState
from vibe.core.tools.builtins.search_replace import (
SearchReplace,
SearchReplaceArgs,
SearchReplaceConfig,
)
@pytest.mark.asyncio
async def test_search_replace_rewrites_with_detected_encoding(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
path = tmp_path / "utf16.txt"
original = "line one café\nline two été\n"
path.write_bytes(original.encode("utf-16"))
tool = SearchReplace(
config_getter=lambda: SearchReplaceConfig(), state=BaseToolState()
)
patch = "<<<<<<< SEARCH\nline one café\n=======\nLINE ONE CAFÉ\n>>>>>>> REPLACE"
await collect_result(
tool.run(SearchReplaceArgs(file_path=str(path), content=patch))
)
assert path.read_bytes().startswith(b"\xff\xfe")
assert path.read_text(encoding="utf-16") == "LINE ONE CAFÉ\nline two été\n"

View file

@ -28,9 +28,7 @@ def _make_resolved_tool_call(
tool_name: str, args_dict: dict[str, Any] tool_name: str, args_dict: dict[str, Any]
) -> ResolvedToolCall: ) -> ResolvedToolCall:
if tool_name == "write_file": if tool_name == "write_file":
validated = WriteFileArgs( validated = WriteFileArgs(path="foo.txt", content="x")
path="foo.txt", content="x", overwrite=args_dict.get("overwrite", False)
)
cls: type[BaseTool] = WriteFile cls: type[BaseTool] = WriteFile
else: else:
validated = FakeToolArgs() validated = FakeToolArgs()
@ -169,12 +167,12 @@ class TestTelemetryClient:
assert telemetry_events[0]["properties"]["message_id"] == "msg-123" assert telemetry_events[0]["properties"]["message_id"] == "msg-123"
def test_send_tool_call_finished_nb_files_created_write_file_new( def test_send_tool_call_finished_nb_files_created_write_file(
self, telemetry_events: list[dict[str, Any]] self, telemetry_events: list[dict[str, Any]]
) -> None: ) -> None:
config = build_test_vibe_config(enable_telemetry=True) config = build_test_vibe_config(enable_telemetry=True)
client = TelemetryClient(config_getter=lambda: config) client = TelemetryClient(config_getter=lambda: config)
tool_call = _make_resolved_tool_call("write_file", {"overwrite": False}) tool_call = _make_resolved_tool_call("write_file", {})
client.send_tool_call_finished( client.send_tool_call_finished(
tool_call=tool_call, tool_call=tool_call,
@ -182,31 +180,12 @@ class TestTelemetryClient:
decision=None, decision=None,
agent_profile_name="default", agent_profile_name="default",
model="mistral-large", model="mistral-large",
result={"file_existed": False}, result={},
) )
assert telemetry_events[0]["properties"]["nb_files_created"] == 1 assert telemetry_events[0]["properties"]["nb_files_created"] == 1
assert telemetry_events[0]["properties"]["nb_files_modified"] == 0 assert telemetry_events[0]["properties"]["nb_files_modified"] == 0
def test_send_tool_call_finished_nb_files_modified_write_file_overwrite(
self, telemetry_events: list[dict[str, Any]]
) -> None:
config = build_test_vibe_config(enable_telemetry=True)
client = TelemetryClient(config_getter=lambda: config)
tool_call = _make_resolved_tool_call("write_file", {"overwrite": True})
client.send_tool_call_finished(
tool_call=tool_call,
status="success",
decision=None,
agent_profile_name="default",
model="mistral-large",
result={"file_existed": True},
)
assert telemetry_events[0]["properties"]["nb_files_created"] == 0
assert telemetry_events[0]["properties"]["nb_files_modified"] == 1
def test_send_tool_call_finished_decision_none( def test_send_tool_call_finished_decision_none(
self, telemetry_events: list[dict[str, Any]] self, telemetry_events: list[dict[str, Any]]
) -> None: ) -> None:

View file

@ -10,6 +10,8 @@ import tomli_w
from vibe.core.paths import AGENTS_MD_FILENAME, TRUSTED_FOLDERS_FILE from vibe.core.paths import AGENTS_MD_FILENAME, TRUSTED_FOLDERS_FILE
from vibe.core.trusted_folders import ( from vibe.core.trusted_folders import (
TrustedFoldersManager, TrustedFoldersManager,
find_git_repo_ancestor,
find_repo_trustable_files_for_cwd,
find_trustable_files, find_trustable_files,
has_agents_md_file, has_agents_md_file,
) )
@ -291,15 +293,46 @@ class TestFindTrustRoot:
# child should find parent (closest), not tmp_path # child should find parent (closest), not tmp_path
assert manager.find_trust_root(child) == parent.resolve() assert manager.find_trust_root(child) == parent.resolve()
def test_ignores_untrusted_ancestors(self, tmp_path: Path) -> None: def test_returns_none_when_closer_ancestor_is_untrusted(
self, tmp_path: Path
) -> None:
parent = tmp_path / "parent" parent = tmp_path / "parent"
child = parent / "child" child = parent / "child"
child.mkdir(parents=True) child.mkdir(parents=True)
manager = TrustedFoldersManager() manager = TrustedFoldersManager()
manager.add_untrusted(parent) manager.add_untrusted(parent)
manager.add_trusted(tmp_path) manager.add_trusted(tmp_path)
# find_trust_root skips untrusted, finds tmp_path # The closer untrusted ancestor blocks the higher trusted one.
assert manager.find_trust_root(child) == tmp_path.resolve() assert manager.find_trust_root(child) is None
def test_returns_none_when_path_itself_is_untrusted(self, tmp_path: Path) -> None:
manager = TrustedFoldersManager()
manager.add_untrusted(tmp_path)
assert manager.find_trust_root(tmp_path) is None
class TestIsExplicitlyUntrusted:
def test_returns_true_for_path_in_untrusted_list(self, tmp_path: Path) -> None:
manager = TrustedFoldersManager()
manager.add_untrusted(tmp_path)
assert manager.is_explicitly_untrusted(tmp_path) is True
def test_returns_false_for_unknown_path(self, tmp_path: Path) -> None:
manager = TrustedFoldersManager()
assert manager.is_explicitly_untrusted(tmp_path) is False
def test_does_not_walk_ancestors(self, tmp_path: Path) -> None:
child = tmp_path / "child"
child.mkdir()
manager = TrustedFoldersManager()
manager.add_untrusted(tmp_path)
# parent is untrusted, but child itself is not in the list
assert manager.is_explicitly_untrusted(child) is False
def test_returns_false_for_trusted_path(self, tmp_path: Path) -> None:
manager = TrustedFoldersManager()
manager.add_trusted(tmp_path)
assert manager.is_explicitly_untrusted(tmp_path) is False
class TestHasAgentsMdFile: class TestHasAgentsMdFile:
@ -351,37 +384,132 @@ class TestFindTrustableFiles:
(tmp_path / "other.txt").write_text("", encoding="utf-8") (tmp_path / "other.txt").write_text("", encoding="utf-8")
assert find_trustable_files(tmp_path) == [] assert find_trustable_files(tmp_path) == []
def test_detects_vibe_config_in_subfolder(self, tmp_path: Path) -> None: def test_ignores_vibe_config_in_subfolder(self, tmp_path: Path) -> None:
(tmp_path / "sub" / ".vibe" / "skills").mkdir(parents=True) (tmp_path / "sub" / ".vibe" / "skills").mkdir(parents=True)
result = find_trustable_files(tmp_path) assert find_trustable_files(tmp_path) == []
assert "sub/.vibe/" in result
def test_detects_agents_skills_in_subfolder(self, tmp_path: Path) -> None: def test_ignores_agents_skills_in_subfolder(self, tmp_path: Path) -> None:
(tmp_path / "deep" / "nested" / ".agents" / "skills").mkdir(parents=True) (tmp_path / "deep" / "nested" / ".agents" / "skills").mkdir(parents=True)
result = find_trustable_files(tmp_path) assert find_trustable_files(tmp_path) == []
assert "deep/nested/.agents/" in result
def test_returns_empty_when_config_only_inside_ignored_dir( def test_returns_empty_when_config_only_in_subfolder(self, tmp_path: Path) -> None:
self, tmp_path: Path
) -> None:
(tmp_path / "node_modules" / ".vibe" / "skills").mkdir(parents=True) (tmp_path / "node_modules" / ".vibe" / "skills").mkdir(parents=True)
assert find_trustable_files(tmp_path) == [] assert find_trustable_files(tmp_path) == []
def test_detects_nested_vibe_dir(self, tmp_path: Path) -> None: def test_detects_multiple_files_at_root(self, tmp_path: Path) -> None:
(tmp_path / "pkg" / ".vibe" / "tools").mkdir(parents=True)
result = find_trustable_files(tmp_path)
assert "pkg/.vibe/" in result
def test_detects_multiple_files(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "skills").mkdir(parents=True) (tmp_path / ".vibe" / "skills").mkdir(parents=True)
(tmp_path / ".agents" / "skills").mkdir(parents=True)
(tmp_path / "AGENTS.md").write_text("# Agent", encoding="utf-8") (tmp_path / "AGENTS.md").write_text("# Agent", encoding="utf-8")
(tmp_path / "sub" / ".agents" / "skills").mkdir(parents=True)
result = find_trustable_files(tmp_path) result = find_trustable_files(tmp_path)
assert ".vibe/" in result assert ".vibe/" in result
assert ".agents/" in result
assert "AGENTS.md" in result assert "AGENTS.md" in result
assert "sub/.agents/" in result
def test_no_duplicates_for_root_vibe_dir(self, tmp_path: Path) -> None: def test_no_duplicates_for_root_vibe_dir(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "tools").mkdir(parents=True) (tmp_path / ".vibe" / "tools").mkdir(parents=True)
result = find_trustable_files(tmp_path) result = find_trustable_files(tmp_path)
assert result.count(".vibe/") == 1 assert result.count(".vibe/") == 1
def _make_git_repo(path: Path) -> None:
git_dir = path / ".git"
git_dir.mkdir()
(git_dir / "HEAD").write_text("ref: refs/heads/main\n", encoding="utf-8")
class TestFindRepoTrustableFilesForCwd:
def test_returns_empty_when_repo_root_is_none(self, tmp_path: Path) -> None:
assert find_repo_trustable_files_for_cwd(tmp_path, None) == []
def test_returns_empty_when_repo_root_is_not_ancestor(self, tmp_path: Path) -> None:
cwd = tmp_path / "cwd"
cwd.mkdir()
repo = tmp_path / "repo"
repo.mkdir()
_make_git_repo(repo)
assert find_repo_trustable_files_for_cwd(cwd, repo) == []
def test_includes_root_trustable_files(self, tmp_path: Path) -> None:
repo = tmp_path / "repo"
repo.mkdir()
_make_git_repo(repo)
(repo / ".vibe" / "skills").mkdir(parents=True)
(repo / ".agents" / "skills").mkdir(parents=True)
(repo / "AGENTS.md").write_text("# Agent", encoding="utf-8")
cwd = repo / "src" / "pkg"
cwd.mkdir(parents=True)
assert find_repo_trustable_files_for_cwd(cwd, repo) == [
".agents/",
".vibe/",
"AGENTS.md",
]
def test_includes_agents_md_between_cwd_and_repo_root(self, tmp_path: Path) -> None:
repo = tmp_path / "repo"
repo.mkdir()
_make_git_repo(repo)
cwd = repo / "src" / "pkg" / "deep"
cwd.mkdir(parents=True)
(repo / "src" / "AGENTS.md").write_text("# Source", encoding="utf-8")
(repo / "src" / "pkg" / "AGENTS.md").write_text("# Package", encoding="utf-8")
(cwd / "AGENTS.md").write_text("# Cwd", encoding="utf-8")
assert find_repo_trustable_files_for_cwd(cwd, repo) == [
"src/AGENTS.md",
"src/pkg/AGENTS.md",
]
class TestFindGitRepoAncestor:
def test_returns_path_when_directly_contains_git(self, tmp_path: Path) -> None:
_make_git_repo(tmp_path)
assert find_git_repo_ancestor(tmp_path) == tmp_path.resolve()
def test_ignores_git_file_pointer(self, tmp_path: Path) -> None:
(tmp_path / ".git").write_text("gitdir: /elsewhere", encoding="utf-8")
# Not a directory, so not treated as a repo root.
assert find_git_repo_ancestor(tmp_path) is None
def test_ignores_empty_git_directory(self, tmp_path: Path) -> None:
(tmp_path / ".git").mkdir()
# Missing HEAD, so not a real repo.
assert find_git_repo_ancestor(tmp_path) is None
def test_returns_closest_ancestor_with_git(self, tmp_path: Path) -> None:
_make_git_repo(tmp_path)
nested = tmp_path / "a" / "b" / "c"
nested.mkdir(parents=True)
assert find_git_repo_ancestor(nested) == tmp_path.resolve()
def test_returns_innermost_when_multiple_git_repos(self, tmp_path: Path) -> None:
_make_git_repo(tmp_path)
inner = tmp_path / "sub"
inner.mkdir()
_make_git_repo(inner)
child = inner / "deep"
child.mkdir()
assert find_git_repo_ancestor(child) == inner.resolve()
def test_returns_none_when_no_git_anywhere(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(Path, "home", classmethod(lambda _cls: tmp_path.parent))
assert find_git_repo_ancestor(tmp_path / "a") is None
def test_excludes_home_directory(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
home = tmp_path / "home"
home.mkdir()
_make_git_repo(home)
monkeypatch.setattr(Path, "home", classmethod(lambda _cls: home))
sub = home / "project"
sub.mkdir()
assert find_git_repo_ancestor(sub) is None
def test_terminates_at_filesystem_root_without_git(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(Path, "home", classmethod(lambda _cls: tmp_path / "nope"))
assert find_git_repo_ancestor(tmp_path) is None

View file

@ -6,6 +6,7 @@ import pytest
from vibe.core.config.layer import LayerImplementationError from vibe.core.config.layer import LayerImplementationError
from vibe.core.config.layers.user import UserConfigLayer from vibe.core.config.layers.user import UserConfigLayer
from vibe.core.config.patch import ConfigPatch
@pytest.mark.asyncio @pytest.mark.asyncio
@ -43,7 +44,7 @@ async def test_apply_raises_not_implemented(tmp_working_directory: Path) -> None
path = tmp_working_directory / "config.toml" path = tmp_working_directory / "config.toml"
layer = UserConfigLayer(path=path, name="user-toml") layer = UserConfigLayer(path=path, name="user-toml")
with pytest.raises(NotImplementedError, match="M2"): with pytest.raises(NotImplementedError, match="M2"):
await layer.apply({"op": "set"}) await layer.apply(ConfigPatch(fingerprint="fp-1"))
@pytest.mark.asyncio @pytest.mark.asyncio

View file

@ -1,12 +1,21 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from pathlib import Path from pathlib import Path
import pytest import pytest
from vibe.core.utils import compact_complete_display, get_server_url_from_api_base from vibe.core.utils import compact_complete_display, get_server_url_from_api_base
import vibe.core.utils.io as io_utils import vibe.core.utils.io as io_utils
from vibe.core.utils.io import decode_safe, read_safe, read_safe_async from vibe.core.utils.io import (
_FILE_WRITE_LOCKS,
decode_safe,
file_write_lock,
read_lines_safe,
read_lines_safe_async,
read_safe,
read_safe_async,
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -201,3 +210,127 @@ class TestReadSafeAsync:
assert (await read_safe_async(f, raise_on_error=False)).text == "maf<EFBFBD>\n" assert (await read_safe_async(f, raise_on_error=False)).text == "maf<EFBFBD>\n"
with pytest.raises(UnicodeDecodeError): with pytest.raises(UnicodeDecodeError):
await read_safe_async(f, raise_on_error=True) await read_safe_async(f, raise_on_error=True)
class TestReadLinesSafe:
def test_small_file_fully_read(self, tmp_path: Path) -> None:
f = tmp_path / "f.txt"
f.write_text("a\nb\nc\n", encoding="utf-8")
got = read_lines_safe(f, limit=100, max_bytes=1024)
assert got.lines == ["a", "b", "c"]
assert got.total_lines == 3
assert got.was_truncated is False
def test_no_trailing_newline(self, tmp_path: Path) -> None:
f = tmp_path / "f.txt"
f.write_text("a\nb", encoding="utf-8")
got = read_lines_safe(f, limit=100, max_bytes=1024)
assert got.lines == ["a", "b"]
assert got.total_lines == 2
assert got.was_truncated is False
def test_truncates_at_limit(self, tmp_path: Path) -> None:
f = tmp_path / "f.txt"
f.write_text("".join(f"line {i}\n" for i in range(1, 101)), encoding="utf-8")
got = read_lines_safe(f, limit=10, max_bytes=1024)
assert got.lines == [f"line {i}" for i in range(1, 11)]
assert got.total_lines is None
assert got.was_truncated is True
def test_offset_skips_leading_lines(self, tmp_path: Path) -> None:
f = tmp_path / "f.txt"
f.write_text("".join(f"line {i}\n" for i in range(1, 11)), encoding="utf-8")
got = read_lines_safe(f, start_line=3, limit=2, max_bytes=1024)
assert got.lines == ["line 3", "line 4"]
assert got.was_truncated is True
def test_offset_past_eof_reports_total(self, tmp_path: Path) -> None:
f = tmp_path / "f.txt"
f.write_text("a\nb\n", encoding="utf-8")
got = read_lines_safe(f, start_line=100, limit=10, max_bytes=1024)
assert got.lines == []
assert got.total_lines == 2
assert got.was_truncated is False
def test_does_not_load_whole_file(self, tmp_path: Path) -> None:
f = tmp_path / "big.txt"
f.write_text("".join(f"line {i}\n" for i in range(1_000_000)), encoding="utf-8")
got = read_lines_safe(f, limit=5, max_bytes=1024)
assert got.lines == [f"line {i}" for i in range(5)]
assert got.total_lines is None
assert got.was_truncated is True
def test_oversized_single_line_returns_partial(self, tmp_path: Path) -> None:
f = tmp_path / "f.txt"
f.write_text("x" * 5000 + "\n", encoding="utf-8")
got = read_lines_safe(f, limit=10, max_bytes=1024)
assert got.lines == ["x" * 1024]
assert got.was_truncated is True
def test_cumulative_byte_budget_truncates(self, tmp_path: Path) -> None:
f = tmp_path / "f.txt"
f.write_text("".join("x" * 200 + "\n" for _ in range(50)), encoding="utf-8")
got = read_lines_safe(f, limit=50, max_bytes=1024)
assert 0 < len(got.lines) < 50
assert got.total_lines is None
assert got.was_truncated is True
def test_oversized_unselected_line_is_skipped(self, tmp_path: Path) -> None:
f = tmp_path / "f.txt"
f.write_text("x" * 5000 + "\nkept\n", encoding="utf-8")
got = read_lines_safe(f, start_line=2, limit=10, max_bytes=1024)
assert got.lines == ["kept"]
@pytest.mark.parametrize("encoding", ["utf-16-le", "utf-16-be", "utf-16"])
def test_utf16_is_decoded(self, tmp_path: Path, encoding: str) -> None:
f = tmp_path / "u16.txt"
f.write_bytes("héllo\nwörld\n".encode(encoding))
got = read_lines_safe(f, limit=10, max_bytes=4096)
assert got.lines[-1] == "wörld"
# A leading BOM may remain as U+FEFF on the first line.
assert got.lines[0].endswith("héllo")
@pytest.mark.asyncio
async def test_async_matches_sync(self, tmp_path: Path) -> None:
f = tmp_path / "f.txt"
f.write_text("a\nb\nc\n", encoding="utf-8")
got = await read_lines_safe_async(f, limit=2, max_bytes=1024)
assert got.lines == ["a", "b"]
assert got.was_truncated is True
class TestFileWriteLock:
@pytest.mark.asyncio
async def test_same_lock_for_different_path_spellings(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
path = tmp_path / "f.txt"
path.touch()
_FILE_WRITE_LOCKS.clear()
order: list[str] = []
held = asyncio.Event()
release = asyncio.Event()
async def first() -> None:
async with file_write_lock(path):
order.append("first-acquired")
held.set()
await release.wait()
order.append("first-released")
async def second() -> None:
await held.wait()
# Same file, different spelling — must contend on the same lock.
async with file_write_lock(Path("f.txt")):
order.append("second-acquired")
t1 = asyncio.create_task(first())
t2 = asyncio.create_task(second())
await held.wait()
await asyncio.sleep(0)
assert order == ["first-acquired"]
release.set()
await asyncio.gather(t1, t2)
assert order == ["first-acquired", "first-released", "second-acquired"]

View file

@ -0,0 +1,62 @@
from __future__ import annotations
from pathlib import Path
import pytest
from vibe.core.config._settings import VibeConfig
from vibe.core.config.vibe_schema import VibeConfigSchema
def test_vibe_config_schema_covers_all_vibe_config_fields() -> None:
legacy_fields = set(VibeConfig.model_fields.keys())
schema_fields = set(VibeConfigSchema.model_fields.keys())
missing = legacy_fields - schema_fields
assert not missing, (
f"VibeConfigSchema is missing {len(missing)} field(s) that exist in VibeConfig: "
f"{sorted(missing)}. "
f"When you add a new field to VibeConfig, also add it to VibeConfigSchema "
f"(vibe/core/config/vibe_schema.py) with the appropriate merge annotation."
)
@pytest.mark.asyncio
async def test_full_toml_to_vibe_config_schema(tmp_path: Path) -> None:
toml_path = tmp_path / "config.toml"
toml_path.write_text(
"""\
vim_keybindings = true
api_timeout = 300.0
active_model = "codestral"
disabled_tools = ["bash"]
default_agent = "plan"
enabled_skills = ["search"]
enable_otel = true
[[models]]
alias = "codestral"
name = "codestral-latest"
provider = "mistral"
"""
)
from vibe.core.config.layers.user import UserConfigLayer
from vibe.core.config.orchestrator import ConfigOrchestrator
class VibeConfig(VibeConfigSchema):
pass
layer = UserConfigLayer(path=toml_path, name="user-toml")
orchestrator = await ConfigOrchestrator[VibeConfig].create(
schema=VibeConfig, layers=[layer]
)
config = orchestrator.config
assert config.vim_keybindings is True
assert config.api_timeout == 300.0
assert config.active_model == "codestral"
assert config.models[0].alias == "codestral"
assert "bash" in config.disabled_tools
assert config.default_agent == "plan"
assert "search" in config.enabled_skills
assert config.enable_otel is True

View file

@ -0,0 +1,230 @@
from __future__ import annotations
import asyncio
from pathlib import Path
import pytest
from tests.mock.utils import collect_result
from vibe.core.tools.base import BaseToolState, ToolError
from vibe.core.tools.builtins.edit import Edit, EditArgs, EditConfig, EditResult
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay
from vibe.core.types import ToolResultEvent
def _make_edit() -> Edit:
return Edit(config_getter=lambda: EditConfig(), state=BaseToolState())
def _write(tmp_path: Path, name: str, content: str) -> Path:
p = tmp_path / name
p.write_text(content)
return p
@pytest.mark.asyncio
async def test_exact_match_replaces(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "hello world\n")
edit = _make_edit()
result = await collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="hello", new_string="goodbye"))
)
assert (tmp_path / "f.txt").read_text() == "goodbye world\n"
assert result.file == str(tmp_path / "f.txt")
assert result.message == "The file has been updated successfully."
@pytest.mark.asyncio
async def test_replace_all(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "aaa bbb aaa\n")
edit = _make_edit()
result = await collect_result(
edit.run(
EditArgs(
file_path="f.txt", old_string="aaa", new_string="ccc", replace_all=True
)
)
)
assert (tmp_path / "f.txt").read_text() == "ccc bbb ccc\n"
assert result.message == (
"The file has been updated. All occurrences were successfully replaced"
)
@pytest.mark.asyncio
async def test_not_found_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "hello world\n")
edit = _make_edit()
with pytest.raises(ToolError, match="String to replace not found in file"):
await collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="missing", new_string="x"))
)
@pytest.mark.asyncio
async def test_multiple_matches_without_replace_all_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "aaa bbb aaa\n")
edit = _make_edit()
with pytest.raises(ToolError, match="Found 2 matches"):
await collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="aaa", new_string="ccc"))
)
@pytest.mark.asyncio
async def test_old_equals_new_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "hello\n")
edit = _make_edit()
with pytest.raises(ToolError, match="No changes to make"):
await collect_result(
edit.run(
EditArgs(file_path="f.txt", old_string="hello", new_string="hello")
)
)
@pytest.mark.asyncio
async def test_empty_old_string_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "hello\n")
edit = _make_edit()
with pytest.raises(ToolError, match="old_string cannot be empty"):
await collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="", new_string="x"))
)
@pytest.mark.asyncio
async def test_file_not_found_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
edit = _make_edit()
with pytest.raises(ToolError, match="File does not exist"):
await collect_result(
edit.run(
EditArgs(
file_path="/nonexistent/file.py", old_string="x", new_string="y"
)
)
)
@pytest.mark.asyncio
async def test_empty_file_path_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
edit = _make_edit()
with pytest.raises(ToolError, match="File path cannot be empty"):
await collect_result(
edit.run(EditArgs(file_path="", old_string="x", new_string="y"))
)
@pytest.mark.asyncio
async def test_deletion_removes_exact_string(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "line1\nline2\nline3\n")
edit = _make_edit()
await collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="line2\n", new_string=""))
)
assert (tmp_path / "f.txt").read_text() == "line1\nline3\n"
@pytest.mark.asyncio
async def test_parallel_edits_same_file_all_land(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "A\nB\nC\nD\n")
edit = _make_edit()
await asyncio.gather(
collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="A", new_string="A1"))
),
collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="B", new_string="B1"))
),
collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="C", new_string="C1"))
),
collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="D", new_string="D1"))
),
)
assert (tmp_path / "f.txt").read_text() == "A1\nB1\nC1\nD1\n"
@pytest.mark.asyncio
async def test_relative_path_resolved_from_cwd(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "sub").mkdir(exist_ok=True)
_write(tmp_path / "sub", "f.txt", "old")
edit = _make_edit()
result = await collect_result(
edit.run(EditArgs(file_path="sub/f.txt", old_string="old", new_string="new"))
)
assert (tmp_path / "sub" / "f.txt").read_text() == "new"
assert result.file == str(tmp_path / "sub" / "f.txt")
def test_format_call_display() -> None:
args = EditArgs(file_path="/abs/foo.py", old_string="old", new_string="new")
display = Edit.format_call_display(args)
assert isinstance(display, ToolCallDisplay)
assert display.summary == "Editing foo.py"
def test_get_result_display() -> None:
result = EditResult(
file="/path/to/foo.py",
message="The file has been updated successfully.",
old_string="old",
new_string="new",
)
event = ToolResultEvent(
tool_call_id="test", tool_name="edit", tool_class=None, result=result
)
display = Edit.get_result_display(event)
assert isinstance(display, ToolResultDisplay)
assert display.success is True
assert "foo.py" in display.message

View file

@ -0,0 +1,355 @@
from __future__ import annotations
from collections.abc import Iterator
from pathlib import Path
import pytest
from tests.mock.utils import collect_result
from vibe.core.config.harness_files import (
init_harness_files_manager,
reset_harness_files_manager,
)
from vibe.core.tools.base import ToolError
from vibe.core.tools.builtins.read import (
DEFAULT_LINE_LIMIT,
MAX_BYTES,
Read,
ReadArgs,
ReadConfig,
ReadResult,
ReadState,
_add_line_numbers,
)
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay
from vibe.core.trusted_folders import trusted_folders_manager
from vibe.core.types import ToolResultEvent
from vibe.core.utils import VIBE_WARNING_TAG
def _make_read() -> Read:
return Read(config_getter=lambda: ReadConfig(), state=ReadState())
@pytest.mark.asyncio
async def test_reads_entire_small_file(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "hello.txt").write_text("line one\nline two\n", encoding="utf-8")
tool = _make_read()
result = await collect_result(
tool.run(ReadArgs(file_path=str(tmp_path / "hello.txt")))
)
assert result.num_lines == 2
assert result.total_lines == 2
assert result.start_line == 1
assert "line one" in result.content
assert "line two" in result.content
@pytest.mark.asyncio
async def test_reads_with_offset_and_limit(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
content = "".join(f"line {i}\n" for i in range(1, 11))
(tmp_path / "f.txt").write_text(content, encoding="utf-8")
tool = _make_read()
result = await collect_result(
tool.run(ReadArgs(file_path=str(tmp_path / "f.txt"), offset=3, limit=2))
)
assert result.num_lines == 2
assert result.start_line == 3
# Bounded read stops at the limit, so the true total is unknown.
assert result.total_lines is None
assert result.was_truncated is True
assert "line 3" in result.content
assert "line 4" in result.content
assert "line 2" not in result.content
assert "line 5" not in result.content
@pytest.mark.asyncio
async def test_empty_file_returns_warning(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "empty.txt").write_text("", encoding="utf-8")
tool = _make_read()
result = await collect_result(
tool.run(ReadArgs(file_path=str(tmp_path / "empty.txt")))
)
assert result.num_lines == 0
assert VIBE_WARNING_TAG in result.content
assert "empty" in result.content
@pytest.mark.asyncio
async def test_offset_beyond_file_returns_warning(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "short.txt").write_text("one\ntwo\n", encoding="utf-8")
tool = _make_read()
result = await collect_result(
tool.run(ReadArgs(file_path=str(tmp_path / "short.txt"), offset=100))
)
assert result.num_lines == 0
assert VIBE_WARNING_TAG in result.content
assert "shorter" in result.content
@pytest.mark.asyncio
async def test_exceeds_max_bytes_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
# Create file that generates output exceeding MAX_BYTES
big_line = "x" * 200 + "\n"
lines_needed = (MAX_BYTES // len(big_line)) + 100
(tmp_path / "big.txt").write_text(big_line * lines_needed, encoding="utf-8")
tool = _make_read()
with pytest.raises(ToolError, match="exceeds maximum allowed size"):
await collect_result(tool.run(ReadArgs(file_path=str(tmp_path / "big.txt"))))
@pytest.mark.asyncio
async def test_truncated_when_more_lines_than_limit(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
content = "".join(f"line {i}\n" for i in range(1, 101))
(tmp_path / "f.txt").write_text(content, encoding="utf-8")
tool = _make_read()
result = await collect_result(
tool.run(ReadArgs(file_path=str(tmp_path / "f.txt"), limit=10))
)
assert result.num_lines == 10
assert result.was_truncated is True
assert result.total_lines is None
@pytest.mark.asyncio
async def test_single_oversized_line_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "wide.txt").write_text("x" * (MAX_BYTES + 10), encoding="utf-8")
tool = _make_read()
with pytest.raises(ToolError, match="exceeds maximum allowed size"):
await collect_result(tool.run(ReadArgs(file_path=str(tmp_path / "wide.txt"))))
@pytest.mark.asyncio
async def test_file_not_found_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
tool = _make_read()
with pytest.raises(ToolError, match="File not found"):
await collect_result(tool.run(ReadArgs(file_path=str(tmp_path / "nope.txt"))))
@pytest.mark.asyncio
async def test_empty_path_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
tool = _make_read()
with pytest.raises(ToolError, match="file_path cannot be empty"):
await collect_result(tool.run(ReadArgs(file_path="")))
@pytest.mark.asyncio
async def test_directory_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "adir").mkdir()
tool = _make_read()
with pytest.raises(ToolError, match="directory"):
await collect_result(tool.run(ReadArgs(file_path=str(tmp_path / "adir"))))
@pytest.mark.asyncio
async def test_relative_path_resolved(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "sub").mkdir()
(tmp_path / "sub" / "f.txt").write_text("ok\n", encoding="utf-8")
tool = _make_read()
result = await collect_result(tool.run(ReadArgs(file_path="sub/f.txt")))
assert result.num_lines == 1
assert str(tmp_path / "sub" / "f.txt") == result.file_path
def test_line_number_format() -> None:
formatted = _add_line_numbers(["hello", "world"], start=1)
lines = formatted.split("\n")
assert lines[0] == " 1\u2192hello"
assert lines[1] == " 2\u2192world"
def test_default_limit_is_2000() -> None:
assert DEFAULT_LINE_LIMIT == 2000
def test_format_call_display() -> None:
args = ReadArgs(file_path="/some/file.py")
display = Read.format_call_display(args)
assert isinstance(display, ToolCallDisplay)
assert "file.py" in display.summary
def test_format_call_display_with_offset_limit() -> None:
args = ReadArgs(file_path="/some/file.py", offset=10, limit=50)
display = Read.format_call_display(args)
assert "from line 10" in display.summary
assert "limit 50" in display.summary
def test_get_result_display() -> None:
result = ReadResult(
file_path="/path/to/foo.py",
content="...",
num_lines=10,
start_line=1,
total_lines=10,
)
event = ToolResultEvent(
tool_call_id="test", tool_name="read", tool_class=None, result=result
)
display = Read.get_result_display(event)
assert isinstance(display, ToolResultDisplay)
assert display.success is True
assert "10 lines" in display.message
assert "foo.py" in display.message
def test_get_result_display_truncated() -> None:
result = ReadResult(
file_path="/path/to/foo.py",
content="...",
num_lines=10,
start_line=1,
total_lines=100,
)
event = ToolResultEvent(
tool_call_id="test", tool_name="read", tool_class=None, result=result
)
display = Read.get_result_display(event)
assert "truncated" in display.message
def test_get_result_display_truncated_via_flag() -> None:
result = ReadResult(
file_path="/path/to/foo.py",
content="...",
num_lines=10,
start_line=1,
total_lines=None,
was_truncated=True,
)
event = ToolResultEvent(
tool_call_id="test", tool_name="read", tool_class=None, result=result
)
display = Read.get_result_display(event)
assert "truncated" in display.message
@pytest.fixture()
def _setup_manager(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
monkeypatch.setattr(
trusted_folders_manager, "find_trust_root", lambda _: tmp_path.resolve()
)
reset_harness_files_manager()
init_harness_files_manager("user", "project")
yield
reset_harness_files_manager()
@pytest.mark.usefixtures("_setup_manager")
def test_agents_md_injection(tmp_path: Path) -> None:
sub = tmp_path / "sub"
sub.mkdir()
(sub / "AGENTS.md").write_text("# Sub instructions", encoding="utf-8")
target = sub / "file.py"
target.write_text("hello", encoding="utf-8")
tool = _make_read()
result = ReadResult(
file_path=str(target), content="hello", num_lines=1, start_line=1, total_lines=1
)
annotation = tool.get_result_extra(result)
assert annotation is not None
assert VIBE_WARNING_TAG in annotation
assert "# Sub instructions" in annotation
@pytest.mark.usefixtures("_setup_manager")
def test_agents_md_deduplicates(tmp_path: Path) -> None:
sub = tmp_path / "sub"
sub.mkdir()
(sub / "AGENTS.md").write_text("# Sub", encoding="utf-8")
(sub / "a.py").write_text("a", encoding="utf-8")
(sub / "b.py").write_text("b", encoding="utf-8")
tool = _make_read()
r1 = ReadResult(
file_path=str(sub / "a.py"),
content="a",
num_lines=1,
start_line=1,
total_lines=1,
)
assert tool.get_result_extra(r1) is not None
r2 = ReadResult(
file_path=str(sub / "b.py"),
content="b",
num_lines=1,
start_line=1,
total_lines=1,
)
assert tool.get_result_extra(r2) is None
def test_agents_md_returns_none_when_not_initialized(tmp_path: Path) -> None:
reset_harness_files_manager()
tool = _make_read()
result = ReadResult(
file_path=str(tmp_path / "file.py"),
content="",
num_lines=0,
start_line=1,
total_lines=0,
)
assert tool.get_result_extra(result) is None
reset_harness_files_manager()

View file

@ -1,217 +0,0 @@
from __future__ import annotations
from collections.abc import Iterator
from pathlib import Path
import pytest
from tests.mock.utils import collect_result
from vibe.core.config.harness_files import (
init_harness_files_manager,
reset_harness_files_manager,
)
from vibe.core.tools.builtins.read_file import (
ReadFile,
ReadFileArgs,
ReadFileResult,
ReadFileState,
ReadFileToolConfig,
)
from vibe.core.trusted_folders import trusted_folders_manager
from vibe.core.utils import VIBE_WARNING_TAG
@pytest.fixture()
def _setup_manager(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
"""Initialize harness files manager for tests, reset after."""
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
monkeypatch.setattr(
trusted_folders_manager, "find_trust_root", lambda _: tmp_path.resolve()
)
reset_harness_files_manager()
init_harness_files_manager("user", "project")
yield
reset_harness_files_manager()
def _make_read_file() -> ReadFile:
return ReadFile(config_getter=lambda: ReadFileToolConfig(), state=ReadFileState())
class TestReadFileExecution:
@pytest.mark.asyncio
async def test_run_with_large_offset_still_reads_lines(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
test_file = tmp_path / "large_file.txt"
test_file.write_text(
"".join(f"line {i}\n" for i in range(200)), encoding="utf-8"
)
tool = ReadFile(
config_getter=lambda: ReadFileToolConfig(max_read_bytes=64),
state=ReadFileState(),
)
result = await collect_result(
tool.run(ReadFileArgs(path=str(test_file), offset=50, limit=2))
)
assert result.content == "line 50\nline 51\n"
assert result.lines_read == 2
assert result.was_truncated
@pytest.mark.asyncio
async def test_run_marks_truncated_when_max_read_bytes_exceeded(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
test_file = tmp_path / "big.txt"
test_file.write_text(
"".join(f"line {i}\n" for i in range(100)), encoding="utf-8"
)
tool = ReadFile(
config_getter=lambda: ReadFileToolConfig(max_read_bytes=20),
state=ReadFileState(),
)
result = await collect_result(tool.run(ReadFileArgs(path=str(test_file))))
assert result.was_truncated
assert result.lines_read > 0
@pytest.mark.asyncio
async def test_run_not_truncated_when_eof_reached(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
test_file = tmp_path / "small.txt"
test_file.write_text("a\nb\nc\n", encoding="utf-8")
tool = ReadFile(
config_getter=lambda: ReadFileToolConfig(max_read_bytes=1024),
state=ReadFileState(),
)
result = await collect_result(tool.run(ReadFileArgs(path=str(test_file))))
assert not result.was_truncated
assert result.lines_read == 3
@pytest.mark.asyncio
async def test_run_not_truncated_when_limit_matches_remaining_lines(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
test_file = tmp_path / "exact.txt"
test_file.write_text("a\nb\nc\n", encoding="utf-8")
tool = ReadFile(
config_getter=lambda: ReadFileToolConfig(max_read_bytes=1024),
state=ReadFileState(),
)
result = await collect_result(
tool.run(ReadFileArgs(path=str(test_file), limit=10))
)
assert not result.was_truncated
assert result.lines_read == 3
class TestGetResultExtra:
@pytest.mark.usefixtures("_setup_manager")
def test_returns_none_when_no_agents_md(self, tmp_path: Path) -> None:
sub = tmp_path / "sub"
sub.mkdir()
target = sub / "file.py"
target.write_text("hello", encoding="utf-8")
tool = _make_read_file()
result = ReadFileResult(
path=str(target), content="hello", lines_read=1, was_truncated=False
)
assert tool.get_result_extra(result) is None
@pytest.mark.usefixtures("_setup_manager")
def test_returns_tagged_content_when_agents_md_found(self, tmp_path: Path) -> None:
sub = tmp_path / "sub"
sub.mkdir()
(sub / "AGENTS.md").write_text("# Sub instructions", encoding="utf-8")
target = sub / "file.py"
target.write_text("hello", encoding="utf-8")
tool = _make_read_file()
result = ReadFileResult(
path=str(target), content="hello", lines_read=1, was_truncated=False
)
annotation = tool.get_result_extra(result)
assert annotation is not None
assert f"<{VIBE_WARNING_TAG}>" in annotation
assert f"</{VIBE_WARNING_TAG}>" in annotation
assert "# Sub instructions" in annotation
assert "project instructions for this directory" in annotation
@pytest.mark.usefixtures("_setup_manager")
def test_deduplicates_across_calls(self, tmp_path: Path) -> None:
sub = tmp_path / "sub"
sub.mkdir()
(sub / "AGENTS.md").write_text("# Sub", encoding="utf-8")
file1 = sub / "a.py"
file2 = sub / "b.py"
file1.write_text("a", encoding="utf-8")
file2.write_text("b", encoding="utf-8")
tool = _make_read_file()
result1 = ReadFileResult(
path=str(file1), content="a", lines_read=1, was_truncated=False
)
assert tool.get_result_extra(result1) is not None
# Second call for a different file in the same dir → no duplicate
result2 = ReadFileResult(
path=str(file2), content="b", lines_read=1, was_truncated=False
)
assert tool.get_result_extra(result2) is None
@pytest.mark.usefixtures("_setup_manager")
def test_injects_new_dir_after_dedup(self, tmp_path: Path) -> None:
sub_a = tmp_path / "a"
sub_b = tmp_path / "b"
sub_a.mkdir()
sub_b.mkdir()
(sub_a / "AGENTS.md").write_text("# A", encoding="utf-8")
(sub_b / "AGENTS.md").write_text("# B", encoding="utf-8")
file_a = sub_a / "f.py"
file_b = sub_b / "f.py"
file_a.write_text("", encoding="utf-8")
file_b.write_text("", encoding="utf-8")
tool = _make_read_file()
r1 = ReadFileResult(
path=str(file_a), content="", lines_read=0, was_truncated=False
)
ann1 = tool.get_result_extra(r1)
assert ann1 is not None
assert "# A" in ann1
# Different subdirectory → should inject its AGENTS.md
r2 = ReadFileResult(
path=str(file_b), content="", lines_read=0, was_truncated=False
)
ann2 = tool.get_result_extra(r2)
assert ann2 is not None
assert "# B" in ann2
def test_returns_none_when_manager_not_initialized(self, tmp_path: Path) -> None:
reset_harness_files_manager()
tool = _make_read_file()
result = ReadFileResult(
path=str(tmp_path / "file.py"),
content="",
lines_read=0,
was_truncated=False,
)
assert tool.get_result_extra(result) is None
reset_harness_files_manager()

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable from collections.abc import Callable
from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
import tomllib import tomllib
from typing import cast from typing import cast
@ -12,9 +13,12 @@ from textual.geometry import Size
from textual.pilot import Pilot from textual.pilot import Pilot
from textual.screen import Screen from textual.screen import Screen
from textual.widget import Widget from textual.widget import Widget
from textual.widgets import Input, Link from textual.widgets import Input, Link, Static
from tests.browser_sign_in.stubs import build_browser_sign_in_service_factory from tests.browser_sign_in.stubs import (
build_browser_sign_in_service_factory,
build_sign_in_process,
)
from tests.conftest import build_test_vibe_config from tests.conftest import build_test_vibe_config
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig
@ -33,8 +37,10 @@ from vibe.core.types import Backend
from vibe.setup.auth import ( from vibe.setup.auth import (
BrowserSignInError, BrowserSignInError,
BrowserSignInErrorCode, BrowserSignInErrorCode,
BrowserSignInEvent,
BrowserSignInService, BrowserSignInService,
BrowserSignInStatus, BrowserSignInStatus,
BrowserSignInStatusChanged,
) )
from vibe.setup.auth.api_key_persistence import persist_api_key from vibe.setup.auth.api_key_persistence import persist_api_key
import vibe.setup.onboarding as onboarding_module import vibe.setup.onboarding as onboarding_module
@ -42,11 +48,19 @@ from vibe.setup.onboarding import OnboardingApp
from vibe.setup.onboarding.context import OnboardingContext from vibe.setup.onboarding.context import OnboardingContext
from vibe.setup.onboarding.screens.api_key import ApiKeyScreen from vibe.setup.onboarding.screens.api_key import ApiKeyScreen
from vibe.setup.onboarding.screens.auth_method import AuthMethodScreen from vibe.setup.onboarding.screens.auth_method import AuthMethodScreen
from vibe.setup.onboarding.screens.browser_sign_in import BrowserSignInScreen from vibe.setup.onboarding.screens.browser_sign_in import (
SIGN_IN_URL_HELP_PREFIX,
BrowserSignInScreen,
)
from vibe.setup.onboarding.screens.theme_selection import THEMES, ThemeSelectionScreen from vibe.setup.onboarding.screens.theme_selection import THEMES, ThemeSelectionScreen
CONSOLE_URL = "https://console.mistral.ai" CONSOLE_URL = "https://console.mistral.ai"
BROWSER_AUTH_API_URL = "https://console.mistral.ai/api" BROWSER_AUTH_API_URL = "https://console.mistral.ai/api"
TEST_NOW = datetime(2026, 3, 16, tzinfo=UTC)
def _expected_browser_sign_in_url(process_id: str = "process-1") -> str:
return build_sign_in_process(TEST_NOW, process_id=process_id).sign_in_url
async def _wait_for( async def _wait_for(
@ -94,6 +108,8 @@ def _build_browser_onboarding_app(
*, *,
browser_sign_in_service_factory: Callable[[], BrowserSignInService] | None = None, browser_sign_in_service_factory: Callable[[], BrowserSignInService] | None = None,
browser_sign_in_success_delay: float = 0, browser_sign_in_success_delay: float = 0,
browser_sign_in_url_help_delay: float = 0,
copy_sign_in_url: Callable[[str], bool] | None = None,
) -> OnboardingApp: ) -> OnboardingApp:
return OnboardingApp( return OnboardingApp(
config=_build_onboarding_config( config=_build_onboarding_config(
@ -102,6 +118,8 @@ def _build_browser_onboarding_app(
), ),
browser_sign_in_service_factory=browser_sign_in_service_factory, browser_sign_in_service_factory=browser_sign_in_service_factory,
browser_sign_in_success_delay=browser_sign_in_success_delay, browser_sign_in_success_delay=browser_sign_in_success_delay,
browser_sign_in_url_help_delay=browser_sign_in_url_help_delay,
copy_sign_in_url=copy_sign_in_url,
) )
@ -157,6 +175,14 @@ def _browser_sign_in_step_text(card: Widget) -> str:
return f"{title.render()}\n{detail.render()}" return f"{title.render()}\n{detail.render()}"
def _browser_sign_in_hint(screen: Screen) -> str:
return str(screen.query_one("#browser-sign-in-hint", NoMarkupStatic).render())
def _browser_sign_in_url_text(screen: Screen) -> str:
return str(screen.query_one("#browser-sign-in-url", Static).render())
def _build_unexpected_browser_sign_in_service_factory( def _build_unexpected_browser_sign_in_service_factory(
outcomes: list[str], outcomes: list[str],
*, *,
@ -173,11 +199,13 @@ def _build_unexpected_browser_sign_in_service_factory(
self._outcome = outcome self._outcome = outcome
async def authenticate( async def authenticate(
self, status_callback: Callable[[BrowserSignInStatus], None] | None = None self, event_callback: Callable[[BrowserSignInEvent], None] | None = None
) -> str: ) -> str:
if self._outcome == "completed": if self._outcome == "completed":
if status_callback is not None: if event_callback is not None:
status_callback(BrowserSignInStatus.COMPLETED) event_callback(
BrowserSignInStatusChanged(status=BrowserSignInStatus.COMPLETED)
)
return api_key return api_key
if self._outcome == "runtime_error": if self._outcome == "runtime_error":
raise RuntimeError("boom") raise RuntimeError("boom")
@ -328,15 +356,24 @@ async def test_ui_does_not_show_browser_opened_before_attempt_starts() -> None:
authenticate_started = asyncio.Event() authenticate_started = asyncio.Event()
finish_authenticate = asyncio.Event() finish_authenticate = asyncio.Event()
keep_authenticate_running = asyncio.Event() keep_authenticate_running = asyncio.Event()
copied_urls: list[str] = []
def copy_sign_in_url(url: str) -> bool:
copied_urls.append(url)
return True
class DelayedBrowserSignInService: class DelayedBrowserSignInService:
async def authenticate( async def authenticate(
self, status_callback: Callable[[BrowserSignInStatus], None] | None = None self, event_callback: Callable[[BrowserSignInEvent], None] | None = None
) -> str: ) -> str:
authenticate_started.set() authenticate_started.set()
await finish_authenticate.wait() await finish_authenticate.wait()
if status_callback is not None: if event_callback is not None:
status_callback(BrowserSignInStatus.OPENING_BROWSER) event_callback(
BrowserSignInStatusChanged(
status=BrowserSignInStatus.OPENING_BROWSER
)
)
await keep_authenticate_running.wait() await keep_authenticate_running.wait()
return "sk-never-reached" return "sk-never-reached"
@ -346,7 +383,8 @@ async def test_ui_does_not_show_browser_opened_before_attempt_starts() -> None:
app = _build_browser_onboarding_app( app = _build_browser_onboarding_app(
browser_sign_in_service_factory=lambda: cast( browser_sign_in_service_factory=lambda: cast(
BrowserSignInService, DelayedBrowserSignInService() BrowserSignInService, DelayedBrowserSignInService()
) ),
copy_sign_in_url=copy_sign_in_url,
) )
async with app.run_test() as pilot: async with app.run_test() as pilot:
@ -358,6 +396,9 @@ async def test_ui_does_not_show_browser_opened_before_attempt_starts() -> None:
assert "Open browser" in active_step_text assert "Open browser" in active_step_text
assert "Getting things ready..." in active_step_text assert "Getting things ready..." in active_step_text
assert "Browser opened" not in active_step_text assert "Browser opened" not in active_step_text
assert _browser_sign_in_url_text(app.screen) == ""
await pilot.press("c")
assert copied_urls == []
finish_authenticate.set() finish_authenticate.set()
await _wait_for( await _wait_for(
@ -371,6 +412,274 @@ async def test_ui_does_not_show_browser_opened_before_attempt_starts() -> None:
) )
@pytest.mark.asyncio
async def test_ui_shows_browser_sign_in_url_copy_prompt_without_raw_url() -> None:
blocker = asyncio.Event()
async def wait_forever(_: float) -> None:
await blocker.wait()
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], sleep=wait_forever
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
url_text = _browser_sign_in_url_text(app.screen)
assert "If your browser did not open, copy this URL (press C)" in url_text
assert "process-1" not in url_text
assert _browser_sign_in_hint(app.screen) == (
"Press M to enter API key manually - Esc to cancel"
)
@pytest.mark.asyncio
async def test_ui_delays_browser_sign_in_url_help() -> None:
blocker = asyncio.Event()
async def wait_forever(_: float) -> None:
await blocker.wait()
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], sleep=wait_forever
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory,
browser_sign_in_url_help_delay=0.3,
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: (
"Waiting for authentication..."
in _browser_sign_in_step_text(
_active_browser_sign_in_step_card(app.screen)
)
),
pilot,
)
assert _browser_sign_in_url_text(app.screen) == ""
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
assert "process-1" not in _browser_sign_in_url_text(app.screen)
@pytest.mark.asyncio
async def test_ui_copies_browser_sign_in_url() -> None:
blocker = asyncio.Event()
copied_urls: list[str] = []
async def wait_forever(_: float) -> None:
await blocker.wait()
def copy_sign_in_url(url: str) -> bool:
copied_urls.append(url)
return True
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], sleep=wait_forever
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory,
copy_sign_in_url=copy_sign_in_url,
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
await pilot.press("c")
assert "process-1" not in _browser_sign_in_url_text(app.screen)
assert copied_urls == [_expected_browser_sign_in_url()]
@pytest.mark.asyncio
async def test_ui_copies_browser_sign_in_url_when_help_text_is_clicked() -> None:
blocker = asyncio.Event()
copied_urls: list[str] = []
async def wait_forever(_: float) -> None:
await blocker.wait()
def copy_sign_in_url(url: str) -> bool:
copied_urls.append(url)
return True
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], sleep=wait_forever
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory,
copy_sign_in_url=copy_sign_in_url,
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
url_widget = app.screen.query_one("#browser-sign-in-url", Static)
link_x = url_widget.styles.padding.left + len(SIGN_IN_URL_HELP_PREFIX) + 1
await pilot.click(url_widget, offset=(link_x, 0))
assert copied_urls == [_expected_browser_sign_in_url()]
@pytest.mark.asyncio
async def test_ui_reveals_browser_sign_in_url_when_copy_fails() -> None:
blocker = asyncio.Event()
async def wait_forever(_: float) -> None:
await blocker.wait()
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], sleep=wait_forever
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory,
copy_sign_in_url=lambda _: False,
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
assert "process-1" not in _browser_sign_in_url_text(app.screen)
await pilot.press("c")
await _wait_for(
lambda: "process-1" in _browser_sign_in_url_text(app.screen), pilot
)
url_text = _browser_sign_in_url_text(app.screen)
assert "Copy failed. Open this URL manually:" in url_text
@pytest.mark.asyncio
async def test_ui_keeps_last_sign_in_url_copy_prompt_after_open_browser_failure() -> (
None
):
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], open_browser=lambda _: False
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: (
"Failed to open browser for sign-in."
in _browser_sign_in_step_text(
_active_browser_sign_in_step_card(app.screen)
)
),
pilot,
)
assert _browser_sign_in_hint(app.screen) == (
"Press R to retry - Press M to enter API key manually - Esc to cancel"
)
url_text = _browser_sign_in_url_text(app.screen)
assert "If your browser did not open, copy this URL (press C)" in url_text
assert "process-1" not in url_text
@pytest.mark.asyncio
async def test_ui_copies_last_browser_sign_in_url_after_open_browser_failure() -> None:
copied_urls: list[str] = []
def copy_sign_in_url(url: str) -> bool:
copied_urls.append(url)
return True
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["completed"], open_browser=lambda _: False
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory,
copy_sign_in_url=copy_sign_in_url,
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
await pilot.press("c")
assert copied_urls == [_expected_browser_sign_in_url()]
@pytest.mark.asyncio
async def test_ui_retry_hides_old_sign_in_url_and_uses_fresh_attempt_url() -> None:
blocker = asyncio.Event()
copied_urls: list[str] = []
async def wait_forever(_: float) -> None:
await blocker.wait()
def copy_sign_in_url(url: str) -> bool:
copied_urls.append(url)
return True
_, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory(
outcomes=["expired", "completed"], sleep=wait_forever
)
app = _build_browser_onboarding_app(
browser_sign_in_service_factory=browser_sign_in_service_factory,
copy_sign_in_url=copy_sign_in_url,
)
async with app.run_test() as pilot:
await _show_browser_sign_in(pilot)
await _wait_for(
lambda: (
"expired"
in _browser_sign_in_step_text(
_active_browser_sign_in_step_card(app.screen)
)
),
pilot,
)
await _wait_for(
lambda: "copy this URL" in _browser_sign_in_url_text(app.screen), pilot
)
await pilot.press("r")
await _wait_for(
lambda: (
"Waiting for authentication..."
in _browser_sign_in_step_text(
_active_browser_sign_in_step_card(app.screen)
)
and "copy this URL" in _browser_sign_in_url_text(app.screen)
),
pilot,
)
assert "process-2" not in _browser_sign_in_url_text(app.screen)
assert "process-1" not in _browser_sign_in_url_text(app.screen)
assert _browser_sign_in_hint(app.screen) == (
"Press M to enter API key manually - Esc to cancel"
)
await pilot.press("c")
assert copied_urls == [_expected_browser_sign_in_url(process_id="process-2")]
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_ui_completes_browser_sign_in_and_retries_after_failure() -> None: async def test_ui_completes_browser_sign_in_and_retries_after_failure() -> None:
gateway, browser_sign_in_service_factory, created_services = ( gateway, browser_sign_in_service_factory, created_services = (

View file

@ -6,7 +6,11 @@ from pathlib import Path
import pytest import pytest
from vibe.core.config import SessionLoggingConfig from vibe.core.config import SessionLoggingConfig
from vibe.core.session.saved_sessions import update_saved_session_title from vibe.core.session import last_session_pointer
from vibe.core.session.saved_sessions import (
delete_saved_session,
update_saved_session_title,
)
@pytest.fixture @pytest.fixture
@ -23,6 +27,21 @@ def session_config(temp_session_dir: Path) -> SessionLoggingConfig:
) )
def write_saved_session(
session_config: SessionLoggingConfig,
timestamp: str,
short_id: str,
metadata: dict[str, object],
) -> Path:
saved_session_dir = Path(session_config.save_dir) / f"test_{timestamp}_{short_id}"
saved_session_dir.mkdir()
(saved_session_dir / "messages.jsonl").write_text(
'{"role": "user", "content": "Hello"}\n', encoding="utf-8"
)
(saved_session_dir / "meta.json").write_text(json.dumps(metadata), encoding="utf-8")
return saved_session_dir
class TestUpdateSavedSessionTitle: class TestUpdateSavedSessionTitle:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_updates_saved_session_title_without_losing_existing_metadata( async def test_updates_saved_session_title_without_losing_existing_metadata(
@ -145,6 +164,22 @@ class TestUpdateSavedSessionTitle:
"missing-session", "Renamed", session_config "missing-session", "Renamed", session_config
) )
@pytest.mark.asyncio
async def test_skips_non_object_candidate_metadata(
self, session_config: SessionLoggingConfig
) -> None:
session_dir = Path(session_config.save_dir)
saved_session_dir = session_dir / "test_20240101_120000_eeeeeeee"
saved_session_dir.mkdir()
(saved_session_dir / "messages.jsonl").write_text(
'{"role": "user", "content": "Hello"}\n', encoding="utf-8"
)
(saved_session_dir / "meta.json").write_text("[]", encoding="utf-8")
with pytest.raises(ValueError, match="Session not found: eeeeeeee-5555"):
await update_saved_session_title("eeeeeeee-5555", "Renamed", session_config)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_requires_exact_saved_session_id( async def test_requires_exact_saved_session_id(
self, session_config: SessionLoggingConfig self, session_config: SessionLoggingConfig
@ -176,3 +211,120 @@ class TestUpdateSavedSessionTitle:
assert ( assert (
json.loads(metadata_file.read_text(encoding="utf-8")) == original_metadata json.loads(metadata_file.read_text(encoding="utf-8")) == original_metadata
) )
class TestDeleteSavedSession:
@pytest.mark.asyncio
async def test_deletes_saved_session_directory(
self, session_config: SessionLoggingConfig
) -> None:
target_dir = write_saved_session(
session_config,
"20240101_120000",
"ffffffff",
{"session_id": "ffffffff-6666", "title": "Target"},
)
other_dir = write_saved_session(
session_config,
"20240101_120000",
"11111111",
{"session_id": "11111111-7777", "title": "Other"},
)
await delete_saved_session("ffffffff-6666", session_config)
assert not target_dir.exists()
assert other_dir.exists()
@pytest.mark.asyncio
async def test_clears_matching_last_session_pointers(
self, session_config: SessionLoggingConfig
) -> None:
target_dir = write_saved_session(
session_config,
"20240101_120000",
"ffffffff",
{"session_id": "ffffffff-6666", "title": "Target"},
)
pointer_dir = (
Path(session_config.save_dir) / last_session_pointer.POINTER_DIR_NAME
)
pointer_dir.mkdir()
matching_pointer = pointer_dir / "ttys001"
other_pointer = pointer_dir / "ttys002"
matching_pointer.write_text("ffffffff-6666\n", encoding="utf-8")
other_pointer.write_text("other-session\n", encoding="utf-8")
await delete_saved_session("ffffffff-6666", session_config)
assert not target_dir.exists()
assert not matching_pointer.exists()
assert other_pointer.read_text(encoding="utf-8") == "other-session\n"
@pytest.mark.asyncio
async def test_succeeds_for_missing_saved_session(
self, session_config: SessionLoggingConfig
) -> None:
await delete_saved_session("missing-session", session_config)
@pytest.mark.asyncio
async def test_clears_stale_last_session_pointer_for_missing_saved_session(
self, session_config: SessionLoggingConfig
) -> None:
pointer_dir = (
Path(session_config.save_dir) / last_session_pointer.POINTER_DIR_NAME
)
pointer_dir.mkdir()
matching_pointer = pointer_dir / "ttys001"
other_pointer = pointer_dir / "ttys002"
matching_pointer.write_text("missing-session\n", encoding="utf-8")
other_pointer.write_text("other-session\n", encoding="utf-8")
await delete_saved_session("missing-session", session_config)
assert not matching_pointer.exists()
assert other_pointer.read_text(encoding="utf-8") == "other-session\n"
@pytest.mark.asyncio
async def test_requires_exact_saved_session_id_before_deleting(
self, session_config: SessionLoggingConfig
) -> None:
collision_dir = write_saved_session(
session_config,
"20240101_120000",
"aaaaaaaa",
{"session_id": "aaaaaaaa-1111", "title": "Collision"},
)
target_dir = write_saved_session(
session_config,
"20240101_120500",
"aaaaaaaa",
{"session_id": "aaaaaaaa-2222", "title": "Target"},
)
await delete_saved_session("aaaaaaaa-2222", session_config)
assert collision_dir.exists()
assert not target_dir.exists()
@pytest.mark.asyncio
async def test_skips_invalid_candidate_metadata_before_deleting(
self, session_config: SessionLoggingConfig
) -> None:
invalid_dir = Path(session_config.save_dir) / "test_20240101_120000_bbbbbbbb"
invalid_dir.mkdir()
(invalid_dir / "messages.jsonl").write_text(
'{"role": "user", "content": "Hello"}\n', encoding="utf-8"
)
(invalid_dir / "meta.json").write_text("[]", encoding="utf-8")
target_dir = write_saved_session(
session_config,
"20240101_120500",
"bbbbbbbb",
{"session_id": "bbbbbbbb-3333", "title": "Target"},
)
await delete_saved_session("bbbbbbbb-3333", session_config)
assert invalid_dir.exists()
assert not target_dir.exists()

View file

@ -115,7 +115,7 @@ class TestSkillManagerParsing:
license="MIT", license="MIT",
compatibility="Requires git", compatibility="Requires git",
metadata={"author": "Test Author", "version": "1.0"}, metadata={"author": "Test Author", "version": "1.0"},
allowed_tools="bash read_file", allowed_tools="bash read",
) )
config = build_test_vibe_config( config = build_test_vibe_config(
@ -132,7 +132,7 @@ class TestSkillManagerParsing:
assert skill.license == "MIT" assert skill.license == "MIT"
assert skill.compatibility == "Requires git" assert skill.compatibility == "Requires git"
assert skill.metadata == {"author": "Test Author", "version": "1.0"} assert skill.metadata == {"author": "Test Author", "version": "1.0"}
assert skill.allowed_tools == ["bash", "read_file"] assert skill.allowed_tools == ["bash", "read"]
def test_sets_correct_skill_path(self, skills_dir: Path) -> None: def test_sets_correct_skill_path(self, skills_dir: Path) -> None:
create_skill(skills_dir, "test-skill", "A test skill") create_skill(skills_dir, "test-skill", "A test skill")
@ -570,6 +570,47 @@ class TestParseSkillCommand:
assert parsed.name == "my-skill" assert parsed.name == "my-skill"
class TestSkillManagerConfigIssues:
def test_invalid_frontmatter_records_config_issue(self, skills_dir: Path) -> None:
broken_dir = skills_dir / "broken-skill"
broken_dir.mkdir()
(broken_dir / "SKILL.md").write_text("No frontmatter here")
config = build_test_vibe_config(
system_prompt_id="tests",
include_project_context=False,
skill_paths=[skills_dir],
)
manager = SkillManager(lambda: config)
assert len(manager.config_issues) == 1
issue = manager.config_issues[0]
assert issue.file == broken_dir / "SKILL.md"
assert "Failed to load" in issue.message
def test_multiple_invalid_skills_accumulate_issues(self, skills_dir: Path) -> None:
broken_dir_a = skills_dir / "broken-skill-a"
broken_dir_a.mkdir()
(broken_dir_a / "SKILL.md").write_text("not valid")
broken_dir = skills_dir / "broken-skill"
broken_dir.mkdir()
(broken_dir / "SKILL.md").write_text("not valid")
create_skill(skills_dir, "good-skill", "Works fine")
config = build_test_vibe_config(
system_prompt_id="tests",
include_project_context=False,
skill_paths=[skills_dir],
)
manager = SkillManager(lambda: config)
assert {
(i.file, i.message.startswith("Failed to load"))
for i in manager.config_issues
} == {(broken_dir_a / "SKILL.md", True), (broken_dir / "SKILL.md", True)}
assert "good-skill" in manager.available_skills
class TestBuildSkillPrompt: class TestBuildSkillPrompt:
def test_without_args(self, skills_dir: Path, skill_config: VibeConfig) -> None: def test_without_args(self, skills_dir: Path, skill_config: VibeConfig) -> None:
create_skill(skills_dir, "my-skill", body="Do the thing.") create_skill(skills_dir, "my-skill", body="Do the thing.")

View file

@ -27,7 +27,7 @@ class TestSkillMetadata:
license="MIT", license="MIT",
compatibility="Requires git", compatibility="Requires git",
metadata={"author": "Test Author", "version": "1.0"}, metadata={"author": "Test Author", "version": "1.0"},
allowed_tools=["bash", "read_file"], allowed_tools=["bash", "read"],
user_invocable=False, user_invocable=False,
) )
@ -36,7 +36,7 @@ class TestSkillMetadata:
assert meta.license == "MIT" assert meta.license == "MIT"
assert meta.compatibility == "Requires git" assert meta.compatibility == "Requires git"
assert meta.metadata == {"author": "Test Author", "version": "1.0"} assert meta.metadata == {"author": "Test Author", "version": "1.0"}
assert meta.allowed_tools == ["bash", "read_file"] assert meta.allowed_tools == ["bash", "read"]
assert meta.user_invocable is False assert meta.user_invocable is False
def test_raises_error_for_uppercase_name(self) -> None: def test_raises_error_for_uppercase_name(self) -> None:
@ -63,17 +63,17 @@ class TestSkillMetadata:
meta = SkillMetadata( meta = SkillMetadata(
name="test", name="test",
description="A test skill", description="A test skill",
allowed_tools="bash read_file grep", # type: ignore[arg-type] allowed_tools="bash read grep", # type: ignore[arg-type]
) )
assert meta.allowed_tools == ["bash", "read_file", "grep"] assert meta.allowed_tools == ["bash", "read", "grep"]
def test_parses_allowed_tools_from_list(self) -> None: def test_parses_allowed_tools_from_list(self) -> None:
meta = SkillMetadata( meta = SkillMetadata(
name="test", description="A test skill", allowed_tools=["bash", "read_file"] name="test", description="A test skill", allowed_tools=["bash", "read"]
) )
assert meta.allowed_tools == ["bash", "read_file"] assert meta.allowed_tools == ["bash", "read"]
def test_parses_allowed_tools_handles_none(self) -> None: def test_parses_allowed_tools_handles_none(self) -> None:
meta = SkillMetadata( meta = SkillMetadata(

View file

@ -29,7 +29,7 @@ compatibility: Requires git
metadata: metadata:
author: Test Author author: Test Author
version: "1.0" version: "1.0"
allowed-tools: bash read_file allowed-tools: bash read
--- ---
Instructions here. Instructions here.
@ -42,7 +42,7 @@ Instructions here.
assert frontmatter["compatibility"] == "Requires git" assert frontmatter["compatibility"] == "Requires git"
assert frontmatter["metadata"]["author"] == "Test Author" assert frontmatter["metadata"]["author"] == "Test Author"
assert frontmatter["metadata"]["version"] == "1.0" assert frontmatter["metadata"]["version"] == "1.0"
assert frontmatter["allowed-tools"] == "bash read_file" assert frontmatter["allowed-tools"] == "bash read"
assert "Instructions here." in body assert "Instructions here." in body
def test_raises_error_for_missing_frontmatter(self) -> None: def test_raises_error_for_missing_frontmatter(self) -> None:

View file

@ -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: #d0b344 }
.terminal-r5 { fill: #868887 }
</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">SnapshotTestAppWithConfigIssues</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="#4b4e55" x="719.8" y="635.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="732" y="635.9" width="707.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="719.8" y="660.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="732" y="660.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="744.2" y="660.3" width="414.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1159" y="660.3" width="280.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="719.8" y="684.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="732" y="684.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="744.2" y="684.7" width="634.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1378.6" y="684.7" width="61" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="719.8" y="709.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="732" y="709.1" width="707.6" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="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)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="605.6" textLength="244" clip-path="url(#terminal-line-24)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type&#160;</text><text class="terminal-r3" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">&#160;for&#160;more&#160;information</text><text class="terminal-r4" x="719.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r4" x="719.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="744.2" y="678.8" textLength="414.8" clip-path="url(#terminal-line-27)">/test/skills/broken-skill/SKILL.md</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r4" x="719.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="744.2" y="703.2" textLength="634.4" clip-path="url(#terminal-line-28)">Failed&#160;to&#160;load:&#160;missing&#160;required&#160;field&#160;&#x27;description&#x27;</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r4" x="719.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-r1" x="0" y="752" textLength="1464" clip-path="url(#terminal-line-30)">──────────────────────────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r2" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">&gt;</text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r1" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -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: #d0b344 }
.terminal-r5 { fill: #868887 }
</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">SnapshotTestAppWithHookConfigIssue</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="#4b4e55" x="719.8" y="635.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="732" y="635.9" width="707.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="719.8" y="660.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="732" y="660.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="744.2" y="660.3" width="341.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1085.8" y="660.3" width="353.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="719.8" y="684.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="732" y="684.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="744.2" y="684.7" width="439.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1183.4" y="684.7" width="256.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="719.8" y="709.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="732" y="709.1" width="707.6" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="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)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="605.6" textLength="244" clip-path="url(#terminal-line-24)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type&#160;</text><text class="terminal-r3" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">&#160;for&#160;more&#160;information</text><text class="terminal-r4" x="719.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r4" x="719.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="744.2" y="678.8" textLength="341.6" clip-path="url(#terminal-line-27)">/test/hooks/broken-hook.toml</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r4" x="719.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="744.2" y="703.2" textLength="439.2" clip-path="url(#terminal-line-28)">Failed&#160;to&#160;parse:&#160;invalid&#160;TOML&#160;syntax</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r4" x="719.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-r1" x="0" y="752" textLength="1464" clip-path="url(#terminal-line-30)">──────────────────────────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r2" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">&gt;</text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r1" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,222 @@
<svg class="rich-terminal" viewBox="0 0 1482 1026.0" 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: #d0b344;font-weight: bold }
.terminal-r5 { fill: #d0b344 }
.terminal-r6 { fill: #608ab1 }
.terminal-r7 { fill: #292929 }
.terminal-r8 { fill: #98a84b;font-weight: bold }
.terminal-r9 { fill: #cc555a }
.terminal-r10 { fill: #868887 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1463.0" height="975.0" />
</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>
<clipPath id="terminal-line-35">
<rect x="0" y="855.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-36">
<rect x="0" y="879.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-37">
<rect x="0" y="904.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-38">
<rect x="0" y="928.7" 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="1024" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">WriteApprovalLongContentApp</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="#4b4e55" x="1427.4" y="318.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1427.4" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1427.4" y="367.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1427.4" y="391.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1427.4" y="416.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1427.4" y="440.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1427.4" y="465.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1427.4" y="489.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1427.4" y="513.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1427.4" y="538.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1427.4" y="562.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1427.4" y="587.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1427.4" y="611.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="1427.4" y="635.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="1427.4" y="660.3" width="12.2" 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="0" y="142" textLength="134.2" clip-path="url(#terminal-line-5)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="142" textLength="146.4" clip-path="url(#terminal-line-5)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="142" textLength="122" clip-path="url(#terminal-line-5)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="142" textLength="244" clip-path="url(#terminal-line-5)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="142" textLength="256.2" clip-path="url(#terminal-line-5)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="0" y="166.4" textLength="134.2" clip-path="url(#terminal-line-6)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="166.4" textLength="414.8" clip-path="url(#terminal-line-6)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</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="0" y="190.8" textLength="134.2" clip-path="url(#terminal-line-7)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="190.8" textLength="61" clip-path="url(#terminal-line-7)">Type&#160;</text><text class="terminal-r3" x="231.8" y="190.8" textLength="61" clip-path="url(#terminal-line-7)">/help</text><text class="terminal-r1" x="292.8" y="190.8" textLength="256.2" clip-path="url(#terminal-line-7)">&#160;for&#160;more&#160;information</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="0" y="288.4" textLength="1464" clip-path="url(#terminal-line-11)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="0" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r4" x="24.4" y="312.8" textLength="414.8" clip-path="url(#terminal-line-12)">Permission&#160;for&#160;the&#160;write_file&#160;tool</text><text class="terminal-r1" x="1451.8" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="0" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="24.4" y="337.2" textLength="97.6" clip-path="url(#terminal-line-13)">line_086</text><text class="terminal-r1" x="134.2" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">=</text><text class="terminal-r5" x="158.6" y="337.2" textLength="36.6" clip-path="url(#terminal-line-13)">602</text><text class="terminal-r1" x="1451.8" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="0" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="24.4" y="361.6" textLength="97.6" clip-path="url(#terminal-line-14)">line_087</text><text class="terminal-r1" x="134.2" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">=</text><text class="terminal-r5" x="158.6" y="361.6" textLength="36.6" clip-path="url(#terminal-line-14)">609</text><text class="terminal-r1" x="1451.8" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="0" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="24.4" y="386" textLength="97.6" clip-path="url(#terminal-line-15)">line_088</text><text class="terminal-r1" x="134.2" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">=</text><text class="terminal-r5" x="158.6" y="386" textLength="36.6" clip-path="url(#terminal-line-15)">616</text><text class="terminal-r1" x="1451.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="24.4" y="410.4" textLength="97.6" clip-path="url(#terminal-line-16)">line_089</text><text class="terminal-r1" x="134.2" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">=</text><text class="terminal-r5" x="158.6" y="410.4" textLength="36.6" clip-path="url(#terminal-line-16)">623</text><text class="terminal-r1" x="1451.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="24.4" y="434.8" textLength="97.6" clip-path="url(#terminal-line-17)">line_090</text><text class="terminal-r1" x="134.2" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">=</text><text class="terminal-r5" x="158.6" y="434.8" textLength="36.6" clip-path="url(#terminal-line-17)">630</text><text class="terminal-r1" x="1451.8" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="24.4" y="459.2" textLength="97.6" clip-path="url(#terminal-line-18)">line_091</text><text class="terminal-r1" x="134.2" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">=</text><text class="terminal-r5" x="158.6" y="459.2" textLength="36.6" clip-path="url(#terminal-line-18)">637</text><text class="terminal-r1" x="1451.8" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="24.4" y="483.6" textLength="97.6" clip-path="url(#terminal-line-19)">line_092</text><text class="terminal-r1" x="134.2" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">=</text><text class="terminal-r5" x="158.6" y="483.6" textLength="36.6" clip-path="url(#terminal-line-19)">644</text><text class="terminal-r1" x="1451.8" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="24.4" y="508" textLength="97.6" clip-path="url(#terminal-line-20)">line_093</text><text class="terminal-r1" x="134.2" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">=</text><text class="terminal-r5" x="158.6" y="508" textLength="36.6" clip-path="url(#terminal-line-20)">651</text><text class="terminal-r1" x="1451.8" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="0" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="24.4" y="532.4" textLength="97.6" clip-path="url(#terminal-line-21)">line_094</text><text class="terminal-r1" x="134.2" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">=</text><text class="terminal-r5" x="158.6" y="532.4" textLength="36.6" clip-path="url(#terminal-line-21)">658</text><text class="terminal-r1" x="1451.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="24.4" y="556.8" textLength="97.6" clip-path="url(#terminal-line-22)">line_095</text><text class="terminal-r1" x="134.2" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">=</text><text class="terminal-r5" x="158.6" y="556.8" textLength="36.6" clip-path="url(#terminal-line-22)">665</text><text class="terminal-r1" x="1451.8" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="0" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="24.4" y="581.2" textLength="97.6" clip-path="url(#terminal-line-23)">line_096</text><text class="terminal-r1" x="134.2" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">=</text><text class="terminal-r5" x="158.6" y="581.2" textLength="36.6" clip-path="url(#terminal-line-23)">672</text><text class="terminal-r1" x="1451.8" 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="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="24.4" y="605.6" textLength="97.6" clip-path="url(#terminal-line-24)">line_097</text><text class="terminal-r1" x="134.2" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">=</text><text class="terminal-r5" x="158.6" y="605.6" textLength="36.6" clip-path="url(#terminal-line-24)">679</text><text class="terminal-r1" x="1451.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="24.4" y="630" textLength="97.6" clip-path="url(#terminal-line-25)">line_098</text><text class="terminal-r1" x="134.2" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">=</text><text class="terminal-r5" x="158.6" y="630" textLength="36.6" clip-path="url(#terminal-line-25)">686</text><text class="terminal-r6" x="1427.4" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="1451.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="0" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="24.4" y="654.4" textLength="97.6" clip-path="url(#terminal-line-26)">line_099</text><text class="terminal-r1" x="134.2" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">=</text><text class="terminal-r5" x="158.6" y="654.4" textLength="36.6" clip-path="url(#terminal-line-26)">693</text><text class="terminal-r1" x="1451.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="24.4" y="678.8" textLength="97.6" clip-path="url(#terminal-line-27)">line_100</text><text class="terminal-r1" x="134.2" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">=</text><text class="terminal-r5" x="158.6" y="678.8" textLength="36.6" clip-path="url(#terminal-line-27)">700</text><text class="terminal-r1" 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-r1" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" 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-r1" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r8" x="24.4" y="727.6" textLength="183" clip-path="url(#terminal-line-29)">&#160;1.&#160;Allow&#160;once</text><text class="terminal-r1" 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-r1" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" 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-r1" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="24.4" y="776.4" textLength="488" clip-path="url(#terminal-line-31)">&#160;&#160;2.&#160;Allow&#160;for&#160;remainder&#160;of&#160;this&#160;session</text><text class="terminal-r1" 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-r1" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" 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-r1" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="24.4" y="825.2" textLength="207.4" clip-path="url(#terminal-line-33)">&#160;&#160;3.&#160;Always&#160;allow</text><text class="terminal-r1" 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-r1" x="0" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)"></text><text class="terminal-r1" x="1451.8" y="849.6" textLength="12.2" 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-r1" x="0" y="874" textLength="12.2" clip-path="url(#terminal-line-35)"></text><text class="terminal-r9" x="24.4" y="874" textLength="109.8" clip-path="url(#terminal-line-35)">&#160;&#160;4.&#160;Deny</text><text class="terminal-r1" x="1451.8" y="874" textLength="12.2" clip-path="url(#terminal-line-35)"></text><text class="terminal-r1" x="1464" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">
</text><text class="terminal-r1" x="0" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)"></text><text class="terminal-r1" x="1451.8" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)"></text><text class="terminal-r1" x="1464" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">
</text><text class="terminal-r1" x="0" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)"></text><text class="terminal-r10" x="24.4" y="922.8" textLength="451.4" clip-path="url(#terminal-line-37)">↑↓&#160;navigate&#160;&#160;Enter&#160;select&#160;&#160;ESC&#160;reject</text><text class="terminal-r1" x="1451.8" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)"></text><text class="terminal-r1" x="1464" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">
</text><text class="terminal-r1" x="0" y="947.2" textLength="1464" clip-path="url(#terminal-line-38)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">
</text><text class="terminal-r10" x="0" y="971.6" textLength="158.6" clip-path="url(#terminal-line-39)">/test/workdir</text><text class="terminal-r10" x="1256.6" y="971.6" textLength="207.4" clip-path="url(#terminal-line-39)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,181 @@
<svg class="rich-terminal" viewBox="0 0 1238 782.0" 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: #d0b344;font-weight: bold }
.terminal-r5 { fill: #d0b344 }
.terminal-r6 { fill: #292929 }
.terminal-r7 { fill: #98a84b;font-weight: bold }
.terminal-r8 { fill: #cc555a }
.terminal-r9 { fill: #868887 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1219.0" height="731.0" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1220" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="780" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">WriteApprovalLongContentApp</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="#4b4e55" x="1183.4" y="245.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1183.4" y="269.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1183.4" y="294.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1183.4" y="318.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1183.4" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1183.4" y="367.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1183.4" y="391.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="1183.4" y="416.3" width="12.2" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="68.8" textLength="146.4" clip-path="url(#terminal-line-2)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="68.8" textLength="122" clip-path="url(#terminal-line-2)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="68.8" textLength="244" clip-path="url(#terminal-line-2)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="68.8" textLength="256.2" clip-path="url(#terminal-line-2)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="0" y="93.2" textLength="134.2" clip-path="url(#terminal-line-3)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="93.2" textLength="414.8" clip-path="url(#terminal-line-3)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="0" y="117.6" textLength="134.2" clip-path="url(#terminal-line-4)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="117.6" textLength="61" clip-path="url(#terminal-line-4)">Type&#160;</text><text class="terminal-r3" x="231.8" y="117.6" textLength="61" clip-path="url(#terminal-line-4)">/help</text><text class="terminal-r1" x="292.8" y="117.6" textLength="256.2" clip-path="url(#terminal-line-4)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="0" y="215.2" textLength="1220" clip-path="url(#terminal-line-8)">┌──────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="0" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r4" x="24.4" y="239.6" textLength="414.8" clip-path="url(#terminal-line-9)">Permission&#160;for&#160;the&#160;write_file&#160;tool</text><text class="terminal-r1" x="1207.8" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="0" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r1" x="24.4" y="264" textLength="97.6" clip-path="url(#terminal-line-10)">line_093</text><text class="terminal-r1" x="134.2" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">=</text><text class="terminal-r5" x="158.6" y="264" textLength="36.6" clip-path="url(#terminal-line-10)">651</text><text class="terminal-r1" x="1207.8" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="0" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r1" x="24.4" y="288.4" textLength="97.6" clip-path="url(#terminal-line-11)">line_094</text><text class="terminal-r1" x="134.2" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">=</text><text class="terminal-r5" x="158.6" y="288.4" textLength="36.6" clip-path="url(#terminal-line-11)">658</text><text class="terminal-r1" x="1207.8" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="0" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="24.4" y="312.8" textLength="97.6" clip-path="url(#terminal-line-12)">line_095</text><text class="terminal-r1" x="134.2" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">=</text><text class="terminal-r5" x="158.6" y="312.8" textLength="36.6" clip-path="url(#terminal-line-12)">665</text><text class="terminal-r1" x="1207.8" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="0" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="24.4" y="337.2" textLength="97.6" clip-path="url(#terminal-line-13)">line_096</text><text class="terminal-r1" x="134.2" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">=</text><text class="terminal-r5" x="158.6" y="337.2" textLength="36.6" clip-path="url(#terminal-line-13)">672</text><text class="terminal-r1" x="1207.8" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="0" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="24.4" y="361.6" textLength="97.6" clip-path="url(#terminal-line-14)">line_097</text><text class="terminal-r1" x="134.2" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">=</text><text class="terminal-r5" x="158.6" y="361.6" textLength="36.6" clip-path="url(#terminal-line-14)">679</text><text class="terminal-r1" x="1207.8" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="0" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="24.4" y="386" textLength="97.6" clip-path="url(#terminal-line-15)">line_098</text><text class="terminal-r1" x="134.2" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">=</text><text class="terminal-r5" x="158.6" y="386" textLength="36.6" clip-path="url(#terminal-line-15)">686</text><text class="terminal-r1" x="1207.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="24.4" y="410.4" textLength="97.6" clip-path="url(#terminal-line-16)">line_099</text><text class="terminal-r1" x="134.2" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">=</text><text class="terminal-r5" x="158.6" y="410.4" textLength="36.6" clip-path="url(#terminal-line-16)">693</text><text class="terminal-r1" x="1207.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="24.4" y="434.8" textLength="97.6" clip-path="url(#terminal-line-17)">line_100</text><text class="terminal-r1" x="134.2" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">=</text><text class="terminal-r5" x="158.6" y="434.8" textLength="36.6" clip-path="url(#terminal-line-17)">700</text><text class="terminal-r1" x="1207.8" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="1207.8" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r7" x="24.4" y="483.6" textLength="183" clip-path="url(#terminal-line-19)">&#160;1.&#160;Allow&#160;once</text><text class="terminal-r1" x="1207.8" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="1207.8" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="0" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="24.4" y="532.4" textLength="488" clip-path="url(#terminal-line-21)">&#160;&#160;2.&#160;Allow&#160;for&#160;remainder&#160;of&#160;this&#160;session</text><text class="terminal-r1" x="1207.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="1207.8" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="0" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="24.4" y="581.2" textLength="207.4" clip-path="url(#terminal-line-23)">&#160;&#160;3.&#160;Always&#160;allow</text><text class="terminal-r1" x="1207.8" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r1" x="0" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="1207.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></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="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r8" x="24.4" y="630" textLength="109.8" clip-path="url(#terminal-line-25)">&#160;&#160;4.&#160;Deny</text><text class="terminal-r1" x="1207.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="0" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1207.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r9" x="24.4" y="678.8" textLength="451.4" clip-path="url(#terminal-line-27)">↑↓&#160;navigate&#160;&#160;Enter&#160;select&#160;&#160;ESC&#160;reject</text><text class="terminal-r1" x="1207.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></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="0" y="703.2" textLength="1220" clip-path="url(#terminal-line-28)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r9" x="0" y="727.6" textLength="158.6" clip-path="url(#terminal-line-29)">/test/workdir</text><text class="terminal-r9" x="1012.6" y="727.6" textLength="207.4" clip-path="url(#terminal-line-29)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,180 @@
<svg class="rich-terminal" viewBox="0 0 1238 782.0" 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: #d0b344;font-weight: bold }
.terminal-r5 { fill: #868887 }
.terminal-r6 { fill: #d0b344 }
.terminal-r7 { fill: #98a84b;font-weight: bold }
.terminal-r8 { fill: #cc555a }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1219.0" height="731.0" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1220" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="780" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">WriteApprovalShortContentApp</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="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="0" y="190.8" textLength="134.2" clip-path="url(#terminal-line-7)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="190.8" textLength="146.4" clip-path="url(#terminal-line-7)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="190.8" textLength="122" clip-path="url(#terminal-line-7)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="190.8" textLength="244" clip-path="url(#terminal-line-7)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="190.8" textLength="256.2" clip-path="url(#terminal-line-7)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="0" y="215.2" textLength="134.2" clip-path="url(#terminal-line-8)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="215.2" textLength="414.8" clip-path="url(#terminal-line-8)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="0" y="239.6" textLength="134.2" clip-path="url(#terminal-line-9)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="239.6" textLength="61" clip-path="url(#terminal-line-9)">Type&#160;</text><text class="terminal-r3" x="231.8" y="239.6" textLength="61" clip-path="url(#terminal-line-9)">/help</text><text class="terminal-r1" x="292.8" y="239.6" textLength="256.2" clip-path="url(#terminal-line-9)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="0" y="337.2" textLength="1220" clip-path="url(#terminal-line-13)">┌──────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="0" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r4" x="24.4" y="361.6" textLength="414.8" clip-path="url(#terminal-line-14)">Permission&#160;for&#160;the&#160;write_file&#160;tool</text><text class="terminal-r1" x="1207.8" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="0" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r5" x="24.4" y="386" textLength="244" clip-path="url(#terminal-line-15)">File:&#160;src/example.py</text><text class="terminal-r1" x="1207.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="1207.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="24.4" y="434.8" textLength="97.6" clip-path="url(#terminal-line-17)">line_001</text><text class="terminal-r1" x="134.2" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">=</text><text class="terminal-r6" x="158.6" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">7</text><text class="terminal-r1" x="1207.8" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="1207.8" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r7" x="24.4" y="483.6" textLength="183" clip-path="url(#terminal-line-19)">&#160;1.&#160;Allow&#160;once</text><text class="terminal-r1" x="1207.8" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="1207.8" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="0" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="24.4" y="532.4" textLength="488" clip-path="url(#terminal-line-21)">&#160;&#160;2.&#160;Allow&#160;for&#160;remainder&#160;of&#160;this&#160;session</text><text class="terminal-r1" x="1207.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="1207.8" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="0" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="24.4" y="581.2" textLength="207.4" clip-path="url(#terminal-line-23)">&#160;&#160;3.&#160;Always&#160;allow</text><text class="terminal-r1" x="1207.8" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r1" x="0" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="1207.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></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="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r8" x="24.4" y="630" textLength="109.8" clip-path="url(#terminal-line-25)">&#160;&#160;4.&#160;Deny</text><text class="terminal-r1" x="1207.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="0" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1207.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r5" x="24.4" y="678.8" textLength="451.4" clip-path="url(#terminal-line-27)">↑↓&#160;navigate&#160;&#160;Enter&#160;select&#160;&#160;ESC&#160;reject</text><text class="terminal-r1" x="1207.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></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="0" y="703.2" textLength="1220" clip-path="url(#terminal-line-28)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="158.6" clip-path="url(#terminal-line-29)">/test/workdir</text><text class="terminal-r5" x="1012.6" y="727.6" textLength="207.4" clip-path="url(#terminal-line-29)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -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: #868887 }
</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">_ImageAttachmentApp</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)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="605.6" textLength="244" clip-path="url(#terminal-line-24)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type&#160;</text><text class="terminal-r3" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r1" x="1464" y="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-r1" x="0" y="752" textLength="1464" clip-path="url(#terminal-line-30)">──────────────────────────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r2" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">&gt;</text><text class="terminal-r1" x="24.4" y="776.4" textLength="1439.6" clip-path="url(#terminal-line-31)">look&#160;@/snap/shot.png&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r1" 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%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -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: #c5c8c6;font-weight: bold }
.terminal-r5 { fill: #868887 }
</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">_ImageAttachmentApp</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="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="434.8" textLength="244" clip-path="url(#terminal-line-17)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type&#160;</text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r2" x="0" y="581.2" textLength="24.4" clip-path="url(#terminal-line-23)">&gt;&#160;</text><text class="terminal-r4" x="24.4" y="581.2" textLength="256.2" clip-path="url(#terminal-line-23)">compare&#160;@a.png&#160;@b.png</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="231.8" clip-path="url(#terminal-line-24)">&#160;attached&#160;images:&#160;</text><text class="terminal-r5" x="256.2" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">a.png</text><text class="terminal-r5" x="317.2" y="605.6" textLength="24.4" clip-path="url(#terminal-line-24)">,&#160;</text><text class="terminal-r5" x="341.6" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">b.png</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r5" x="0" y="630" textLength="1464" clip-path="url(#terminal-line-25)">────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="24.4" y="678.8" textLength="85.4" clip-path="url(#terminal-line-27)">Got&#160;it.</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-r1" x="0" y="752" textLength="1464" clip-path="url(#terminal-line-30)">──────────────────────────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r2" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">&gt;</text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r1" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -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: #c5c8c6;font-weight: bold }
.terminal-r5 { fill: #868887 }
</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">_ImageAttachmentApp</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="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral&#160;Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="439.2" y="434.8" textLength="244" clip-path="url(#terminal-line-17)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type&#160;</text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r2" x="0" y="581.2" textLength="24.4" clip-path="url(#terminal-line-23)">&gt;&#160;</text><text class="terminal-r4" x="24.4" y="581.2" textLength="170.8" clip-path="url(#terminal-line-23)">look&#160;@shot.png</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="219.6" clip-path="url(#terminal-line-24)">&#160;attached&#160;image:&#160;</text><text class="terminal-r5" x="244" y="605.6" textLength="97.6" clip-path="url(#terminal-line-24)">shot.png</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r5" x="0" y="630" textLength="1464" clip-path="url(#terminal-line-25)">────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="24.4" y="678.8" textLength="85.4" clip-path="url(#terminal-line-27)">Got&#160;it.</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-r1" x="0" y="752" textLength="1464" clip-path="url(#terminal-line-30)">──────────────────────────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r2" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">&gt;</text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r1" x="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r5" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r5" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View file

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

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

View file

@ -188,7 +188,7 @@
</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="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="24.4" y="630" textLength="695.4" clip-path="url(#terminal-line-25)">I&#x27;m&#160;doing&#160;well,&#160;thank&#160;you!&#160;Let&#160;me&#160;read&#160;that&#160;file&#160;for&#160;you.</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"> </text><text class="terminal-r1" x="24.4" y="630" textLength="695.4" clip-path="url(#terminal-line-25)">I&#x27;m&#160;doing&#160;well,&#160;thank&#160;you!&#160;Let&#160;me&#160;read&#160;that&#160;file&#160;for&#160;you.</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="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r6" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="24.4" y="678.8" textLength="109.8" clip-path="url(#terminal-line-27)">read_file</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"> </text><text class="terminal-r6" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="24.4" y="678.8" textLength="48.8" clip-path="url(#terminal-line-27)">read</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="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-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r1" x="0" y="752" textLength="1464" clip-path="url(#terminal-line-30)">──────────────────────────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"> </text><text class="terminal-r1" x="0" y="752" textLength="1464" clip-path="url(#terminal-line-30)">──────────────────────────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -77,10 +77,10 @@
</g> </g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)"> <g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#121212" x="0" y="1.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="12.2" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="24.4" y="25.9" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="134.2" y="25.9" width="841.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="50.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/> <rect fill="#121212" x="0" y="1.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="12.2" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="24.4" y="25.9" width="48.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="73.2" y="25.9" width="902.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="50.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#121212" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix"> <g class="terminal-matrix">
<text class="terminal-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-line-0)"> <text class="terminal-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r2" x="0" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)"></text><text class="terminal-r2" x="24.4" y="44.4" textLength="109.8" clip-path="url(#terminal-line-1)">read_file</text><text class="terminal-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)"> </text><text class="terminal-r2" x="0" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)"></text><text class="terminal-r2" x="24.4" y="44.4" textLength="48.8" clip-path="url(#terminal-line-1)">read</text><text class="terminal-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)"> </text><text class="terminal-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)"> </text><text class="terminal-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)"> </text><text class="terminal-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before After
Before After

View file

@ -0,0 +1,219 @@
<svg class="rich-terminal" viewBox="0 0 994 1026.0" 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: #4b4e55 }
.terminal-r3 { fill: #d0b344;font-weight: bold }
.terminal-r4 { fill: #608ab1 }
.terminal-r5 { fill: #868887;font-weight: bold }
.terminal-r6 { fill: #c5c8c6;font-weight: bold }
.terminal-r7 { fill: #868887 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="975.0" height="975.0" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-35">
<rect x="0" y="855.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-36">
<rect x="0" y="879.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-37">
<rect x="0" y="904.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-38">
<rect x="0" y="928.7" width="976" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="1024" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">TrustFolderDialogSnapshotApp</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="976" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r2" x="61" y="190.8" textLength="854" clip-path="url(#terminal-line-7)">╭────────────────────────────────────────────────────────────────────╮</text><text class="terminal-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r2" x="61" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r2" x="902.8" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r2" x="61" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r3" x="378.2" y="239.6" textLength="219.6" clip-path="url(#terminal-line-9)">Trust&#160;this&#160;folder?</text><text class="terminal-r2" x="902.8" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r2" x="61" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r2" x="902.8" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r2" x="61" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r4" x="305" y="288.4" textLength="366" clip-path="url(#terminal-line-11)">/home/user/projects/my-project</text><text class="terminal-r2" x="902.8" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r2" x="61" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r2" x="902.8" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r2" x="61" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="146.4" y="337.2" textLength="683.2" clip-path="url(#terminal-line-13)">Files&#160;here&#160;can&#160;modify&#160;AI&#160;behavior.&#160;Malicious&#160;configs&#160;may</text><text class="terminal-r2" x="902.8" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r2" x="61" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="158.6" y="361.6" textLength="658.8" clip-path="url(#terminal-line-14)">exfiltrate&#160;data,&#160;run&#160;destructive&#160;commands,&#160;or&#160;silently</text><text class="terminal-r2" x="902.8" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r2" x="61" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="390.4" y="386" textLength="195.2" clip-path="url(#terminal-line-15)">alter&#160;your&#160;code.</text><text class="terminal-r2" x="902.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r2" x="61" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r2" x="902.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r2" x="61" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r5" x="317.2" y="434.8" textLength="329.4" clip-path="url(#terminal-line-17)">Detected&#160;in&#160;current&#160;folder:</text><text class="terminal-r2" x="902.8" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r2" x="61" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="414.8" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;AGENTS.md</text><text class="terminal-r2" x="902.8" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r2" x="61" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="439.2" y="483.6" textLength="97.6" clip-path="url(#terminal-line-19)">&#160;.vibe/</text><text class="terminal-r2" x="902.8" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r2" x="61" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r2" x="902.8" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r2" x="61" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r2" x="134.2" y="532.4" textLength="707.6" clip-path="url(#terminal-line-21)">──────────────────────────────────────────────────────────</text><text class="terminal-r2" x="902.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r2" x="61" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r3" x="268.4" y="556.8" textLength="439.2" clip-path="url(#terminal-line-22)">Only&#160;trust&#160;folders&#160;you&#160;fully&#160;control</text><text class="terminal-r2" x="902.8" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r2" x="61" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r2" x="134.2" y="581.2" textLength="707.6" clip-path="url(#terminal-line-23)">──────────────────────────────────────────────────────────</text><text class="terminal-r2" x="902.8" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r2" x="61" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r2" x="902.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r2" x="61" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="305" y="630" textLength="170.8" clip-path="url(#terminal-line-25)">&#160;&#160;Trust&#160;folder</text><text class="terminal-r6" x="512.4" y="630" textLength="158.6" clip-path="url(#terminal-line-25)">&#160;Don&#x27;t&#160;trust</text><text class="terminal-r2" x="902.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r2" x="61" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r2" x="902.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r2" x="61" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r7" x="329.4" y="678.8" textLength="317.2" clip-path="url(#terminal-line-27)">&#160;&#160;navigate&#160;&#160;Enter&#160;select</text><text class="terminal-r2" x="902.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r2" x="61" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r2" x="902.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r2" x="61" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r7" x="329.4" y="727.6" textLength="305" clip-path="url(#terminal-line-29)">Setting&#160;will&#160;be&#160;saved&#160;in:</text><text class="terminal-r2" x="902.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r2" x="61" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r7" x="256.2" y="752" textLength="451.4" clip-path="url(#terminal-line-30)">/home/user/.vibe/trusted_folders.toml</text><text class="terminal-r2" x="902.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r2" x="61" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r2" x="902.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r2" x="61" y="800.8" textLength="854" clip-path="url(#terminal-line-32)">╰────────────────────────────────────────────────────────────────────╯</text><text class="terminal-r1" x="976" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r1" x="976" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r1" x="976" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r1" x="976" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">
</text><text class="terminal-r1" x="976" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">
</text><text class="terminal-r1" x="976" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">
</text><text class="terminal-r1" x="976" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">
</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,220 @@
<svg class="rich-terminal" viewBox="0 0 994 1026.0" 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: #4b4e55 }
.terminal-r3 { fill: #d0b344;font-weight: bold }
.terminal-r4 { fill: #608ab1 }
.terminal-r5 { fill: #292929 }
.terminal-r6 { fill: #868887;font-weight: bold }
.terminal-r7 { fill: #c5c8c6;font-weight: bold }
.terminal-r8 { fill: #868887 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="975.0" height="975.0" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-35">
<rect x="0" y="855.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-36">
<rect x="0" y="879.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-37">
<rect x="0" y="904.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-38">
<rect x="0" y="928.7" width="976" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="1024" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">TrustFolderDialogManyFilesSnapshotApp</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="829.6" y="294.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="318.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="367.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="829.6" y="391.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="829.6" y="416.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="829.6" y="440.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="829.6" y="465.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="829.6" y="489.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="829.6" y="513.9" width="12.2" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r2" x="61" y="166.4" textLength="854" clip-path="url(#terminal-line-6)">╭────────────────────────────────────────────────────────────────────╮</text><text class="terminal-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r2" x="61" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)"></text><text class="terminal-r2" x="902.8" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)"></text><text class="terminal-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r2" x="61" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r3" x="378.2" y="215.2" textLength="219.6" clip-path="url(#terminal-line-8)">Trust&#160;this&#160;folder?</text><text class="terminal-r2" x="902.8" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r2" x="61" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r2" x="902.8" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r2" x="61" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r4" x="305" y="264" textLength="366" clip-path="url(#terminal-line-10)">/home/user/projects/my-project</text><text class="terminal-r2" x="902.8" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r2" x="61" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r2" x="902.8" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r2" x="61" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="134.2" y="312.8" textLength="683.2" clip-path="url(#terminal-line-12)">Files&#160;here&#160;can&#160;modify&#160;AI&#160;behavior.&#160;Malicious&#160;configs&#160;may</text><text class="terminal-r2" x="902.8" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r2" x="61" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="146.4" y="337.2" textLength="658.8" clip-path="url(#terminal-line-13)">exfiltrate&#160;data,&#160;run&#160;destructive&#160;commands,&#160;or&#160;silently</text><text class="terminal-r2" x="902.8" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r2" x="61" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="378.2" y="361.6" textLength="195.2" clip-path="url(#terminal-line-14)">alter&#160;your&#160;code.</text><text class="terminal-r2" x="902.8" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r2" x="61" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r2" x="829.6" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r2" x="902.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r2" x="61" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r6" x="317.2" y="410.4" textLength="329.4" clip-path="url(#terminal-line-16)">Detected&#160;in&#160;current&#160;folder:</text><text class="terminal-r2" x="902.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r2" x="61" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="378.2" y="434.8" textLength="195.2" clip-path="url(#terminal-line-17)">&#160;sub0/AGENTS.md</text><text class="terminal-r2" x="902.8" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r2" x="61" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="378.2" y="459.2" textLength="195.2" clip-path="url(#terminal-line-18)">&#160;sub1/AGENTS.md</text><text class="terminal-r2" x="902.8" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r2" x="61" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="378.2" y="483.6" textLength="195.2" clip-path="url(#terminal-line-19)">&#160;sub2/AGENTS.md</text><text class="terminal-r2" x="902.8" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r2" x="61" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="378.2" y="508" textLength="195.2" clip-path="url(#terminal-line-20)">&#160;sub3/AGENTS.md</text><text class="terminal-r2" x="902.8" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r2" x="61" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="378.2" y="532.4" textLength="195.2" clip-path="url(#terminal-line-21)">&#160;sub4/AGENTS.md</text><text class="terminal-r2" x="902.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r2" x="61" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r2" x="134.2" y="556.8" textLength="707.6" clip-path="url(#terminal-line-22)">──────────────────────────────────────────────────────────</text><text class="terminal-r2" x="902.8" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r2" x="61" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r3" x="268.4" y="581.2" textLength="439.2" clip-path="url(#terminal-line-23)">Only&#160;trust&#160;folders&#160;you&#160;fully&#160;control</text><text class="terminal-r2" x="902.8" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r2" x="61" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r2" x="134.2" y="605.6" textLength="707.6" clip-path="url(#terminal-line-24)">──────────────────────────────────────────────────────────</text><text class="terminal-r2" x="902.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r2" x="61" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r2" x="902.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r2" x="61" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="305" y="654.4" textLength="170.8" clip-path="url(#terminal-line-26)">&#160;&#160;Trust&#160;folder</text><text class="terminal-r7" x="512.4" y="654.4" textLength="158.6" clip-path="url(#terminal-line-26)">&#160;Don&#x27;t&#160;trust</text><text class="terminal-r2" x="902.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r2" x="61" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r2" x="902.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r2" x="61" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r8" x="329.4" y="703.2" textLength="317.2" clip-path="url(#terminal-line-28)">&#160;&#160;navigate&#160;&#160;Enter&#160;select</text><text class="terminal-r2" x="902.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r2" x="61" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r2" x="902.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r2" x="61" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r8" x="329.4" y="752" textLength="305" clip-path="url(#terminal-line-30)">Setting&#160;will&#160;be&#160;saved&#160;in:</text><text class="terminal-r2" x="902.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r2" x="61" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r8" x="256.2" y="776.4" textLength="451.4" clip-path="url(#terminal-line-31)">/home/user/.vibe/trusted_folders.toml</text><text class="terminal-r2" x="902.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r2" x="61" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r2" x="902.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="976" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r2" x="61" y="825.2" textLength="854" clip-path="url(#terminal-line-33)">╰────────────────────────────────────────────────────────────────────╯</text><text class="terminal-r1" x="976" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r1" x="976" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r1" x="976" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">
</text><text class="terminal-r1" x="976" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">
</text><text class="terminal-r1" x="976" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">
</text><text class="terminal-r1" x="976" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">
</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,156 @@
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" 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: #4b4e55 }
.terminal-r3 { fill: #d0b344;font-weight: bold }
.terminal-r4 { fill: #292929 }
.terminal-r5 { fill: #608ab1 }
.terminal-r6 { fill: #868887;font-weight: bold }
.terminal-r7 { fill: #c5c8c6;font-weight: bold }
.terminal-r8 { fill: #868887 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="975.0" height="584.5999999999999" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="976" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">TrustFolderDialogSnapshotApp</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="829.6" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="123.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="196.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="221.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="245.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="269.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="294.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="318.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="367.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="391.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="416.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="440.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="465.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="489.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="829.6" y="513.9" width="12.2" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r2" x="61" y="20" textLength="854" clip-path="url(#terminal-line-0)">╭────────────────────────────────────────────────────────────────────╮</text><text class="terminal-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r2" x="61" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)"></text><text class="terminal-r2" x="902.8" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)"></text><text class="terminal-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r2" x="61" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)"></text><text class="terminal-r3" x="366" y="68.8" textLength="219.6" clip-path="url(#terminal-line-2)">Trust&#160;this&#160;folder?</text><text class="terminal-r2" x="902.8" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)"></text><text class="terminal-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r2" x="61" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)"></text><text class="terminal-r2" x="902.8" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)"></text><text class="terminal-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r2" x="61" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)"></text><text class="terminal-r5" x="292.8" y="117.6" textLength="366" clip-path="url(#terminal-line-4)">/home/user/projects/my-project</text><text class="terminal-r2" x="902.8" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)"></text><text class="terminal-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r2" x="61" y="142" textLength="12.2" clip-path="url(#terminal-line-5)"></text><text class="terminal-r2" x="902.8" y="142" textLength="12.2" clip-path="url(#terminal-line-5)"></text><text class="terminal-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r2" x="61" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)"></text><text class="terminal-r1" x="134.2" y="166.4" textLength="683.2" clip-path="url(#terminal-line-6)">Files&#160;here&#160;can&#160;modify&#160;AI&#160;behavior.&#160;Malicious&#160;configs&#160;may</text><text class="terminal-r2" x="902.8" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)"></text><text class="terminal-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r2" x="61" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)"></text><text class="terminal-r1" x="146.4" y="190.8" textLength="658.8" clip-path="url(#terminal-line-7)">exfiltrate&#160;data,&#160;run&#160;destructive&#160;commands,&#160;or&#160;silently</text><text class="terminal-r2" x="902.8" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)"></text><text class="terminal-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r2" x="61" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r1" x="378.2" y="215.2" textLength="195.2" clip-path="url(#terminal-line-8)">alter&#160;your&#160;code.</text><text class="terminal-r2" x="902.8" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r2" x="61" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r2" x="902.8" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r2" x="61" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r6" x="317.2" y="264" textLength="329.4" clip-path="url(#terminal-line-10)">Detected&#160;in&#160;current&#160;folder:</text><text class="terminal-r2" x="902.8" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r2" x="61" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r1" x="414.8" y="288.4" textLength="134.2" clip-path="url(#terminal-line-11)">&#160;AGENTS.md</text><text class="terminal-r2" x="902.8" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r2" x="61" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="427" y="312.8" textLength="97.6" clip-path="url(#terminal-line-12)">&#160;.vibe/</text><text class="terminal-r2" x="902.8" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r2" x="61" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r2" x="902.8" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r2" x="61" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r2" x="134.2" y="361.6" textLength="695.4" clip-path="url(#terminal-line-14)">─────────────────────────────────────────────────────────</text><text class="terminal-r2" x="902.8" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r2" x="61" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r3" x="256.2" y="386" textLength="439.2" clip-path="url(#terminal-line-15)">Only&#160;trust&#160;folders&#160;you&#160;fully&#160;control</text><text class="terminal-r2" x="902.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r2" x="61" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r2" x="134.2" y="410.4" textLength="695.4" clip-path="url(#terminal-line-16)">─────────────────────────────────────────────────────────</text><text class="terminal-r2" x="902.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r2" x="61" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r2" x="902.8" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r2" x="61" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="292.8" y="459.2" textLength="170.8" clip-path="url(#terminal-line-18)">&#160;&#160;Trust&#160;folder</text><text class="terminal-r7" x="500.2" y="459.2" textLength="158.6" clip-path="url(#terminal-line-18)">&#160;Don&#x27;t&#160;trust</text><text class="terminal-r2" x="902.8" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r2" x="61" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r2" x="902.8" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r2" x="61" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r8" x="317.2" y="508" textLength="317.2" clip-path="url(#terminal-line-20)">&#160;&#160;navigate&#160;&#160;Enter&#160;select</text><text class="terminal-r2" x="829.6" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r2" x="902.8" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r2" x="61" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r2" x="902.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r2" x="61" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r2" x="902.8" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r2" x="61" y="581.2" textLength="854" clip-path="url(#terminal-line-23)">╰────────────────────────────────────────────────────────────────────╯</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,220 @@
<svg class="rich-terminal" viewBox="0 0 994 1026.0" 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: #4b4e55 }
.terminal-r3 { fill: #d0b344;font-weight: bold }
.terminal-r4 { fill: #608ab1 }
.terminal-r5 { fill: #d0b344;font-style: italic; }
.terminal-r6 { fill: #868887;font-weight: bold }
.terminal-r7 { fill: #c5c8c6;font-weight: bold }
.terminal-r8 { fill: #868887 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="975.0" height="975.0" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-35">
<rect x="0" y="855.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-36">
<rect x="0" y="879.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-37">
<rect x="0" y="904.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-38">
<rect x="0" y="928.7" width="976" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="1024" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">TrustFolderDialogUntrustedRepoSnapshotApp</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="976" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r2" x="61" y="166.4" textLength="854" clip-path="url(#terminal-line-6)">╭────────────────────────────────────────────────────────────────────╮</text><text class="terminal-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r2" x="61" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)"></text><text class="terminal-r2" x="902.8" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)"></text><text class="terminal-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r2" x="61" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r3" x="378.2" y="215.2" textLength="219.6" clip-path="url(#terminal-line-8)">Trust&#160;this&#160;folder?</text><text class="terminal-r2" x="902.8" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r2" x="61" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r2" x="902.8" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r2" x="61" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r4" x="256.2" y="264" textLength="463.6" clip-path="url(#terminal-line-10)">/home/user/projects/my-project/src/pkg</text><text class="terminal-r2" x="902.8" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r2" x="61" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r5" x="134.2" y="288.4" textLength="695.4" clip-path="url(#terminal-line-11)">&#160;git&#160;repository&#160;/home/user/projects/my-project&#160;is&#160;marked</text><text class="terminal-r2" x="902.8" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r2" x="61" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r5" x="427" y="312.8" textLength="109.8" clip-path="url(#terminal-line-12)">untrusted</text><text class="terminal-r2" x="902.8" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r2" x="61" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r2" x="902.8" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r2" x="61" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="146.4" y="361.6" textLength="683.2" clip-path="url(#terminal-line-14)">Files&#160;here&#160;can&#160;modify&#160;AI&#160;behavior.&#160;Malicious&#160;configs&#160;may</text><text class="terminal-r2" x="902.8" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r2" x="61" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="158.6" y="386" textLength="658.8" clip-path="url(#terminal-line-15)">exfiltrate&#160;data,&#160;run&#160;destructive&#160;commands,&#160;or&#160;silently</text><text class="terminal-r2" x="902.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r2" x="61" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="390.4" y="410.4" textLength="195.2" clip-path="url(#terminal-line-16)">alter&#160;your&#160;code.</text><text class="terminal-r2" x="902.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r2" x="61" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r2" x="902.8" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r2" x="61" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r6" x="317.2" y="459.2" textLength="329.4" clip-path="url(#terminal-line-18)">Detected&#160;in&#160;current&#160;folder:</text><text class="terminal-r2" x="902.8" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r2" x="61" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="414.8" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;AGENTS.md</text><text class="terminal-r2" x="902.8" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r2" x="61" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r2" x="902.8" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r2" x="61" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r2" x="134.2" y="532.4" textLength="707.6" clip-path="url(#terminal-line-21)">──────────────────────────────────────────────────────────</text><text class="terminal-r2" x="902.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r2" x="61" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r3" x="268.4" y="556.8" textLength="439.2" clip-path="url(#terminal-line-22)">Only&#160;trust&#160;folders&#160;you&#160;fully&#160;control</text><text class="terminal-r2" x="902.8" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r2" x="61" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r2" x="134.2" y="581.2" textLength="707.6" clip-path="url(#terminal-line-23)">──────────────────────────────────────────────────────────</text><text class="terminal-r2" x="902.8" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r2" x="61" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r2" x="902.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r2" x="61" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="305" y="630" textLength="170.8" clip-path="url(#terminal-line-25)">&#160;&#160;Trust&#160;folder</text><text class="terminal-r7" x="512.4" y="630" textLength="158.6" clip-path="url(#terminal-line-25)">&#160;Don&#x27;t&#160;trust</text><text class="terminal-r2" x="902.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r2" x="61" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r2" x="902.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r2" x="61" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r8" x="329.4" y="678.8" textLength="317.2" clip-path="url(#terminal-line-27)">&#160;&#160;navigate&#160;&#160;Enter&#160;select</text><text class="terminal-r2" x="902.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r2" x="61" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r2" x="902.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r2" x="61" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r8" x="329.4" y="727.6" textLength="305" clip-path="url(#terminal-line-29)">Setting&#160;will&#160;be&#160;saved&#160;in:</text><text class="terminal-r2" x="902.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r2" x="61" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r8" x="256.2" y="752" textLength="451.4" clip-path="url(#terminal-line-30)">/home/user/.vibe/trusted_folders.toml</text><text class="terminal-r2" x="902.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r2" x="61" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r2" x="902.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r2" x="61" y="800.8" textLength="854" clip-path="url(#terminal-line-32)">╰────────────────────────────────────────────────────────────────────╯</text><text class="terminal-r1" x="976" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r1" x="976" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r1" x="976" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r1" x="976" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">
</text><text class="terminal-r1" x="976" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">
</text><text class="terminal-r1" x="976" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">
</text><text class="terminal-r1" x="976" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">
</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,221 @@
<svg class="rich-terminal" viewBox="0 0 994 1026.0" 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: #4b4e55 }
.terminal-r3 { fill: #d0b344;font-weight: bold }
.terminal-r4 { fill: #608ab1 }
.terminal-r5 { fill: #868887;font-style: italic; }
.terminal-r6 { fill: #292929 }
.terminal-r7 { fill: #868887;font-weight: bold }
.terminal-r8 { fill: #c5c8c6;font-weight: bold }
.terminal-r9 { fill: #868887 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="975.0" height="975.0" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-35">
<rect x="0" y="855.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-36">
<rect x="0" y="879.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-37">
<rect x="0" y="904.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-38">
<rect x="0" y="928.7" width="976" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="1024" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">TrustFolderDialogWithRepoSnapshotApp</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="829.6" y="294.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="318.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="367.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="391.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="416.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="440.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="465.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="489.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="829.6" y="513.9" width="12.2" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r2" x="61" y="142" textLength="854" clip-path="url(#terminal-line-5)">╭────────────────────────────────────────────────────────────────────╮</text><text class="terminal-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r2" x="61" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)"></text><text class="terminal-r2" x="902.8" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)"></text><text class="terminal-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r2" x="61" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)"></text><text class="terminal-r3" x="317.2" y="190.8" textLength="329.4" clip-path="url(#terminal-line-7)">Trust&#160;folder&#160;or&#160;repository?</text><text class="terminal-r2" x="902.8" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)"></text><text class="terminal-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r2" x="61" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r2" x="902.8" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)"></text><text class="terminal-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r2" x="61" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r4" x="256.2" y="239.6" textLength="463.6" clip-path="url(#terminal-line-9)">/home/user/projects/my-project/src/pkg</text><text class="terminal-r2" x="902.8" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)"></text><text class="terminal-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r2" x="61" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r5" x="195.2" y="264" textLength="585.6" clip-path="url(#terminal-line-10)">&#160;git&#160;repository:&#160;/home/user/projects/my-project</text><text class="terminal-r2" x="902.8" y="264" textLength="12.2" clip-path="url(#terminal-line-10)"></text><text class="terminal-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r2" x="61" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r2" x="902.8" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)"></text><text class="terminal-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r2" x="61" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="134.2" y="312.8" textLength="683.2" clip-path="url(#terminal-line-12)">Files&#160;here&#160;can&#160;modify&#160;AI&#160;behavior.&#160;Malicious&#160;configs&#160;may</text><text class="terminal-r2" x="902.8" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r2" x="61" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="146.4" y="337.2" textLength="658.8" clip-path="url(#terminal-line-13)">exfiltrate&#160;data,&#160;run&#160;destructive&#160;commands,&#160;or&#160;silently</text><text class="terminal-r2" x="902.8" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r2" x="61" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="378.2" y="361.6" textLength="195.2" clip-path="url(#terminal-line-14)">alter&#160;your&#160;code.</text><text class="terminal-r2" x="902.8" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r2" x="61" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r2" x="902.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r2" x="61" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r7" x="317.2" y="410.4" textLength="329.4" clip-path="url(#terminal-line-16)">Detected&#160;in&#160;current&#160;folder:</text><text class="terminal-r2" x="902.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r2" x="61" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="414.8" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">&#160;AGENTS.md</text><text class="terminal-r2" x="902.8" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r2" x="61" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r2" x="902.8" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r2" x="61" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r7" x="292.8" y="483.6" textLength="378.2" clip-path="url(#terminal-line-19)">Detected&#160;in&#160;repository&#160;context:</text><text class="terminal-r2" x="902.8" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r2" x="61" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="427" y="508" textLength="97.6" clip-path="url(#terminal-line-20)">&#160;.vibe/</text><text class="terminal-r2" x="902.8" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r2" x="61" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="390.4" y="532.4" textLength="183" clip-path="url(#terminal-line-21)">&#160;src/AGENTS.md</text><text class="terminal-r2" x="829.6" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r2" x="902.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r2" x="61" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r2" x="134.2" y="556.8" textLength="707.6" clip-path="url(#terminal-line-22)">──────────────────────────────────────────────────────────</text><text class="terminal-r2" x="902.8" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r2" x="61" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r3" x="268.4" y="581.2" textLength="439.2" clip-path="url(#terminal-line-23)">Only&#160;trust&#160;folders&#160;you&#160;fully&#160;control</text><text class="terminal-r2" x="902.8" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="976" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r2" x="61" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r2" x="134.2" y="605.6" textLength="707.6" clip-path="url(#terminal-line-24)">──────────────────────────────────────────────────────────</text><text class="terminal-r2" x="902.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="976" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r2" x="61" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r2" x="902.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="976" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r2" x="61" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="183" y="654.4" textLength="207.4" clip-path="url(#terminal-line-26)">&#160;&#160;Trust&#160;full&#160;repo</text><text class="terminal-r1" x="427" y="654.4" textLength="170.8" clip-path="url(#terminal-line-26)">&#160;&#160;Trust&#160;folder</text><text class="terminal-r8" x="634.4" y="654.4" textLength="158.6" clip-path="url(#terminal-line-26)">&#160;Don&#x27;t&#160;trust</text><text class="terminal-r2" x="902.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="976" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r2" x="61" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r2" x="902.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="976" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r2" x="61" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r9" x="329.4" y="703.2" textLength="317.2" clip-path="url(#terminal-line-28)">&#160;&#160;navigate&#160;&#160;Enter&#160;select</text><text class="terminal-r2" x="902.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="976" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r2" x="61" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r2" x="902.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="976" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r2" x="61" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r9" x="329.4" y="752" textLength="305" clip-path="url(#terminal-line-30)">Setting&#160;will&#160;be&#160;saved&#160;in:</text><text class="terminal-r2" x="902.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="976" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r2" x="61" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r9" x="256.2" y="776.4" textLength="451.4" clip-path="url(#terminal-line-31)">/home/user/.vibe/trusted_folders.toml</text><text class="terminal-r2" x="902.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="976" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r2" x="61" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r2" x="902.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="976" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r2" x="61" y="825.2" textLength="854" clip-path="url(#terminal-line-33)">╰────────────────────────────────────────────────────────────────────╯</text><text class="terminal-r1" x="976" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r1" x="976" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r1" x="976" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">
</text><text class="terminal-r1" x="976" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">
</text><text class="terminal-r1" x="976" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">
</text><text class="terminal-r1" x="976" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">
</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

View file

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

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

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -63,8 +63,9 @@ class BaseSnapshotTestApp(VibeApp):
agent_loop=agent_loop, plan_offer_gateway=plan_offer_gateway, **kwargs agent_loop=agent_loop, plan_offer_gateway=plan_offer_gateway, **kwargs
) )
async def on_mount(self) -> None: async def on_ready(self):
await super().on_mount() # on_ready is called once all the on_mount in the MRO chain have been called
# https://textual.textualize.io/api/events/#textual.events.Ready
self._hide_chat_input_cursor() self._hide_chat_input_cursor()
def _hide_chat_input_cursor(self) -> None: def _hide_chat_input_cursor(self) -> None:

View file

@ -0,0 +1,56 @@
from __future__ import annotations
from pathlib import Path
from textual.pilot import Pilot
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp
from tests.snapshots.snap_compare import SnapCompare
from vibe.core.hooks.models import HookConfigIssue
from vibe.core.skills.models import SkillConfigIssue
class SnapshotTestAppWithConfigIssues(BaseSnapshotTestApp):
def __init__(self) -> None:
super().__init__()
self.agent_loop.skill_manager._config_issues = [
SkillConfigIssue(
file=Path("/test/skills/broken-skill/SKILL.md"),
message="Failed to load: missing required field 'description'",
)
]
class SnapshotTestAppWithHookConfigIssue(BaseSnapshotTestApp):
def __init__(self) -> None:
super().__init__()
self.agent_loop.hook_config_issues = [
HookConfigIssue(
file=Path("/test/hooks/broken-hook.toml"),
message="Failed to parse: invalid TOML syntax",
)
]
def test_snapshot_shows_config_issue_notification(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.3)
assert snap_compare(
"test_ui_snapshot_config_issues.py:SnapshotTestAppWithConfigIssues",
terminal_size=(120, 36),
run_before=run_before,
)
def test_snapshot_shows_hook_config_issue_notification(
snap_compare: SnapCompare,
) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.3)
assert snap_compare(
"test_ui_snapshot_config_issues.py:SnapshotTestAppWithHookConfigIssue",
terminal_size=(120, 36),
run_before=run_before,
)

View file

@ -0,0 +1,71 @@
from __future__ import annotations
from textual.containers import VerticalScroll
from textual.pilot import Pilot
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp
from tests.snapshots.snap_compare import SnapCompare
from vibe.cli.textual_ui.widgets.approval_app import ApprovalApp
from vibe.core.tools.builtins.write_file import WriteFileArgs
WF_CONTENT_LONG = "\n".join(f"line_{i:03d} = {i * 7}" for i in range(1, 101))
WF_CONTENT_SHORT = "line_001 = 7"
class WriteApprovalLongContentApp(BaseSnapshotTestApp):
async def on_ready(self) -> None:
args = WriteFileArgs(path="src/example.py", content=WF_CONTENT_LONG)
await self._switch_to_approval_app("write_file", args)
class WriteApprovalShortContentApp(BaseSnapshotTestApp):
async def on_ready(self) -> None:
args = WriteFileArgs(path="src/example.py", content=WF_CONTENT_SHORT)
await self._switch_to_approval_app("write_file", args)
def test_snapshot_write_approval_long_content_bottom_lines_hidden(
snap_compare: SnapCompare,
) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.3)
approval = pilot.app.query_one(ApprovalApp)
scroll = approval.query_one(".approval-tool-info-scroll", VerticalScroll)
scroll.scroll_end(animate=False, immediate=True)
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_diff_view_truncation.py:WriteApprovalLongContentApp",
terminal_size=(100, 30),
run_before=run_before,
)
def test_snapshot_write_approval_short_content(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.3)
assert snap_compare(
"test_ui_snapshot_diff_view_truncation.py:WriteApprovalShortContentApp",
terminal_size=(100, 30),
run_before=run_before,
)
def test_snapshot_write_approval_long_content_after_resize(
snap_compare: SnapCompare,
) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.3)
await pilot.resize_terminal(120, 40)
await pilot.pause(0.2)
approval = pilot.app.query_one(ApprovalApp)
scroll = approval.query_one(".approval-tool-info-scroll", VerticalScroll)
scroll.scroll_end(animate=False, immediate=True)
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_diff_view_truncation.py:WriteApprovalLongContentApp",
terminal_size=(100, 30),
run_before=run_before,
)

View file

@ -0,0 +1,116 @@
from __future__ import annotations
from pathlib import Path
import pytest
from textual.pilot import Pilot
from tests.conftest import build_test_vibe_config
from tests.mock.utils import mock_llm_chunk
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp, default_config
from tests.snapshots.snap_compare import SnapCompare
from tests.stubs.fake_backend import FakeBackend
from vibe.cli.textual_ui.widgets.chat_input import paste_path
from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig
from vibe.core.types import Backend
PNG_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00" * 16
SNAP_ROOT = "/snap"
def _vision_config() -> VibeConfig:
base = default_config()
models = [
ModelConfig(
name="mistral-vibe-cli-latest",
provider="mistral",
alias="devstral-latest",
supports_images=True,
)
]
providers = [
ProviderConfig(
name="mistral",
api_base="https://api.mistral.ai/v1",
api_key_env_var="MISTRAL_API_KEY",
backend=Backend.MISTRAL,
)
]
return build_test_vibe_config(
disable_welcome_banner_animation=True,
displayed_workdir=base.displayed_workdir,
active_model="devstral-latest",
models=models,
providers=providers,
)
@pytest.fixture(autouse=True)
def _accept_snap_paths_as_images(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_is_image_file(candidate: str) -> bool:
return candidate.startswith(f"{SNAP_ROOT}/") and candidate.endswith((
".png",
".jpg",
".jpeg",
".gif",
".webp",
))
monkeypatch.setattr(paste_path, "_is_image_file", fake_is_image_file)
class _ImageAttachmentApp(BaseSnapshotTestApp):
def __init__(self) -> None:
super().__init__(
config=_vision_config(),
backend=FakeBackend(mock_llm_chunk(content="Got it.")),
)
def test_snapshot_textbox_rewrites_typed_image_path(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.press(*f"look {SNAP_ROOT}/shot.png")
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_image_attachments.py:_ImageAttachmentApp",
terminal_size=(120, 36),
run_before=run_before,
)
def test_snapshot_user_message_with_image_footer_singular(
snap_compare: SnapCompare, tmp_working_directory: Path
) -> None:
(tmp_working_directory / "shot.png").write_bytes(PNG_BYTES)
async def run_before(pilot: Pilot) -> None:
# Trailing space dismisses the @-mention autocomplete popup so that
# `enter` submits the message instead of accepting a suggestion.
await pilot.press(*"look @shot.png ")
await pilot.press("enter")
await pilot.pause(0.4)
assert snap_compare(
"test_ui_snapshot_image_attachments.py:_ImageAttachmentApp",
terminal_size=(120, 36),
run_before=run_before,
)
def test_snapshot_user_message_with_image_footer_plural(
snap_compare: SnapCompare, tmp_working_directory: Path
) -> None:
(tmp_working_directory / "a.png").write_bytes(PNG_BYTES)
(tmp_working_directory / "b.png").write_bytes(PNG_BYTES)
async def run_before(pilot: Pilot) -> None:
await pilot.press(*"compare @a.png @b.png ")
await pilot.press("enter")
await pilot.pause(0.4)
assert snap_compare(
"test_ui_snapshot_image_attachments.py:_ImageAttachmentApp",
terminal_size=(120, 36),
run_before=run_before,
)

View file

@ -174,7 +174,8 @@ def test_snapshot_mcp_refresh_shortcut(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None: async def run_before(pilot: Pilot) -> None:
await _run_mcp_command(pilot, "/mcp") await _run_mcp_command(pilot, "/mcp")
await pilot.press("r") await pilot.press("r")
await pilot.pause(0.2) await pilot.app.workers.wait_for_complete()
await pilot.pause(0.1)
assert snap_compare( assert snap_compare(
"test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers",

View file

@ -10,7 +10,7 @@ from textual.widget import Widget
from tests.snapshots.snap_compare import SnapCompare from tests.snapshots.snap_compare import SnapCompare
from vibe.cli.textual_ui.handlers.event_handler import EventHandler from vibe.cli.textual_ui.handlers.event_handler import EventHandler
from vibe.cli.textual_ui.widgets.tools import ToolCallMessage from vibe.cli.textual_ui.widgets.tools import ToolCallMessage
from vibe.core.tools.builtins.read_file import ReadFile, ReadFileArgs, ReadFileResult from vibe.core.tools.builtins.read import Read, ReadArgs, ReadResult
from vibe.core.types import ToolCallEvent, ToolResultEvent from vibe.core.types import ToolCallEvent, ToolResultEvent
@ -49,9 +49,9 @@ class ParallelToolCallsApp(App):
ToolCallEvent( ToolCallEvent(
tool_call_id=f"tc_{i}", tool_call_id=f"tc_{i}",
tool_call_index=i, tool_call_index=i,
tool_name="read_file", tool_name="read",
tool_class=ReadFile, tool_class=Read,
args=ReadFileArgs(path=f"/src/file_{i}.py"), args=ReadArgs(file_path=f"/src/file_{i}.py"),
) )
) )
@ -71,13 +71,14 @@ class ParallelToolCallsApp(App):
for i in range(3): for i in range(3):
await self._handler.handle_event( await self._handler.handle_event(
ToolResultEvent( ToolResultEvent(
tool_name="read_file", tool_name="read",
tool_class=ReadFile, tool_class=Read,
result=ReadFileResult( result=ReadResult(
path=f"/src/file_{i}.py", file_path=f"/src/file_{i}.py",
content=f"# content of file_{i}.py", content=f" 1→# content of file_{i}.py",
lines_read=1, num_lines=1,
was_truncated=False, start_line=1,
total_lines=1,
), ),
tool_call_id=f"tc_{i}", tool_call_id=f"tc_{i}",
) )

View file

@ -11,7 +11,7 @@ from tests.update_notifier.adapters.fake_update_cache_repository import (
FakeUpdateCacheRepository, FakeUpdateCacheRepository,
) )
from tests.update_notifier.adapters.fake_update_gateway import FakeUpdateGateway from tests.update_notifier.adapters.fake_update_gateway import FakeUpdateGateway
from vibe.cli.update_notifier import Update from vibe.cli.update_notifier import Update, UpdateCache
class SnapshotTestAppWithUpdate(BaseSnapshotTestApp): class SnapshotTestAppWithUpdate(BaseSnapshotTestApp):
@ -19,7 +19,13 @@ class SnapshotTestAppWithUpdate(BaseSnapshotTestApp):
config = default_config() config = default_config()
config.enable_update_checks = True config.enable_update_checks = True
update_notifier = FakeUpdateGateway(update=Update(latest_version="1000.2.0")) update_notifier = FakeUpdateGateway(update=Update(latest_version="1000.2.0"))
update_cache_repository = FakeUpdateCacheRepository() update_cache_repository = FakeUpdateCacheRepository(
UpdateCache(
latest_version="1.0.4",
stored_at_timestamp=0,
seen_whats_new_version=None,
)
)
super().__init__( super().__init__(
config=config, config=config,
update_notifier=update_notifier, update_notifier=update_notifier,

View file

@ -20,7 +20,7 @@ class SnapshotTestAppWithResumedSession(BaseSnapshotTestApp):
id="tool_call_1", id="tool_call_1",
index=0, index=0,
function=FunctionCall( function=FunctionCall(
name="read_file", arguments='{"path": "test.txt"}' name="read", arguments='{"file_path": "test.txt"}'
), ),
) )
], ],
@ -28,7 +28,7 @@ class SnapshotTestAppWithResumedSession(BaseSnapshotTestApp):
tool_result_msg = LLMMessage( tool_result_msg = LLMMessage(
role=Role.tool, role=Role.tool,
content="File content: This is a test file with some content.", content="File content: This is a test file with some content.",
name="read_file", name="read",
tool_call_id="tool_call_1", tool_call_id="tool_call_1",
) )

View file

@ -10,7 +10,7 @@ from textual.pilot import Pilot
from tests.snapshots.snap_compare import SnapCompare from tests.snapshots.snap_compare import SnapCompare
from vibe.cli.textual_ui.widgets.status_message import StatusMessage from vibe.cli.textual_ui.widgets.status_message import StatusMessage
from vibe.cli.textual_ui.widgets.tools import ToolCallMessage from vibe.cli.textual_ui.widgets.tools import ToolCallMessage
from vibe.core.tools.builtins.read_file import ReadFile, ReadFileArgs from vibe.core.tools.builtins.read import Read, ReadArgs
from vibe.core.types import ToolCallEvent from vibe.core.types import ToolCallEvent
@ -25,8 +25,8 @@ class ToolCallStreamingUpdateTest(App):
partial_event = ToolCallEvent( partial_event = ToolCallEvent(
tool_call_id="tc_streaming", tool_call_id="tc_streaming",
tool_call_index=0, tool_call_index=0,
tool_name="read_file", tool_name="read",
tool_class=ReadFile, tool_class=Read,
args=None, args=None,
) )
self._widget = ToolCallMessage(partial_event) self._widget = ToolCallMessage(partial_event)
@ -41,9 +41,9 @@ class ToolCallStreamingUpdateTest(App):
full_event = ToolCallEvent( full_event = ToolCallEvent(
tool_call_id="tc_streaming", tool_call_id="tc_streaming",
tool_call_index=0, tool_call_index=0,
tool_name="read_file", tool_name="read",
tool_class=ReadFile, tool_class=Read,
args=ReadFileArgs(path="/test/example.py"), args=ReadArgs(file_path="/test/example.py"),
) )
self._widget.update_event(full_event) self._widget.update_event(full_event)

View file

@ -0,0 +1,119 @@
from __future__ import annotations
from pathlib import Path
import pytest
from tests.snapshots.snap_compare import SnapCompare
from vibe.setup.trusted_folders.trust_folder_dialog import TrustFolderApp
@pytest.fixture(autouse=True)
def _pin_vibe_home(monkeypatch: pytest.MonkeyPatch) -> None:
# The dialog renders TRUSTED_FOLDERS_FILE.path in its footer. Pin the
# default so the path is stable across runs. Setting the VIBE_HOME env
# var is not enough: _get_vibe_home() calls Path.resolve(), which on
# macOS rewrites /home/user via the /System/Volumes/Data firmlink.
monkeypatch.delenv("VIBE_HOME", raising=False)
monkeypatch.setattr(
"vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", Path("/home/user/.vibe")
)
_DIALOG_CSS = str(
Path(__file__).parents[2]
/ "vibe"
/ "setup"
/ "trusted_folders"
/ "trust_folder_dialog.tcss"
)
class TrustFolderDialogSnapshotApp(TrustFolderApp):
"""cwd is itself the trust target (two-option dialog)."""
CSS_PATH = _DIALOG_CSS
def __init__(self) -> None:
super().__init__(
cwd=Path("/home/user/projects/my-project"),
repo_root=None,
detected_files=["AGENTS.md", ".vibe/"],
)
class TrustFolderDialogWithRepoSnapshotApp(TrustFolderApp):
"""cwd inside a git repo (three-option dialog)."""
CSS_PATH = _DIALOG_CSS
def __init__(self) -> None:
super().__init__(
cwd=Path("/home/user/projects/my-project/src/pkg"),
repo_root=Path("/home/user/projects/my-project"),
detected_files=["AGENTS.md"],
repo_detected_files=[".vibe/", "src/AGENTS.md"],
offer_repo_trust=True,
)
class TrustFolderDialogUntrustedRepoSnapshotApp(TrustFolderApp):
"""cwd inside a git repo that was previously marked untrusted."""
CSS_PATH = _DIALOG_CSS
def __init__(self) -> None:
super().__init__(
cwd=Path("/home/user/projects/my-project/src/pkg"),
repo_root=Path("/home/user/projects/my-project"),
offer_repo_trust=False,
repo_explicitly_untrusted=True,
detected_files=["AGENTS.md"],
)
class TrustFolderDialogManyFilesSnapshotApp(TrustFolderApp):
CSS_PATH = _DIALOG_CSS
def __init__(self) -> None:
detected = [f"sub{i}/AGENTS.md" for i in range(20)] + [".vibe/", ".agents/"]
super().__init__(
cwd=Path("/home/user/projects/my-project"),
repo_root=None,
detected_files=detected,
)
def test_snapshot_trust_folder_dialog(snap_compare: SnapCompare) -> None:
assert snap_compare(
"test_ui_snapshot_trust_folder_dialog.py:TrustFolderDialogSnapshotApp",
terminal_size=(80, 40),
)
def test_snapshot_trust_folder_dialog_with_repo(snap_compare: SnapCompare) -> None:
assert snap_compare(
"test_ui_snapshot_trust_folder_dialog.py:TrustFolderDialogWithRepoSnapshotApp",
terminal_size=(80, 40),
)
def test_snapshot_trust_folder_dialog_untrusted_repo(snap_compare: SnapCompare) -> None:
assert snap_compare(
"test_ui_snapshot_trust_folder_dialog.py:TrustFolderDialogUntrustedRepoSnapshotApp",
terminal_size=(80, 40),
)
def test_snapshot_trust_folder_dialog_small_terminal(snap_compare: SnapCompare) -> None:
assert snap_compare(
"test_ui_snapshot_trust_folder_dialog.py:TrustFolderDialogSnapshotApp",
terminal_size=(80, 24),
)
def test_snapshot_trust_folder_dialog_many_files(snap_compare: SnapCompare) -> None:
assert snap_compare(
"test_ui_snapshot_trust_folder_dialog.py:TrustFolderDialogManyFilesSnapshotApp",
terminal_size=(80, 40),
)

Some files were not shown because too many files have changed in this diff Show more