v2.14.0 (#743)
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>
39
.github/workflows/build-and-upload.yml
vendored
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
31
CHANGELOG.md
|
|
@ -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
|
||||||
|
|
|
||||||
13
README.md
|
|
@ -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"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
35
tests/acp/test_acp_disabled_tools_merge.py
Normal 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)
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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"}'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
||||||
177
tests/acp/test_session_delete.py
Normal 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
|
|
@ -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"
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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("") == []
|
||||||
|
|
|
||||||
70
tests/autocompletion/test_path_prompt_image.py
Normal 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"
|
||||||
68
tests/backend/test_image_encoding_cache.py
Normal 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)
|
||||||
146
tests/backend/test_image_mapping.py
Normal 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"
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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()
|
||||||
193
tests/cli/test_check_and_resolve_trusted_folder.py
Normal 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
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
64
tests/cli/textual_ui/test_chat_input_keybindings.py
Normal 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 == ">"
|
||||||
149
tests/cli/textual_ui/test_image_input_validation.py
Normal 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)
|
||||||
221
tests/cli/textual_ui/test_paste_path.py
Normal 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}'"
|
||||||
18
tests/cli/textual_ui/test_read_widget.py
Normal 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"
|
||||||
78
tests/cli/textual_ui/test_user_message_attachments.py
Normal 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)
|
||||||
71
tests/core/session/test_image_snapshot.py
Normal 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"
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"}}
|
||||||
|
|
|
||||||
86
tests/core/test_edit_encoding.py
Normal 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"))
|
||||||
|
)
|
||||||
48
tests/core/test_environment_layer.py
Normal 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
|
||||||
51
tests/core/test_llm_message_merge.py
Normal 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
|
||||||
114
tests/core/test_local_config_files.py
Normal 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
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
62
tests/core/test_vibe_config_schema.py
Normal 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
|
||||||
230
tests/core/tools/builtins/test_edit.py
Normal 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
|
||||||
355
tests/core/tools/builtins/test_read.py
Normal 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()
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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 = (
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="605.6" textLength="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)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||||
|
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||||
|
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-r3" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)"> for more information</text><text class="terminal-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 to load: missing required field 'description'</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)">────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||||
|
</text><text class="terminal-r2" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">></text><text class="terminal-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% of 200k tokens</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,201 @@
|
||||||
|
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Generated with Rich https://www.textualize.io -->
|
||||||
|
<style>
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Fira Code";
|
||||||
|
src: local("FiraCode-Regular"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Fira Code";
|
||||||
|
src: local("FiraCode-Bold"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
||||||
|
font-style: bold;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-matrix {
|
||||||
|
font-family: Fira Code, monospace;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 24.4px;
|
||||||
|
font-variant-east-asian: full-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: arial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-r1 { fill: #c5c8c6 }
|
||||||
|
.terminal-r2 { fill: #ff8205;font-weight: bold }
|
||||||
|
.terminal-r3 { fill: #68a0b3 }
|
||||||
|
.terminal-r4 { fill: #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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="605.6" textLength="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)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||||
|
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||||
|
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-r3" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)"> for more information</text><text class="terminal-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 to parse: invalid TOML 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)">────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||||
|
</text><text class="terminal-r2" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">></text><text class="terminal-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% of 200k tokens</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="142" textLength="146.4" clip-path="url(#terminal-line-5)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="142" textLength="122" clip-path="url(#terminal-line-5)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="166.4" textLength="414.8" clip-path="url(#terminal-line-6)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="190.8" textLength="61" clip-path="url(#terminal-line-7)">Type </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)"> for more 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 for the write_file 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)">› 1. Allow 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)">  2. Allow for remainder of this 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)">  3. Always 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)">  4. 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)">↑↓ navigate  Enter select  ESC 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% of 200k tokens</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 26 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="68.8" textLength="146.4" clip-path="url(#terminal-line-2)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="68.8" textLength="122" clip-path="url(#terminal-line-2)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="93.2" textLength="414.8" clip-path="url(#terminal-line-3)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="117.6" textLength="61" clip-path="url(#terminal-line-4)">Type </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)"> for more 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 for the write_file 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)">› 1. Allow 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)">  2. Allow for remainder of this 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)">  3. Always 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)">  4. 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)">↑↓ navigate  Enter select  ESC 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% of 200k tokens</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 19 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="190.8" textLength="146.4" clip-path="url(#terminal-line-7)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="190.8" textLength="122" clip-path="url(#terminal-line-7)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="215.2" textLength="414.8" clip-path="url(#terminal-line-8)">1 model · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="239.6" textLength="61" clip-path="url(#terminal-line-9)">Type </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)"> for more 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 for the write_file 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: 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)">› 1. Allow 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)">  2. Allow for remainder of this 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)">  3. Always 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)">  4. 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)">↑↓ navigate  Enter select  ESC 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% of 200k tokens</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="605.6" textLength="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)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||||
|
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||||
|
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-r3" x="231.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="292.8" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)"> for more information</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||||
|
</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||||
|
</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||||
|
</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||||
|
</text><text class="terminal-r1" x="0" y="752" textLength="1464" clip-path="url(#terminal-line-30)">────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||||
|
</text><text class="terminal-r2" 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="1439.6" clip-path="url(#terminal-line-31)">look @/snap/shot.png                                                                                                  </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% of 200k tokens</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -0,0 +1,201 @@
|
||||||
|
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Generated with Rich https://www.textualize.io -->
|
||||||
|
<style>
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Fira Code";
|
||||||
|
src: local("FiraCode-Regular"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Fira Code";
|
||||||
|
src: local("FiraCode-Bold"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
||||||
|
font-style: bold;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-matrix {
|
||||||
|
font-family: Fira Code, monospace;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 24.4px;
|
||||||
|
font-variant-east-asian: full-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: arial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-r1 { fill: #c5c8c6 }
|
||||||
|
.terminal-r2 { fill: #ff8205;font-weight: bold }
|
||||||
|
.terminal-r3 { fill: #68a0b3 }
|
||||||
|
.terminal-r4 { fill: #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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="434.8" textLength="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)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
|
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
|
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> for more information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
|
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
|
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
|
</text><text class="terminal-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)">> </text><text class="terminal-r4" x="24.4" y="581.2" textLength="256.2" clip-path="url(#terminal-line-23)">compare @a.png @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)">└ attached images: </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)">, </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 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)">────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||||
|
</text><text class="terminal-r2" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">></text><text class="terminal-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% of 200k tokens</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,201 @@
|
||||||
|
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Generated with Rich https://www.textualize.io -->
|
||||||
|
<style>
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Fira Code";
|
||||||
|
src: local("FiraCode-Regular"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Fira Code";
|
||||||
|
src: local("FiraCode-Bold"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
||||||
|
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
||||||
|
font-style: bold;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-matrix {
|
||||||
|
font-family: Fira Code, monospace;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 24.4px;
|
||||||
|
font-variant-east-asian: full-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: arial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-r1 { fill: #c5c8c6 }
|
||||||
|
.terminal-r2 { fill: #ff8205;font-weight: bold }
|
||||||
|
.terminal-r3 { fill: #68a0b3 }
|
||||||
|
.terminal-r4 { fill: #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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="434.8" textLength="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)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
|
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
|
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> for more information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
|
</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
|
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
|
</text><text class="terminal-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)">> </text><text class="terminal-r4" x="24.4" y="581.2" textLength="170.8" clip-path="url(#terminal-line-23)">look @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)">└ attached image: </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 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)">────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||||
|
</text><text class="terminal-r2" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">></text><text class="terminal-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% of 200k tokens</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="386" textLength="146.4" clip-path="url(#terminal-line-15)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="386" textLength="122" clip-path="url(#terminal-line-15)"> v0.0.0 · </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)"> · [Subscription] 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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="386" textLength="146.4" clip-path="url(#terminal-line-15)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="386" textLength="122" clip-path="url(#terminal-line-15)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="410.4" textLength="597.8" clip-path="url(#terminal-line-16)">1 model · 3 connectors · 0 MCP servers · 0 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="410.4" textLength="622.2" clip-path="url(#terminal-line-16)">1 model · 1/3 connectors · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="434.8" textLength="61" clip-path="url(#terminal-line-17)">Type </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)"> for more 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="434.8" textLength="61" clip-path="url(#terminal-line-17)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)"> v0.0.0 · </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)"> · [Subscription] 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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="337.2" textLength="597.8" clip-path="url(#terminal-line-13)">1 model · 3 connectors · 0 MCP servers · 0 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="337.2" textLength="622.2" clip-path="url(#terminal-line-13)">1 model · 1/3 connectors · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="361.6" textLength="61" clip-path="url(#terminal-line-14)">Type </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)"> for more 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="361.6" textLength="61" clip-path="url(#terminal-line-14)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="264" textLength="146.4" clip-path="url(#terminal-line-10)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="264" textLength="122" clip-path="url(#terminal-line-10)"> v0.0.0 · </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)"> · [Subscription] 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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="264" textLength="146.4" clip-path="url(#terminal-line-10)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="264" textLength="122" clip-path="url(#terminal-line-10)"> v0.0.0 · </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)"> · [Subscription] 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="288.4" textLength="597.8" clip-path="url(#terminal-line-11)">1 model · 3 connectors · 0 MCP servers · 0 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="288.4" textLength="622.2" clip-path="url(#terminal-line-11)">1 model · 1/3 connectors · 0 MCP servers · 0 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="312.8" textLength="61" clip-path="url(#terminal-line-12)">Type </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)"> for more 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)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="312.8" textLength="61" clip-path="url(#terminal-line-12)">Type </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)"> for more 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="410.4" textLength="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)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="410.4" textLength="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)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||||
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="597.8" clip-path="url(#terminal-line-17)">1 model · 3 connectors · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="622.2" clip-path="url(#terminal-line-17)">1 model · 1/3 connectors · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> for more information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> for more information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="1464" y="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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="410.4" textLength="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)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="410.4" textLength="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)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||||
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="597.8" clip-path="url(#terminal-line-17)">1 model · 3 connectors · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="622.2" clip-path="url(#terminal-line-17)">1 model · 1/3 connectors · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> for more information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> for more information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="1464" y="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 |
|
|
@ -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'm doing well, thank you! Let me read that file for 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'm doing well, thank you! Let me read that file for 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)">────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─</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)">────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─</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 |
|
|
@ -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 |
|
|
@ -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 this 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 here can modify AI behavior. Malicious configs 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 data, run destructive commands, or 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 your 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 in current 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)">• 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)">• .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 trust folders you fully 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)">  Trust folder</text><text class="terminal-r6" x="512.4" y="630" textLength="158.6" clip-path="url(#terminal-line-25)">› Don't 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)">← → navigate  Enter 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 will be saved 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 |
|
|
@ -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 this 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 here can modify AI behavior. Malicious configs 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 data, run destructive commands, or 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 your 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 in current 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)">• 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)">• 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)">• 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)">• 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)">• 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 trust folders you fully 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)">  Trust folder</text><text class="terminal-r7" x="512.4" y="654.4" textLength="158.6" clip-path="url(#terminal-line-26)">› Don't 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)">← → navigate  Enter 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 will be saved 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 |
|
|
@ -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 this 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 here can modify AI behavior. Malicious configs 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 data, run destructive commands, or 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 your 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 in current 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)">• 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)">• .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 trust folders you fully 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)">  Trust folder</text><text class="terminal-r7" x="500.2" y="459.2" textLength="158.6" clip-path="url(#terminal-line-18)">› Don't 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)">← → navigate  Enter 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 |
|
|
@ -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 this 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)">⚠ git repository /home/user/projects/my-project is 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 here can modify AI behavior. Malicious configs 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 data, run destructive commands, or 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 your 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 in current 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)">• 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 trust folders you fully 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)">  Trust folder</text><text class="terminal-r7" x="512.4" y="630" textLength="158.6" clip-path="url(#terminal-line-25)">› Don't 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)">← → navigate  Enter 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 will be saved 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 |
|
|
@ -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 folder or 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)">↳ git repository: /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 here can modify AI behavior. Malicious configs 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 data, run destructive commands, or 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 your 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 in current 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)">• 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 in repository 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)">• .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)">• 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 trust folders you fully 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)">  Trust full repo</text><text class="terminal-r1" x="427" y="654.4" textLength="170.8" clip-path="url(#terminal-line-26)">  Trust folder</text><text class="terminal-r8" x="634.4" y="654.4" textLength="158.6" clip-path="url(#terminal-line-26)">› Don't 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)">← → navigate  Enter 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 will be saved 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 |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="410.4" textLength="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)"> · [API] Experiment 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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="410.4" textLength="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)"> · 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> for more information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> for more information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="410.4" textLength="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)"> · [API] Experiment 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)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="410.4" textLength="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)"> · 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)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> for more information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="231.8" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="292.8" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> for more information</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -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:
|
||||||
|
|
|
||||||
56
tests/snapshots/test_ui_snapshot_config_issues.py
Normal 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,
|
||||||
|
)
|
||||||
71
tests/snapshots/test_ui_snapshot_diff_view_truncation.py
Normal 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,
|
||||||
|
)
|
||||||
116
tests/snapshots/test_ui_snapshot_image_attachments.py
Normal 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,
|
||||||
|
)
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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}",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
119
tests/snapshots/test_ui_snapshot_trust_folder_dialog.py
Normal 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),
|
||||||
|
)
|
||||||