v2.15.0 (#773)
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> Co-authored-by: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com> Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com> 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: Mistral Vibe <vibe@mistral.ai>
37
.github/workflows/build-and-upload.yml
vendored
|
|
@ -74,34 +74,17 @@ jobs:
|
||||||
|
|
||||||
- name: Install Python (Linux)
|
- name: Install Python (Linux)
|
||||||
if: ${{ matrix.os == 'linux' }}
|
if: ${{ matrix.os == 'linux' }}
|
||||||
run: |
|
shell: bash
|
||||||
uv python install 3.12
|
run: bash ./scripts/ci/setup-linux-pyinstaller-build.sh
|
||||||
# Install patchelf >= 0.18 (yum/EPEL8 ships 0.12 which lacks --clear-execstack)
|
|
||||||
PATCHELF_VERSION=0.18.0
|
|
||||||
curl -sL "https://github.com/NixOS/patchelf/releases/download/${PATCHELF_VERSION}/patchelf-${PATCHELF_VERSION}-$(uname -m).tar.gz" \
|
|
||||||
| tar xz -C /usr/local
|
|
||||||
# python-build-standalone ships libpython with GNU_STACK RWE (executable stack)
|
|
||||||
# which is rejected by hardened Linux kernels — clear it with patchelf
|
|
||||||
find "$(uv python dir)" -name 'libpython*.so*' -exec patchelf --clear-execstack {} \;
|
|
||||||
|
|
||||||
- name: Sync dependencies
|
- name: Build PyInstaller binaries
|
||||||
run: uv sync --no-dev --group build
|
shell: bash
|
||||||
|
run: bash ./scripts/ci/build-pyinstaller-binaries.sh vibe-acp.spec vibe.spec
|
||||||
- name: Build ACP with PyInstaller
|
|
||||||
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: |
|
shell: bash
|
||||||
find dist/vibe-acp-dir/_internal -name '*.so*' -type f -print0 \
|
run: bash ./scripts/ci/clear-linux-execstack.sh dist/vibe-acp-dir dist/vibe-dir
|
||||||
| xargs -0 -I{} patchelf --clear-execstack {}
|
|
||||||
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
|
||||||
|
|
@ -193,6 +176,12 @@ jobs:
|
||||||
- name: Run ACP smoke tests
|
- 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
|
||||||
|
|
||||||
|
- name: Run CLI smoke tests
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PYTHON_BIN: ${{ matrix.container && 'python3.11' || 'python' }}
|
||||||
|
run: bash ./scripts/ci/smoke-pyinstaller-cli.sh dist/vibe-dir
|
||||||
|
|
||||||
attach-to-release:
|
attach-to-release:
|
||||||
needs: [build-and-upload, smoke-test]
|
needs: [build-and-upload, smoke-test]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ Always go through `uv` — never invoke bare `python` or `pip`.
|
||||||
- Never use `git commit --amend`, `git push --force`, or `git push --force-with-lease`.
|
- Never use `git commit --amend`, `git push --force`, or `git push --force-with-lease`.
|
||||||
- Always create new commits and push with a plain `git push`.
|
- Always create new commits and push with a plain `git push`.
|
||||||
- If a push is rejected due to upstream changes, rebase onto the updated remote branch — never merge and never force-push.
|
- If a push is rejected due to upstream changes, rebase onto the updated remote branch — never merge and never force-push.
|
||||||
|
- Run git commands through `uv run` (e.g. `uv run git commit`, `uv run git push`) so pre-commit hooks resolve the project's venv — bare `git commit` fails pre-commit with `reportMissingImports` because pyright can't find third-party packages.
|
||||||
|
|
||||||
## Editor tip
|
## Editor tip
|
||||||
|
|
||||||
|
|
|
||||||
30
CHANGELOG.md
|
|
@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
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.15.0] - 2026-06-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **[Experimental]** `before_tool` and `after_tool` hooks: shell scripts declared in `hooks.toml` that fire around every tool call; hooks can deny the call, rewrite tool inputs, or append context to the output — enable with `enable_experimental_hooks = true`
|
||||||
|
- Message queue: messages typed while the agent or a `!bash` command is running are queued and shown above the input; Esc pauses the queue, Ctrl+C drops the last queued message (LIFO), Enter flushes the queue when paused
|
||||||
|
- Tool result output is now collapsed by default to keep responses scannable
|
||||||
|
- Common read-only shell commands (`ls`, `cat`, `pwd`, etc.) are allowed without approval by default
|
||||||
|
- Session deletion available directly from the resume picker
|
||||||
|
- `[mcp_servers.auth]` block in config for per-server MCP authentication
|
||||||
|
- Collapsed web tool output now shows the URL and search query at a glance
|
||||||
|
- `max_turns` support exposed over ACP via `set_config_option`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Compaction now re-injects prior user messages so the agent retains the original task goals across context resets
|
||||||
|
- **[Breaking — experimental hooks]** `post_agent_turn` retries no longer use exit code `2`; hooks must now exit `0` and return `{"decision": "deny", "reason": "..."}` on stdout to trigger a retry — exit code `2` is treated as a failure
|
||||||
|
- Model refusal stop reason is now surfaced to the user instead of stopping silently
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- CLI UI no longer goes blank during app switches
|
||||||
|
- User messages are preserved correctly across repeated compactions
|
||||||
|
- Non-UTF-8 input is handled gracefully across all CLI surfaces
|
||||||
|
- `--resume` now shows history when launched from a dangerous directory
|
||||||
|
- Startup no longer crashes in unowned folders or when a git ancestor is unreadable
|
||||||
|
- Plain-string answers from `web_search` are handled without crashing
|
||||||
|
- NaN SGR mouse reports no longer leak into the chat input
|
||||||
|
|
||||||
|
|
||||||
## [2.14.1] - 2026-06-08
|
## [2.14.1] - 2026-06-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
92
README.md
|
|
@ -136,9 +136,9 @@ Valid values are `default`, `plan`, `accept-edits`, `auto-approve`,
|
||||||
custom agent file in `~/.vibe/agents/` or the project's `.vibe/agents/`
|
custom agent file in `~/.vibe/agents/` or the project's `.vibe/agents/`
|
||||||
directory. Subagents such as `explore` are not accepted.
|
directory. Subagents such as `explore` are not accepted.
|
||||||
|
|
||||||
> Note: `default_agent` only applies to interactive sessions. In
|
> Note: `default_agent` applies in both interactive and programmatic
|
||||||
> programmatic mode (`-p` / `--prompt`), Vibe falls back to `auto-approve`
|
> (`-p` / `--prompt`) sessions. Pass `--auto-approve` when a run should
|
||||||
> when `--agent` is not provided, so `default_agent` is ignored.
|
> approve all tool calls without prompting.
|
||||||
|
|
||||||
### Subagents and Task Delegation
|
### Subagents and Task Delegation
|
||||||
|
|
||||||
|
|
@ -237,6 +237,7 @@ Simply run `vibe` to enter the interactive chat loop.
|
||||||
- **Todo View Toggle**: Press `Ctrl+T` to toggle the todo list view.
|
- **Todo View Toggle**: Press `Ctrl+T` to toggle the todo list view.
|
||||||
- **Debug Console**: Press `Ctrl+\` to toggle the debug console.
|
- **Debug Console**: Press `Ctrl+\` to toggle the debug console.
|
||||||
- **Agent Selection**: Press `Shift+Tab` to cycle through agents (default, plan, ...).
|
- **Agent Selection**: Press `Shift+Tab` to cycle through agents (default, plan, ...).
|
||||||
|
- **Exit**: Type `/exit`, `exit`, `quit`, `:q`, or `:quit` in the input box, or press `Ctrl+C` / `Ctrl+D` twice within ~1 second.
|
||||||
|
|
||||||
You can start Vibe with a prompt using the following command:
|
You can start Vibe with a prompt using the following command:
|
||||||
|
|
||||||
|
|
@ -260,7 +261,13 @@ You can run Vibe non-interactively by piping input or using the `--prompt` flag.
|
||||||
vibe --prompt "Refactor the main function in cli/main.py to be more modular."
|
vibe --prompt "Refactor the main function in cli/main.py to be more modular."
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, it uses `auto-approve` mode.
|
By default, it uses your configured `default_agent` (`default` unless changed).
|
||||||
|
To approve all tool calls without prompting, pass `--auto-approve` (also
|
||||||
|
available for interactive sessions):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vibe --prompt "Refactor the main function in cli/main.py to be more modular." --auto-approve
|
||||||
|
```
|
||||||
|
|
||||||
#### Programmatic Mode Options
|
#### Programmatic Mode Options
|
||||||
|
|
||||||
|
|
@ -269,6 +276,8 @@ When using `--prompt`, you can specify additional options:
|
||||||
- **`--max-turns N`**: Limit the maximum number of assistant turns. The session will stop after N turns.
|
- **`--max-turns N`**: Limit the maximum number of assistant turns. The session will stop after N turns.
|
||||||
- **`--max-price DOLLARS`**: Set a maximum cost limit in dollars. The session will be interrupted if the cost exceeds this limit.
|
- **`--max-price DOLLARS`**: Set a maximum cost limit in dollars. The session will be interrupted if the cost exceeds this limit.
|
||||||
- **`--max-tokens N`**: Set a maximum cumulative LLM token budget for the session, counting both prompt and completion tokens. The session will be interrupted if usage exceeds this limit.
|
- **`--max-tokens N`**: Set a maximum cumulative LLM token budget for the session, counting both prompt and completion tokens. The session will be interrupted if usage exceeds this limit.
|
||||||
|
- **`--agent NAME`**: Select the agent profile for this run.
|
||||||
|
- **`--auto-approve`**: Shortcut for `--agent auto-approve`. Approves all tool calls without prompting, including in interactive sessions.
|
||||||
- **`--enabled-tools TOOL`**: Enable specific tools. In programmatic mode, this disables all other tools. Can be specified multiple times. Supports exact names, glob patterns (e.g., `bash*`), or regex with `re:` prefix (e.g., `re:^serena_.*$`).
|
- **`--enabled-tools TOOL`**: Enable specific tools. In programmatic mode, this disables all other tools. Can be specified multiple times. Supports exact names, glob patterns (e.g., `bash*`), or regex with `re:` prefix (e.g., `re:^serena_.*$`).
|
||||||
- **`--output FORMAT`**: Set the output format. Options:
|
- **`--output FORMAT`**: Set the output format. Options:
|
||||||
- `text` (default): Human-readable text output
|
- `text` (default): Human-readable text output
|
||||||
|
|
@ -605,6 +614,76 @@ startup_timeout_sec = 15
|
||||||
tool_timeout_sec = 120
|
tool_timeout_sec = 120
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Hooks (Experimental)
|
||||||
|
|
||||||
|
Hooks wire arbitrary shell commands into Vibe's lifecycle to gate, audit, or rewrite agent behavior. **Experimental**, gated behind:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# config.toml
|
||||||
|
enable_experimental_hooks = true # or env VIBE_ENABLE_EXPERIMENTAL_HOOKS=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Declared in `<project>/.vibe/hooks.toml` (project, loaded first; trusted only) and `~/.vibe/hooks.toml` (user-global, loaded second; duplicates by `name` lose to the project entry):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[hooks]]
|
||||||
|
name = "deny-rm-rf"
|
||||||
|
type = "before_tool"
|
||||||
|
match = "bash" # tool-name matcher (fnmatch glob + `re:` regex escape, case-insensitive)
|
||||||
|
command = "uv run python /path/to/guard-bash"
|
||||||
|
timeout = 60.0 # seconds; default 60 for all hooks
|
||||||
|
strict = false # tool hooks only: turn failures into denials (before) / text-clears (after)
|
||||||
|
description = "Reject dangerous shell commands."
|
||||||
|
```
|
||||||
|
|
||||||
|
Subagents inherit the parent's hook config so policies apply transitively.
|
||||||
|
|
||||||
|
#### Common ground
|
||||||
|
|
||||||
|
Every hook is spawned with a JSON invocation on **stdin** (UTF-8) containing the session context: `session_id`, `parent_session_id`, `transcript_path`, `cwd`, plus `hook_event_name` discriminating the hook type. Tool hooks add tool-specific fields (below).
|
||||||
|
|
||||||
|
Every hook signals back via its **exit code** and **stdout**. The contract on stdout is strict: either empty (do nothing), or a JSON object matching the schema below. Use **stderr** for diagnostics / debug logs.
|
||||||
|
|
||||||
|
- **Exit `0`, empty stdout** — passthrough.
|
||||||
|
- **Exit `0`, valid JSON object on stdout** — structured response. Universal top-level fields:
|
||||||
|
- `system_message` (string, optional) — shown to the user in the UI.
|
||||||
|
- `decision` (`"allow"` | `"deny"`, optional, default `"allow"`) — the effect of `"deny"` depends on the hook type.
|
||||||
|
- `reason` (string, optional) — accompanies `decision: "deny"`.
|
||||||
|
- Event-specific payload under `hook_specific_output`.
|
||||||
|
- **Exit `0`, non-empty but non-conforming stdout** (free-form text, broken JSON, JSON scalar/array, schema mismatch) — treated as a hook failure with the parse error as the message. Warning by default; escalated to deny / clear under `strict = true` on a tool hook.
|
||||||
|
- **Any non-zero exit / timeout / spawn failure** — same failure path. Diagnostic taken from stderr (falling back to stdout, then the exit code).
|
||||||
|
|
||||||
|
Unknown JSON fields are tolerated at every level (forward-compatible). Fields that aren't meaningful for the current hook type are silently ignored.
|
||||||
|
|
||||||
|
#### `post_agent_turn`
|
||||||
|
|
||||||
|
Fires after every assistant turn that ends without pending tool calls.
|
||||||
|
|
||||||
|
- **Receives** (in addition to the session context): no extra fields.
|
||||||
|
- **Can return**:
|
||||||
|
- `decision: "deny"` + `reason` — `reason` is injected as a new user message asking for a retry. Capped at **3 retries per hook per user turn**; further denies become terminal warnings.
|
||||||
|
- `system_message` — UI-only.
|
||||||
|
|
||||||
|
#### `before_tool`
|
||||||
|
|
||||||
|
Fires per tool call, **before** the user permission prompt. First deny short-circuits remaining `before_tool` hooks for that call.
|
||||||
|
|
||||||
|
- **Receives** (in addition to the session context): `tool_name`, `tool_call_id`, `tool_input` (the model's raw arguments).
|
||||||
|
- **Can return**:
|
||||||
|
- `decision: "deny"` + `reason` — denies the tool call; `reason` becomes the tool error the LLM sees.
|
||||||
|
- `hook_specific_output.tool_input` (object) — **full replacement** of the model's arguments. Re-validated against the tool's schema (validation failure → synthesized denial). Rewrites compose left-to-right across hooks. The rewritten arguments are also what the permission prompt displays, what the tool runs with, and what subsequent LLM turns see on the assistant message.
|
||||||
|
- `system_message` — UI-only.
|
||||||
|
|
||||||
|
#### `after_tool`
|
||||||
|
|
||||||
|
Fires per tool call **if and only if the tool body actually ran**. `tool_status` is `success`, `failure`, or `cancelled` (cancellation during the tool body — cancellation is shielded so audit hooks still run). Does not fire when the tool never executed: `before_tool` denial, user denial at the approval prompt, permission `NEVER`, or cancellation before the body started.
|
||||||
|
|
||||||
|
- **Receives** (in addition to the session context): `tool_name`, `tool_call_id`, `tool_input` (post-rewrite), `tool_status`, `tool_output` (structured result dict; null on failure), `tool_output_text` (the running text the LLM will see, mutable by prior hooks), `tool_error`, `duration_ms`.
|
||||||
|
- **Can return**:
|
||||||
|
- `decision: "deny"` + `reason` — replaces `tool_output_text` with `reason`. Pipeline continues; subsequent hooks see the replacement.
|
||||||
|
- `hook_specific_output.additional_context` (string) — **appended** (with a `\n` separator) to `tool_output_text`. Composes with a same-hook deny: deny replaces first, then `additional_context` is appended to the replacement.
|
||||||
|
- `system_message` — UI-only.
|
||||||
|
|
||||||
### Session Management
|
### Session Management
|
||||||
|
|
||||||
#### Session Continuation and Resumption
|
#### Session Continuation and Resumption
|
||||||
|
|
@ -612,12 +691,17 @@ tool_timeout_sec = 120
|
||||||
Vibe supports continuing from previous sessions:
|
Vibe supports continuing from previous sessions:
|
||||||
|
|
||||||
- **`--continue`** or **`-c`**: Continue from the most recent saved session
|
- **`--continue`** or **`-c`**: Continue from the most recent saved session
|
||||||
|
- **`--resume`**: Open an interactive session picker
|
||||||
- **`--resume SESSION_ID`**: Resume a specific session by ID (supports partial matching)
|
- **`--resume SESSION_ID`**: Resume a specific session by ID (supports partial matching)
|
||||||
|
- **`/resume`** or **`/continue`**: Open the session picker from inside Vibe; press `D` twice to delete a local saved session. The active session cannot be deleted from this picker.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Continue from last session
|
# Continue from last session
|
||||||
vibe --continue
|
vibe --continue
|
||||||
|
|
||||||
|
# Open session picker
|
||||||
|
vibe --resume
|
||||||
|
|
||||||
# Resume specific session
|
# Resume specific session
|
||||||
vibe --resume abc123
|
vibe --resume abc123
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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.14.1"
|
version = "2.15.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.14.1/vibe-acp-darwin-aarch64-2.14.1.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.15.0/vibe-acp-darwin-aarch64-2.15.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.14.1/vibe-acp-darwin-x86_64-2.14.1.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.15.0/vibe-acp-darwin-x86_64-2.15.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.14.1/vibe-acp-linux-aarch64-2.14.1.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.15.0/vibe-acp-linux-aarch64-2.15.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.14.1/vibe-acp-linux-x86_64-2.14.1.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.15.0/vibe-acp-linux-x86_64-2.15.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.14.1/vibe-acp-windows-x86_64-2.14.1.zip"
|
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.15.0/vibe-acp-windows-x86_64-2.15.0.zip"
|
||||||
cmd = "./vibe-acp.exe"
|
cmd = "./vibe-acp.exe"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "mistral-vibe"
|
name = "mistral-vibe"
|
||||||
version = "2.14.1"
|
version = "2.15.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"
|
||||||
|
|
@ -155,6 +155,7 @@ required-version = ">=0.8.0"
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
"cryptography>=44.0.0",
|
||||||
"debugpy>=1.8.19",
|
"debugpy>=1.8.19",
|
||||||
"pre-commit>=4.2.0",
|
"pre-commit>=4.2.0",
|
||||||
"pyinstrument>=5.1.2",
|
"pyinstrument>=5.1.2",
|
||||||
|
|
|
||||||
13
scripts/ci/build-pyinstaller-binaries.sh
Executable file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "$#" -eq 0 ]; then
|
||||||
|
echo "Usage: $0 <pyinstaller-spec> [<pyinstaller-spec> ...]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
uv sync --no-dev --group build
|
||||||
|
|
||||||
|
for spec in "$@"; do
|
||||||
|
uv run --no-dev --group build pyinstaller "${spec}"
|
||||||
|
done
|
||||||
22
scripts/ci/clear-linux-execstack.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ "$#" -eq 0 ]; then
|
||||||
|
echo "Usage: $0 <pyinstaller-bundle-dir> [<pyinstaller-bundle-dir> ...]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
for bundle_dir in "$@"; do
|
||||||
|
internal_dir="${bundle_dir}/_internal"
|
||||||
|
binary_name="$(basename "${bundle_dir}")"
|
||||||
|
binary_name="${binary_name%-dir}"
|
||||||
|
binary_path="${bundle_dir}/${binary_name}"
|
||||||
|
|
||||||
|
find "${internal_dir}" -name '*.so*' -type f -print0 | while IFS= read -r -d '' library; do
|
||||||
|
patchelf --clear-execstack "${library}"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -f "${binary_path}" ]; then
|
||||||
|
patchelf --clear-execstack "${binary_path}" || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
14
scripts/ci/setup-linux-pyinstaller-build.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Build in manylinux for glibc compatibility, then clear executable-stack
|
||||||
|
# flags rejected by hardened Linux kernels.
|
||||||
|
python_version="${PYTHON_VERSION:-3.12}"
|
||||||
|
patchelf_version="${PATCHELF_VERSION:-0.18.0}"
|
||||||
|
|
||||||
|
uv python install "${python_version}"
|
||||||
|
|
||||||
|
curl -sL "https://github.com/NixOS/patchelf/releases/download/${patchelf_version}/patchelf-${patchelf_version}-$(uname -m).tar.gz" \
|
||||||
|
| tar xz -C /usr/local
|
||||||
|
|
||||||
|
find "$(uv python dir)" -name 'libpython*.so*' -type f -exec patchelf --clear-execstack {} \;
|
||||||
18
scripts/ci/smoke-pyinstaller-cli.sh
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
binary_dir="${1:-dist/vibe-dir}"
|
||||||
|
|
||||||
|
if [ -n "${PYTHON_BIN:-}" ]; then
|
||||||
|
read -r -a python_cmd <<< "${PYTHON_BIN}"
|
||||||
|
elif command -v uv >/dev/null 2>&1; then
|
||||||
|
python_cmd=(uv run python)
|
||||||
|
elif [ -x ".venv/bin/python" ]; then
|
||||||
|
python_cmd=(.venv/bin/python)
|
||||||
|
elif [ -x ".venv/Scripts/python.exe" ]; then
|
||||||
|
python_cmd=(.venv/Scripts/python.exe)
|
||||||
|
else
|
||||||
|
python_cmd=(python)
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${python_cmd[@]}" tests/cli/smoke_binary.py "${binary_dir}"
|
||||||
|
|
@ -127,7 +127,7 @@ class TestAcpHooksLoading:
|
||||||
assert result.hooks[0].name == "lint"
|
assert result.hooks[0].name == "lint"
|
||||||
assert result.hooks[0].type == HookType.POST_AGENT_TURN
|
assert result.hooks[0].type == HookType.POST_AGENT_TURN
|
||||||
assert result.hooks[0].command == "eslint ."
|
assert result.hooks[0].command == "eslint ."
|
||||||
assert result.hooks[0].timeout == 30.0
|
assert result.hooks[0].timeout == 60.0
|
||||||
assert result.issues == []
|
assert result.issues == []
|
||||||
|
|
||||||
async def test_new_session_hooks_enabled_invalid_toml(
|
async def test_new_session_hooks_enabled_invalid_toml(
|
||||||
|
|
@ -167,5 +167,5 @@ class TestAcpHooksLoading:
|
||||||
assert result.hooks[0].name == "lint"
|
assert result.hooks[0].name == "lint"
|
||||||
assert result.hooks[0].type == HookType.POST_AGENT_TURN
|
assert result.hooks[0].type == HookType.POST_AGENT_TURN
|
||||||
assert result.hooks[0].command == "eslint ."
|
assert result.hooks[0].command == "eslint ."
|
||||||
assert result.hooks[0].timeout == 30.0
|
assert result.hooks[0].timeout == 60.0
|
||||||
assert result.issues == []
|
assert result.issues == []
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ class TestACPInitialize:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert response.agent_info == Implementation(
|
assert response.agent_info == Implementation(
|
||||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.14.1"
|
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.15.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.auth_methods is not None
|
assert response.auth_methods is not None
|
||||||
|
|
@ -101,7 +101,7 @@ class TestACPInitialize:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert response.agent_info == Implementation(
|
assert response.agent_info == Implementation(
|
||||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.14.1"
|
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.15.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.auth_methods is not None
|
assert response.auth_methods is not None
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ class TestWebSearchFieldMeta:
|
||||||
|
|
||||||
def test_result_locations_are_source_urls_with_titles(self) -> None:
|
def test_result_locations_are_source_urls_with_titles(self) -> None:
|
||||||
result = WebSearchResult(
|
result = WebSearchResult(
|
||||||
|
query="python",
|
||||||
answer="found it",
|
answer="found it",
|
||||||
sources=[
|
sources=[
|
||||||
WebSearchSource(title="Docs", url="https://docs.python.org"),
|
WebSearchSource(title="Docs", url="https://docs.python.org"),
|
||||||
|
|
|
||||||
|
|
@ -498,6 +498,83 @@ class TestAdapterParseResponse:
|
||||||
assert chunk.message.content == "Hello!"
|
assert chunk.message.content == "Hello!"
|
||||||
assert chunk.usage.prompt_tokens == 10
|
assert chunk.usage.prompt_tokens == 10
|
||||||
|
|
||||||
|
def test_non_streaming_captures_refusal_stop_reason(self, adapter, provider):
|
||||||
|
data = {
|
||||||
|
"content": [],
|
||||||
|
"usage": {"input_tokens": 10, "output_tokens": 2},
|
||||||
|
"stop_reason": "refusal",
|
||||||
|
}
|
||||||
|
chunk = adapter.parse_response(data, provider)
|
||||||
|
assert chunk.stop is not None
|
||||||
|
assert chunk.stop.reason == "refusal"
|
||||||
|
|
||||||
|
def test_streaming_message_delta_captures_refusal_stop_reason(
|
||||||
|
self, adapter, provider
|
||||||
|
):
|
||||||
|
data = {
|
||||||
|
"type": "message_delta",
|
||||||
|
"delta": {"stop_reason": "refusal", "stop_sequence": None},
|
||||||
|
"usage": {"output_tokens": 7},
|
||||||
|
}
|
||||||
|
chunk = adapter.parse_response(data, provider)
|
||||||
|
assert chunk.stop is not None
|
||||||
|
assert chunk.stop.reason == "refusal"
|
||||||
|
assert chunk.usage.completion_tokens == 7
|
||||||
|
|
||||||
|
def test_streaming_message_delta_end_turn_stop_reason(self, adapter, provider):
|
||||||
|
data = {
|
||||||
|
"type": "message_delta",
|
||||||
|
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
|
||||||
|
"usage": {"output_tokens": 3},
|
||||||
|
}
|
||||||
|
chunk = adapter.parse_response(data, provider)
|
||||||
|
assert chunk.stop is not None
|
||||||
|
assert chunk.stop.reason == "end_turn"
|
||||||
|
|
||||||
|
def test_non_streaming_captures_refusal_stop_details(self, adapter, provider):
|
||||||
|
data = {
|
||||||
|
"content": [],
|
||||||
|
"usage": {"input_tokens": 10, "output_tokens": 2},
|
||||||
|
"stop_reason": "refusal",
|
||||||
|
"stop_details": {
|
||||||
|
"type": "refusal",
|
||||||
|
"category": "cyber",
|
||||||
|
"explanation": "This request was declined.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
chunk = adapter.parse_response(data, provider)
|
||||||
|
assert chunk.stop is not None
|
||||||
|
assert chunk.stop.category == "cyber"
|
||||||
|
assert chunk.stop.explanation == "This request was declined."
|
||||||
|
|
||||||
|
def test_non_streaming_without_stop_details_is_none(self, adapter, provider):
|
||||||
|
data = {
|
||||||
|
"content": [],
|
||||||
|
"usage": {"input_tokens": 10, "output_tokens": 2},
|
||||||
|
"stop_reason": "end_turn",
|
||||||
|
}
|
||||||
|
chunk = adapter.parse_response(data, provider)
|
||||||
|
assert chunk.stop is not None
|
||||||
|
assert chunk.stop.category is None
|
||||||
|
assert chunk.stop.explanation is None
|
||||||
|
|
||||||
|
def test_streaming_message_delta_captures_refusal_stop_details(
|
||||||
|
self, adapter, provider
|
||||||
|
):
|
||||||
|
data = {
|
||||||
|
"type": "message_delta",
|
||||||
|
"delta": {
|
||||||
|
"stop_reason": "refusal",
|
||||||
|
"stop_sequence": None,
|
||||||
|
"stop_details": {"type": "refusal", "category": "bio"},
|
||||||
|
},
|
||||||
|
"usage": {"output_tokens": 7},
|
||||||
|
}
|
||||||
|
chunk = adapter.parse_response(data, provider)
|
||||||
|
assert chunk.stop is not None
|
||||||
|
assert chunk.stop.category == "bio"
|
||||||
|
assert chunk.stop.explanation is None
|
||||||
|
|
||||||
def test_streaming_text_delta(self, adapter, provider):
|
def test_streaming_text_delta(self, adapter, provider):
|
||||||
data = {
|
data = {
|
||||||
"type": "content_block_delta",
|
"type": "content_block_delta",
|
||||||
|
|
|
||||||
|
|
@ -228,3 +228,67 @@ class TestBannerConnectorsCount:
|
||||||
|
|
||||||
assert banner._initial_state.connectors_connected == 3
|
assert banner._initial_state.connectors_connected == 3
|
||||||
assert banner._initial_state.connectors_total == 5
|
assert banner._initial_state.connectors_total == 5
|
||||||
|
|
||||||
|
|
||||||
|
class TestBannerHooksCount:
|
||||||
|
def test_hooks_count_passed_through(self) -> None:
|
||||||
|
skill_manager = Mock(spec=SkillManager)
|
||||||
|
skill_manager.custom_skills_count = 0
|
||||||
|
|
||||||
|
banner = Banner(
|
||||||
|
config=_make_mock_config(), skill_manager=skill_manager, hooks_count=4
|
||||||
|
)
|
||||||
|
|
||||||
|
assert banner._initial_state.hooks_count == 4
|
||||||
|
|
||||||
|
def test_hooks_count_defaults_to_zero(self) -> None:
|
||||||
|
skill_manager = Mock(spec=SkillManager)
|
||||||
|
skill_manager.custom_skills_count = 0
|
||||||
|
|
||||||
|
banner = Banner(config=_make_mock_config(), skill_manager=skill_manager)
|
||||||
|
|
||||||
|
assert banner._initial_state.hooks_count == 0
|
||||||
|
|
||||||
|
def test_format_meta_counts_shows_hooks_when_present(self) -> None:
|
||||||
|
skill_manager = Mock(spec=SkillManager)
|
||||||
|
skill_manager.custom_skills_count = 0
|
||||||
|
|
||||||
|
banner = Banner(config=_make_mock_config(), skill_manager=skill_manager)
|
||||||
|
banner.state = BannerState(models_count=1, skills_count=0, hooks_count=3)
|
||||||
|
|
||||||
|
result = banner._format_meta_counts()
|
||||||
|
assert "3 hooks" in result
|
||||||
|
|
||||||
|
def test_format_meta_counts_singular_hook(self) -> None:
|
||||||
|
skill_manager = Mock(spec=SkillManager)
|
||||||
|
skill_manager.custom_skills_count = 0
|
||||||
|
|
||||||
|
banner = Banner(config=_make_mock_config(), skill_manager=skill_manager)
|
||||||
|
banner.state = BannerState(models_count=1, skills_count=0, hooks_count=1)
|
||||||
|
|
||||||
|
result = banner._format_meta_counts()
|
||||||
|
assert "1 hook" in result
|
||||||
|
assert "1 hooks" not in result
|
||||||
|
|
||||||
|
def test_format_meta_counts_hides_hooks_when_zero(self) -> None:
|
||||||
|
skill_manager = Mock(spec=SkillManager)
|
||||||
|
skill_manager.custom_skills_count = 0
|
||||||
|
|
||||||
|
banner = Banner(config=_make_mock_config(), skill_manager=skill_manager)
|
||||||
|
banner.state = BannerState(models_count=1, skills_count=0, hooks_count=0)
|
||||||
|
|
||||||
|
result = banner._format_meta_counts()
|
||||||
|
assert "hook" not in result
|
||||||
|
|
||||||
|
def test_set_state_updates_hooks_count(self) -> None:
|
||||||
|
skill_manager = Mock(spec=SkillManager)
|
||||||
|
skill_manager.custom_skills_count = 0
|
||||||
|
|
||||||
|
banner = Banner(
|
||||||
|
config=_make_mock_config(), skill_manager=skill_manager, hooks_count=0
|
||||||
|
)
|
||||||
|
banner.set_state(
|
||||||
|
config=_make_mock_config(), skill_manager=skill_manager, hooks_count=7
|
||||||
|
)
|
||||||
|
|
||||||
|
assert banner.state.hooks_count == 7
|
||||||
|
|
|
||||||
|
|
@ -268,8 +268,16 @@ def test_installed_bundle_launches(binary_dir: Path, binary_name: str) -> None:
|
||||||
env = _isolated_env(vibe_home)
|
env = _isolated_env(vibe_home)
|
||||||
env["PATH"] = f"{install_dir}{os.pathsep}{env.get('PATH', '')}"
|
env["PATH"] = f"{install_dir}{os.pathsep}{env.get('PATH', '')}"
|
||||||
|
|
||||||
|
command = binary_name
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
# subprocess on Windows does not resolve executables through a PATH
|
||||||
|
# value supplied only via env, so resolve it against that PATH first.
|
||||||
|
if (resolved := shutil.which(binary_name, path=env["PATH"])) is None:
|
||||||
|
_fail(f"installed binary not found on PATH: {binary_name}")
|
||||||
|
command = resolved
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[binary_name, "--version"],
|
[command, "--version"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
cwd=workdir,
|
cwd=workdir,
|
||||||
env=env,
|
env=env,
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ class TestCommandRegistry:
|
||||||
assert result is not None
|
assert result is not None
|
||||||
_, cmd, _ = result
|
_, cmd, _ = result
|
||||||
assert cmd.handler == "_show_session_picker"
|
assert cmd.handler == "_show_session_picker"
|
||||||
|
assert cmd.description == "Browse, resume, or delete saved sessions"
|
||||||
|
|
||||||
def test_rename_command_registration(self) -> None:
|
def test_rename_command_registration(self) -> None:
|
||||||
registry = CommandRegistry()
|
registry = CommandRegistry()
|
||||||
|
|
@ -143,3 +144,47 @@ class TestCommandRegistry:
|
||||||
assert cmd_name == "loop"
|
assert cmd_name == "loop"
|
||||||
assert cmd.handler == "_loop_command"
|
assert cmd.handler == "_loop_command"
|
||||||
assert cmd_args == "30s ping"
|
assert cmd_args == "30s ping"
|
||||||
|
|
||||||
|
def test_exit_command_accepts_bare_synonyms(self) -> None:
|
||||||
|
registry = CommandRegistry()
|
||||||
|
for alias in ["/exit", "exit", "quit", ":q", ":quit"]:
|
||||||
|
assert registry.get_command_name(alias) == "exit", alias
|
||||||
|
result = registry.parse_command(alias)
|
||||||
|
assert result is not None, alias
|
||||||
|
cmd_name, cmd, _ = result
|
||||||
|
assert cmd_name == "exit"
|
||||||
|
assert cmd.handler == "_exit_app"
|
||||||
|
assert cmd.exits is True
|
||||||
|
|
||||||
|
def test_bare_exit_synonym_with_trailing_text_is_not_a_command(self) -> None:
|
||||||
|
registry = CommandRegistry()
|
||||||
|
assert registry.parse_command("exit the function early") is None
|
||||||
|
assert registry.parse_command("quit your job") is None
|
||||||
|
|
||||||
|
def test_bare_exit_synonym_in_multiline_message_is_not_a_command(self) -> None:
|
||||||
|
registry = CommandRegistry()
|
||||||
|
assert registry.parse_command("exit\nplease refactor this module") is None
|
||||||
|
|
||||||
|
def test_slash_exit_still_parses_with_trailing_text(self) -> None:
|
||||||
|
registry = CommandRegistry()
|
||||||
|
result = registry.parse_command("/exit now")
|
||||||
|
assert result is not None
|
||||||
|
cmd_name, _, cmd_args = result
|
||||||
|
assert cmd_name == "exit"
|
||||||
|
assert cmd_args == "now"
|
||||||
|
|
||||||
|
def test_exit_command_synonyms_are_case_insensitive(self) -> None:
|
||||||
|
registry = CommandRegistry()
|
||||||
|
for alias in ["EXIT", "Quit", " exit ", ":Q"]:
|
||||||
|
assert registry.get_command_name(alias) == "exit", alias
|
||||||
|
|
||||||
|
def test_exit_synonyms_excluded_when_command_disabled(self) -> None:
|
||||||
|
registry = CommandRegistry(excluded_commands=["exit"])
|
||||||
|
for alias in ["/exit", "exit", "quit", ":q", ":quit"]:
|
||||||
|
assert registry.get_command_name(alias) is None, alias
|
||||||
|
|
||||||
|
def test_help_text_lists_exit_synonyms(self) -> None:
|
||||||
|
registry = CommandRegistry()
|
||||||
|
help_text = registry.get_help_text()
|
||||||
|
for alias in ["`/exit`", "`exit`", "`quit`", "`:q`", "`:quit`"]:
|
||||||
|
assert alias in help_text, alias
|
||||||
|
|
|
||||||
31
tests/cli/test_help.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from vibe.cli.entrypoint import parse_arguments
|
||||||
|
|
||||||
|
|
||||||
|
def test_help_shows_auto_approve_flag(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr("sys.argv", ["vibe", "--help"])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as exc_info:
|
||||||
|
parse_arguments()
|
||||||
|
|
||||||
|
assert exc_info.value.code == 0
|
||||||
|
output = capsys.readouterr().out
|
||||||
|
assert "--auto-approve" in output
|
||||||
|
assert "Shortcut for --agent auto-approve" in output
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_approve_conflicts_with_agent(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr("sys.argv", ["vibe", "--agent", "plan", "--auto-approve"])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as exc_info:
|
||||||
|
parse_arguments()
|
||||||
|
|
||||||
|
assert exc_info.value.code == 2
|
||||||
|
assert "not allowed with argument --agent" in capsys.readouterr().err
|
||||||
|
|
@ -7,8 +7,10 @@ from vibe.core.agents.models import BuiltinAgentName
|
||||||
from vibe.core.config import VibeConfig
|
from vibe.core.config import VibeConfig
|
||||||
|
|
||||||
|
|
||||||
def _make_args(*, agent: str | None, prompt: str | None) -> argparse.Namespace:
|
def _make_args(
|
||||||
return argparse.Namespace(agent=agent, prompt=prompt)
|
*, agent: str | None, prompt: str | None, auto_approve: bool = False
|
||||||
|
) -> argparse.Namespace:
|
||||||
|
return argparse.Namespace(agent=agent, prompt=prompt, auto_approve=auto_approve)
|
||||||
|
|
||||||
|
|
||||||
def test_uses_args_agent_when_provided() -> None:
|
def test_uses_args_agent_when_provided() -> None:
|
||||||
|
|
@ -51,3 +53,10 @@ def test_programmatic_mode_keeps_explicit_agent_arg() -> None:
|
||||||
args = _make_args(agent="accept-edits", prompt="hello")
|
args = _make_args(agent="accept-edits", prompt="hello")
|
||||||
|
|
||||||
assert get_initial_agent_name(args, config) == "accept-edits"
|
assert get_initial_agent_name(args, config) == "accept-edits"
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_approve_flag_selects_auto_approve_agent() -> None:
|
||||||
|
config = VibeConfig.model_construct(default_agent=BuiltinAgentName.PLAN)
|
||||||
|
args = _make_args(agent=None, prompt="hello", auto_approve=True)
|
||||||
|
|
||||||
|
assert get_initial_agent_name(args, config) == BuiltinAgentName.AUTO_APPROVE
|
||||||
|
|
|
||||||
230
tests/cli/test_session_delete.py
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
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.widgets.messages import ErrorMessage, UserCommandMessage
|
||||||
|
from vibe.cli.textual_ui.widgets.session_picker import SessionPickerApp
|
||||||
|
from vibe.core.config import SessionLoggingConfig
|
||||||
|
from vibe.core.session.resume_sessions import short_session_id
|
||||||
|
|
||||||
|
|
||||||
|
def _enabled_session_config(save_dir: Path) -> SessionLoggingConfig:
|
||||||
|
return SessionLoggingConfig(enabled=True, save_dir=str(save_dir))
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSessionPicker:
|
||||||
|
def __init__(self, *, has_sessions_after_remove: bool = True) -> None:
|
||||||
|
self.has_sessions = True
|
||||||
|
self.removed_option_ids: list[str] = []
|
||||||
|
self.cleared_pending_option_ids: list[str] = []
|
||||||
|
self._has_sessions_after_remove = has_sessions_after_remove
|
||||||
|
|
||||||
|
def remove_session(self, option_id: str) -> bool:
|
||||||
|
self.removed_option_ids.append(option_id)
|
||||||
|
self.has_sessions = self._has_sessions_after_remove
|
||||||
|
return True
|
||||||
|
|
||||||
|
def clear_pending_delete(self, option_id: str) -> bool:
|
||||||
|
self.cleared_pending_option_ids.append(option_id)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_session_delete_request_deletes_local_session(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
config = build_test_vibe_config(
|
||||||
|
session_logging=_enabled_session_config(tmp_path), enable_connectors=False
|
||||||
|
)
|
||||||
|
app = build_test_vibe_app(config=config)
|
||||||
|
picker = FakeSessionPicker()
|
||||||
|
mounted_widgets: list[object] = []
|
||||||
|
deleted_sessions: list[tuple[str, SessionLoggingConfig]] = []
|
||||||
|
|
||||||
|
async def delete_session(
|
||||||
|
session_id: str, session_config: SessionLoggingConfig
|
||||||
|
) -> None:
|
||||||
|
deleted_sessions.append((session_id, session_config))
|
||||||
|
|
||||||
|
async def mount_and_scroll(widget: object, after: object | None = None) -> None:
|
||||||
|
mounted_widgets.append(widget)
|
||||||
|
|
||||||
|
monkeypatch.setattr("vibe.cli.textual_ui.app.delete_saved_session", delete_session)
|
||||||
|
monkeypatch.setattr(app, "query_one", lambda _selector: picker)
|
||||||
|
monkeypatch.setattr(app, "_mount_and_scroll", mount_and_scroll)
|
||||||
|
|
||||||
|
await app.on_session_picker_app_session_delete_requested(
|
||||||
|
SessionPickerApp.SessionDeleteRequested(
|
||||||
|
"local:deleted-session", "local", "deleted-session"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert deleted_sessions == [("deleted-session", config.session_logging)]
|
||||||
|
assert picker.removed_option_ids == ["local:deleted-session"]
|
||||||
|
assert any(
|
||||||
|
isinstance(widget, UserCommandMessage)
|
||||||
|
and widget._content
|
||||||
|
== f"Deleted session `{short_session_id('deleted-session')}`."
|
||||||
|
for widget in mounted_widgets
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_session_delete_request_keeps_picker_on_delete_error(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
config = build_test_vibe_config(
|
||||||
|
session_logging=_enabled_session_config(tmp_path), enable_connectors=False
|
||||||
|
)
|
||||||
|
app = build_test_vibe_app(config=config)
|
||||||
|
picker = FakeSessionPicker()
|
||||||
|
mounted_widgets: list[object] = []
|
||||||
|
|
||||||
|
async def delete_session(
|
||||||
|
session_id: str, session_config: SessionLoggingConfig
|
||||||
|
) -> None:
|
||||||
|
raise RuntimeError("disk said no")
|
||||||
|
|
||||||
|
async def mount_and_scroll(widget: object, after: object | None = None) -> None:
|
||||||
|
mounted_widgets.append(widget)
|
||||||
|
|
||||||
|
monkeypatch.setattr("vibe.cli.textual_ui.app.delete_saved_session", delete_session)
|
||||||
|
monkeypatch.setattr(app, "query_one", lambda _selector: picker)
|
||||||
|
monkeypatch.setattr(app, "_mount_and_scroll", mount_and_scroll)
|
||||||
|
|
||||||
|
await app.on_session_picker_app_session_delete_requested(
|
||||||
|
SessionPickerApp.SessionDeleteRequested(
|
||||||
|
"local:deleted-session", "local", "deleted-session"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert picker.removed_option_ids == []
|
||||||
|
assert picker.cleared_pending_option_ids == ["local:deleted-session"]
|
||||||
|
assert any(
|
||||||
|
isinstance(widget, ErrorMessage)
|
||||||
|
and widget._error == "Failed to delete session: disk said no"
|
||||||
|
for widget in mounted_widgets
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_session_delete_request_returns_to_input_when_picker_becomes_empty(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
config = build_test_vibe_config(
|
||||||
|
session_logging=_enabled_session_config(tmp_path), enable_connectors=False
|
||||||
|
)
|
||||||
|
app = build_test_vibe_app(config=config)
|
||||||
|
picker = FakeSessionPicker(has_sessions_after_remove=False)
|
||||||
|
mounted_widgets: list[object] = []
|
||||||
|
switched_to_input = False
|
||||||
|
|
||||||
|
async def delete_session(
|
||||||
|
session_id: str, session_config: SessionLoggingConfig
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def mount_and_scroll(widget: object, after: object | None = None) -> None:
|
||||||
|
mounted_widgets.append(widget)
|
||||||
|
|
||||||
|
async def switch_to_input() -> None:
|
||||||
|
nonlocal switched_to_input
|
||||||
|
switched_to_input = True
|
||||||
|
|
||||||
|
monkeypatch.setattr("vibe.cli.textual_ui.app.delete_saved_session", delete_session)
|
||||||
|
monkeypatch.setattr(app, "query_one", lambda _selector: picker)
|
||||||
|
monkeypatch.setattr(app, "_mount_and_scroll", mount_and_scroll)
|
||||||
|
monkeypatch.setattr(app, "_switch_to_input_app", switch_to_input)
|
||||||
|
|
||||||
|
await app.on_session_picker_app_session_delete_requested(
|
||||||
|
SessionPickerApp.SessionDeleteRequested(
|
||||||
|
"local:deleted-session", "local", "deleted-session"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert switched_to_input is True
|
||||||
|
assert picker.removed_option_ids == ["local:deleted-session"]
|
||||||
|
assert [
|
||||||
|
widget._content
|
||||||
|
for widget in mounted_widgets
|
||||||
|
if isinstance(widget, UserCommandMessage)
|
||||||
|
] == [
|
||||||
|
f"Deleted session `{short_session_id('deleted-session')}`.",
|
||||||
|
"No saved sessions left for this directory.",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_session_delete_request_rejects_remote_sessions(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
config = build_test_vibe_config(
|
||||||
|
session_logging=_enabled_session_config(tmp_path), enable_connectors=False
|
||||||
|
)
|
||||||
|
app = build_test_vibe_app(config=config)
|
||||||
|
mounted_widgets: list[object] = []
|
||||||
|
|
||||||
|
async def delete_session(
|
||||||
|
session_id: str, session_config: SessionLoggingConfig
|
||||||
|
) -> None:
|
||||||
|
pytest.fail("remote sessions should not be deleted")
|
||||||
|
|
||||||
|
async def mount_and_scroll(widget: object, after: object | None = None) -> None:
|
||||||
|
mounted_widgets.append(widget)
|
||||||
|
|
||||||
|
monkeypatch.setattr("vibe.cli.textual_ui.app.delete_saved_session", delete_session)
|
||||||
|
monkeypatch.setattr(app, "_mount_and_scroll", mount_and_scroll)
|
||||||
|
|
||||||
|
await app.on_session_picker_app_session_delete_requested(
|
||||||
|
SessionPickerApp.SessionDeleteRequested(
|
||||||
|
"remote:remote-session", "remote", "remote-session"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert any(
|
||||||
|
isinstance(widget, ErrorMessage)
|
||||||
|
and widget._error == "Deleting remote sessions is not supported."
|
||||||
|
for widget in mounted_widgets
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_session_delete_request_rejects_current_session(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
config = build_test_vibe_config(
|
||||||
|
session_logging=_enabled_session_config(tmp_path), enable_connectors=False
|
||||||
|
)
|
||||||
|
app = build_test_vibe_app(config=config)
|
||||||
|
picker = FakeSessionPicker()
|
||||||
|
mounted_widgets: list[object] = []
|
||||||
|
deleted_sessions: list[str] = []
|
||||||
|
|
||||||
|
async def delete_session(
|
||||||
|
session_id: str, session_config: SessionLoggingConfig
|
||||||
|
) -> None:
|
||||||
|
deleted_sessions.append(session_id)
|
||||||
|
|
||||||
|
async def mount_and_scroll(widget: object, after: object | None = None) -> None:
|
||||||
|
mounted_widgets.append(widget)
|
||||||
|
|
||||||
|
monkeypatch.setattr("vibe.cli.textual_ui.app.delete_saved_session", delete_session)
|
||||||
|
monkeypatch.setattr(app, "query_one", lambda _selector: picker)
|
||||||
|
monkeypatch.setattr(app, "_mount_and_scroll", mount_and_scroll)
|
||||||
|
|
||||||
|
await app.on_session_picker_app_session_delete_requested(
|
||||||
|
SessionPickerApp.SessionDeleteRequested(
|
||||||
|
f"local:{app.agent_loop.session_id}", "local", app.agent_loop.session_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert deleted_sessions == []
|
||||||
|
assert picker.cleared_pending_option_ids == [f"local:{app.agent_loop.session_id}"]
|
||||||
|
assert any(
|
||||||
|
isinstance(widget, ErrorMessage)
|
||||||
|
and widget._error == "Deleting the current session is not supported."
|
||||||
|
for widget in mounted_widgets
|
||||||
|
)
|
||||||
|
|
@ -145,6 +145,35 @@ async def test_ui_displays_multiple_user_assistant_turns(
|
||||||
assert assistant_messages[1]._content == "Second answer"
|
assert assistant_messages[1]._content == "Second answer"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ui_displays_messages_when_resuming_in_dangerous_directory(
|
||||||
|
monkeypatch: pytest.MonkeyPatch, vibe_config: VibeConfig
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"vibe.cli.textual_ui.app.is_dangerous_directory",
|
||||||
|
lambda: (True, "You are in the home directory"),
|
||||||
|
)
|
||||||
|
|
||||||
|
agent_loop = build_test_agent_loop(config=vibe_config)
|
||||||
|
agent_loop.messages.extend([
|
||||||
|
LLMMessage(role=Role.user, content="Hello from a previous run"),
|
||||||
|
LLMMessage(role=Role.assistant, content="Welcome back!"),
|
||||||
|
])
|
||||||
|
|
||||||
|
app = build_test_vibe_app(agent_loop=agent_loop)
|
||||||
|
|
||||||
|
async with app.run_test() as pilot:
|
||||||
|
await pilot.pause(0.5)
|
||||||
|
|
||||||
|
user_messages = app.query(UserMessage)
|
||||||
|
assistant_messages = app.query(AssistantMessage)
|
||||||
|
|
||||||
|
assert len(user_messages) == 1
|
||||||
|
assert user_messages[0]._content == "Hello from a previous run"
|
||||||
|
assert len(assistant_messages) == 1
|
||||||
|
assert assistant_messages[0]._content == "Welcome back!"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_ui_rebuilds_history_when_whats_new_is_shown(
|
async def test_ui_rebuilds_history_when_whats_new_is_shown(
|
||||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||||
|
|
|
||||||
|
|
@ -129,3 +129,30 @@ async def test_skill_without_args_does_not_prepend_invocation_line(
|
||||||
vibe_app_with_skills, pilot, "Do the thing."
|
vibe_app_with_skills, pilot, "Do the thing."
|
||||||
)
|
)
|
||||||
assert "/my-skill" not in message._content
|
assert "/my-skill" not in message._content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_popped_queued_skill_does_not_fire_telemetry(
|
||||||
|
vibe_app_with_skills: VibeApp, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
async with vibe_app_with_skills.run_test() as pilot:
|
||||||
|
events: list[tuple[str, str]] = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
vibe_app_with_skills.agent_loop.telemetry_client,
|
||||||
|
"send_slash_command_used",
|
||||||
|
lambda name, kind: events.append((name, kind)),
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_input = vibe_app_with_skills.query_one(ChatInputContainer)
|
||||||
|
vibe_app_with_skills._agent_running = True
|
||||||
|
try:
|
||||||
|
chat_input.post_message(ChatInputContainer.Submitted("/my-skill"))
|
||||||
|
await pilot.pause(0.1)
|
||||||
|
assert len(vibe_app_with_skills._input_queue) == 1
|
||||||
|
|
||||||
|
await pilot.press("ctrl+c")
|
||||||
|
await pilot.pause(0.1)
|
||||||
|
assert len(vibe_app_with_skills._input_queue) == 0
|
||||||
|
assert events == []
|
||||||
|
finally:
|
||||||
|
vibe_app_with_skills._agent_running = False
|
||||||
|
|
|
||||||
|
|
@ -4,38 +4,55 @@ from unittest.mock import AsyncMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from tests.stubs.fake_tool import FakeTool, FakeToolArgs
|
||||||
import vibe.cli.textual_ui.handlers.event_handler as event_handler_module
|
import vibe.cli.textual_ui.handlers.event_handler as event_handler_module
|
||||||
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.messages import HookSystemMessageLine
|
||||||
from vibe.core.hooks.models import (
|
from vibe.core.hooks.models import (
|
||||||
HookEndEvent,
|
HookEndEvent,
|
||||||
HookMessageSeverity,
|
HookMessageSeverity,
|
||||||
HookRunEndEvent,
|
HookRunEndEvent,
|
||||||
HookRunStartEvent,
|
HookRunStartEvent,
|
||||||
|
HookStartEvent,
|
||||||
|
HookType,
|
||||||
)
|
)
|
||||||
|
from vibe.core.types import ToolCallEvent, ToolResultEvent
|
||||||
|
|
||||||
|
|
||||||
class FakeHookRunContainer:
|
class FakeHookRunContainer:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.display = False
|
self.display = False
|
||||||
self.remove = AsyncMock()
|
self.remove = AsyncMock()
|
||||||
|
self.messages: list[HookSystemMessageLine] = []
|
||||||
|
self.classes: set[str] = set()
|
||||||
|
|
||||||
async def add_message(self, _widget: object) -> None:
|
def add_class(self, cls: str) -> None:
|
||||||
|
self.classes.add(cls)
|
||||||
|
|
||||||
|
async def add_message(self, widget: HookSystemMessageLine) -> None:
|
||||||
self.display = True
|
self.display = True
|
||||||
|
self.messages.append(widget)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def hook_container_factory(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> list[FakeHookRunContainer]:
|
||||||
|
created: list[FakeHookRunContainer] = []
|
||||||
|
|
||||||
|
def make_container() -> FakeHookRunContainer:
|
||||||
|
container = FakeHookRunContainer()
|
||||||
|
created.append(container)
|
||||||
|
return container
|
||||||
|
|
||||||
|
monkeypatch.setattr(event_handler_module, "HookRunContainer", make_container)
|
||||||
|
return created
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_hook_run_end_removes_empty_container(
|
async def test_hook_run_end_removes_empty_container(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
hook_container_factory: list[FakeHookRunContainer],
|
||||||
) -> None:
|
) -> None:
|
||||||
created_containers: list[FakeHookRunContainer] = []
|
|
||||||
|
|
||||||
def make_container() -> FakeHookRunContainer:
|
|
||||||
container = FakeHookRunContainer()
|
|
||||||
created_containers.append(container)
|
|
||||||
return container
|
|
||||||
|
|
||||||
monkeypatch.setattr(event_handler_module, "HookRunContainer", make_container)
|
|
||||||
|
|
||||||
mount_callback = AsyncMock()
|
mount_callback = AsyncMock()
|
||||||
handler = EventHandler(
|
handler = EventHandler(
|
||||||
mount_callback=mount_callback, get_tools_collapsed=lambda: False
|
mount_callback=mount_callback, get_tools_collapsed=lambda: False
|
||||||
|
|
@ -44,30 +61,22 @@ async def test_hook_run_end_removes_empty_container(
|
||||||
await handler.handle_event(HookRunStartEvent())
|
await handler.handle_event(HookRunStartEvent())
|
||||||
await handler.handle_event(HookRunEndEvent())
|
await handler.handle_event(HookRunEndEvent())
|
||||||
|
|
||||||
assert len(created_containers) == 1
|
assert len(hook_container_factory) == 1
|
||||||
created_containers[0].remove.assert_awaited_once()
|
hook_container_factory[0].remove.assert_awaited_once()
|
||||||
assert handler._hook_run_container is None
|
assert "agent_turn" not in handler._hook_containers
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_hook_run_end_keeps_container_with_messages(
|
async def test_hook_run_end_keeps_container_with_messages(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
hook_container_factory: list[FakeHookRunContainer],
|
||||||
) -> None:
|
) -> None:
|
||||||
created_containers: list[FakeHookRunContainer] = []
|
|
||||||
|
|
||||||
def make_container() -> FakeHookRunContainer:
|
|
||||||
container = FakeHookRunContainer()
|
|
||||||
created_containers.append(container)
|
|
||||||
return container
|
|
||||||
|
|
||||||
monkeypatch.setattr(event_handler_module, "HookRunContainer", make_container)
|
|
||||||
|
|
||||||
mount_callback = AsyncMock()
|
mount_callback = AsyncMock()
|
||||||
handler = EventHandler(
|
handler = EventHandler(
|
||||||
mount_callback=mount_callback, get_tools_collapsed=lambda: False
|
mount_callback=mount_callback, get_tools_collapsed=lambda: False
|
||||||
)
|
)
|
||||||
|
|
||||||
await handler.handle_event(HookRunStartEvent())
|
await handler.handle_event(HookRunStartEvent())
|
||||||
|
await handler.handle_event(HookStartEvent(hook_name="post-turn"))
|
||||||
await handler.handle_event(
|
await handler.handle_event(
|
||||||
HookEndEvent(
|
HookEndEvent(
|
||||||
hook_name="post-turn", status=HookMessageSeverity.OK, content="Hook output"
|
hook_name="post-turn", status=HookMessageSeverity.OK, content="Hook output"
|
||||||
|
|
@ -75,7 +84,279 @@ async def test_hook_run_end_keeps_container_with_messages(
|
||||||
)
|
)
|
||||||
await handler.handle_event(HookRunEndEvent())
|
await handler.handle_event(HookRunEndEvent())
|
||||||
|
|
||||||
assert len(created_containers) == 1
|
assert len(hook_container_factory) == 1
|
||||||
created_containers[0].remove.assert_not_awaited()
|
hook_container_factory[0].remove.assert_not_awaited()
|
||||||
assert created_containers[0].display is True
|
assert hook_container_factory[0].display is True
|
||||||
assert handler._hook_run_container is None
|
assert "agent_turn" not in handler._hook_containers
|
||||||
|
|
||||||
|
|
||||||
|
def _tool_call_event(call_id: str) -> ToolCallEvent:
|
||||||
|
return ToolCallEvent(
|
||||||
|
tool_name="stub_tool",
|
||||||
|
tool_class=FakeTool,
|
||||||
|
args=FakeToolArgs(),
|
||||||
|
tool_call_id=call_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _tool_result_event(call_id: str) -> ToolResultEvent:
|
||||||
|
return ToolResultEvent(
|
||||||
|
tool_name="stub_tool",
|
||||||
|
tool_class=FakeTool,
|
||||||
|
result=None,
|
||||||
|
cancelled=False,
|
||||||
|
tool_call_id=call_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_before_tool_container_scoped_to_tool_call_id(
|
||||||
|
hook_container_factory: list[FakeHookRunContainer],
|
||||||
|
) -> None:
|
||||||
|
"""Two concurrent tool calls each get their own before_tool container."""
|
||||||
|
mount_callback = AsyncMock()
|
||||||
|
handler = EventHandler(
|
||||||
|
mount_callback=mount_callback, get_tools_collapsed=lambda: False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Two tool calls arrive
|
||||||
|
await handler.handle_event(_tool_call_event("call_A"))
|
||||||
|
await handler.handle_event(_tool_call_event("call_B"))
|
||||||
|
|
||||||
|
# before_tool starts for both, interleaved
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunStartEvent(
|
||||||
|
scope=HookType.BEFORE_TOOL, tool_name="stub_tool", tool_call_id="call_A"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunStartEvent(
|
||||||
|
scope=HookType.BEFORE_TOOL, tool_name="stub_tool", tool_call_id="call_B"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Two distinct containers were created.
|
||||||
|
assert len(hook_container_factory) == 2
|
||||||
|
assert "before_tool:call_A" in handler._hook_containers
|
||||||
|
assert "before_tool:call_B" in handler._hook_containers
|
||||||
|
assert (
|
||||||
|
handler._hook_containers["before_tool:call_A"]
|
||||||
|
is not handler._hook_containers["before_tool:call_B"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# End them — both are empty, both should be removed.
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunEndEvent(scope=HookType.BEFORE_TOOL, tool_call_id="call_A")
|
||||||
|
)
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunEndEvent(scope=HookType.BEFORE_TOOL, tool_call_id="call_B")
|
||||||
|
)
|
||||||
|
assert "before_tool:call_A" not in handler._hook_containers
|
||||||
|
assert "before_tool:call_B" not in handler._hook_containers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_after_tool_container_anchors_after_tool_result(
|
||||||
|
hook_container_factory: list[FakeHookRunContainer],
|
||||||
|
) -> None:
|
||||||
|
mount_callback = AsyncMock()
|
||||||
|
handler = EventHandler(
|
||||||
|
mount_callback=mount_callback, get_tools_collapsed=lambda: False
|
||||||
|
)
|
||||||
|
|
||||||
|
await handler.handle_event(_tool_call_event("call_X"))
|
||||||
|
await handler.handle_event(_tool_result_event("call_X"))
|
||||||
|
# After-tool start mounts after the tool result, which is the current
|
||||||
|
# anchor for this tool_call_id.
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunStartEvent(
|
||||||
|
scope=HookType.AFTER_TOOL, tool_name="stub_tool", tool_call_id="call_X"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert "after_tool:call_X" in handler._hook_containers
|
||||||
|
|
||||||
|
# mount_callback was called with after=<tool_result_widget> for the after_tool
|
||||||
|
# container. Inspect the last call's kwargs.
|
||||||
|
last_call = mount_callback.call_args_list[-1]
|
||||||
|
assert last_call.kwargs.get("after") is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_before_tool_container_for_unknown_tool_call_id(
|
||||||
|
hook_container_factory: list[FakeHookRunContainer],
|
||||||
|
) -> None:
|
||||||
|
mount_callback = AsyncMock()
|
||||||
|
handler = EventHandler(
|
||||||
|
mount_callback=mount_callback, get_tools_collapsed=lambda: False
|
||||||
|
)
|
||||||
|
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunStartEvent(
|
||||||
|
scope=HookType.BEFORE_TOOL, tool_name="bash", tool_call_id="unknown"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
last_call = mount_callback.call_args_list[-1]
|
||||||
|
assert last_call.kwargs.get("after") is None
|
||||||
|
assert last_call.kwargs.get("before") is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_before_tool_container_mounts_before_tool_call_widget(
|
||||||
|
hook_container_factory: list[FakeHookRunContainer],
|
||||||
|
) -> None:
|
||||||
|
mount_callback = AsyncMock()
|
||||||
|
handler = EventHandler(
|
||||||
|
mount_callback=mount_callback, get_tools_collapsed=lambda: False
|
||||||
|
)
|
||||||
|
|
||||||
|
await handler.handle_event(_tool_call_event("call_Y"))
|
||||||
|
tool_call_widget = handler._tool_call_anchors["call_Y"]
|
||||||
|
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunStartEvent(
|
||||||
|
scope=HookType.BEFORE_TOOL, tool_name="stub_tool", tool_call_id="call_Y"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
last_call = mount_callback.call_args_list[-1]
|
||||||
|
assert last_call.kwargs.get("before") is tool_call_widget
|
||||||
|
assert last_call.kwargs.get("after") is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_before_tool_run_end_keeps_tool_call_widget_as_anchor(
|
||||||
|
hook_container_factory: list[FakeHookRunContainer],
|
||||||
|
) -> None:
|
||||||
|
mount_callback = AsyncMock()
|
||||||
|
handler = EventHandler(
|
||||||
|
mount_callback=mount_callback, get_tools_collapsed=lambda: False
|
||||||
|
)
|
||||||
|
|
||||||
|
await handler.handle_event(_tool_call_event("call_Z"))
|
||||||
|
tool_call_widget = handler._tool_call_anchors["call_Z"]
|
||||||
|
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunStartEvent(
|
||||||
|
scope=HookType.BEFORE_TOOL, tool_name="stub_tool", tool_call_id="call_Z"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await handler.handle_event(HookStartEvent(hook_name="before"))
|
||||||
|
await handler.handle_event(
|
||||||
|
HookEndEvent(hook_name="before", status=HookMessageSeverity.OK, content="ok")
|
||||||
|
)
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunEndEvent(scope=HookType.BEFORE_TOOL, tool_call_id="call_Z")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Even though the before_tool container has content and stays in the DOM,
|
||||||
|
# the anchor for the next widget (the tool result) must remain the call
|
||||||
|
# widget — the container lives *above* the call, not below it.
|
||||||
|
assert handler._tool_call_anchors["call_Z"] is tool_call_widget
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_hook_end_routes_to_matching_container_when_chains_interleave(
|
||||||
|
hook_container_factory: list[FakeHookRunContainer],
|
||||||
|
) -> None:
|
||||||
|
mount_callback = AsyncMock()
|
||||||
|
handler = EventHandler(
|
||||||
|
mount_callback=mount_callback, get_tools_collapsed=lambda: False
|
||||||
|
)
|
||||||
|
|
||||||
|
await handler.handle_event(_tool_call_event("call_A"))
|
||||||
|
await handler.handle_event(_tool_call_event("call_B"))
|
||||||
|
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunStartEvent(
|
||||||
|
scope=HookType.BEFORE_TOOL, tool_name="stub_tool", tool_call_id="call_A"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert len(hook_container_factory) == 1
|
||||||
|
container_a = hook_container_factory[-1]
|
||||||
|
|
||||||
|
await handler.handle_event(HookStartEvent(hook_name="hook_a"))
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunStartEvent(
|
||||||
|
scope=HookType.BEFORE_TOOL, tool_name="stub_tool", tool_call_id="call_B"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert len(hook_container_factory) == 2
|
||||||
|
container_b = hook_container_factory[-1]
|
||||||
|
assert container_a is not container_b
|
||||||
|
|
||||||
|
await handler.handle_event(
|
||||||
|
HookEndEvent(
|
||||||
|
hook_name="hook_a",
|
||||||
|
status=HookMessageSeverity.OK,
|
||||||
|
content="from A",
|
||||||
|
scope=HookType.BEFORE_TOOL,
|
||||||
|
tool_call_id="call_A",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await handler.handle_event(HookStartEvent(hook_name="hook_b"))
|
||||||
|
await handler.handle_event(
|
||||||
|
HookEndEvent(
|
||||||
|
hook_name="hook_b",
|
||||||
|
status=HookMessageSeverity.OK,
|
||||||
|
content="from B",
|
||||||
|
scope=HookType.BEFORE_TOOL,
|
||||||
|
tool_call_id="call_B",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(container_a.messages) == 1
|
||||||
|
assert len(container_b.messages) == 1
|
||||||
|
assert container_a.messages[0]._content == "from A"
|
||||||
|
assert container_b.messages[0]._content == "from B"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_hook_end_after_other_chain_ended_still_routes_correctly(
|
||||||
|
hook_container_factory: list[FakeHookRunContainer],
|
||||||
|
) -> None:
|
||||||
|
mount_callback = AsyncMock()
|
||||||
|
handler = EventHandler(
|
||||||
|
mount_callback=mount_callback, get_tools_collapsed=lambda: False
|
||||||
|
)
|
||||||
|
|
||||||
|
await handler.handle_event(_tool_call_event("call_A"))
|
||||||
|
await handler.handle_event(_tool_call_event("call_B"))
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunStartEvent(
|
||||||
|
scope=HookType.BEFORE_TOOL, tool_name="stub_tool", tool_call_id="call_A"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
container_a = hook_container_factory[-1]
|
||||||
|
await handler.handle_event(HookStartEvent(hook_name="hook_a"))
|
||||||
|
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunStartEvent(
|
||||||
|
scope=HookType.BEFORE_TOOL, tool_name="stub_tool", tool_call_id="call_B"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await handler.handle_event(HookStartEvent(hook_name="hook_b"))
|
||||||
|
await handler.handle_event(
|
||||||
|
HookEndEvent(
|
||||||
|
hook_name="hook_b",
|
||||||
|
status=HookMessageSeverity.OK,
|
||||||
|
content="from B",
|
||||||
|
scope=HookType.BEFORE_TOOL,
|
||||||
|
tool_call_id="call_B",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await handler.handle_event(
|
||||||
|
HookRunEndEvent(scope=HookType.BEFORE_TOOL, tool_call_id="call_B")
|
||||||
|
)
|
||||||
|
|
||||||
|
await handler.handle_event(
|
||||||
|
HookEndEvent(
|
||||||
|
hook_name="hook_a",
|
||||||
|
status=HookMessageSeverity.OK,
|
||||||
|
content="from A",
|
||||||
|
scope=HookType.BEFORE_TOOL,
|
||||||
|
tool_call_id="call_A",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(container_a.messages) == 1
|
||||||
|
assert container_a.messages[0]._content == "from A"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from vibe.cli.textual_ui.message_queue import MessageQueue, QueuedItemKind
|
from vibe.cli.textual_ui.message_queue import MessageQueue, QueuedItem, QueuedItemKind
|
||||||
|
|
||||||
|
|
||||||
def test_empty_queue_is_falsy() -> None:
|
def test_empty_queue_is_falsy() -> None:
|
||||||
|
|
@ -38,6 +38,17 @@ def test_pop_last_returns_newest() -> None:
|
||||||
assert [item.content for item in queue.items] == ["a", "b"]
|
assert [item.content for item in queue.items] == ["a", "b"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_pop_last_resumes_when_queue_becomes_empty() -> None:
|
||||||
|
queue = MessageQueue()
|
||||||
|
queue.append_prompt("a")
|
||||||
|
queue.pause()
|
||||||
|
|
||||||
|
queue.pop_last()
|
||||||
|
|
||||||
|
assert not queue
|
||||||
|
assert not queue.paused
|
||||||
|
|
||||||
|
|
||||||
def test_pop_first_returns_oldest() -> None:
|
def test_pop_first_returns_oldest() -> None:
|
||||||
queue = MessageQueue()
|
queue = MessageQueue()
|
||||||
queue.append_prompt("a")
|
queue.append_prompt("a")
|
||||||
|
|
@ -74,11 +85,9 @@ def test_pause_and_resume() -> None:
|
||||||
|
|
||||||
def test_pause_is_idempotent() -> None:
|
def test_pause_is_idempotent() -> None:
|
||||||
queue = MessageQueue()
|
queue = MessageQueue()
|
||||||
calls = []
|
|
||||||
queue.set_change_listener(lambda: calls.append(None))
|
|
||||||
queue.pause()
|
queue.pause()
|
||||||
queue.pause()
|
queue.pause()
|
||||||
assert len(calls) == 1
|
assert queue.paused
|
||||||
|
|
||||||
|
|
||||||
def test_clear_resets_state() -> None:
|
def test_clear_resets_state() -> None:
|
||||||
|
|
@ -90,34 +99,30 @@ def test_clear_resets_state() -> None:
|
||||||
assert not queue.paused
|
assert not queue.paused
|
||||||
|
|
||||||
|
|
||||||
def test_change_listener_fires_on_mutations() -> None:
|
def test_prepend_prompts_inserts_at_head_preserving_order() -> None:
|
||||||
queue = MessageQueue()
|
queue = MessageQueue()
|
||||||
calls: list[None] = []
|
queue.append_prompt("x")
|
||||||
queue.set_change_listener(lambda: calls.append(None))
|
queue.append_prompt("y")
|
||||||
|
queue.prepend_prompts([
|
||||||
queue.append_prompt("a")
|
QueuedItem(QueuedItemKind.PROMPT, "a"),
|
||||||
assert len(calls) == 1
|
QueuedItem(QueuedItemKind.PROMPT, "b"),
|
||||||
|
])
|
||||||
queue.append_bash("ls")
|
assert [item.content for item in queue.items] == ["a", "b", "x", "y"]
|
||||||
assert len(calls) == 2
|
|
||||||
|
|
||||||
queue.pop_last()
|
|
||||||
assert len(calls) == 3
|
|
||||||
|
|
||||||
queue.pause()
|
|
||||||
assert len(calls) == 4
|
|
||||||
|
|
||||||
queue.resume()
|
|
||||||
assert len(calls) == 5
|
|
||||||
|
|
||||||
|
|
||||||
def test_change_listener_can_be_cleared() -> None:
|
def test_prepend_prompts_empty_is_noop() -> None:
|
||||||
queue = MessageQueue()
|
queue = MessageQueue()
|
||||||
calls: list[None] = []
|
queue.append_prompt("x")
|
||||||
queue.set_change_listener(lambda: calls.append(None))
|
queue.prepend_prompts([])
|
||||||
queue.set_change_listener(None)
|
assert [item.content for item in queue.items] == ["x"]
|
||||||
queue.append_prompt("a")
|
|
||||||
assert calls == []
|
|
||||||
|
def test_append_prompt_with_skill_name() -> None:
|
||||||
|
queue = MessageQueue()
|
||||||
|
queue.append_prompt("expanded prompt", skill_name="my-skill")
|
||||||
|
item = queue.items[0]
|
||||||
|
assert item.skill_name == "my-skill"
|
||||||
|
assert item.content == "expanded prompt"
|
||||||
|
|
||||||
|
|
||||||
def test_items_returns_copy() -> None:
|
def test_items_returns_copy() -> None:
|
||||||
|
|
|
||||||
200
tests/cli/textual_ui/test_message_queue_ui.py
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.conftest import build_test_vibe_app
|
||||||
|
from vibe.cli.textual_ui.app import VibeApp
|
||||||
|
from vibe.cli.textual_ui.widgets.chat_input.container import ChatInputContainer
|
||||||
|
from vibe.cli.textual_ui.widgets.messages import (
|
||||||
|
BashOutputMessage,
|
||||||
|
QueueHeaderMessage,
|
||||||
|
WarningMessage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def vibe_app() -> VibeApp:
|
||||||
|
return build_test_vibe_app()
|
||||||
|
|
||||||
|
|
||||||
|
async def _wait_until(pilot, predicate, timeout: float = 2.0) -> bool:
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
if predicate():
|
||||||
|
return True
|
||||||
|
await pilot.pause(0.05)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_no_queue_header_when_empty(vibe_app: VibeApp) -> None:
|
||||||
|
async with vibe_app.run_test():
|
||||||
|
headers = list(vibe_app.query(QueueHeaderMessage))
|
||||||
|
assert headers == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bash_submitted_during_running_bash_is_queued(vibe_app: VibeApp) -> None:
|
||||||
|
async with vibe_app.run_test() as pilot:
|
||||||
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||||
|
chat_input.value = "!sleep 0.3"
|
||||||
|
await pilot.press("enter")
|
||||||
|
|
||||||
|
await _wait_until(pilot, lambda: vibe_app._bash_task is not None, timeout=1.0)
|
||||||
|
|
||||||
|
chat_input.value = "!echo queued"
|
||||||
|
await pilot.press("enter")
|
||||||
|
|
||||||
|
assert len(vibe_app._input_queue) == 1
|
||||||
|
assert vibe_app._input_queue.items[0].content == "echo queued"
|
||||||
|
|
||||||
|
headers = list(vibe_app.query(QueueHeaderMessage))
|
||||||
|
assert len(headers) == 1
|
||||||
|
|
||||||
|
queued_bashes = [w for w in vibe_app.query(BashOutputMessage) if w._queued]
|
||||||
|
assert len(queued_bashes) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_slash_command_rejected_with_warning_when_busy(vibe_app: VibeApp) -> None:
|
||||||
|
async with vibe_app.run_test() as pilot:
|
||||||
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||||
|
chat_input.value = "!sleep 0.3"
|
||||||
|
await pilot.press("enter")
|
||||||
|
|
||||||
|
await _wait_until(pilot, lambda: vibe_app._bash_task is not None, timeout=1.0)
|
||||||
|
|
||||||
|
chat_input.value = "/help"
|
||||||
|
await pilot.press("enter")
|
||||||
|
|
||||||
|
assert not list(vibe_app.query(WarningMessage))
|
||||||
|
assert any(
|
||||||
|
"Slash commands cannot be queued" in notification.message
|
||||||
|
for notification in vibe_app._notifications
|
||||||
|
)
|
||||||
|
assert len(vibe_app._input_queue) == 0
|
||||||
|
assert chat_input.value.startswith("/help")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ctrl_c_pops_last_queued_item_lifo(vibe_app: VibeApp) -> None:
|
||||||
|
async with vibe_app.run_test() as pilot:
|
||||||
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||||
|
chat_input.value = "!sleep 2"
|
||||||
|
await pilot.press("enter")
|
||||||
|
|
||||||
|
await _wait_until(pilot, lambda: vibe_app._bash_task is not None, timeout=2.0)
|
||||||
|
|
||||||
|
chat_input.value = "!echo first"
|
||||||
|
await pilot.press("enter")
|
||||||
|
chat_input.value = "!echo second"
|
||||||
|
await pilot.press("enter")
|
||||||
|
|
||||||
|
assert len(vibe_app._input_queue) == 2
|
||||||
|
|
||||||
|
await pilot.press("ctrl+c")
|
||||||
|
assert len(vibe_app._input_queue) == 1
|
||||||
|
assert vibe_app._input_queue.items[0].content == "echo first"
|
||||||
|
|
||||||
|
await pilot.press("escape")
|
||||||
|
await _wait_until(pilot, lambda: vibe_app._bash_task is None, timeout=5.0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_escape_pauses_queue_when_job_running(vibe_app: VibeApp) -> None:
|
||||||
|
async with vibe_app.run_test() as pilot:
|
||||||
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||||
|
chat_input.value = "!sleep 2"
|
||||||
|
await pilot.press("enter")
|
||||||
|
|
||||||
|
await _wait_until(pilot, lambda: vibe_app._bash_task is not None, timeout=2.0)
|
||||||
|
|
||||||
|
chat_input.value = "!echo queued"
|
||||||
|
await pilot.press("enter")
|
||||||
|
assert len(vibe_app._input_queue) == 1
|
||||||
|
|
||||||
|
await pilot.press("escape")
|
||||||
|
assert vibe_app._input_queue.paused
|
||||||
|
assert len(vibe_app._input_queue) == 1
|
||||||
|
|
||||||
|
await _wait_until(pilot, lambda: vibe_app._bash_task is None, timeout=5.0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_drain_runs_queued_bashes_in_fifo_order(vibe_app: VibeApp) -> None:
|
||||||
|
async with vibe_app.run_test() as pilot:
|
||||||
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||||
|
chat_input.value = "!sleep 0.2"
|
||||||
|
await pilot.press("enter")
|
||||||
|
|
||||||
|
await _wait_until(pilot, lambda: vibe_app._bash_task is not None, timeout=1.0)
|
||||||
|
|
||||||
|
chat_input.value = "!echo first"
|
||||||
|
await pilot.press("enter")
|
||||||
|
chat_input.value = "!echo second"
|
||||||
|
await pilot.press("enter")
|
||||||
|
|
||||||
|
await _wait_until(
|
||||||
|
pilot,
|
||||||
|
lambda: (
|
||||||
|
len(list(vibe_app.query(BashOutputMessage))) == 3
|
||||||
|
and all(not m._pending for m in vibe_app.query(BashOutputMessage))
|
||||||
|
),
|
||||||
|
timeout=5.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
msgs = list(vibe_app.query(BashOutputMessage))
|
||||||
|
assert len(msgs) == 3
|
||||||
|
assert vibe_app._input_queue.paused is False
|
||||||
|
assert len(vibe_app._input_queue) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_enter_on_empty_input_flushes_paused_queue(vibe_app: VibeApp) -> None:
|
||||||
|
async with vibe_app.run_test() as pilot:
|
||||||
|
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||||
|
chat_input.value = "!sleep 2"
|
||||||
|
await pilot.press("enter")
|
||||||
|
|
||||||
|
await _wait_until(pilot, lambda: vibe_app._bash_task is not None, timeout=2.0)
|
||||||
|
|
||||||
|
chat_input.value = "!echo queued"
|
||||||
|
await pilot.press("enter")
|
||||||
|
assert len(vibe_app._input_queue) == 1
|
||||||
|
|
||||||
|
await pilot.press("escape")
|
||||||
|
assert vibe_app._input_queue.paused
|
||||||
|
|
||||||
|
await _wait_until(pilot, lambda: vibe_app._bash_task is None, timeout=10.0)
|
||||||
|
|
||||||
|
chat_input.value = ""
|
||||||
|
await pilot.press("enter")
|
||||||
|
|
||||||
|
await _wait_until(
|
||||||
|
pilot,
|
||||||
|
lambda: (
|
||||||
|
not vibe_app._input_queue.paused and len(vibe_app._input_queue) == 0
|
||||||
|
),
|
||||||
|
timeout=10.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not vibe_app._input_queue.paused
|
||||||
|
assert len(vibe_app._input_queue) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_quit_warning_shows_queue_count(vibe_app: VibeApp) -> None:
|
||||||
|
async with vibe_app.run_test():
|
||||||
|
vibe_app._input_queue.append_prompt("a")
|
||||||
|
vibe_app._input_queue.append_prompt("b")
|
||||||
|
warning = vibe_app._queue.quit_warning_extra()
|
||||||
|
assert warning == "2 queued messages will be discarded"
|
||||||
|
|
||||||
|
vibe_app._input_queue.pop_last()
|
||||||
|
warning = vibe_app._queue.quit_warning_extra()
|
||||||
|
assert warning == "1 queued message will be discarded"
|
||||||
|
|
||||||
|
vibe_app._input_queue.pop_last()
|
||||||
|
assert vibe_app._queue.quit_warning_extra() == ""
|
||||||
|
|
@ -92,11 +92,12 @@ class TestActionInterruptOrQuit:
|
||||||
mock_container.value = ""
|
mock_container.value = ""
|
||||||
with (
|
with (
|
||||||
patch.object(app, "_get_chat_input", return_value=mock_container),
|
patch.object(app, "_get_chat_input", return_value=mock_container),
|
||||||
patch.object(app, "_try_interrupt", return_value=False),
|
patch.object(app, "_try_interrupt_no_job_steps", return_value=False),
|
||||||
|
patch.object(app, "_try_interrupt_running_job", return_value=False),
|
||||||
patch.object(app._quit_manager, "request_confirmation") as mock_confirm,
|
patch.object(app._quit_manager, "request_confirmation") as mock_confirm,
|
||||||
):
|
):
|
||||||
app.action_interrupt_or_quit()
|
app.action_interrupt_or_quit()
|
||||||
mock_confirm.assert_called_once_with("Ctrl+C")
|
mock_confirm.assert_called_once_with("Ctrl+C", "")
|
||||||
|
|
||||||
def test_quits_on_confirmed(self, app: VibeApp) -> None:
|
def test_quits_on_confirmed(self, app: VibeApp) -> None:
|
||||||
app._quit_manager._confirm_time = time.monotonic()
|
app._quit_manager._confirm_time = time.monotonic()
|
||||||
|
|
@ -111,7 +112,9 @@ class TestActionInterruptOrQuit:
|
||||||
def test_interrupts_before_requesting_confirmation(self, app: VibeApp) -> None:
|
def test_interrupts_before_requesting_confirmation(self, app: VibeApp) -> None:
|
||||||
with (
|
with (
|
||||||
patch.object(app, "_get_chat_input", return_value=None),
|
patch.object(app, "_get_chat_input", return_value=None),
|
||||||
patch.object(app, "_try_interrupt", return_value=True) as mock_interrupt,
|
patch.object(
|
||||||
|
app, "_try_interrupt_no_job_steps", return_value=True
|
||||||
|
) as mock_interrupt,
|
||||||
patch.object(app._quit_manager, "request_confirmation") as mock_confirm,
|
patch.object(app._quit_manager, "request_confirmation") as mock_confirm,
|
||||||
):
|
):
|
||||||
app.action_interrupt_or_quit()
|
app.action_interrupt_or_quit()
|
||||||
|
|
@ -123,11 +126,12 @@ class TestActionInterruptOrQuit:
|
||||||
) -> None:
|
) -> None:
|
||||||
with (
|
with (
|
||||||
patch.object(app, "_get_chat_input", return_value=None),
|
patch.object(app, "_get_chat_input", return_value=None),
|
||||||
patch.object(app, "_try_interrupt", return_value=False),
|
patch.object(app, "_try_interrupt_no_job_steps", return_value=False),
|
||||||
|
patch.object(app, "_try_interrupt_running_job", return_value=False),
|
||||||
patch.object(app._quit_manager, "request_confirmation") as mock_confirm,
|
patch.object(app._quit_manager, "request_confirmation") as mock_confirm,
|
||||||
):
|
):
|
||||||
app.action_interrupt_or_quit()
|
app.action_interrupt_or_quit()
|
||||||
mock_confirm.assert_called_once_with("Ctrl+C")
|
mock_confirm.assert_called_once_with("Ctrl+C", "")
|
||||||
|
|
||||||
|
|
||||||
class TestActionDeleteRightOrQuit:
|
class TestActionDeleteRightOrQuit:
|
||||||
|
|
@ -148,7 +152,7 @@ class TestActionDeleteRightOrQuit:
|
||||||
patch.object(app._quit_manager, "request_confirmation") as mock_confirm,
|
patch.object(app._quit_manager, "request_confirmation") as mock_confirm,
|
||||||
):
|
):
|
||||||
app.action_delete_right_or_quit()
|
app.action_delete_right_or_quit()
|
||||||
mock_confirm.assert_called_once_with("Ctrl+D")
|
mock_confirm.assert_called_once_with("Ctrl+D", "")
|
||||||
|
|
||||||
def test_quits_on_confirmed(self, app: VibeApp) -> None:
|
def test_quits_on_confirmed(self, app: VibeApp) -> None:
|
||||||
app._quit_manager._confirm_time = time.monotonic()
|
app._quit_manager._confirm_time = time.monotonic()
|
||||||
|
|
@ -166,4 +170,15 @@ class TestActionDeleteRightOrQuit:
|
||||||
patch.object(app._quit_manager, "request_confirmation") as mock_confirm,
|
patch.object(app._quit_manager, "request_confirmation") as mock_confirm,
|
||||||
):
|
):
|
||||||
app.action_delete_right_or_quit()
|
app.action_delete_right_or_quit()
|
||||||
mock_confirm.assert_called_once_with("Ctrl+D")
|
mock_confirm.assert_called_once_with("Ctrl+D", "")
|
||||||
|
|
||||||
|
def test_shows_queue_warning_when_queue_non_empty(self, app: VibeApp) -> None:
|
||||||
|
app._input_queue.append_prompt("queued")
|
||||||
|
with (
|
||||||
|
patch.object(app, "_get_chat_input", return_value=None),
|
||||||
|
patch.object(app._quit_manager, "request_confirmation") as mock_confirm,
|
||||||
|
):
|
||||||
|
app.action_delete_right_or_quit()
|
||||||
|
mock_confirm.assert_called_once_with(
|
||||||
|
"Ctrl+D", "1 queued message will be discarded"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from vibe.cli.textual_ui.widgets.tool_widgets import _strip_line_numbers
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from vibe.cli.textual_ui.widgets.collapsible import CollapsibleSection
|
||||||
|
from vibe.cli.textual_ui.widgets.tool_widgets import (
|
||||||
|
ToolResultWidget,
|
||||||
|
_fenced_code_block,
|
||||||
|
_strip_line_numbers,
|
||||||
|
get_result_widget,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_strips_numbered_prefixes() -> None:
|
def test_strips_numbered_prefixes() -> None:
|
||||||
|
|
@ -16,3 +24,57 @@ def test_leaves_warning_lines_untouched() -> None:
|
||||||
def test_preserves_arrows_inside_content() -> None:
|
def test_preserves_arrows_inside_content() -> None:
|
||||||
content = " 1→a → b → c"
|
content = " 1→a → b → c"
|
||||||
assert _strip_line_numbers(content) == "a → b → c"
|
assert _strip_line_numbers(content) == "a → b → c"
|
||||||
|
|
||||||
|
|
||||||
|
def test_fence_uses_three_backticks_for_plain_content() -> None:
|
||||||
|
block = _fenced_code_block("hello\nworld", "py")
|
||||||
|
assert block == "```py\nhello\nworld\n```"
|
||||||
|
|
||||||
|
|
||||||
|
def test_fence_outgrows_embedded_triple_backticks() -> None:
|
||||||
|
content = "before\n```\n[click me](http://evil)\n```\nafter"
|
||||||
|
block = _fenced_code_block(content, "md")
|
||||||
|
fence = "````"
|
||||||
|
assert block == f"{fence}md\n{content}\n{fence}"
|
||||||
|
assert block.startswith(fence)
|
||||||
|
assert block.endswith(fence)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fence_outgrows_longest_backtick_run() -> None:
|
||||||
|
content = "a ```` b ``` c"
|
||||||
|
block = _fenced_code_block(content, "")
|
||||||
|
assert block.startswith("`````")
|
||||||
|
assert block.endswith("`````")
|
||||||
|
|
||||||
|
|
||||||
|
def test_fence_strips_newlines_from_ext() -> None:
|
||||||
|
block = _fenced_code_block("safe", "x\n[click](http://evil)")
|
||||||
|
assert block == "```xclickhttpevil\nsafe\n```"
|
||||||
|
assert "\n[click]" not in block
|
||||||
|
|
||||||
|
|
||||||
|
def test_fence_strips_backticks_from_ext() -> None:
|
||||||
|
block = _fenced_code_block("safe", "py`\n```md")
|
||||||
|
first_line = block.split("\n", 1)[0]
|
||||||
|
assert first_line == "```pymd"
|
||||||
|
|
||||||
|
|
||||||
|
def test_fence_caps_ext_length() -> None:
|
||||||
|
block = _fenced_code_block("safe", "a" * 500)
|
||||||
|
first_line = block.split("\n", 1)[0]
|
||||||
|
assert first_line == "```" + "a" * 32
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_tool_uses_default_widget() -> None:
|
||||||
|
widget = get_result_widget("unknown_tool", None, True, "done")
|
||||||
|
assert type(widget) is ToolResultWidget
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_widget_renders_fields_collapsibly() -> None:
|
||||||
|
class _Result(BaseModel):
|
||||||
|
server: str
|
||||||
|
text: str
|
||||||
|
|
||||||
|
widget = ToolResultWidget(_Result(server="s", text="hello"), True, "ok")
|
||||||
|
children = list(widget.compose())
|
||||||
|
assert any(isinstance(child, CollapsibleSection) for child in children)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from rich.text import Text
|
||||||
|
from textual.widgets import OptionList
|
||||||
|
|
||||||
from vibe.cli.textual_ui.widgets.session_picker import (
|
from vibe.cli.textual_ui.widgets.session_picker import (
|
||||||
SessionPickerApp,
|
SessionPickerApp,
|
||||||
|
|
@ -48,6 +51,12 @@ def sample_latest_messages() -> dict[str, str]:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def assert_delete_state(picker: SessionPickerApp, *, kind: str, option_id: str) -> None:
|
||||||
|
assert picker._delete_state is not None
|
||||||
|
assert picker._delete_state.kind == kind
|
||||||
|
assert picker._delete_state.option_id == option_id
|
||||||
|
|
||||||
|
|
||||||
class TestFormatRelativeTime:
|
class TestFormatRelativeTime:
|
||||||
def test_just_now(self) -> None:
|
def test_just_now(self) -> None:
|
||||||
now = datetime.now(UTC).isoformat()
|
now = datetime.now(UTC).isoformat()
|
||||||
|
|
@ -99,6 +108,7 @@ class TestSessionPickerAppInit:
|
||||||
)
|
)
|
||||||
assert picker._sessions == sample_sessions
|
assert picker._sessions == sample_sessions
|
||||||
assert picker._latest_messages == sample_latest_messages
|
assert picker._latest_messages == sample_latest_messages
|
||||||
|
assert picker._current_session_id is None
|
||||||
|
|
||||||
def test_id_is_sessionpicker_app(self) -> None:
|
def test_id_is_sessionpicker_app(self) -> None:
|
||||||
picker = SessionPickerApp(sessions=[], latest_messages={})
|
picker = SessionPickerApp(sessions=[], latest_messages={})
|
||||||
|
|
@ -107,6 +117,29 @@ class TestSessionPickerAppInit:
|
||||||
def test_can_focus_children_is_true(self) -> None:
|
def test_can_focus_children_is_true(self) -> None:
|
||||||
assert SessionPickerApp.can_focus_children is True
|
assert SessionPickerApp.can_focus_children is True
|
||||||
|
|
||||||
|
def test_delete_confirmation_state_starts_empty(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
assert picker._delete_state is None
|
||||||
|
|
||||||
|
def test_has_sessions_tracks_session_list(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
assert picker.has_sessions is True
|
||||||
|
|
||||||
|
empty_picker = SessionPickerApp(sessions=[], latest_messages={})
|
||||||
|
assert empty_picker.has_sessions is False
|
||||||
|
|
||||||
|
|
||||||
class TestSessionPickerMessages:
|
class TestSessionPickerMessages:
|
||||||
def test_session_selected_stores_option_id(self) -> None:
|
def test_session_selected_stores_option_id(self) -> None:
|
||||||
|
|
@ -129,16 +162,377 @@ class TestSessionPickerMessages:
|
||||||
msg = SessionPickerApp.Cancelled()
|
msg = SessionPickerApp.Cancelled()
|
||||||
assert isinstance(msg, SessionPickerApp.Cancelled)
|
assert isinstance(msg, SessionPickerApp.Cancelled)
|
||||||
|
|
||||||
|
def test_session_delete_requested_stores_session_info(self) -> None:
|
||||||
|
msg = SessionPickerApp.SessionDeleteRequested(
|
||||||
|
"local:test-session-id", "local", "test-session-id"
|
||||||
|
)
|
||||||
|
assert msg.option_id == "local:test-session-id"
|
||||||
|
assert msg.source == "local"
|
||||||
|
assert msg.session_id == "test-session-id"
|
||||||
|
|
||||||
|
|
||||||
class TestSessionPickerAppBindings:
|
class TestSessionPickerAppBindings:
|
||||||
def _get_binding_keys(self) -> list[str]:
|
def _get_binding_keys(self) -> list[str]:
|
||||||
keys = []
|
keys = []
|
||||||
for binding in SessionPickerApp.BINDINGS:
|
for binding in SessionPickerApp.BINDINGS:
|
||||||
if isinstance(binding, tuple) and len(binding) >= 1:
|
if isinstance(binding, tuple) and len(binding) >= 1:
|
||||||
keys.append(binding[0])
|
keys.extend(binding[0].split(","))
|
||||||
else:
|
else:
|
||||||
keys.append(binding.key)
|
keys.extend(binding.key.split(","))
|
||||||
return keys
|
return keys
|
||||||
|
|
||||||
def test_has_escape_binding(self) -> None:
|
def test_has_escape_binding(self) -> None:
|
||||||
assert "escape" in self._get_binding_keys()
|
assert "escape" in self._get_binding_keys()
|
||||||
|
|
||||||
|
def test_has_delete_binding(self) -> None:
|
||||||
|
assert "d" in self._get_binding_keys()
|
||||||
|
assert "D" in self._get_binding_keys()
|
||||||
|
|
||||||
|
|
||||||
|
class TestSessionPickerSessionRemoval:
|
||||||
|
def test_first_delete_request_enters_confirmation(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
option_list = FakeOptionList(highlighted_option_id="local:session-a")
|
||||||
|
posted_messages: list[object] = []
|
||||||
|
monkeypatch.setattr(picker, "query_one", lambda _selector: option_list)
|
||||||
|
monkeypatch.setattr(picker, "post_message", posted_messages.append)
|
||||||
|
|
||||||
|
picker.action_request_delete()
|
||||||
|
|
||||||
|
assert_delete_state(picker, kind="confirmation", option_id="local:session-a")
|
||||||
|
assert option_list.replaced_prompts[-1].option_id == "local:session-a"
|
||||||
|
assert (
|
||||||
|
"Press D again to delete" in option_list.replaced_prompts[-1].prompt.plain
|
||||||
|
)
|
||||||
|
assert posted_messages == []
|
||||||
|
|
||||||
|
def test_second_delete_request_posts_delete_message(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
option_list = FakeOptionList(highlighted_option_id="local:session-a")
|
||||||
|
posted_messages: list[object] = []
|
||||||
|
monkeypatch.setattr(picker, "query_one", lambda _selector: option_list)
|
||||||
|
monkeypatch.setattr(picker, "post_message", posted_messages.append)
|
||||||
|
|
||||||
|
picker.action_request_delete()
|
||||||
|
picker.action_request_delete()
|
||||||
|
|
||||||
|
assert_delete_state(picker, kind="pending", option_id="local:session-a")
|
||||||
|
assert option_list.replaced_prompts[-1].option_id == "local:session-a"
|
||||||
|
assert "Deleting..." in option_list.replaced_prompts[-1].prompt.plain
|
||||||
|
assert len(posted_messages) == 1
|
||||||
|
message = posted_messages[0]
|
||||||
|
assert isinstance(message, SessionPickerApp.SessionDeleteRequested)
|
||||||
|
assert message.option_id == "local:session-a"
|
||||||
|
assert message.source == "local"
|
||||||
|
assert message.session_id == "session-a"
|
||||||
|
|
||||||
|
def test_delete_confirmation_is_consumed_after_request(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
option_list = FakeOptionList(highlighted_option_id="local:session-a")
|
||||||
|
posted_messages: list[object] = []
|
||||||
|
monkeypatch.setattr(picker, "query_one", lambda _selector: option_list)
|
||||||
|
monkeypatch.setattr(picker, "post_message", posted_messages.append)
|
||||||
|
|
||||||
|
picker.action_request_delete()
|
||||||
|
picker.action_request_delete()
|
||||||
|
picker.action_request_delete()
|
||||||
|
|
||||||
|
assert len(posted_messages) == 1
|
||||||
|
assert_delete_state(picker, kind="pending", option_id="local:session-a")
|
||||||
|
assert option_list.replaced_prompts[-1].option_id == "local:session-a"
|
||||||
|
assert "Deleting..." in option_list.replaced_prompts[-1].prompt.plain
|
||||||
|
|
||||||
|
def test_delete_request_shows_feedback_for_remote_sessions(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
option_list = FakeOptionList(highlighted_option_id="remote:session-c")
|
||||||
|
posted_messages: list[object] = []
|
||||||
|
monkeypatch.setattr(picker, "query_one", lambda _selector: option_list)
|
||||||
|
monkeypatch.setattr(picker, "post_message", posted_messages.append)
|
||||||
|
|
||||||
|
picker.action_request_delete()
|
||||||
|
|
||||||
|
assert_delete_state(picker, kind="feedback", option_id="remote:session-c")
|
||||||
|
assert option_list.replaced_prompts[-1].option_id == "remote:session-c"
|
||||||
|
assert (
|
||||||
|
"Can't delete remote session"
|
||||||
|
in option_list.replaced_prompts[-1].prompt.plain
|
||||||
|
)
|
||||||
|
assert posted_messages == []
|
||||||
|
|
||||||
|
def test_delete_request_shows_feedback_for_current_session(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions,
|
||||||
|
latest_messages=sample_latest_messages,
|
||||||
|
current_session_id="session-a",
|
||||||
|
)
|
||||||
|
option_list = FakeOptionList(highlighted_option_id="local:session-a")
|
||||||
|
posted_messages: list[object] = []
|
||||||
|
monkeypatch.setattr(picker, "query_one", lambda _selector: option_list)
|
||||||
|
monkeypatch.setattr(picker, "post_message", posted_messages.append)
|
||||||
|
|
||||||
|
picker.action_request_delete()
|
||||||
|
|
||||||
|
assert_delete_state(picker, kind="feedback", option_id="local:session-a")
|
||||||
|
assert option_list.replaced_prompts[-1].option_id == "local:session-a"
|
||||||
|
assert (
|
||||||
|
"Can't delete current session"
|
||||||
|
in option_list.replaced_prompts[-1].prompt.plain
|
||||||
|
)
|
||||||
|
assert posted_messages == []
|
||||||
|
|
||||||
|
picker.action_request_delete()
|
||||||
|
|
||||||
|
assert_delete_state(picker, kind="feedback", option_id="local:session-a")
|
||||||
|
assert posted_messages == []
|
||||||
|
|
||||||
|
def test_pending_delete_blocks_resume_selection(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
option_list = FakeOptionList(highlighted_option_id="local:session-a")
|
||||||
|
posted_messages: list[object] = []
|
||||||
|
monkeypatch.setattr(picker, "query_one", lambda _selector: option_list)
|
||||||
|
monkeypatch.setattr(picker, "post_message", posted_messages.append)
|
||||||
|
|
||||||
|
picker.action_request_delete()
|
||||||
|
picker.action_request_delete()
|
||||||
|
picker.on_option_list_option_selected(
|
||||||
|
cast(OptionList.OptionSelected, FakeOptionEvent("local:session-a"))
|
||||||
|
)
|
||||||
|
picker.on_option_list_option_selected(
|
||||||
|
cast(OptionList.OptionSelected, FakeOptionEvent("local:session-b"))
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(posted_messages) == 1
|
||||||
|
assert isinstance(posted_messages[0], SessionPickerApp.SessionDeleteRequested)
|
||||||
|
assert_delete_state(picker, kind="pending", option_id="local:session-a")
|
||||||
|
|
||||||
|
def test_clear_pending_delete_restores_session_option(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
option_list = FakeOptionList(highlighted_option_id="local:session-a")
|
||||||
|
posted_messages: list[object] = []
|
||||||
|
monkeypatch.setattr(picker, "query_one", lambda _selector: option_list)
|
||||||
|
monkeypatch.setattr(picker, "post_message", posted_messages.append)
|
||||||
|
|
||||||
|
picker.action_request_delete()
|
||||||
|
picker.action_request_delete()
|
||||||
|
|
||||||
|
assert picker.clear_pending_delete("local:session-a") is True
|
||||||
|
assert picker._delete_state is None
|
||||||
|
assert option_list.replaced_prompts[-1].option_id == "local:session-a"
|
||||||
|
assert "Help me fix this bug" in option_list.replaced_prompts[-1].prompt.plain
|
||||||
|
|
||||||
|
def test_highlighting_another_session_clears_confirmation(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
option_list = FakeOptionList(highlighted_option_id="local:session-a")
|
||||||
|
monkeypatch.setattr(picker, "query_one", lambda _selector: option_list)
|
||||||
|
picker.action_request_delete()
|
||||||
|
|
||||||
|
picker.on_option_list_option_highlighted(
|
||||||
|
cast(OptionList.OptionHighlighted, FakeOptionEvent("local:session-b"))
|
||||||
|
)
|
||||||
|
|
||||||
|
assert picker._delete_state is None
|
||||||
|
assert option_list.replaced_prompts[-1].option_id == "local:session-a"
|
||||||
|
assert "Help me fix this bug" in option_list.replaced_prompts[-1].prompt.plain
|
||||||
|
|
||||||
|
def test_highlighting_another_session_clears_delete_feedback(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
option_list = FakeOptionList(highlighted_option_id="remote:session-c")
|
||||||
|
monkeypatch.setattr(picker, "query_one", lambda _selector: option_list)
|
||||||
|
picker.action_request_delete()
|
||||||
|
|
||||||
|
picker.on_option_list_option_highlighted(
|
||||||
|
cast(OptionList.OptionHighlighted, FakeOptionEvent("local:session-b"))
|
||||||
|
)
|
||||||
|
|
||||||
|
assert picker._delete_state is None
|
||||||
|
assert option_list.replaced_prompts[-1].option_id == "remote:session-c"
|
||||||
|
assert (
|
||||||
|
"Add unit tests for the API"
|
||||||
|
in option_list.replaced_prompts[-1].prompt.plain
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_escape_clears_confirmation_before_cancelling(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
option_list = FakeOptionList(highlighted_option_id="local:session-a")
|
||||||
|
posted_messages: list[object] = []
|
||||||
|
monkeypatch.setattr(picker, "query_one", lambda _selector: option_list)
|
||||||
|
monkeypatch.setattr(picker, "post_message", posted_messages.append)
|
||||||
|
picker.action_request_delete()
|
||||||
|
|
||||||
|
picker.action_cancel()
|
||||||
|
|
||||||
|
assert picker._delete_state is None
|
||||||
|
assert posted_messages == []
|
||||||
|
|
||||||
|
picker.action_cancel()
|
||||||
|
|
||||||
|
assert len(posted_messages) == 1
|
||||||
|
assert isinstance(posted_messages[0], SessionPickerApp.Cancelled)
|
||||||
|
|
||||||
|
def test_escape_clears_delete_feedback_before_cancelling(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
option_list = FakeOptionList(highlighted_option_id="remote:session-c")
|
||||||
|
posted_messages: list[object] = []
|
||||||
|
monkeypatch.setattr(picker, "query_one", lambda _selector: option_list)
|
||||||
|
monkeypatch.setattr(picker, "post_message", posted_messages.append)
|
||||||
|
picker.action_request_delete()
|
||||||
|
|
||||||
|
picker.action_cancel()
|
||||||
|
|
||||||
|
assert picker._delete_state is None
|
||||||
|
assert posted_messages == []
|
||||||
|
|
||||||
|
picker.action_cancel()
|
||||||
|
|
||||||
|
assert len(posted_messages) == 1
|
||||||
|
assert isinstance(posted_messages[0], SessionPickerApp.Cancelled)
|
||||||
|
|
||||||
|
def test_remove_session_updates_picker_state(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
option_list = FakeOptionList(highlighted_option_id="local:session-a")
|
||||||
|
monkeypatch.setattr(picker, "query_one", lambda _selector: option_list)
|
||||||
|
picker.action_request_delete()
|
||||||
|
assert_delete_state(picker, kind="confirmation", option_id="local:session-a")
|
||||||
|
|
||||||
|
assert picker.remove_session("local:session-a") is True
|
||||||
|
|
||||||
|
assert [session.option_id for session in picker._sessions] == [
|
||||||
|
"local:session-b",
|
||||||
|
"remote:session-c",
|
||||||
|
]
|
||||||
|
assert "local:session-a" not in picker._latest_messages
|
||||||
|
assert picker._delete_state is None
|
||||||
|
assert option_list.removed_option_ids == ["local:session-a"]
|
||||||
|
|
||||||
|
def test_remove_missing_session_returns_false(
|
||||||
|
self,
|
||||||
|
sample_sessions: list[ResumeSessionInfo],
|
||||||
|
sample_latest_messages: dict[str, str],
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
picker = SessionPickerApp(
|
||||||
|
sessions=sample_sessions, latest_messages=sample_latest_messages
|
||||||
|
)
|
||||||
|
option_list = FakeOptionList()
|
||||||
|
monkeypatch.setattr(picker, "query_one", lambda _selector: option_list)
|
||||||
|
|
||||||
|
assert picker.remove_session("local:missing") is False
|
||||||
|
|
||||||
|
assert picker._sessions == sample_sessions
|
||||||
|
assert picker._latest_messages == sample_latest_messages
|
||||||
|
assert option_list.removed_option_ids == []
|
||||||
|
|
||||||
|
|
||||||
|
class FakeOption:
|
||||||
|
def __init__(self, option_id: str) -> None:
|
||||||
|
self.id = option_id
|
||||||
|
|
||||||
|
|
||||||
|
class FakeOptionEvent:
|
||||||
|
def __init__(self, option_id: str) -> None:
|
||||||
|
self.option = FakeOption(option_id)
|
||||||
|
|
||||||
|
|
||||||
|
class ReplacedPrompt:
|
||||||
|
def __init__(self, option_id: str, prompt: Text) -> None:
|
||||||
|
self.option_id = option_id
|
||||||
|
self.prompt = prompt
|
||||||
|
|
||||||
|
|
||||||
|
class FakeOptionList:
|
||||||
|
def __init__(self, highlighted_option_id: str | None = None) -> None:
|
||||||
|
self.highlighted_option = (
|
||||||
|
FakeOption(highlighted_option_id)
|
||||||
|
if highlighted_option_id is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
self.removed_option_ids: list[str] = []
|
||||||
|
self.replaced_prompts: list[ReplacedPrompt] = []
|
||||||
|
|
||||||
|
def remove_option(self, option_id: str) -> None:
|
||||||
|
self.removed_option_ids.append(option_id)
|
||||||
|
|
||||||
|
def replace_option_prompt(self, option_id: str, prompt: Text) -> None:
|
||||||
|
self.replaced_prompts.append(ReplacedPrompt(option_id, prompt))
|
||||||
|
|
|
||||||
75
tests/cli/textual_ui/test_terminal_input_filter.py
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from textual import events
|
||||||
|
|
||||||
|
from vibe.cli.textual_ui.terminal_input_filter import (
|
||||||
|
FilteringXTermParser,
|
||||||
|
patch_driver_parser,
|
||||||
|
strip_malformed_mouse,
|
||||||
|
)
|
||||||
|
|
||||||
|
NOISE = [
|
||||||
|
"\x1b[<32;NaN;NaNM", # malformed SGR mouse (extended button on focus change)
|
||||||
|
"\x1b[<35;NaN;NaNm",
|
||||||
|
]
|
||||||
|
|
||||||
|
PRESERVED = [
|
||||||
|
"\x1b[A", # arrow up
|
||||||
|
"\x1b[15~", # F5
|
||||||
|
"\x1b[97u", # plain kitty key 'a'
|
||||||
|
"\x1b[<0;5;5M", # valid mouse down
|
||||||
|
"\x1b[<0;5;5m", # valid mouse up
|
||||||
|
"\x1b[<128;10;10M", # valid extended-button mouse
|
||||||
|
"\x1b[<0;-1;-1M", # negative SGR-Pixels coords (Textual handles these)
|
||||||
|
"\x1b[24;80R", # cursor position report
|
||||||
|
"\x1b[I", # focus in
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("seq", NOISE)
|
||||||
|
def test_strip_removes_malformed_mouse(seq: str) -> None:
|
||||||
|
assert strip_malformed_mouse(seq) == ""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("seq", PRESERVED)
|
||||||
|
def test_strip_preserves_real_input(seq: str) -> None:
|
||||||
|
assert strip_malformed_mouse(seq) == seq
|
||||||
|
|
||||||
|
|
||||||
|
def test_strip_keeps_real_key_after_noise() -> None:
|
||||||
|
assert strip_malformed_mouse("\x1b[<32;NaN;NaNM\x1b[A") == "\x1b[A"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("seq", NOISE)
|
||||||
|
def test_parser_emits_no_keys_for_noise(seq: str) -> None:
|
||||||
|
tokens = list(FilteringXTermParser().feed(seq))
|
||||||
|
assert [t for t in tokens if isinstance(t, events.Key) and t.character] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_parser_still_emits_key_for_arrow() -> None:
|
||||||
|
tokens = list(FilteringXTermParser().feed("\x1b[A"))
|
||||||
|
assert any(isinstance(t, events.Key) and t.key == "up" for t in tokens)
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_noise_chunk_does_not_trip_eof() -> None:
|
||||||
|
parser = FilteringXTermParser()
|
||||||
|
assert list(parser.feed("\x1b[<32;NaN;NaNM")) == []
|
||||||
|
# The parser must still be alive for the next real chunk.
|
||||||
|
assert any(
|
||||||
|
isinstance(t, events.Key) and t.key == "up" for t in parser.feed("\x1b[A")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_patch_driver_parser_rebinds_module_global() -> None:
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from textual.drivers.linux_driver import LinuxDriver
|
||||||
|
|
||||||
|
namespace = sys.modules[LinuxDriver.__module__].__dict__
|
||||||
|
original = namespace["XTermParser"]
|
||||||
|
try:
|
||||||
|
patch_driver_parser(LinuxDriver)
|
||||||
|
assert namespace["XTermParser"] is FilteringXTermParser
|
||||||
|
finally:
|
||||||
|
namespace["XTermParser"] = original
|
||||||
|
|
@ -52,7 +52,6 @@ def test_resumed_user_message_with_images_renders_footer(tmp_path: Path) -> None
|
||||||
[msg],
|
[msg],
|
||||||
tool_call_map={},
|
tool_call_map={},
|
||||||
start_index=0,
|
start_index=0,
|
||||||
tools_collapsed=False,
|
|
||||||
history_widget_indices=WeakKeyDictionary(),
|
history_widget_indices=WeakKeyDictionary(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -70,7 +69,6 @@ def test_resumed_user_message_with_images_only_still_mounts(tmp_path: Path) -> N
|
||||||
[msg],
|
[msg],
|
||||||
tool_call_map={},
|
tool_call_map={},
|
||||||
start_index=0,
|
start_index=0,
|
||||||
tools_collapsed=False,
|
|
||||||
history_widget_indices=WeakKeyDictionary(),
|
history_widget_indices=WeakKeyDictionary(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,8 +111,25 @@ class TestAgentManager:
|
||||||
include_prompt_detail=False,
|
include_prompt_detail=False,
|
||||||
disabled_agents=["plan"],
|
disabled_agents=["plan"],
|
||||||
)
|
)
|
||||||
with pytest.raises(ValueError, match="not available"):
|
with pytest.raises(ValueError, match="disabled_agents") as exc_info:
|
||||||
AgentManager(lambda: config, initial_agent="plan")
|
AgentManager(lambda: config, initial_agent="plan")
|
||||||
|
message = str(exc_info.value)
|
||||||
|
assert "default_agent" not in message
|
||||||
|
assert message.startswith("Agent 'plan'")
|
||||||
|
|
||||||
|
def test_explicit_agent_excluded_by_enabled_agents_does_not_blame_default(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
config = build_test_vibe_config(
|
||||||
|
include_project_context=False,
|
||||||
|
include_prompt_detail=False,
|
||||||
|
enabled_agents=["default"],
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="enabled_agents") as exc_info:
|
||||||
|
AgentManager(lambda: config, initial_agent="plan")
|
||||||
|
message = str(exc_info.value)
|
||||||
|
assert "default_agent" not in message
|
||||||
|
assert message.startswith("Agent 'plan'")
|
||||||
|
|
||||||
def test_initial_agent_raises_when_agent_does_not_exist(self) -> None:
|
def test_initial_agent_raises_when_agent_does_not_exist(self) -> None:
|
||||||
config = build_test_vibe_config(
|
config = build_test_vibe_config(
|
||||||
|
|
@ -120,3 +137,55 @@ class TestAgentManager:
|
||||||
)
|
)
|
||||||
with pytest.raises(ValueError, match="not found"):
|
with pytest.raises(ValueError, match="not found"):
|
||||||
AgentManager(lambda: config, initial_agent="nonexistent-agent")
|
AgentManager(lambda: config, initial_agent="nonexistent-agent")
|
||||||
|
|
||||||
|
def test_default_agent_excluded_by_enabled_agents_raises_config_contradiction(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
config = build_test_vibe_config(
|
||||||
|
include_project_context=False,
|
||||||
|
include_prompt_detail=False,
|
||||||
|
enabled_agents=["plan"],
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="enabled_agents") as exc_info:
|
||||||
|
AgentManager(lambda: config)
|
||||||
|
message = str(exc_info.value)
|
||||||
|
assert "default" in message
|
||||||
|
assert "default_agent" in message
|
||||||
|
|
||||||
|
def test_default_agent_excluded_by_disabled_agents_raises_config_contradiction(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
config = build_test_vibe_config(
|
||||||
|
include_project_context=False,
|
||||||
|
include_prompt_detail=False,
|
||||||
|
disabled_agents=["default"],
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="disabled_agents") as exc_info:
|
||||||
|
AgentManager(lambda: config)
|
||||||
|
assert "default_agent" in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_disabled_agents_ignored_entirely_when_enabled_agents_set(
|
||||||
|
self, caplog: pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
config = build_test_vibe_config(
|
||||||
|
include_project_context=False,
|
||||||
|
include_prompt_detail=False,
|
||||||
|
enabled_agents=["plan"],
|
||||||
|
disabled_agents=["plan"],
|
||||||
|
)
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
manager = AgentManager(lambda: config, initial_agent="plan")
|
||||||
|
assert manager.active_profile.name == "plan"
|
||||||
|
assert caplog.text == ""
|
||||||
|
|
||||||
|
def test_install_required_agent_reports_install_not_disabled_agents(self) -> None:
|
||||||
|
# 'lean' is install_required and enabled but not installed: the message
|
||||||
|
# must point to installation, not blame disabled_agents.
|
||||||
|
config = build_test_vibe_config(
|
||||||
|
include_project_context=False,
|
||||||
|
include_prompt_detail=False,
|
||||||
|
enabled_agents=["lean"],
|
||||||
|
)
|
||||||
|
with pytest.raises(ValueError, match="requires installation") as exc_info:
|
||||||
|
AgentManager(lambda: config, initial_agent="lean")
|
||||||
|
assert "disabled_agents" not in str(exc_info.value)
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import asdict
|
|
||||||
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
||||||
|
|
||||||
from vibe.core.auth import EncryptedPayload, decrypt, encrypt
|
|
||||||
|
|
||||||
|
|
||||||
def _generate_test_key_pair() -> tuple[bytes, bytes]:
|
|
||||||
private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
|
|
||||||
private_pem = private_key.private_bytes(
|
|
||||||
encoding=serialization.Encoding.PEM,
|
|
||||||
format=serialization.PrivateFormat.PKCS8,
|
|
||||||
encryption_algorithm=serialization.NoEncryption(),
|
|
||||||
)
|
|
||||||
public_pem = private_key.public_key().public_bytes(
|
|
||||||
encoding=serialization.Encoding.PEM,
|
|
||||||
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
||||||
)
|
|
||||||
return private_pem, public_pem
|
|
||||||
|
|
||||||
|
|
||||||
class TestEncryptDecrypt:
|
|
||||||
def test_encrypt_decrypt_roundtrip(self) -> None:
|
|
||||||
private_pem, public_pem = _generate_test_key_pair()
|
|
||||||
|
|
||||||
plaintext = "ghp_test_token_12345"
|
|
||||||
encrypted = encrypt(plaintext, public_pem)
|
|
||||||
|
|
||||||
assert encrypted.encrypted_key != plaintext
|
|
||||||
assert encrypted.ciphertext != plaintext
|
|
||||||
|
|
||||||
decrypted = decrypt(encrypted, private_pem)
|
|
||||||
assert decrypted == plaintext
|
|
||||||
|
|
||||||
def test_encrypted_payload_serialization(self) -> None:
|
|
||||||
payload = EncryptedPayload(
|
|
||||||
encrypted_key="enc_key", nonce="nonce123", ciphertext="cipher"
|
|
||||||
)
|
|
||||||
|
|
||||||
data = asdict(payload)
|
|
||||||
restored = EncryptedPayload(**data)
|
|
||||||
|
|
||||||
assert restored == payload
|
|
||||||
|
|
@ -1,286 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
|
||||||
|
|
||||||
import httpx
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from vibe.core.auth.github import (
|
|
||||||
DeviceFlowHandle,
|
|
||||||
DeviceFlowInfo,
|
|
||||||
GitHubAuthError,
|
|
||||||
GitHubAuthProvider,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestDeviceFlowModels:
|
|
||||||
def test_device_flow_info(self) -> None:
|
|
||||||
info = DeviceFlowInfo(
|
|
||||||
user_code="ABC-123", verification_uri="https://example.com"
|
|
||||||
)
|
|
||||||
assert info.user_code == "ABC-123"
|
|
||||||
assert info.verification_uri == "https://example.com"
|
|
||||||
|
|
||||||
def test_device_flow_handle(self) -> None:
|
|
||||||
info = DeviceFlowInfo(
|
|
||||||
user_code="ABC-123", verification_uri="https://example.com"
|
|
||||||
)
|
|
||||||
handle = DeviceFlowHandle(device_code="dc_123", expires_in=900, info=info)
|
|
||||||
assert handle.device_code == "dc_123"
|
|
||||||
assert handle.expires_in == 900
|
|
||||||
assert handle.info.user_code == "ABC-123"
|
|
||||||
|
|
||||||
|
|
||||||
class TestGitHubAuthProviderContextManager:
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_creates_client_on_enter(self) -> None:
|
|
||||||
provider = GitHubAuthProvider()
|
|
||||||
assert provider._client is None
|
|
||||||
async with provider:
|
|
||||||
assert provider._client is not None
|
|
||||||
assert provider._client is None
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_uses_provided_client(self) -> None:
|
|
||||||
external_client = httpx.AsyncClient()
|
|
||||||
provider = GitHubAuthProvider(client=external_client)
|
|
||||||
async with provider:
|
|
||||||
assert provider._client is external_client
|
|
||||||
assert provider._client is external_client
|
|
||||||
await external_client.aclose()
|
|
||||||
|
|
||||||
|
|
||||||
class TestGitHubAuthProviderGetToken:
|
|
||||||
def test_returns_token_from_keyring(self) -> None:
|
|
||||||
with patch("vibe.core.auth.github.keyring") as mock_keyring:
|
|
||||||
mock_keyring.get_password.return_value = "ghp_test_token"
|
|
||||||
provider = GitHubAuthProvider()
|
|
||||||
token = provider.get_token()
|
|
||||||
assert token == "ghp_test_token"
|
|
||||||
|
|
||||||
def test_returns_none_on_keyring_error(self) -> None:
|
|
||||||
with patch("vibe.core.auth.github.keyring") as mock_keyring:
|
|
||||||
import keyring.errors
|
|
||||||
|
|
||||||
mock_keyring.errors = keyring.errors
|
|
||||||
mock_keyring.get_password.side_effect = keyring.errors.KeyringError("error")
|
|
||||||
provider = GitHubAuthProvider()
|
|
||||||
token = provider.get_token()
|
|
||||||
assert token is None
|
|
||||||
|
|
||||||
def test_returns_none_when_no_token(self) -> None:
|
|
||||||
with patch("vibe.core.auth.github.keyring") as mock_keyring:
|
|
||||||
mock_keyring.get_password.return_value = None
|
|
||||||
provider = GitHubAuthProvider()
|
|
||||||
token = provider.get_token()
|
|
||||||
assert token is None
|
|
||||||
|
|
||||||
|
|
||||||
class TestGitHubAuthProviderHasToken:
|
|
||||||
def test_returns_true_when_token_exists(self) -> None:
|
|
||||||
with patch("vibe.core.auth.github.keyring") as mock_keyring:
|
|
||||||
mock_keyring.get_password.return_value = "ghp_token"
|
|
||||||
provider = GitHubAuthProvider()
|
|
||||||
assert provider.has_token() is True
|
|
||||||
|
|
||||||
def test_returns_false_when_no_token(self) -> None:
|
|
||||||
with patch("vibe.core.auth.github.keyring") as mock_keyring:
|
|
||||||
mock_keyring.get_password.return_value = None
|
|
||||||
provider = GitHubAuthProvider()
|
|
||||||
assert provider.has_token() is False
|
|
||||||
|
|
||||||
|
|
||||||
class TestGitHubAuthProviderStartDeviceFlow:
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_client(self) -> MagicMock:
|
|
||||||
return MagicMock(spec=httpx.AsyncClient)
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def provider(self, mock_client: MagicMock) -> GitHubAuthProvider:
|
|
||||||
return GitHubAuthProvider(client=mock_client)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_device_flow_success(
|
|
||||||
self, provider: GitHubAuthProvider, mock_client: MagicMock
|
|
||||||
) -> None:
|
|
||||||
mock_response = MagicMock()
|
|
||||||
mock_response.is_success = True
|
|
||||||
mock_response.json.return_value = {
|
|
||||||
"device_code": "dc_123",
|
|
||||||
"user_code": "ABC-123",
|
|
||||||
"verification_uri": "https://github.com/login/device",
|
|
||||||
"expires_in": 900,
|
|
||||||
}
|
|
||||||
mock_client.post = AsyncMock(return_value=mock_response)
|
|
||||||
|
|
||||||
with patch("vibe.core.auth.github.webbrowser") as mock_browser:
|
|
||||||
handle = await provider.start_device_flow(open_browser=True)
|
|
||||||
mock_browser.open.assert_called_once_with("https://github.com/login/device")
|
|
||||||
|
|
||||||
assert handle.device_code == "dc_123"
|
|
||||||
assert handle.info.user_code == "ABC-123"
|
|
||||||
assert handle.expires_in == 900
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_device_flow_without_browser(
|
|
||||||
self, provider: GitHubAuthProvider, mock_client: MagicMock
|
|
||||||
) -> None:
|
|
||||||
mock_response = MagicMock()
|
|
||||||
mock_response.is_success = True
|
|
||||||
mock_response.json.return_value = {
|
|
||||||
"device_code": "dc_123",
|
|
||||||
"user_code": "ABC-123",
|
|
||||||
"verification_uri": "https://github.com/login/device",
|
|
||||||
"expires_in": 900,
|
|
||||||
}
|
|
||||||
mock_client.post = AsyncMock(return_value=mock_response)
|
|
||||||
|
|
||||||
with patch("vibe.core.auth.github.webbrowser") as mock_browser:
|
|
||||||
await provider.start_device_flow(open_browser=False)
|
|
||||||
mock_browser.open.assert_not_called()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_device_flow_failure(
|
|
||||||
self, provider: GitHubAuthProvider, mock_client: MagicMock
|
|
||||||
) -> None:
|
|
||||||
mock_response = MagicMock()
|
|
||||||
mock_response.is_success = False
|
|
||||||
mock_response.text = "Bad request"
|
|
||||||
mock_client.post = AsyncMock(return_value=mock_response)
|
|
||||||
|
|
||||||
with pytest.raises(GitHubAuthError, match="Failed to initiate device flow"):
|
|
||||||
await provider.start_device_flow()
|
|
||||||
|
|
||||||
|
|
||||||
class TestGitHubAuthProviderPollForToken:
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_client(self) -> MagicMock:
|
|
||||||
return MagicMock(spec=httpx.AsyncClient)
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def provider(self, mock_client: MagicMock) -> GitHubAuthProvider:
|
|
||||||
return GitHubAuthProvider(client=mock_client)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_poll_returns_token_on_success(
|
|
||||||
self, provider: GitHubAuthProvider, mock_client: MagicMock
|
|
||||||
) -> None:
|
|
||||||
mock_response = MagicMock()
|
|
||||||
mock_response.json.return_value = {"access_token": "ghp_new_token"}
|
|
||||||
mock_client.post = AsyncMock(return_value=mock_response)
|
|
||||||
|
|
||||||
with patch("vibe.core.auth.github.asyncio.sleep", new_callable=AsyncMock):
|
|
||||||
token = await provider._poll_for_token(
|
|
||||||
mock_client, "dc_123", expires_in=10, interval=1
|
|
||||||
)
|
|
||||||
assert token == "ghp_new_token"
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_poll_handles_slow_down(
|
|
||||||
self, provider: GitHubAuthProvider, mock_client: MagicMock
|
|
||||||
) -> None:
|
|
||||||
responses = [
|
|
||||||
MagicMock(
|
|
||||||
json=MagicMock(return_value={"error": "slow_down", "interval": 5})
|
|
||||||
),
|
|
||||||
MagicMock(json=MagicMock(return_value={"access_token": "ghp_token"})),
|
|
||||||
]
|
|
||||||
mock_client.post = AsyncMock(side_effect=responses)
|
|
||||||
|
|
||||||
with patch("vibe.core.auth.github.asyncio.sleep", new_callable=AsyncMock):
|
|
||||||
token = await provider._poll_for_token(
|
|
||||||
mock_client, "dc_123", expires_in=30, interval=1
|
|
||||||
)
|
|
||||||
assert token == "ghp_token"
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_poll_raises_on_expired_token(
|
|
||||||
self, provider: GitHubAuthProvider, mock_client: MagicMock
|
|
||||||
) -> None:
|
|
||||||
mock_response = MagicMock()
|
|
||||||
mock_response.json.return_value = {"error": "expired_token"}
|
|
||||||
mock_client.post = AsyncMock(return_value=mock_response)
|
|
||||||
|
|
||||||
with patch("vibe.core.auth.github.asyncio.sleep", new_callable=AsyncMock):
|
|
||||||
with pytest.raises(GitHubAuthError, match="expired_token"):
|
|
||||||
await provider._poll_for_token(
|
|
||||||
mock_client, "dc_123", expires_in=10, interval=1
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_poll_raises_on_access_denied(
|
|
||||||
self, provider: GitHubAuthProvider, mock_client: MagicMock
|
|
||||||
) -> None:
|
|
||||||
mock_response = MagicMock()
|
|
||||||
mock_response.json.return_value = {"error": "access_denied"}
|
|
||||||
mock_client.post = AsyncMock(return_value=mock_response)
|
|
||||||
|
|
||||||
with patch("vibe.core.auth.github.asyncio.sleep", new_callable=AsyncMock):
|
|
||||||
with pytest.raises(GitHubAuthError, match="access_denied"):
|
|
||||||
await provider._poll_for_token(
|
|
||||||
mock_client, "dc_123", expires_in=10, interval=1
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_poll_raises_on_timeout(
|
|
||||||
self, provider: GitHubAuthProvider, mock_client: MagicMock
|
|
||||||
) -> None:
|
|
||||||
mock_response = MagicMock()
|
|
||||||
mock_response.json.return_value = {"error": "authorization_pending"}
|
|
||||||
mock_client.post = AsyncMock(return_value=mock_response)
|
|
||||||
|
|
||||||
with patch("vibe.core.auth.github.asyncio.sleep", new_callable=AsyncMock):
|
|
||||||
with pytest.raises(GitHubAuthError, match="timed out"):
|
|
||||||
await provider._poll_for_token(
|
|
||||||
mock_client, "dc_123", expires_in=2, interval=1
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestGitHubAuthProviderSaveToken:
|
|
||||||
def test_save_token_success(self) -> None:
|
|
||||||
with patch("vibe.core.auth.github.keyring") as mock_keyring:
|
|
||||||
provider = GitHubAuthProvider()
|
|
||||||
provider._save_token("ghp_token")
|
|
||||||
mock_keyring.set_password.assert_called_once_with(
|
|
||||||
"vibe", "github_token", "ghp_token"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_save_token_raises_on_keyring_error(self) -> None:
|
|
||||||
with patch("vibe.core.auth.github.keyring") as mock_keyring:
|
|
||||||
import keyring.errors
|
|
||||||
|
|
||||||
mock_keyring.errors = keyring.errors
|
|
||||||
mock_keyring.set_password.side_effect = keyring.errors.KeyringError("error")
|
|
||||||
provider = GitHubAuthProvider()
|
|
||||||
with pytest.raises(GitHubAuthError, match="Failed to save token"):
|
|
||||||
provider._save_token("ghp_token")
|
|
||||||
|
|
||||||
|
|
||||||
class TestGitHubAuthProviderWaitForToken:
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_client(self) -> MagicMock:
|
|
||||||
return MagicMock(spec=httpx.AsyncClient)
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def provider(self, mock_client: MagicMock) -> GitHubAuthProvider:
|
|
||||||
return GitHubAuthProvider(client=mock_client)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_wait_for_token_polls_and_saves(
|
|
||||||
self, provider: GitHubAuthProvider, mock_client: MagicMock
|
|
||||||
) -> None:
|
|
||||||
mock_response = MagicMock()
|
|
||||||
mock_response.json.return_value = {"access_token": "ghp_token"}
|
|
||||||
mock_client.post = AsyncMock(return_value=mock_response)
|
|
||||||
|
|
||||||
info = DeviceFlowInfo(user_code="ABC", verification_uri="https://example.com")
|
|
||||||
handle = DeviceFlowHandle(device_code="dc_123", expires_in=10, info=info)
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch("vibe.core.auth.github.asyncio.sleep", new_callable=AsyncMock),
|
|
||||||
patch("vibe.core.auth.github.keyring") as mock_keyring,
|
|
||||||
):
|
|
||||||
token = await provider.wait_for_token(handle)
|
|
||||||
|
|
||||||
assert token == "ghp_token"
|
|
||||||
mock_keyring.set_password.assert_called_once()
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from vibe.core.compaction import collect_prior_user_messages
|
from vibe.core.compaction import (
|
||||||
|
collect_prior_user_messages,
|
||||||
|
parse_previous_user_messages,
|
||||||
|
render_compaction_context,
|
||||||
|
)
|
||||||
from vibe.core.types import LLMMessage, Role
|
from vibe.core.types import LLMMessage, Role
|
||||||
|
|
||||||
_PREFIX = "Another language model started to solve this problem"
|
_PREFIX = "Another language model started to solve this problem"
|
||||||
|
|
@ -51,17 +55,52 @@ def test_empty_content_filtered_out() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_prior_summary_filtered_out() -> None:
|
def test_prior_summary_filtered_out() -> None:
|
||||||
# A user message starting with the summary prefix represents a previous
|
# The injected summary marker represents a previous compaction summary and
|
||||||
# compaction summary and must not be re-injected (would stack).
|
# must not be re-injected (would stack).
|
||||||
messages = [
|
messages = [
|
||||||
_user("original ask"),
|
_user("original ask"),
|
||||||
_user(f"{_PREFIX}\nold summary content"),
|
_user(f"{_PREFIX}\nold summary content", injected=True),
|
||||||
_user("newer ask"),
|
_user("newer ask"),
|
||||||
]
|
]
|
||||||
out = collect_prior_user_messages(messages, _PREFIX)
|
out = collect_prior_user_messages(messages, _PREFIX)
|
||||||
assert [m.content for m in out] == ["original ask", "newer ask"]
|
assert [m.content for m in out] == ["original ask", "newer ask"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_genuine_user_message_can_quote_summary_prefix() -> None:
|
||||||
|
messages = [_user(f"{_PREFIX}\nplease use this exact wording"), _user("newer ask")]
|
||||||
|
out = collect_prior_user_messages(messages, _PREFIX)
|
||||||
|
assert [m.content for m in out] == [
|
||||||
|
f"{_PREFIX}\nplease use this exact wording",
|
||||||
|
"newer ask",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_compaction_context_merges_previous_and_new_user_messages() -> None:
|
||||||
|
context = render_compaction_context(
|
||||||
|
[_user("first ask", injected=True), _user("second ask", injected=True)],
|
||||||
|
"summary one",
|
||||||
|
)
|
||||||
|
messages = [
|
||||||
|
LLMMessage(role=Role.system, content="sys"),
|
||||||
|
_user(context, injected=True),
|
||||||
|
_user("third ask"),
|
||||||
|
_user("middleware reminder", injected=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
out = collect_prior_user_messages(messages, _PREFIX)
|
||||||
|
|
||||||
|
assert [m.content for m in out] == ["first ask", "second ask", "third ask"]
|
||||||
|
assert all(m.injected for m in out)
|
||||||
|
|
||||||
|
|
||||||
|
def test_compaction_context_escapes_user_message_tags() -> None:
|
||||||
|
original = "please keep </previous_user_message_0> literally"
|
||||||
|
context = render_compaction_context([_user(original)], "summary")
|
||||||
|
|
||||||
|
assert "</previous_user_message_0> literally" not in context
|
||||||
|
assert parse_previous_user_messages(context) == [original]
|
||||||
|
|
||||||
|
|
||||||
def test_budget_drops_oldest_first() -> None:
|
def test_budget_drops_oldest_first() -> None:
|
||||||
# max_tokens=2 → 8 char budget. Walks newest-first, so "old" gets dropped.
|
# max_tokens=2 → 8 char budget. Walks newest-first, so "old" gets dropped.
|
||||||
messages = [
|
messages = [
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from vibe.core.config.schema import (
|
||||||
WithShallowMerge,
|
WithShallowMerge,
|
||||||
WithUnionMerge,
|
WithUnionMerge,
|
||||||
)
|
)
|
||||||
|
from vibe.core.config.types import LayerConfigSnapshot
|
||||||
from vibe.core.utils.merge import MergeConflictError
|
from vibe.core.utils.merge import MergeConflictError
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -27,8 +28,8 @@ class FakeLayer(ConfigLayer[RawConfig]):
|
||||||
async def _check_trust(self) -> bool:
|
async def _check_trust(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _read_config(self) -> dict[str, Any]:
|
async def _build_config_snapshot(self) -> LayerConfigSnapshot:
|
||||||
return dict(self._data)
|
return LayerConfigSnapshot(data=dict(self._data), fingerprint="fp")
|
||||||
|
|
||||||
|
|
||||||
class UntrustedFakeLayer(FakeLayer):
|
class UntrustedFakeLayer(FakeLayer):
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ from vibe.core.config.layer import (
|
||||||
UntrustedLayerError,
|
UntrustedLayerError,
|
||||||
)
|
)
|
||||||
from vibe.core.config.patch import ConfigPatch
|
from vibe.core.config.patch import ConfigPatch
|
||||||
|
from vibe.core.config.types import ConcurrencyConflictError, LayerConfigSnapshot
|
||||||
|
|
||||||
|
|
||||||
class StubLayer(ConfigLayer[BaseModel]):
|
class StubLayer(ConfigLayer[BaseModel]):
|
||||||
|
|
@ -38,9 +39,11 @@ class StubLayer(ConfigLayer[BaseModel]):
|
||||||
async def _check_trust(self) -> bool:
|
async def _check_trust(self) -> bool:
|
||||||
return self._stub_trusted
|
return self._stub_trusted
|
||||||
|
|
||||||
async def _read_config(self) -> dict[str, Any]:
|
async def _build_config_snapshot(self) -> LayerConfigSnapshot:
|
||||||
self.read_count += 1
|
self.read_count += 1
|
||||||
return dict(self._data)
|
return LayerConfigSnapshot(
|
||||||
|
data=dict(self._data), fingerprint=f"fp-{self.read_count}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ObservableStubLayer(StubLayer):
|
class ObservableStubLayer(StubLayer):
|
||||||
|
|
@ -59,7 +62,7 @@ class SampleSchema(BaseModel):
|
||||||
count: int = 0
|
count: int = 0
|
||||||
|
|
||||||
|
|
||||||
def test_abstract_read_config_enforced() -> None:
|
def test_abstract_build_config_snapshot_enforced() -> None:
|
||||||
class IncompleteLayer(ConfigLayer[BaseModel]):
|
class IncompleteLayer(ConfigLayer[BaseModel]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -72,11 +75,21 @@ def test_repr() -> None:
|
||||||
assert repr(layer) == "StubLayer(name='my-layer')"
|
assert repr(layer) == "StubLayer(name='my-layer')"
|
||||||
|
|
||||||
|
|
||||||
|
def test_layer_config_snapshot_strips_fingerprint() -> None:
|
||||||
|
snapshot = LayerConfigSnapshot(data={}, fingerprint=" fp ")
|
||||||
|
assert snapshot.fingerprint == "fp"
|
||||||
|
|
||||||
|
|
||||||
|
def test_layer_config_snapshot_rejects_empty_fingerprint() -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
LayerConfigSnapshot(data={}, fingerprint=" ")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_default_check_trust_returns_false() -> None:
|
async def test_default_check_trust_returns_false() -> None:
|
||||||
class DefaultTrustLayer(ConfigLayer[BaseModel]):
|
class DefaultTrustLayer(ConfigLayer[BaseModel]):
|
||||||
async def _read_config(self) -> dict[str, Any]:
|
async def _build_config_snapshot(self) -> LayerConfigSnapshot:
|
||||||
return {}
|
return LayerConfigSnapshot(data={}, fingerprint="fp")
|
||||||
|
|
||||||
layer = DefaultTrustLayer(name="default")
|
layer = DefaultTrustLayer(name="default")
|
||||||
result = await layer.resolve_trust()
|
result = await layer.resolve_trust()
|
||||||
|
|
@ -224,6 +237,7 @@ async def test_load_returns_data() -> None:
|
||||||
result = await layer.load()
|
result = await layer.load()
|
||||||
assert isinstance(result, RawConfig)
|
assert isinstance(result, RawConfig)
|
||||||
assert result.model_extra == {"key": "value"}
|
assert result.model_extra == {"key": "value"}
|
||||||
|
assert layer.fingerprint == "fp-1"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -233,6 +247,10 @@ async def test_load_auto_resolves_trust() -> None:
|
||||||
result = await layer.load()
|
result = await layer.load()
|
||||||
assert layer.is_trusted is True
|
assert layer.is_trusted is True
|
||||||
assert result.model_extra == {"a": 1}
|
assert result.model_extra == {"a": 1}
|
||||||
|
assert layer.fingerprint == "fp-1"
|
||||||
|
|
||||||
|
await layer.resolve_trust()
|
||||||
|
assert layer.fingerprint == "fp-1"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -251,6 +269,7 @@ async def test_load_force_bypasses_cache() -> None:
|
||||||
assert layer.read_count == 1
|
assert layer.read_count == 1
|
||||||
await layer.load(force=True)
|
await layer.load(force=True)
|
||||||
assert layer.read_count == 2
|
assert layer.read_count == 2
|
||||||
|
assert layer.fingerprint == "fp-2"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -312,6 +331,7 @@ async def test_resolve_trust_clears_data_on_revocation() -> None:
|
||||||
layer._stub_trusted = False
|
layer._stub_trusted = False
|
||||||
await layer.resolve_trust()
|
await layer.resolve_trust()
|
||||||
assert layer.is_trusted is False
|
assert layer.is_trusted is False
|
||||||
|
assert layer.fingerprint is None
|
||||||
|
|
||||||
# Re-trust and update backing data while revoked
|
# Re-trust and update backing data while revoked
|
||||||
layer._stub_trusted = True
|
layer._stub_trusted = True
|
||||||
|
|
@ -335,17 +355,30 @@ async def test_load_returns_deep_copy() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_read_config_failure_wrapped() -> None:
|
async def test_build_config_snapshot_failure_wrapped() -> None:
|
||||||
class BrokenReadLayer(StubLayer):
|
class BrokenReadLayer(StubLayer):
|
||||||
async def _read_config(self) -> dict[str, Any]:
|
async def _build_config_snapshot(self) -> LayerConfigSnapshot:
|
||||||
raise OSError("config file missing")
|
raise OSError("config file missing")
|
||||||
|
|
||||||
layer = BrokenReadLayer()
|
layer = BrokenReadLayer()
|
||||||
with pytest.raises(LayerImplementationError, match="_read_config") as exc_info:
|
with pytest.raises(
|
||||||
|
LayerImplementationError, match="_build_config_snapshot"
|
||||||
|
) as exc_info:
|
||||||
await layer.load()
|
await layer.load()
|
||||||
assert isinstance(exc_info.value.__cause__, IOError)
|
assert isinstance(exc_info.value.__cause__, IOError)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_build_config_snapshot_concurrency_conflict_propagates() -> None:
|
||||||
|
class ConflictingReadLayer(StubLayer):
|
||||||
|
async def _build_config_snapshot(self) -> LayerConfigSnapshot:
|
||||||
|
raise ConcurrencyConflictError(expected_fp="before", actual_fp="after")
|
||||||
|
|
||||||
|
layer = ConflictingReadLayer()
|
||||||
|
with pytest.raises(ConcurrencyConflictError):
|
||||||
|
await layer.load()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_default_schema_preserves_extras() -> None:
|
async def test_default_schema_preserves_extras() -> None:
|
||||||
layer = StubLayer(data={"anything": "goes"})
|
layer = StubLayer(data={"anything": "goes"})
|
||||||
|
|
@ -364,10 +397,13 @@ async def test_custom_schema_validates() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_invalid_data_raises_validation_error() -> None:
|
async def test_invalid_data_raises_layer_implementation_error() -> None:
|
||||||
layer = StubLayer(output_schema=SampleSchema, data={"count": "bad"})
|
layer = StubLayer(output_schema=SampleSchema, data={"count": "bad"})
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(
|
||||||
|
LayerImplementationError, match="_build_config_snapshot"
|
||||||
|
) as exc_info:
|
||||||
await layer.load()
|
await layer.load()
|
||||||
|
assert isinstance(exc_info.value.__cause__, ValidationError)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -380,10 +416,12 @@ async def test_concurrent_loads_serialize() -> None:
|
||||||
async def _check_trust(self) -> bool:
|
async def _check_trust(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _read_config(self) -> dict[str, Any]:
|
async def _build_config_snapshot(self) -> LayerConfigSnapshot:
|
||||||
self.read_count += 1
|
self.read_count += 1
|
||||||
await asyncio.sleep(0.05)
|
await asyncio.sleep(0.05)
|
||||||
return {"v": self.read_count}
|
return LayerConfigSnapshot(
|
||||||
|
data={"v": self.read_count}, fingerprint=f"fp-{self.read_count}"
|
||||||
|
)
|
||||||
|
|
||||||
layer = SlowLayer()
|
layer = SlowLayer()
|
||||||
results = await asyncio.gather(layer.load(), layer.load(), layer.load())
|
results = await asyncio.gather(layer.load(), layer.load(), layer.load())
|
||||||
|
|
@ -392,10 +430,9 @@ async def test_concurrent_loads_serialize() -> None:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_fingerprint_not_implemented() -> None:
|
async def test_fingerprint_returns_none_before_load() -> None:
|
||||||
layer = StubLayer()
|
layer = StubLayer()
|
||||||
with pytest.raises(NotImplementedError):
|
assert layer.fingerprint is None
|
||||||
await layer.get_fingerprint()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -459,8 +496,8 @@ class FakeLocalUserLayer(ConfigLayer[UserConfigSchema]):
|
||||||
async def _check_trust(self) -> bool:
|
async def _check_trust(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _read_config(self) -> dict[str, Any]:
|
async def _build_config_snapshot(self) -> LayerConfigSnapshot:
|
||||||
return dict(self._data)
|
return LayerConfigSnapshot(data=dict(self._data), fingerprint="user-fp")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -504,8 +541,8 @@ class FakeLocalProjectLayer(ConfigLayer[BaseModel]):
|
||||||
else:
|
else:
|
||||||
self._trust_store.pop(self._project_path, None)
|
self._trust_store.pop(self._project_path, None)
|
||||||
|
|
||||||
async def _read_config(self) -> dict[str, Any]:
|
async def _build_config_snapshot(self) -> LayerConfigSnapshot:
|
||||||
return dict(self._data)
|
return LayerConfigSnapshot(data=dict(self._data), fingerprint="project-fp")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
||||||
213
tests/core/test_config_mcp_auth.py
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pydantic import ValidationError
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from vibe.core.config import MCPHttp, MCPOAuth, MCPStaticAuth, MCPStreamableHttp
|
||||||
|
from vibe.core.tools.mcp.registry import MCPRegistry
|
||||||
|
|
||||||
|
HTTP_TRANSPORTS = [
|
||||||
|
pytest.param(MCPHttp, "http", id="http"),
|
||||||
|
pytest.param(MCPStreamableHttp, "streamable-http", id="streamable-http"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||||
|
def test_legacy_top_level_keys_promote_to_static_auth(
|
||||||
|
cls: type[MCPHttp | MCPStreamableHttp],
|
||||||
|
transport: str,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setenv("MCP_TOKEN", "secret-token")
|
||||||
|
|
||||||
|
srv = cls.model_validate({
|
||||||
|
"name": "remote",
|
||||||
|
"transport": transport,
|
||||||
|
"url": "https://mcp.example.com",
|
||||||
|
"api_key_env": "MCP_TOKEN",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert isinstance(srv.auth, MCPStaticAuth)
|
||||||
|
assert srv.auth.api_key_env == "MCP_TOKEN"
|
||||||
|
assert srv.http_headers() == {"Authorization": "Bearer secret-token"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||||
|
def test_legacy_custom_header_and_format(
|
||||||
|
cls: type[MCPHttp | MCPStreamableHttp],
|
||||||
|
transport: str,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setenv("MCP_TOKEN", "k")
|
||||||
|
|
||||||
|
srv = cls.model_validate({
|
||||||
|
"name": "remote",
|
||||||
|
"transport": transport,
|
||||||
|
"url": "https://mcp.example.com",
|
||||||
|
"api_key_env": "MCP_TOKEN",
|
||||||
|
"api_key_header": "X-API-Key",
|
||||||
|
"api_key_format": "{token}",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert srv.http_headers() == {"X-API-Key": "k"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||||
|
def test_explicit_static_auth_round_trips(
|
||||||
|
cls: type[MCPHttp | MCPStreamableHttp], transport: str
|
||||||
|
) -> None:
|
||||||
|
srv = cls.model_validate({
|
||||||
|
"name": "remote",
|
||||||
|
"transport": transport,
|
||||||
|
"url": "https://mcp.example.com",
|
||||||
|
"auth": {"type": "static", "api_key_env": "X", "api_key_header": "X-API-Key"},
|
||||||
|
})
|
||||||
|
|
||||||
|
dumped = srv.model_dump()
|
||||||
|
rebuilt = cls.model_validate(dumped)
|
||||||
|
|
||||||
|
assert isinstance(rebuilt.auth, MCPStaticAuth)
|
||||||
|
assert rebuilt.auth.api_key_env == "X"
|
||||||
|
assert rebuilt.auth.api_key_header == "X-API-Key"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||||
|
def test_oauth_auth_parses(
|
||||||
|
cls: type[MCPHttp | MCPStreamableHttp], transport: str
|
||||||
|
) -> None:
|
||||||
|
srv = cls.model_validate({
|
||||||
|
"name": "linear",
|
||||||
|
"transport": transport,
|
||||||
|
"url": "https://mcp.linear.app/mcp",
|
||||||
|
"auth": {"type": "oauth", "scopes": ["read", "write"]},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert isinstance(srv.auth, MCPOAuth)
|
||||||
|
assert srv.auth.scopes == ["read", "write"]
|
||||||
|
assert srv.auth.redirect_port == 47823
|
||||||
|
assert srv.http_headers() == {}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||||
|
def test_mixing_legacy_keys_with_auth_block_is_rejected(
|
||||||
|
cls: type[MCPHttp | MCPStreamableHttp], transport: str
|
||||||
|
) -> None:
|
||||||
|
with pytest.raises(ValidationError, match="cannot mix top-level"):
|
||||||
|
cls.model_validate({
|
||||||
|
"name": "remote",
|
||||||
|
"transport": transport,
|
||||||
|
"url": "https://mcp.example.com",
|
||||||
|
"api_key_env": "LEGACY",
|
||||||
|
"auth": {"type": "static", "api_key_env": "NEW"},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||||
|
def test_default_auth_is_static(
|
||||||
|
cls: type[MCPHttp | MCPStreamableHttp], transport: str
|
||||||
|
) -> None:
|
||||||
|
srv = cls.model_validate({
|
||||||
|
"name": "remote",
|
||||||
|
"transport": transport,
|
||||||
|
"url": "https://mcp.example.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert isinstance(srv.auth, MCPStaticAuth)
|
||||||
|
assert srv.http_headers() == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_oauth_client_id_and_metadata_url_mutually_exclusive() -> None:
|
||||||
|
with pytest.raises(ValidationError, match="mutually exclusive"):
|
||||||
|
MCPOAuth.model_validate({
|
||||||
|
"type": "oauth",
|
||||||
|
"scopes": ["read"],
|
||||||
|
"client_id": "abc",
|
||||||
|
"client_metadata_url": "https://example.com/cm.json",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def test_oauth_client_metadata_url_must_be_http_url() -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
MCPOAuth.model_validate({
|
||||||
|
"type": "oauth",
|
||||||
|
"scopes": ["read"],
|
||||||
|
"client_metadata_url": "not-a-url",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def test_oauth_client_id_rejects_empty_string() -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
MCPOAuth.model_validate({"type": "oauth", "scopes": ["read"], "client_id": ""})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("port", [80, 1023, 0, 65536, 70000])
|
||||||
|
def test_oauth_redirect_port_out_of_range(port: int) -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
MCPOAuth.model_validate({
|
||||||
|
"type": "oauth",
|
||||||
|
"scopes": ["read"],
|
||||||
|
"redirect_port": port,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def test_oauth_redirect_port_inside_range() -> None:
|
||||||
|
auth = MCPOAuth.model_validate({
|
||||||
|
"type": "oauth",
|
||||||
|
"scopes": ["read"],
|
||||||
|
"redirect_port": 1024,
|
||||||
|
})
|
||||||
|
assert auth.redirect_port == 1024
|
||||||
|
|
||||||
|
|
||||||
|
def test_static_auth_forbids_extra_keys() -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
MCPStaticAuth.model_validate({"type": "static", "headerz": {}})
|
||||||
|
|
||||||
|
|
||||||
|
def test_oauth_forbids_extra_keys() -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
MCPOAuth.model_validate({"type": "oauth", "scopes": ["read"], "scope": "x"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_oauth_scopes_required() -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
MCPOAuth.model_validate({"type": "oauth"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_oauth_scopes_empty_list_allowed() -> None:
|
||||||
|
auth = MCPOAuth.model_validate({"type": "oauth", "scopes": []})
|
||||||
|
assert auth.scopes == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(("cls", "transport"), HTTP_TRANSPORTS)
|
||||||
|
async def test_registry_skips_oauth_servers_with_gated_warning(
|
||||||
|
cls: type[MCPHttp | MCPStreamableHttp],
|
||||||
|
transport: str,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
srv = cls.model_validate({
|
||||||
|
"name": "linear",
|
||||||
|
"transport": transport,
|
||||||
|
"url": "https://mcp.linear.app/mcp",
|
||||||
|
"auth": {"type": "oauth", "scopes": ["read"]},
|
||||||
|
})
|
||||||
|
registry = MCPRegistry()
|
||||||
|
|
||||||
|
with caplog.at_level(logging.WARNING, logger="vibe"):
|
||||||
|
first = await registry.get_tools_async([srv])
|
||||||
|
|
||||||
|
assert first == {}
|
||||||
|
assert (
|
||||||
|
"OAuth support for MCP servers is not yet enabled; coming in a future release"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
caplog.clear()
|
||||||
|
with caplog.at_level(logging.WARNING, logger="vibe"):
|
||||||
|
second = await registry.get_tools_async([srv])
|
||||||
|
|
||||||
|
assert second == {}
|
||||||
|
assert "OAuth support" not in caplog.text
|
||||||
|
|
@ -9,6 +9,7 @@ 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.patch import ConfigPatch
|
||||||
from vibe.core.config.schema import ConfigSchema, WithReplaceMerge
|
from vibe.core.config.schema import ConfigSchema, WithReplaceMerge
|
||||||
|
from vibe.core.config.types import LayerConfigSnapshot
|
||||||
|
|
||||||
|
|
||||||
class FakeLayer(ConfigLayer[RawConfig]):
|
class FakeLayer(ConfigLayer[RawConfig]):
|
||||||
|
|
@ -19,8 +20,8 @@ class FakeLayer(ConfigLayer[RawConfig]):
|
||||||
async def _check_trust(self) -> bool:
|
async def _check_trust(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _read_config(self) -> dict[str, Any]:
|
async def _build_config_snapshot(self) -> LayerConfigSnapshot:
|
||||||
return dict(self._data)
|
return LayerConfigSnapshot(data=dict(self._data), fingerprint="fp")
|
||||||
|
|
||||||
|
|
||||||
class SimpleSchema(ConfigSchema):
|
class SimpleSchema(ConfigSchema):
|
||||||
|
|
|
||||||
|
|
@ -342,7 +342,10 @@ class TestMigrateLeavesFindInBashAllowlist:
|
||||||
) -> None:
|
) -> None:
|
||||||
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||||
config_file = tmp_path / "config.toml"
|
config_file = tmp_path / "config.toml"
|
||||||
data = {"tools": {"bash": {"allowlist": ["echo", "ls"]}}}
|
data = {
|
||||||
|
"applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
|
||||||
|
"tools": {"bash": {"allowlist": ["echo", "ls"]}},
|
||||||
|
}
|
||||||
with config_file.open("wb") as f:
|
with config_file.open("wb") as f:
|
||||||
tomli_w.dump(data, f)
|
tomli_w.dump(data, f)
|
||||||
|
|
||||||
|
|
@ -359,7 +362,10 @@ class TestMigrateLeavesFindInBashAllowlist:
|
||||||
) -> None:
|
) -> None:
|
||||||
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||||
config_file = tmp_path / "config.toml"
|
config_file = tmp_path / "config.toml"
|
||||||
data = {"tools": {"bash": {"allowlist": ["echo", "find", "ls"]}}}
|
data = {
|
||||||
|
"applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
|
||||||
|
"tools": {"bash": {"allowlist": ["echo", "find", "ls"]}},
|
||||||
|
}
|
||||||
with config_file.open("wb") as f:
|
with config_file.open("wb") as f:
|
||||||
tomli_w.dump(data, f)
|
tomli_w.dump(data, f)
|
||||||
|
|
||||||
|
|
@ -396,7 +402,8 @@ class TestMigrateStripsBashAllowlistWildcardSuffix:
|
||||||
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||||
config_file = tmp_path / "config.toml"
|
config_file = tmp_path / "config.toml"
|
||||||
data = {
|
data = {
|
||||||
"tools": {"bash": {"allowlist": ["git commit *", "npm install *", "echo"]}}
|
"applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
|
||||||
|
"tools": {"bash": {"allowlist": ["git commit *", "npm install *", "echo"]}},
|
||||||
}
|
}
|
||||||
with config_file.open("wb") as f:
|
with config_file.open("wb") as f:
|
||||||
tomli_w.dump(data, f)
|
tomli_w.dump(data, f)
|
||||||
|
|
@ -420,7 +427,8 @@ class TestMigrateStripsBashAllowlistWildcardSuffix:
|
||||||
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||||
config_file = tmp_path / "config.toml"
|
config_file = tmp_path / "config.toml"
|
||||||
data = {
|
data = {
|
||||||
"tools": {"bash": {"allowlist": ["git commit *", "git commit", "find"]}}
|
"applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
|
||||||
|
"tools": {"bash": {"allowlist": ["git commit *", "git commit", "find"]}},
|
||||||
}
|
}
|
||||||
with config_file.open("wb") as f:
|
with config_file.open("wb") as f:
|
||||||
tomli_w.dump(data, f)
|
tomli_w.dump(data, f)
|
||||||
|
|
@ -438,7 +446,10 @@ class TestMigrateStripsBashAllowlistWildcardSuffix:
|
||||||
) -> None:
|
) -> None:
|
||||||
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||||
config_file = tmp_path / "config.toml"
|
config_file = tmp_path / "config.toml"
|
||||||
data = {"tools": {"bash": {"allowlist": ["echo", "find", "ls"]}}}
|
data = {
|
||||||
|
"applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
|
||||||
|
"tools": {"bash": {"allowlist": ["echo", "find", "ls"]}},
|
||||||
|
}
|
||||||
with config_file.open("wb") as f:
|
with config_file.open("wb") as f:
|
||||||
tomli_w.dump(data, f)
|
tomli_w.dump(data, f)
|
||||||
|
|
||||||
|
|
@ -451,6 +462,68 @@ class TestMigrateStripsBashAllowlistWildcardSuffix:
|
||||||
assert result["tools"]["bash"]["allowlist"] == ["echo", "find", "ls"]
|
assert result["tools"]["bash"]["allowlist"] == ["echo", "find", "ls"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestMigrateBashReadOnlyDefaults:
|
||||||
|
def test_merges_read_only_commands_into_existing_allowlist(
|
||||||
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
from vibe.core.tools.builtins.bash import default_read_only_commands
|
||||||
|
|
||||||
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||||
|
config_file = tmp_path / "config.toml"
|
||||||
|
data = {"tools": {"bash": {"allowlist": ["echo", "git commit"]}}}
|
||||||
|
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)
|
||||||
|
allowlist = result["tools"]["bash"]["allowlist"]
|
||||||
|
assert "git commit" in allowlist
|
||||||
|
for cmd in default_read_only_commands():
|
||||||
|
assert cmd in allowlist
|
||||||
|
assert VibeConfig._BASH_READ_ONLY_MIGRATION in result["applied_migrations"]
|
||||||
|
|
||||||
|
def test_does_not_readd_removed_command_after_migration(
|
||||||
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||||
|
config_file = tmp_path / "config.toml"
|
||||||
|
data = {
|
||||||
|
"applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
|
||||||
|
"tools": {"bash": {"allowlist": ["echo", "find"]}},
|
||||||
|
}
|
||||||
|
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"]["bash"]["allowlist"] == ["echo", "find"]
|
||||||
|
|
||||||
|
def test_noop_when_no_bash_allowlist(
|
||||||
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setenv("VIBE_HOME", str(tmp_path))
|
||||||
|
config_file = tmp_path / "config.toml"
|
||||||
|
data = {"active_model": "test"}
|
||||||
|
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 == {"active_model": "test"}
|
||||||
|
|
||||||
|
|
||||||
class TestMigrateMistralVibeCliLatestDefaults:
|
class TestMigrateMistralVibeCliLatestDefaults:
|
||||||
def test_updates_alias_temperature_and_thinking_for_default_model(
|
def test_updates_alias_temperature_and_thinking_for_default_model(
|
||||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,26 @@ async def test_no_vars_set_returns_empty() -> None:
|
||||||
assert data.model_dump() == {}
|
assert data.model_dump() == {}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fingerprint_changes_when_env_changes() -> None:
|
||||||
|
with patch.dict(os.environ, {"VIBE_ACTIVE_MODEL": "first-model"}, clear=True):
|
||||||
|
layer = EnvironmentLayer(schema=VibeConfigSchema)
|
||||||
|
data1 = await layer.load()
|
||||||
|
fp1 = layer.fingerprint
|
||||||
|
|
||||||
|
os.environ["VIBE_ACTIVE_MODEL"] = "second-model"
|
||||||
|
data2 = await layer.load(force=True)
|
||||||
|
fp2 = layer.fingerprint
|
||||||
|
|
||||||
|
assert data1.model_dump() == {"active_model": "first-model"}
|
||||||
|
assert data2.model_dump() == {"active_model": "second-model"}
|
||||||
|
assert isinstance(fp1, str)
|
||||||
|
assert fp1
|
||||||
|
assert isinstance(fp2, str)
|
||||||
|
assert fp2
|
||||||
|
assert fp1 != fp2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_always_trusted() -> None:
|
async def test_always_trusted() -> None:
|
||||||
assert await EnvironmentLayer(schema=VibeConfigSchema).resolve_trust() is True
|
assert await EnvironmentLayer(schema=VibeConfigSchema).resolve_trust() is True
|
||||||
|
|
|
||||||
118
tests/core/test_fingerprint.py
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from vibe.core.config.fingerprint import capture_stable_file, create_dict_fingerprint
|
||||||
|
from vibe.core.config.types import ConcurrencyConflictError
|
||||||
|
|
||||||
|
|
||||||
|
class TestCaptureStableFile:
|
||||||
|
def test_captures_unchanged_file(self, tmp_working_directory: Path) -> None:
|
||||||
|
path = tmp_working_directory / "config.toml"
|
||||||
|
path.write_text("key = 1")
|
||||||
|
|
||||||
|
with capture_stable_file(path) as (file, first_fingerprint):
|
||||||
|
assert file.read() == b"key = 1"
|
||||||
|
|
||||||
|
with capture_stable_file(path) as (file, second_fingerprint):
|
||||||
|
assert file.read() == b"key = 1"
|
||||||
|
|
||||||
|
assert isinstance(first_fingerprint, str)
|
||||||
|
assert first_fingerprint
|
||||||
|
assert first_fingerprint == second_fingerprint
|
||||||
|
|
||||||
|
def test_raises_when_file_changes(self, tmp_working_directory: Path) -> None:
|
||||||
|
path = tmp_working_directory / "config.toml"
|
||||||
|
path.write_text("key = 1")
|
||||||
|
|
||||||
|
with pytest.raises(ConcurrencyConflictError) as exc_info:
|
||||||
|
with capture_stable_file(path):
|
||||||
|
path.write_text("key = 123")
|
||||||
|
|
||||||
|
assert exc_info.value.actual_fp != exc_info.value.expected_fp
|
||||||
|
|
||||||
|
def test_raises_when_path_is_replaced_after_open(
|
||||||
|
self, tmp_working_directory: Path
|
||||||
|
) -> None:
|
||||||
|
path = tmp_working_directory / "config.toml"
|
||||||
|
replacement = tmp_working_directory / "replacement.toml"
|
||||||
|
path.write_text("key = 1")
|
||||||
|
replacement.write_text("key = 2")
|
||||||
|
|
||||||
|
with pytest.raises(ConcurrencyConflictError) as exc_info:
|
||||||
|
with capture_stable_file(path) as (file, _):
|
||||||
|
replacement.replace(path)
|
||||||
|
assert file.read() == b"key = 1"
|
||||||
|
|
||||||
|
assert exc_info.value.actual_fp != exc_info.value.expected_fp
|
||||||
|
|
||||||
|
def test_raises_when_file_disappears_after_read(
|
||||||
|
self, tmp_working_directory: Path
|
||||||
|
) -> None:
|
||||||
|
path = tmp_working_directory / "config.toml"
|
||||||
|
path.write_text("key = 1")
|
||||||
|
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
with capture_stable_file(path) as (file, _):
|
||||||
|
assert file.read() == b"key = 1"
|
||||||
|
path.unlink()
|
||||||
|
|
||||||
|
def test_raises_when_file_is_missing(self, tmp_working_directory: Path) -> None:
|
||||||
|
path = tmp_working_directory / "missing.toml"
|
||||||
|
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
with capture_stable_file(path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateDictFingerprint:
|
||||||
|
def test_empty_dict_returns_stable_non_empty_token(self) -> None:
|
||||||
|
first_fingerprint = create_dict_fingerprint({})
|
||||||
|
second_fingerprint = create_dict_fingerprint({})
|
||||||
|
|
||||||
|
assert isinstance(first_fingerprint, str)
|
||||||
|
assert first_fingerprint
|
||||||
|
assert first_fingerprint == second_fingerprint
|
||||||
|
|
||||||
|
def test_stable_for_same_dict(self) -> None:
|
||||||
|
data = {
|
||||||
|
"VIBE_MODEL": "mistral-large",
|
||||||
|
"VIBE_THEME": "dark",
|
||||||
|
"VIBE_TOOLS": ["read", "write"],
|
||||||
|
}
|
||||||
|
fp1 = create_dict_fingerprint(data)
|
||||||
|
fp2 = create_dict_fingerprint(data)
|
||||||
|
assert fp1 == fp2
|
||||||
|
|
||||||
|
def test_order_independent(self) -> None:
|
||||||
|
fp1 = create_dict_fingerprint({"a": "1", "b": "2"})
|
||||||
|
fp2 = create_dict_fingerprint({"b": "2", "a": "1"})
|
||||||
|
assert fp1 == fp2
|
||||||
|
|
||||||
|
def test_serializes_path_values(self) -> None:
|
||||||
|
fp1 = create_dict_fingerprint({
|
||||||
|
"tool_paths": [Path("/tmp/custom-tools")],
|
||||||
|
"agent_paths": [Path("agents")],
|
||||||
|
})
|
||||||
|
fp2 = create_dict_fingerprint({
|
||||||
|
"tool_paths": ["/tmp/custom-tools"],
|
||||||
|
"agent_paths": ["agents"],
|
||||||
|
})
|
||||||
|
assert fp1 == fp2
|
||||||
|
|
||||||
|
def test_changes_when_list_order_changes(self) -> None:
|
||||||
|
fp1 = create_dict_fingerprint({"tools": ["read", "write"]})
|
||||||
|
fp2 = create_dict_fingerprint({"tools": ["write", "read"]})
|
||||||
|
assert fp1 != fp2
|
||||||
|
|
||||||
|
def test_changes_when_value_changes(self) -> None:
|
||||||
|
fp1 = create_dict_fingerprint({"VIBE_MODEL": "mistral-large"})
|
||||||
|
fp2 = create_dict_fingerprint({"VIBE_MODEL": "devstral-2"})
|
||||||
|
assert fp1 != fp2
|
||||||
|
|
||||||
|
def test_changes_when_key_added(self) -> None:
|
||||||
|
fp1 = create_dict_fingerprint({"a": "1"})
|
||||||
|
fp2 = create_dict_fingerprint({"a": "1", "b": "2"})
|
||||||
|
assert fp1 != fp2
|
||||||
|
|
@ -2,6 +2,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from vibe.core.paths._local_config_files import LocalConfigDirs, find_local_config_dirs
|
from vibe.core.paths._local_config_files import LocalConfigDirs, find_local_config_dirs
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -87,6 +89,21 @@ class TestConfigDirs:
|
||||||
assert resolved / ".vibe" in result.config_dirs
|
assert resolved / ".vibe" in result.config_dirs
|
||||||
assert resolved / ".agents" in result.config_dirs
|
assert resolved / ".agents" in result.config_dirs
|
||||||
|
|
||||||
|
def test_unreadable_config_dirs_do_not_crash(
|
||||||
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
def fake_is_dir(self: Path) -> bool:
|
||||||
|
raise PermissionError(13, "Permission denied")
|
||||||
|
|
||||||
|
def fake_is_file(self: Path) -> bool:
|
||||||
|
raise PermissionError(13, "Permission denied")
|
||||||
|
|
||||||
|
monkeypatch.setattr(Path, "is_dir", fake_is_dir)
|
||||||
|
monkeypatch.setattr(Path, "is_file", fake_is_file)
|
||||||
|
|
||||||
|
result = find_local_config_dirs(tmp_path)
|
||||||
|
assert result == LocalConfigDirs()
|
||||||
|
|
||||||
|
|
||||||
class TestLocalConfigDirsOr:
|
class TestLocalConfigDirsOr:
|
||||||
def test_or_concatenates_each_field(self) -> None:
|
def test_or_concatenates_each_field(self) -> None:
|
||||||
|
|
|
||||||
496
tests/core/test_mcp_oauth.py
Normal file
|
|
@ -0,0 +1,496 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from collections.abc import Callable, Iterator
|
||||||
|
from contextlib import suppress
|
||||||
|
import socket
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import keyring
|
||||||
|
from keyring.backend import KeyringBackend
|
||||||
|
import keyring.backends.fail
|
||||||
|
import keyring.errors
|
||||||
|
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
|
||||||
|
import pytest
|
||||||
|
import respx
|
||||||
|
|
||||||
|
from vibe.core.auth.mcp_oauth import (
|
||||||
|
Fingerprint,
|
||||||
|
KeyringTokenStorage,
|
||||||
|
LoopbackCallbackHandler,
|
||||||
|
MCPOAuthError,
|
||||||
|
MCPOAuthHeadlessError,
|
||||||
|
MCPOAuthPortInUse,
|
||||||
|
build_oauth_provider,
|
||||||
|
perform_oauth_login,
|
||||||
|
)
|
||||||
|
from vibe.core.config import MCPOAuth, MCPStreamableHttp
|
||||||
|
|
||||||
|
|
||||||
|
class _MemoryKeyring(KeyringBackend):
|
||||||
|
priority = 100 # type: ignore[assignment]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.store: dict[tuple[str, str], str] = {}
|
||||||
|
|
||||||
|
def get_password(self, service: str, username: str) -> str | None:
|
||||||
|
return self.store.get((service, username))
|
||||||
|
|
||||||
|
def set_password(self, service: str, username: str, password: str) -> None:
|
||||||
|
self.store[(service, username)] = password
|
||||||
|
|
||||||
|
def delete_password(self, service: str, username: str) -> None:
|
||||||
|
if (service, username) not in self.store:
|
||||||
|
raise keyring.errors.PasswordDeleteError(username)
|
||||||
|
del self.store[(service, username)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def memory_keyring() -> Iterator[_MemoryKeyring]:
|
||||||
|
original = keyring.get_keyring()
|
||||||
|
fake = _MemoryKeyring()
|
||||||
|
keyring.set_keyring(fake)
|
||||||
|
try:
|
||||||
|
yield fake
|
||||||
|
finally:
|
||||||
|
keyring.set_keyring(original)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def headless_keyring() -> Iterator[None]:
|
||||||
|
original = keyring.get_keyring()
|
||||||
|
keyring.set_keyring(keyring.backends.fail.Keyring())
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
keyring.set_keyring(original)
|
||||||
|
|
||||||
|
|
||||||
|
def _oauth_server(
|
||||||
|
*,
|
||||||
|
name: str = "demo",
|
||||||
|
url: str = "https://mcp.example.com/mcp",
|
||||||
|
scopes: list[str] | None = None,
|
||||||
|
client_id: str | None = None,
|
||||||
|
client_metadata_url: str | None = None,
|
||||||
|
redirect_port: int = 47823,
|
||||||
|
) -> MCPStreamableHttp:
|
||||||
|
auth = MCPOAuth(
|
||||||
|
type="oauth",
|
||||||
|
scopes=scopes if scopes is not None else ["read", "write"],
|
||||||
|
client_id=client_id,
|
||||||
|
client_metadata_url=client_metadata_url, # type: ignore[arg-type]
|
||||||
|
redirect_port=redirect_port,
|
||||||
|
)
|
||||||
|
return MCPStreamableHttp(transport="streamable-http", name=name, url=url, auth=auth)
|
||||||
|
|
||||||
|
|
||||||
|
def _free_port() -> int:
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
|
s.bind(("127.0.0.1", 0))
|
||||||
|
return int(s.getsockname()[1])
|
||||||
|
|
||||||
|
|
||||||
|
async def _send_callback(port: int, query: str, *, timeout: float = 5.0) -> bytes:
|
||||||
|
deadline = asyncio.get_event_loop().time() + timeout
|
||||||
|
last_err: BaseException | None = None
|
||||||
|
while asyncio.get_event_loop().time() < deadline:
|
||||||
|
try:
|
||||||
|
reader, writer = await asyncio.open_connection("127.0.0.1", port)
|
||||||
|
except (ConnectionRefusedError, OSError) as exc:
|
||||||
|
last_err = exc
|
||||||
|
await asyncio.sleep(0.02)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
request = (
|
||||||
|
f"GET /callback?{query} HTTP/1.0\r\n"
|
||||||
|
"Host: 127.0.0.1\r\n"
|
||||||
|
"Connection: close\r\n"
|
||||||
|
"\r\n"
|
||||||
|
)
|
||||||
|
writer.write(request.encode("ascii"))
|
||||||
|
await writer.drain()
|
||||||
|
return await reader.read()
|
||||||
|
finally:
|
||||||
|
writer.close()
|
||||||
|
with suppress(Exception):
|
||||||
|
await writer.wait_closed()
|
||||||
|
raise RuntimeError(f"loopback never bound on port {port}: {last_err}")
|
||||||
|
|
||||||
|
|
||||||
|
class TestKeyringTokenStorage:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_round_trip_tokens(self, memory_keyring: _MemoryKeyring) -> None:
|
||||||
|
storage = KeyringTokenStorage(alias="linear")
|
||||||
|
assert await storage.get_tokens() is None
|
||||||
|
|
||||||
|
tokens = OAuthToken(
|
||||||
|
access_token="at",
|
||||||
|
token_type="Bearer",
|
||||||
|
expires_in=3600,
|
||||||
|
refresh_token="rt",
|
||||||
|
scope="read write",
|
||||||
|
)
|
||||||
|
await storage.set_tokens(tokens)
|
||||||
|
|
||||||
|
loaded = await storage.get_tokens()
|
||||||
|
assert loaded is not None
|
||||||
|
assert loaded.access_token == "at"
|
||||||
|
assert loaded.refresh_token == "rt"
|
||||||
|
assert loaded.scope == "read write"
|
||||||
|
assert ("vibe", "mcp-oauth:linear:tokens") in memory_keyring.store
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_round_trip_client_info(self, memory_keyring: _MemoryKeyring) -> None:
|
||||||
|
storage = KeyringTokenStorage(alias="linear")
|
||||||
|
assert await storage.get_client_info() is None
|
||||||
|
|
||||||
|
info = OAuthClientInformationFull(
|
||||||
|
client_id="abc123",
|
||||||
|
redirect_uris=["http://127.0.0.1:47823/callback"], # type: ignore[list-item]
|
||||||
|
token_endpoint_auth_method="none",
|
||||||
|
)
|
||||||
|
await storage.set_client_info(info)
|
||||||
|
|
||||||
|
loaded = await storage.get_client_info()
|
||||||
|
assert loaded is not None
|
||||||
|
assert loaded.client_id == "abc123"
|
||||||
|
assert ("vibe", "mcp-oauth:linear:client_info") in memory_keyring.store
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_per_alias_isolation(self, memory_keyring: _MemoryKeyring) -> None:
|
||||||
|
a = KeyringTokenStorage(alias="linear")
|
||||||
|
b = KeyringTokenStorage(alias="notion")
|
||||||
|
await a.set_tokens(
|
||||||
|
OAuthToken(access_token="A", token_type="Bearer", expires_in=60)
|
||||||
|
)
|
||||||
|
await b.set_tokens(
|
||||||
|
OAuthToken(access_token="B", token_type="Bearer", expires_in=60)
|
||||||
|
)
|
||||||
|
loaded_a = await a.get_tokens()
|
||||||
|
loaded_b = await b.get_tokens()
|
||||||
|
assert loaded_a is not None and loaded_a.access_token == "A"
|
||||||
|
assert loaded_b is not None and loaded_b.access_token == "B"
|
||||||
|
|
||||||
|
def test_headless_init_raises(self, headless_keyring: None) -> None:
|
||||||
|
with pytest.raises(MCPOAuthHeadlessError) as exc_info:
|
||||||
|
KeyringTokenStorage(alias="linear")
|
||||||
|
msg = str(exc_info.value)
|
||||||
|
assert "linear" in msg
|
||||||
|
assert "api_key_env" in msg
|
||||||
|
assert exc_info.value.server_alias == "linear"
|
||||||
|
|
||||||
|
|
||||||
|
class TestFingerprint:
|
||||||
|
def test_compute_stable_across_scope_order(
|
||||||
|
self, memory_keyring: _MemoryKeyring
|
||||||
|
) -> None:
|
||||||
|
a = _oauth_server(scopes=["read", "write", "admin"])
|
||||||
|
b = _oauth_server(scopes=["admin", "write", "read"])
|
||||||
|
assert Fingerprint.compute(a) == Fingerprint.compute(b)
|
||||||
|
|
||||||
|
def test_compute_strips_whitespace_and_dedupes(
|
||||||
|
self, memory_keyring: _MemoryKeyring
|
||||||
|
) -> None:
|
||||||
|
a = _oauth_server(scopes=["read", "write"])
|
||||||
|
b = _oauth_server(scopes=[" read ", "write", "write", ""])
|
||||||
|
assert Fingerprint.compute(a) == Fingerprint.compute(b)
|
||||||
|
|
||||||
|
def test_compute_marker_for_client_id(self, memory_keyring: _MemoryKeyring) -> None:
|
||||||
|
srv = _oauth_server(client_id="pre-registered-id")
|
||||||
|
assert Fingerprint.compute(srv).client_marker == "pre-registered-id"
|
||||||
|
|
||||||
|
def test_compute_marker_for_client_metadata_url(
|
||||||
|
self, memory_keyring: _MemoryKeyring
|
||||||
|
) -> None:
|
||||||
|
srv = _oauth_server(client_metadata_url="https://vibe.example/cm.json")
|
||||||
|
fp = Fingerprint.compute(srv)
|
||||||
|
assert fp.client_marker.startswith("https://vibe.example/cm.json")
|
||||||
|
|
||||||
|
def test_compute_marker_for_dcr(self, memory_keyring: _MemoryKeyring) -> None:
|
||||||
|
assert Fingerprint.compute(_oauth_server()).client_marker == "<dcr>"
|
||||||
|
|
||||||
|
def test_compute_rejects_static_auth(self, memory_keyring: _MemoryKeyring) -> None:
|
||||||
|
from vibe.core.config import MCPStaticAuth
|
||||||
|
|
||||||
|
srv = MCPStreamableHttp(
|
||||||
|
transport="streamable-http",
|
||||||
|
name="x",
|
||||||
|
url="https://x/mcp",
|
||||||
|
auth=MCPStaticAuth(),
|
||||||
|
)
|
||||||
|
with pytest.raises(TypeError, match="OAuth"):
|
||||||
|
Fingerprint.compute(srv)
|
||||||
|
|
||||||
|
def test_matches_detects_url_change(self, memory_keyring: _MemoryKeyring) -> None:
|
||||||
|
a = Fingerprint.compute(_oauth_server(url="https://a/mcp"))
|
||||||
|
b = Fingerprint.compute(_oauth_server(url="https://b/mcp"))
|
||||||
|
assert a != b
|
||||||
|
|
||||||
|
def test_matches_detects_scope_change(self, memory_keyring: _MemoryKeyring) -> None:
|
||||||
|
a = Fingerprint.compute(_oauth_server(scopes=["read"]))
|
||||||
|
b = Fingerprint.compute(_oauth_server(scopes=["read", "write"]))
|
||||||
|
assert a != b
|
||||||
|
|
||||||
|
def test_matches_detects_marker_change(
|
||||||
|
self, memory_keyring: _MemoryKeyring
|
||||||
|
) -> None:
|
||||||
|
a = Fingerprint.compute(_oauth_server(client_id="x"))
|
||||||
|
b = Fingerprint.compute(_oauth_server(client_id="y"))
|
||||||
|
assert a != b
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_load_returns_none_when_missing(
|
||||||
|
self, memory_keyring: _MemoryKeyring
|
||||||
|
) -> None:
|
||||||
|
assert await Fingerprint.load("nope") is None
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_save_and_load_round_trip(
|
||||||
|
self, memory_keyring: _MemoryKeyring
|
||||||
|
) -> None:
|
||||||
|
fp = Fingerprint.compute(_oauth_server(name="linear"))
|
||||||
|
await fp.save("linear")
|
||||||
|
loaded = await Fingerprint.load("linear")
|
||||||
|
assert loaded == fp
|
||||||
|
|
||||||
|
|
||||||
|
class TestLoopbackCallbackHandler:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_happy_path(self) -> None:
|
||||||
|
port = _free_port()
|
||||||
|
handler = LoopbackCallbackHandler(port=port, server_alias="demo")
|
||||||
|
|
||||||
|
async def driver() -> bytes:
|
||||||
|
return await _send_callback(port, "code=AUTH_CODE_123&state=STATE_XYZ")
|
||||||
|
|
||||||
|
serve_task = asyncio.create_task(handler.serve_once())
|
||||||
|
driver_task = asyncio.create_task(driver())
|
||||||
|
code, state = await serve_task
|
||||||
|
response = await driver_task
|
||||||
|
|
||||||
|
assert code == "AUTH_CODE_123"
|
||||||
|
assert state == "STATE_XYZ"
|
||||||
|
assert b"200 OK" in response
|
||||||
|
assert b"Login complete" in response
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_port_in_use_raises(self) -> None:
|
||||||
|
port = _free_port()
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 0)
|
||||||
|
sock.bind(("127.0.0.1", port))
|
||||||
|
sock.listen(1)
|
||||||
|
try:
|
||||||
|
handler = LoopbackCallbackHandler(port=port, server_alias="demo")
|
||||||
|
with pytest.raises(MCPOAuthPortInUse) as exc_info:
|
||||||
|
await handler.serve_once()
|
||||||
|
assert exc_info.value.port == port
|
||||||
|
assert exc_info.value.server_alias == "demo"
|
||||||
|
assert "redirect_port" in str(exc_info.value)
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_missing_code_raises(self) -> None:
|
||||||
|
port = _free_port()
|
||||||
|
handler = LoopbackCallbackHandler(port=port, server_alias="demo")
|
||||||
|
|
||||||
|
async def driver() -> bytes:
|
||||||
|
return await _send_callback(port, "error=access_denied&state=S")
|
||||||
|
|
||||||
|
serve_task = asyncio.create_task(handler.serve_once())
|
||||||
|
driver_task = asyncio.create_task(driver())
|
||||||
|
with pytest.raises(MCPOAuthError, match="missing 'code'"):
|
||||||
|
await serve_task
|
||||||
|
response = await driver_task
|
||||||
|
assert b"400 Bad Request" in response
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildOAuthProvider:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_metadata_matches_config(
|
||||||
|
self, memory_keyring: _MemoryKeyring
|
||||||
|
) -> None:
|
||||||
|
srv = _oauth_server(scopes=["read", "write"], redirect_port=51234)
|
||||||
|
|
||||||
|
async def on_url(_url: str) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def cb() -> tuple[str, str | None]:
|
||||||
|
return "code", None
|
||||||
|
|
||||||
|
provider = build_oauth_provider(
|
||||||
|
srv, redirect_handler=on_url, callback_handler=cb
|
||||||
|
)
|
||||||
|
md = provider.context.client_metadata
|
||||||
|
assert md.scope == "read write"
|
||||||
|
assert md.client_name == "Mistral Vibe"
|
||||||
|
assert md.token_endpoint_auth_method == "none"
|
||||||
|
assert md.grant_types == ["authorization_code", "refresh_token"]
|
||||||
|
assert md.redirect_uris is not None
|
||||||
|
assert str(md.redirect_uris[0]) == "http://127.0.0.1:51234/callback"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_empty_scopes_becomes_none(
|
||||||
|
self, memory_keyring: _MemoryKeyring
|
||||||
|
) -> None:
|
||||||
|
srv = _oauth_server(scopes=[])
|
||||||
|
|
||||||
|
async def on_url(_url: str) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def cb() -> tuple[str, str | None]:
|
||||||
|
return "code", None
|
||||||
|
|
||||||
|
provider = build_oauth_provider(
|
||||||
|
srv, redirect_handler=on_url, callback_handler=cb
|
||||||
|
)
|
||||||
|
assert provider.context.client_metadata.scope is None
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_client_metadata_url_forwarded(
|
||||||
|
self, memory_keyring: _MemoryKeyring
|
||||||
|
) -> None:
|
||||||
|
srv = _oauth_server(client_metadata_url="https://vibe.example/cm.json")
|
||||||
|
|
||||||
|
async def on_url(_url: str) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def cb() -> tuple[str, str | None]:
|
||||||
|
return "code", None
|
||||||
|
|
||||||
|
provider = build_oauth_provider(
|
||||||
|
srv, redirect_handler=on_url, callback_handler=cb
|
||||||
|
)
|
||||||
|
assert provider.context.client_metadata_url == "https://vibe.example/cm.json"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_rejects_static_auth(self, memory_keyring: _MemoryKeyring) -> None:
|
||||||
|
from vibe.core.config import MCPStaticAuth
|
||||||
|
|
||||||
|
srv = MCPStreamableHttp(
|
||||||
|
transport="streamable-http",
|
||||||
|
name="x",
|
||||||
|
url="https://x/mcp",
|
||||||
|
auth=MCPStaticAuth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_url(_url: str) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def cb() -> tuple[str, str | None]:
|
||||||
|
return "code", None
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="OAuth"):
|
||||||
|
build_oauth_provider(srv, redirect_handler=on_url, callback_handler=cb)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPerformOAuthLogin:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_full_flow_persists_tokens_and_fingerprint(
|
||||||
|
self, memory_keyring: _MemoryKeyring
|
||||||
|
) -> None:
|
||||||
|
port = _free_port()
|
||||||
|
server_url = "https://mcp.example.com/mcp"
|
||||||
|
as_url = "https://as.example.com"
|
||||||
|
srv = _oauth_server(
|
||||||
|
name="demo", url=server_url, scopes=["read"], redirect_port=port
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_url(url: str) -> None:
|
||||||
|
qs = urllib.parse.urlparse(url).query
|
||||||
|
state = urllib.parse.parse_qs(qs)["state"][0]
|
||||||
|
|
||||||
|
async def fire() -> None:
|
||||||
|
await _send_callback(port, f"code=THE_CODE&state={state}")
|
||||||
|
|
||||||
|
asyncio.get_event_loop().create_task(fire())
|
||||||
|
|
||||||
|
async with respx.mock(assert_all_called=False) as router:
|
||||||
|
router.get(server_url).mock(side_effect=_mcp_responses())
|
||||||
|
router.get(
|
||||||
|
"https://mcp.example.com/.well-known/oauth-protected-resource"
|
||||||
|
).mock(
|
||||||
|
return_value=httpx.Response(
|
||||||
|
200,
|
||||||
|
json={"resource": server_url, "authorization_servers": [as_url]},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
router.get(
|
||||||
|
"https://as.example.com/.well-known/oauth-authorization-server"
|
||||||
|
).mock(
|
||||||
|
return_value=httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"issuer": as_url,
|
||||||
|
"authorization_endpoint": f"{as_url}/authorize",
|
||||||
|
"token_endpoint": f"{as_url}/token",
|
||||||
|
"registration_endpoint": f"{as_url}/register",
|
||||||
|
"response_types_supported": ["code"],
|
||||||
|
"code_challenge_methods_supported": ["S256"],
|
||||||
|
"grant_types_supported": [
|
||||||
|
"authorization_code",
|
||||||
|
"refresh_token",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
router.post(f"{as_url}/register").mock(
|
||||||
|
return_value=httpx.Response(
|
||||||
|
201,
|
||||||
|
json={
|
||||||
|
"client_id": "dcr-client-id",
|
||||||
|
"redirect_uris": [f"http://127.0.0.1:{port}/callback"],
|
||||||
|
"token_endpoint_auth_method": "none",
|
||||||
|
"grant_types": ["authorization_code", "refresh_token"],
|
||||||
|
"response_types": ["code"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
router.post(f"{as_url}/token").mock(
|
||||||
|
return_value=httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"access_token": "ACCESS_TOKEN",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"refresh_token": "REFRESH_TOKEN",
|
||||||
|
"scope": "read",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
router.route(host="127.0.0.1").pass_through()
|
||||||
|
|
||||||
|
await perform_oauth_login(srv, on_url=on_url)
|
||||||
|
|
||||||
|
storage = KeyringTokenStorage(alias="demo")
|
||||||
|
tokens = await storage.get_tokens()
|
||||||
|
assert tokens is not None
|
||||||
|
assert tokens.access_token == "ACCESS_TOKEN"
|
||||||
|
assert tokens.refresh_token == "REFRESH_TOKEN"
|
||||||
|
|
||||||
|
fp = await Fingerprint.load("demo")
|
||||||
|
assert fp is not None
|
||||||
|
assert fp == Fingerprint.compute(srv)
|
||||||
|
|
||||||
|
|
||||||
|
def _mcp_responses() -> Callable[[httpx.Request], httpx.Response]:
|
||||||
|
state = {"calls": 0}
|
||||||
|
|
||||||
|
def _factory(_request: httpx.Request) -> httpx.Response:
|
||||||
|
state["calls"] += 1
|
||||||
|
if state["calls"] == 1:
|
||||||
|
return httpx.Response(
|
||||||
|
401,
|
||||||
|
headers={
|
||||||
|
"WWW-Authenticate": (
|
||||||
|
"Bearer resource_metadata="
|
||||||
|
'"https://mcp.example.com/.well-known/oauth-protected-resource"'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return httpx.Response(200, json={"ok": True})
|
||||||
|
|
||||||
|
return _factory
|
||||||
|
|
@ -80,6 +80,10 @@ async def test_live_reference_picks_up_caller_mutation() -> None:
|
||||||
data: dict[str, object] = {"key": "original"}
|
data: dict[str, object] = {"key": "original"}
|
||||||
layer = OverridesLayer(data=data)
|
layer = OverridesLayer(data=data)
|
||||||
await layer.load()
|
await layer.load()
|
||||||
|
fp1 = layer.fingerprint
|
||||||
data["key"] = "updated"
|
data["key"] = "updated"
|
||||||
result = await layer.load(force=True)
|
result = await layer.load(force=True)
|
||||||
|
fp2 = layer.fingerprint
|
||||||
|
|
||||||
assert result.model_extra == {"key": "updated"}
|
assert result.model_extra == {"key": "updated"}
|
||||||
|
assert fp1 != fp2
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,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.config.patch import ConfigPatch
|
||||||
|
from vibe.core.config.types import MISSING_CONFIG_FILE_FINGERPRINT
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -21,6 +22,14 @@ async def test_reads_toml_when_trusted(tmp_working_directory: Path) -> None:
|
||||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||||
data = await layer.load()
|
data = await layer.load()
|
||||||
assert data.model_extra == {"active_model": "project-model"}
|
assert data.model_extra == {"active_model": "project-model"}
|
||||||
|
fp1 = layer.fingerprint
|
||||||
|
assert isinstance(fp1, str)
|
||||||
|
assert fp1
|
||||||
|
|
||||||
|
config_path.unlink()
|
||||||
|
data = await layer.load(force=True)
|
||||||
|
assert data.model_extra == {}
|
||||||
|
assert layer.fingerprint == MISSING_CONFIG_FILE_FINGERPRINT
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -55,6 +64,7 @@ async def test_missing_file_returns_empty(tmp_working_directory: Path) -> None:
|
||||||
layer = ProjectConfigLayer(path=tmp_working_directory)
|
layer = ProjectConfigLayer(path=tmp_working_directory)
|
||||||
data = await layer.load()
|
data = await layer.load()
|
||||||
assert data.model_extra == {}
|
assert data.model_extra == {}
|
||||||
|
assert layer.fingerprint == MISSING_CONFIG_FILE_FINGERPRINT
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.conftest import build_test_vibe_config
|
from tests.conftest import build_test_vibe_config
|
||||||
|
|
@ -14,7 +16,7 @@ from vibe.core.telemetry.build_metadata import (
|
||||||
build_base_metadata,
|
build_base_metadata,
|
||||||
build_request_metadata,
|
build_request_metadata,
|
||||||
)
|
)
|
||||||
from vibe.core.telemetry.send import TelemetryClient
|
from vibe.core.telemetry.send import TelemetryClient, _extract_file_extension
|
||||||
from vibe.core.telemetry.types import (
|
from vibe.core.telemetry.types import (
|
||||||
AttachmentKind,
|
AttachmentKind,
|
||||||
EntrypointMetadata,
|
EntrypointMetadata,
|
||||||
|
|
@ -25,16 +27,33 @@ from vibe.core.types import Backend
|
||||||
from vibe.core.utils import get_user_agent
|
from vibe.core.utils import get_user_agent
|
||||||
|
|
||||||
_original_send_telemetry_event = TelemetryClient.send_telemetry_event
|
_original_send_telemetry_event = TelemetryClient.send_telemetry_event
|
||||||
|
from vibe.core.tools.builtins.edit import Edit, EditArgs
|
||||||
|
from vibe.core.tools.builtins.read import Read, ReadArgs
|
||||||
from vibe.core.tools.builtins.write_file import WriteFile, WriteFileArgs
|
from vibe.core.tools.builtins.write_file import WriteFile, WriteFileArgs
|
||||||
|
|
||||||
|
|
||||||
def _make_resolved_tool_call(
|
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":
|
validated: BaseModel
|
||||||
validated = WriteFileArgs(path="foo.txt", content="x")
|
cls: type[BaseTool]
|
||||||
cls: type[BaseTool] = WriteFile
|
match tool_name:
|
||||||
else:
|
case "write_file":
|
||||||
|
validated = WriteFileArgs(
|
||||||
|
path=args_dict.get("path", "foo.txt"), content="x"
|
||||||
|
)
|
||||||
|
cls = WriteFile
|
||||||
|
case "edit":
|
||||||
|
validated = EditArgs(
|
||||||
|
file_path=args_dict.get("file_path", "foo.txt"),
|
||||||
|
old_string="a",
|
||||||
|
new_string="b",
|
||||||
|
)
|
||||||
|
cls = Edit
|
||||||
|
case "read":
|
||||||
|
validated = ReadArgs(file_path=args_dict.get("file_path", "foo.txt"))
|
||||||
|
cls = Read
|
||||||
|
case _:
|
||||||
validated = FakeToolArgs()
|
validated = FakeToolArgs()
|
||||||
cls = FakeTool
|
cls = FakeTool
|
||||||
return ResolvedToolCall(
|
return ResolvedToolCall(
|
||||||
|
|
@ -50,6 +69,37 @@ def _run_telemetry_tasks() -> None:
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtractFileExtension:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("path", "expected"),
|
||||||
|
[
|
||||||
|
("/tmp/foo.py", ".py"),
|
||||||
|
("foo.TSX", ".tsx"),
|
||||||
|
("/tmp/Makefile", None),
|
||||||
|
("/tmp/.bashrc", None),
|
||||||
|
("archive.tar.gz", ".gz"),
|
||||||
|
("", None),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_string_inputs(self, path: str, expected: str | None) -> None:
|
||||||
|
assert _extract_file_extension(path) == expected
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("path", "expected"),
|
||||||
|
[
|
||||||
|
(Path("/tmp/foo.py"), ".py"),
|
||||||
|
(Path("foo.TSX"), ".tsx"),
|
||||||
|
(Path("/tmp/Makefile"), None),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_path_inputs(self, path: Path, expected: str | None) -> None:
|
||||||
|
assert _extract_file_extension(path) == expected
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("path", [None, 42, ["foo.py"], {"path": "foo.py"}])
|
||||||
|
def test_non_path_like_inputs_return_none(self, path: object) -> None:
|
||||||
|
assert _extract_file_extension(path) is None
|
||||||
|
|
||||||
|
|
||||||
class TestTelemetryClient:
|
class TestTelemetryClient:
|
||||||
def test_send_telemetry_event_swallows_config_getter_value_error(self) -> None:
|
def test_send_telemetry_event_swallows_config_getter_value_error(self) -> None:
|
||||||
def _raise_config_error() -> Any:
|
def _raise_config_error() -> Any:
|
||||||
|
|
@ -151,6 +201,7 @@ class TestTelemetryClient:
|
||||||
assert properties["model"] == "mistral-large"
|
assert properties["model"] == "mistral-large"
|
||||||
assert properties["nb_files_created"] == 0
|
assert properties["nb_files_created"] == 0
|
||||||
assert properties["nb_files_modified"] == 0
|
assert properties["nb_files_modified"] == 0
|
||||||
|
assert properties["file_extension"] is None
|
||||||
assert properties["message_id"] is None
|
assert properties["message_id"] is None
|
||||||
|
|
||||||
def test_send_tool_call_finished_with_message_id(
|
def test_send_tool_call_finished_with_message_id(
|
||||||
|
|
@ -176,7 +227,7 @@ class TestTelemetryClient:
|
||||||
) -> 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", {})
|
tool_call = _make_resolved_tool_call("write_file", {"path": "/tmp/foo.PY"})
|
||||||
|
|
||||||
client.send_tool_call_finished(
|
client.send_tool_call_finished(
|
||||||
tool_call=tool_call,
|
tool_call=tool_call,
|
||||||
|
|
@ -189,6 +240,86 @@ class TestTelemetryClient:
|
||||||
|
|
||||||
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
|
||||||
|
assert telemetry_events[0]["properties"]["file_extension"] == ".py"
|
||||||
|
|
||||||
|
def test_send_tool_call_finished_file_extension_edit(
|
||||||
|
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(
|
||||||
|
"edit", {"file_path": "/tmp/component.tsx"}
|
||||||
|
)
|
||||||
|
|
||||||
|
client.send_tool_call_finished(
|
||||||
|
tool_call=tool_call,
|
||||||
|
status="success",
|
||||||
|
decision=None,
|
||||||
|
agent_profile_name="default",
|
||||||
|
model="mistral-large",
|
||||||
|
result={},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert telemetry_events[0]["properties"]["nb_files_modified"] == 1
|
||||||
|
assert telemetry_events[0]["properties"]["nb_files_created"] == 0
|
||||||
|
assert telemetry_events[0]["properties"]["file_extension"] == ".tsx"
|
||||||
|
|
||||||
|
def test_send_tool_call_finished_file_extension_read(
|
||||||
|
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("read", {"file_path": "/tmp/lib.rs"})
|
||||||
|
|
||||||
|
client.send_tool_call_finished(
|
||||||
|
tool_call=tool_call,
|
||||||
|
status="success",
|
||||||
|
decision=None,
|
||||||
|
agent_profile_name="default",
|
||||||
|
model="mistral-large",
|
||||||
|
result={},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert telemetry_events[0]["properties"]["nb_files_created"] == 0
|
||||||
|
assert telemetry_events[0]["properties"]["nb_files_modified"] == 0
|
||||||
|
assert telemetry_events[0]["properties"]["file_extension"] == ".rs"
|
||||||
|
|
||||||
|
def test_send_tool_call_finished_file_extension_none_when_no_suffix(
|
||||||
|
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", {"path": "/tmp/Makefile"})
|
||||||
|
|
||||||
|
client.send_tool_call_finished(
|
||||||
|
tool_call=tool_call,
|
||||||
|
status="success",
|
||||||
|
decision=None,
|
||||||
|
agent_profile_name="default",
|
||||||
|
model="mistral-large",
|
||||||
|
result={},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert telemetry_events[0]["properties"]["file_extension"] is None
|
||||||
|
|
||||||
|
def test_send_tool_call_finished_file_extension_none_on_failure(
|
||||||
|
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", {"path": "/tmp/foo.py"})
|
||||||
|
|
||||||
|
client.send_tool_call_finished(
|
||||||
|
tool_call=tool_call,
|
||||||
|
status="failure",
|
||||||
|
decision=None,
|
||||||
|
agent_profile_name="default",
|
||||||
|
model="mistral-large",
|
||||||
|
result={},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert telemetry_events[0]["properties"]["nb_files_created"] == 0
|
||||||
|
assert telemetry_events[0]["properties"]["file_extension"] is None
|
||||||
|
|
||||||
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]]
|
||||||
|
|
@ -378,6 +509,40 @@ class TestTelemetryClient:
|
||||||
"nb_session_messages": 4,
|
"nb_session_messages": 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_send_remote_resume_requested_payload(
|
||||||
|
self, telemetry_events: list[dict[str, Any]]
|
||||||
|
) -> None:
|
||||||
|
config = build_test_vibe_config(enable_telemetry=True)
|
||||||
|
client = TelemetryClient(config_getter=lambda: config)
|
||||||
|
client.send_remote_resume_requested(session_id="remote-123")
|
||||||
|
assert len(telemetry_events) == 1
|
||||||
|
assert telemetry_events[0]["event_name"] == "vibe.remote_resume_requested"
|
||||||
|
assert telemetry_events[0]["properties"] == {"session_id": "remote-123"}
|
||||||
|
|
||||||
|
def test_send_teleport_failed_payload_includes_error_details(
|
||||||
|
self, telemetry_events: list[dict[str, Any]]
|
||||||
|
) -> None:
|
||||||
|
config = build_test_vibe_config(enable_telemetry=True)
|
||||||
|
client = TelemetryClient(config_getter=lambda: config)
|
||||||
|
|
||||||
|
client.send_teleport_failed(
|
||||||
|
stage="workflow_start",
|
||||||
|
error_class="ServiceTeleportError",
|
||||||
|
push_required=False,
|
||||||
|
nb_session_messages=4,
|
||||||
|
error_details={"failure_kind": "http_error", "http_status_code": 502},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert telemetry_events[0]["event_name"] == "vibe.teleport_failed"
|
||||||
|
assert telemetry_events[0]["properties"] == {
|
||||||
|
"stage": "workflow_start",
|
||||||
|
"error_class": "ServiceTeleportError",
|
||||||
|
"push_required": False,
|
||||||
|
"nb_session_messages": 4,
|
||||||
|
"failure_kind": "http_error",
|
||||||
|
"http_status_code": 502,
|
||||||
|
}
|
||||||
|
|
||||||
def test_send_new_session_payload(
|
def test_send_new_session_payload(
|
||||||
self, telemetry_events: list[dict[str, Any]]
|
self, telemetry_events: list[dict[str, Any]]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
|
|
@ -174,9 +174,14 @@ async def test_start_raises_for_unsuccessful_response() -> None:
|
||||||
|
|
||||||
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
||||||
nuage = NuageClient("https://chat.example.com", "api-key", client=client)
|
nuage = NuageClient("https://chat.example.com", "api-key", client=client)
|
||||||
with pytest.raises(ServiceTeleportError, match="Nuage start"):
|
with pytest.raises(ServiceTeleportError, match="status 401") as exc_info:
|
||||||
await nuage.start(_request())
|
await nuage.start(_request())
|
||||||
|
|
||||||
|
assert exc_info.value.telemetry_details == {
|
||||||
|
"failure_kind": "http_error",
|
||||||
|
"http_status_code": 401,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_start_raises_for_invalid_response() -> None:
|
async def test_start_raises_for_invalid_response() -> None:
|
||||||
|
|
@ -185,5 +190,30 @@ async def test_start_raises_for_invalid_response() -> None:
|
||||||
|
|
||||||
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
||||||
nuage = NuageClient("https://chat.example.com", "api-key", client=client)
|
nuage = NuageClient("https://chat.example.com", "api-key", client=client)
|
||||||
with pytest.raises(ServiceTeleportError, match="response was invalid"):
|
with pytest.raises(
|
||||||
|
ServiceTeleportError, match="response was invalid"
|
||||||
|
) as exc_info:
|
||||||
await nuage.start(_request())
|
await nuage.start(_request())
|
||||||
|
|
||||||
|
assert exc_info.value.telemetry_details == {
|
||||||
|
"failure_kind": "invalid_schema",
|
||||||
|
"http_status_code": 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_raises_for_invalid_json_response() -> None:
|
||||||
|
async def handler(request: httpx.Request) -> httpx.Response:
|
||||||
|
return httpx.Response(
|
||||||
|
200, text="not-json", headers={"content-type": "text/plain"}
|
||||||
|
)
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
||||||
|
nuage = NuageClient("https://chat.example.com", "api-key", client=client)
|
||||||
|
with pytest.raises(ServiceTeleportError, match="not valid JSON") as exc_info:
|
||||||
|
await nuage.start(_request())
|
||||||
|
|
||||||
|
assert exc_info.value.telemetry_details == {
|
||||||
|
"failure_kind": "invalid_json",
|
||||||
|
"http_status_code": 200,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,10 @@ class TestTeleportAgentLoopTelemetry:
|
||||||
) -> AsyncGenerator[object, object]:
|
) -> AsyncGenerator[object, object]:
|
||||||
yield TeleportCheckingGitEvent()
|
yield TeleportCheckingGitEvent()
|
||||||
yield TeleportStartingWorkflowEvent()
|
yield TeleportStartingWorkflowEvent()
|
||||||
raise ServiceTeleportError("Workflow api-key-123 could not be started.")
|
raise ServiceTeleportError(
|
||||||
|
"Workflow api-key-123 could not be started.",
|
||||||
|
telemetry_details={"http_status_code": 502},
|
||||||
|
)
|
||||||
|
|
||||||
agent_loop.messages.append(LLMMessage(role=Role.user, content="hello"))
|
agent_loop.messages.append(LLMMessage(role=Role.user, content="hello"))
|
||||||
_set_teleport_service(agent_loop, FakeTeleportService())
|
_set_teleport_service(agent_loop, FakeTeleportService())
|
||||||
|
|
@ -103,6 +106,7 @@ class TestTeleportAgentLoopTelemetry:
|
||||||
"error_class": "ServiceTeleportError",
|
"error_class": "ServiceTeleportError",
|
||||||
"push_required": False,
|
"push_required": False,
|
||||||
"nb_session_messages": 1,
|
"nb_session_messages": 1,
|
||||||
|
"http_status_code": 502,
|
||||||
"session_id": agent_loop.session_id,
|
"session_id": agent_loop.session_id,
|
||||||
}
|
}
|
||||||
assert "api-key-123" not in str(telemetry_events[-1]["properties"])
|
assert "api-key-123" not in str(telemetry_events[-1]["properties"])
|
||||||
|
|
|
||||||
|
|
@ -351,6 +351,19 @@ class TestHasAgentsMdFile:
|
||||||
def test_agents_md_filename_constant(self) -> None:
|
def test_agents_md_filename_constant(self) -> None:
|
||||||
assert AGENTS_MD_FILENAME == "AGENTS.md"
|
assert AGENTS_MD_FILENAME == "AGENTS.md"
|
||||||
|
|
||||||
|
def test_unreadable_agents_md_does_not_crash(
|
||||||
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
denied = (tmp_path / "AGENTS.md").resolve()
|
||||||
|
|
||||||
|
def fake_is_file(self: Path) -> bool:
|
||||||
|
if self.resolve() == denied:
|
||||||
|
raise PermissionError(13, "Permission denied")
|
||||||
|
return False
|
||||||
|
|
||||||
|
monkeypatch.setattr(Path, "is_file", fake_is_file)
|
||||||
|
assert has_agents_md_file(tmp_path) is False
|
||||||
|
|
||||||
|
|
||||||
class TestFindTrustableFiles:
|
class TestFindTrustableFiles:
|
||||||
def test_returns_empty_for_clean_directory(self, tmp_path: Path) -> None:
|
def test_returns_empty_for_clean_directory(self, tmp_path: Path) -> None:
|
||||||
|
|
@ -513,3 +526,24 @@ class TestFindGitRepoAncestor:
|
||||||
) -> None:
|
) -> None:
|
||||||
monkeypatch.setattr(Path, "home", classmethod(lambda _cls: tmp_path / "nope"))
|
monkeypatch.setattr(Path, "home", classmethod(lambda _cls: tmp_path / "nope"))
|
||||||
assert find_git_repo_ancestor(tmp_path) is None
|
assert find_git_repo_ancestor(tmp_path) is None
|
||||||
|
|
||||||
|
def test_unreadable_ancestor_git_head_does_not_crash(
|
||||||
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
ancestor = tmp_path / "mnt" / "vast"
|
||||||
|
(ancestor / ".git").mkdir(parents=True)
|
||||||
|
cwd = ancestor / "project"
|
||||||
|
cwd.mkdir()
|
||||||
|
|
||||||
|
real_is_file = Path.is_file
|
||||||
|
denied = (ancestor / ".git" / "HEAD").resolve()
|
||||||
|
|
||||||
|
def fake_is_file(self: Path) -> bool:
|
||||||
|
if self.resolve() == denied:
|
||||||
|
raise PermissionError(13, "Permission denied")
|
||||||
|
return real_is_file(self)
|
||||||
|
|
||||||
|
monkeypatch.setattr(Path, "is_file", fake_is_file)
|
||||||
|
monkeypatch.setattr(Path, "home", classmethod(lambda _cls: tmp_path / "home"))
|
||||||
|
|
||||||
|
assert find_git_repo_ancestor(cwd) is None
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,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
|
from vibe.core.config.patch import ConfigPatch
|
||||||
|
from vibe.core.config.types import MISSING_CONFIG_FILE_FINGERPRINT
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -17,6 +18,9 @@ async def test_reads_toml_file(tmp_working_directory: Path) -> None:
|
||||||
layer = UserConfigLayer(path=path, name="user-toml")
|
layer = UserConfigLayer(path=path, name="user-toml")
|
||||||
data = await layer.load()
|
data = await layer.load()
|
||||||
assert data.model_extra == {"active_model": "mistral-large", "count": 42}
|
assert data.model_extra == {"active_model": "mistral-large", "count": 42}
|
||||||
|
fingerprint = layer.fingerprint
|
||||||
|
assert isinstance(fingerprint, str)
|
||||||
|
assert fingerprint
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -37,6 +41,7 @@ async def test_missing_file_returns_empty(tmp_working_directory: Path) -> None:
|
||||||
layer = UserConfigLayer(path=path, name="user-toml")
|
layer = UserConfigLayer(path=path, name="user-toml")
|
||||||
data = await layer.load()
|
data = await layer.load()
|
||||||
assert data.model_extra == {}
|
assert data.model_extra == {}
|
||||||
|
assert layer.fingerprint == MISSING_CONFIG_FILE_FINGERPRINT
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
@ -70,7 +75,7 @@ async def test_invalid_toml_raises(tmp_working_directory: Path) -> None:
|
||||||
path = tmp_working_directory / "bad.toml"
|
path = tmp_working_directory / "bad.toml"
|
||||||
path.write_text("this is not valid = = = toml [[[")
|
path.write_text("this is not valid = = = toml [[[")
|
||||||
layer = UserConfigLayer(path=path, name="user-toml")
|
layer = UserConfigLayer(path=path, name="user-toml")
|
||||||
with pytest.raises(LayerImplementationError, match="_read_config"):
|
with pytest.raises(LayerImplementationError, match="_build_config_snapshot"):
|
||||||
await layer.load()
|
await layer.load()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -81,11 +86,23 @@ async def test_force_reload_reads_fresh_data(tmp_working_directory: Path) -> Non
|
||||||
layer = UserConfigLayer(path=path, name="user-toml")
|
layer = UserConfigLayer(path=path, name="user-toml")
|
||||||
|
|
||||||
data1 = await layer.load()
|
data1 = await layer.load()
|
||||||
|
fp1 = layer.fingerprint
|
||||||
assert data1.model_extra == {"value": "first"}
|
assert data1.model_extra == {"value": "first"}
|
||||||
|
assert isinstance(fp1, str)
|
||||||
|
assert fp1
|
||||||
|
|
||||||
path.write_text('value = "second"\n')
|
path.write_text('value = "second"\n')
|
||||||
data2 = await layer.load(force=True)
|
data2 = await layer.load(force=True)
|
||||||
|
fp2 = layer.fingerprint
|
||||||
assert data2.model_extra == {"value": "second"}
|
assert data2.model_extra == {"value": "second"}
|
||||||
|
assert isinstance(fp2, str)
|
||||||
|
assert fp2
|
||||||
|
assert fp1 != fp2
|
||||||
|
|
||||||
|
path.unlink()
|
||||||
|
data3 = await layer.load(force=True)
|
||||||
|
assert data3.model_extra == {}
|
||||||
|
assert layer.fingerprint == MISSING_CONFIG_FILE_FINGERPRINT
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,28 @@ class TestReadSafe:
|
||||||
with pytest.raises(FileNotFoundError):
|
with pytest.raises(FileNotFoundError):
|
||||||
read_safe(tmp_path / "nope.txt")
|
read_safe(tmp_path / "nope.txt")
|
||||||
|
|
||||||
|
def test_from_subprocess_prefers_oem_over_locale_ansi(
|
||||||
|
self, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
# \x82 is invalid UTF-8 and decodes differently across single-byte
|
||||||
|
# encodings: cp1252 (Windows ANSI) -> "‚" (low-9 quote);
|
||||||
|
# cp850 (Windows OEM) -> "é". Subprocess output prefers OEM over the
|
||||||
|
# ANSI locale; file reads (from_subprocess=False) must not.
|
||||||
|
raw = "café\n".encode("cp850")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
io_utils.locale, "getpreferredencoding", lambda _do_setlocale: "cp1252"
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(io_utils, "_encoding_from_best_match", lambda _raw: None)
|
||||||
|
monkeypatch.setattr(io_utils, "_windows_oem_encoding", lambda: "cp850")
|
||||||
|
|
||||||
|
from_file = decode_safe(raw)
|
||||||
|
assert from_file.encoding == "cp1252"
|
||||||
|
assert from_file.text == raw.decode("cp1252")
|
||||||
|
|
||||||
|
from_subprocess = decode_safe(raw, from_subprocess=True)
|
||||||
|
assert from_subprocess.encoding == "cp850"
|
||||||
|
assert from_subprocess.text == "café\n"
|
||||||
|
|
||||||
|
|
||||||
class TestReadSafeNewlines:
|
class TestReadSafeNewlines:
|
||||||
def test_lf(self, tmp_path: Path) -> None:
|
def test_lf(self, tmp_path: Path) -> None:
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,6 @@ def test_get_result_display() -> None:
|
||||||
|
|
||||||
assert isinstance(display, ToolResultDisplay)
|
assert isinstance(display, ToolResultDisplay)
|
||||||
assert display.success is True
|
assert display.success is True
|
||||||
assert "10 lines" in display.message
|
|
||||||
assert "foo.py" in display.message
|
assert "foo.py" in display.message
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -261,7 +260,7 @@ def test_get_result_display_truncated() -> None:
|
||||||
)
|
)
|
||||||
display = Read.get_result_display(event)
|
display = Read.get_result_display(event)
|
||||||
|
|
||||||
assert "truncated" in display.message
|
assert "truncated" in display.suffix
|
||||||
|
|
||||||
|
|
||||||
def test_get_result_display_truncated_via_flag() -> None:
|
def test_get_result_display_truncated_via_flag() -> None:
|
||||||
|
|
@ -278,7 +277,7 @@ def test_get_result_display_truncated_via_flag() -> None:
|
||||||
)
|
)
|
||||||
display = Read.get_result_display(event)
|
display = Read.get_result_display(event)
|
||||||
|
|
||||||
assert "truncated" in display.message
|
assert "truncated" in display.suffix
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
|
|
|
||||||
|
|
@ -85,3 +85,7 @@ def test_spawn_cli_runs_configured_hook_after_turn(
|
||||||
assert invocation["hook_event_name"] == "post_agent_turn"
|
assert invocation["hook_event_name"] == "post_agent_turn"
|
||||||
assert isinstance(invocation["cwd"], str) and invocation["cwd"]
|
assert isinstance(invocation["cwd"], str) and invocation["cwd"]
|
||||||
assert isinstance(invocation["session_id"], str) and invocation["session_id"]
|
assert isinstance(invocation["session_id"], str) and invocation["session_id"]
|
||||||
|
# New in the discriminated-union payload: top-level sessions have no
|
||||||
|
# parent. Tool hook invocations in this test never fire (no tool calls
|
||||||
|
# in the mock response), so we only assert the post_agent_turn shape.
|
||||||
|
assert invocation.get("parent_session_id") is None
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import io
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
@ -12,12 +14,14 @@ import pytest
|
||||||
from tests.e2e.common import (
|
from tests.e2e.common import (
|
||||||
SpawnedVibeProcessFixture,
|
SpawnedVibeProcessFixture,
|
||||||
ansi_tolerant_pattern,
|
ansi_tolerant_pattern,
|
||||||
|
poll_until,
|
||||||
send_ctrl_c_until_quit_confirmation,
|
send_ctrl_c_until_quit_confirmation,
|
||||||
strip_ansi,
|
strip_ansi,
|
||||||
wait_for_main_screen,
|
wait_for_main_screen,
|
||||||
wait_for_request_count,
|
wait_for_request_count,
|
||||||
)
|
)
|
||||||
from tests.e2e.mock_server import StreamingMockServer
|
from tests.e2e.mock_server import StreamingMockServer
|
||||||
|
from vibe.core.utils.io import read_safe
|
||||||
|
|
||||||
|
|
||||||
def _usage_by_run_factory(
|
def _usage_by_run_factory(
|
||||||
|
|
@ -42,6 +46,47 @@ def _usage_by_run_factory(
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _saved_session_has_usage(
|
||||||
|
vibe_home: Path, expected_prompt_tokens: int, expected_completion_tokens: int
|
||||||
|
) -> bool:
|
||||||
|
session_log_dir = vibe_home / "logs" / "session"
|
||||||
|
if not session_log_dir.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
for metadata_path in session_log_dir.glob("session_*/meta.json"):
|
||||||
|
try:
|
||||||
|
metadata = json.loads(read_safe(metadata_path).text)
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
stats = metadata.get("stats", {})
|
||||||
|
if not isinstance(stats, dict):
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
stats.get("session_prompt_tokens") == expected_prompt_tokens
|
||||||
|
and stats.get("session_completion_tokens") == expected_completion_tokens
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_for_saved_session_usage(
|
||||||
|
expected_prompt_tokens: int, expected_completion_tokens: int
|
||||||
|
) -> None:
|
||||||
|
vibe_home = Path(os.environ["VIBE_HOME"])
|
||||||
|
poll_until(
|
||||||
|
lambda: _saved_session_has_usage(
|
||||||
|
vibe_home, expected_prompt_tokens, expected_completion_tokens
|
||||||
|
),
|
||||||
|
timeout=10,
|
||||||
|
message=(
|
||||||
|
"Timed out waiting for saved session usage "
|
||||||
|
f"input={expected_prompt_tokens} output={expected_completion_tokens}."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _finish_turn(
|
def _finish_turn(
|
||||||
child: pexpect.spawn,
|
child: pexpect.spawn,
|
||||||
captured: io.StringIO,
|
captured: io.StringIO,
|
||||||
|
|
@ -103,6 +148,9 @@ def test_resumed_session_prints_only_fresh_token_usage_on_exit(
|
||||||
expected_request_count=1,
|
expected_request_count=1,
|
||||||
request_count_getter=lambda: len(streaming_mock_server.requests),
|
request_count_getter=lambda: len(streaming_mock_server.requests),
|
||||||
)
|
)
|
||||||
|
_wait_for_saved_session_usage(
|
||||||
|
expected_prompt_tokens=11, expected_completion_tokens=7
|
||||||
|
)
|
||||||
|
|
||||||
send_ctrl_c_until_quit_confirmation(child, captured, timeout=5)
|
send_ctrl_c_until_quit_confirmation(child, captured, timeout=5)
|
||||||
child.expect(pexpect.EOF, timeout=10)
|
child.expect(pexpect.EOF, timeout=10)
|
||||||
|
|
@ -130,6 +178,9 @@ def test_resumed_session_prints_only_fresh_token_usage_on_exit(
|
||||||
expected_request_count=2,
|
expected_request_count=2,
|
||||||
request_count_getter=lambda: len(streaming_mock_server.requests),
|
request_count_getter=lambda: len(streaming_mock_server.requests),
|
||||||
)
|
)
|
||||||
|
_wait_for_saved_session_usage(
|
||||||
|
expected_prompt_tokens=2, expected_completion_tokens=1
|
||||||
|
)
|
||||||
|
|
||||||
send_ctrl_c_until_quit_confirmation(resumed_child, resumed_captured, timeout=5)
|
send_ctrl_c_until_quit_confirmation(resumed_child, resumed_captured, timeout=5)
|
||||||
resumed_child.expect(pexpect.EOF, timeout=10)
|
resumed_child.expect(pexpect.EOF, timeout=10)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from vibe.core.session.resume_sessions import (
|
from vibe.core.session.resume_sessions import (
|
||||||
|
ResumeSessionInfo,
|
||||||
|
can_delete_resume_session_source,
|
||||||
list_remote_resume_sessions,
|
list_remote_resume_sessions,
|
||||||
short_session_id,
|
short_session_id,
|
||||||
)
|
)
|
||||||
|
|
@ -45,6 +47,25 @@ class TestShortSessionId:
|
||||||
assert short_session_id("") == ""
|
assert short_session_id("") == ""
|
||||||
|
|
||||||
|
|
||||||
|
class TestCanDeleteResumeSession:
|
||||||
|
def test_local_source_can_delete(self) -> None:
|
||||||
|
assert can_delete_resume_session_source("local") is True
|
||||||
|
|
||||||
|
def test_remote_source_cannot_delete(self) -> None:
|
||||||
|
assert can_delete_resume_session_source("remote") is False
|
||||||
|
|
||||||
|
def test_session_info_can_delete_matches_source(self) -> None:
|
||||||
|
session = ResumeSessionInfo(
|
||||||
|
session_id="session-a",
|
||||||
|
source="local",
|
||||||
|
cwd="/test",
|
||||||
|
title=None,
|
||||||
|
end_time=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert session.can_delete is True
|
||||||
|
|
||||||
|
|
||||||
class TestListRemoteResumeSessions:
|
class TestListRemoteResumeSessions:
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_returns_empty_when_vibe_code_disabled(self) -> None:
|
async def test_returns_empty_when_vibe_code_disabled(self) -> None:
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,22 @@ class TestBuiltinSkills:
|
||||||
def test_vibe_skill_has_inline_prompt(self) -> None:
|
def test_vibe_skill_has_inline_prompt(self) -> None:
|
||||||
assert BUILTIN_SKILLS["vibe"].prompt
|
assert BUILTIN_SKILLS["vibe"].prompt
|
||||||
|
|
||||||
|
def test_vibe_skill_pins_readme_url_to_running_version(self) -> None:
|
||||||
|
from vibe import __version__
|
||||||
|
|
||||||
|
prompt = BUILTIN_SKILLS["vibe"].prompt
|
||||||
|
assert "__VIBE_VERSION__" not in prompt
|
||||||
|
assert (
|
||||||
|
f"https://github.com/mistralai/mistral-vibe/blob/v{__version__}/README.md"
|
||||||
|
in prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_vibe_skill_references_user_docs_url(self) -> None:
|
||||||
|
assert (
|
||||||
|
"https://docs.mistral.ai/vibe/code/overview"
|
||||||
|
in BUILTIN_SKILLS["vibe"].prompt
|
||||||
|
)
|
||||||
|
|
||||||
def test_discovers_builtin_skills(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_discovers_builtin_skills(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr("vibe.core.skills.manager.BUILTIN_SKILLS", BUILTIN_SKILLS)
|
monkeypatch.setattr("vibe.core.skills.manager.BUILTIN_SKILLS", BUILTIN_SKILLS)
|
||||||
config = build_test_vibe_config(
|
config = build_test_vibe_config(
|
||||||
|
|
|
||||||
|
|
@ -114,15 +114,15 @@
|
||||||
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
<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="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="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
</text><text class="terminal-r1" x="0" y="93.2" textLength="134.2" clip-path="url(#terminal-line-3)">  ⡠⣒⠄  ⡔⢄⠔⡄</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="117.6" textLength="134.2" clip-path="url(#terminal-line-4)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</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="0" y="142" textLength="134.2" clip-path="url(#terminal-line-5)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</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="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-r2" x="0" y="190.8" textLength="146.4" clip-path="url(#terminal-line-7)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="190.8" textLength="122" clip-path="url(#terminal-line-7)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="190.8" textLength="244" clip-path="url(#terminal-line-7)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="190.8" textLength="256.2" clip-path="url(#terminal-line-7)"> · [Subscription] Pro</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="0" y="215.2" textLength="134.2" clip-path="url(#terminal-line-8)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="215.2" textLength="146.4" clip-path="url(#terminal-line-8)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="215.2" textLength="122" clip-path="url(#terminal-line-8)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="215.2" textLength="244" clip-path="url(#terminal-line-8)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="215.2" textLength="256.2" clip-path="url(#terminal-line-8)"> · [Subscription] Pro</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="0" 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="1464" 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="414.8" clip-path="url(#terminal-line-9)">1 model · 0 MCP servers · 0 skills</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="239.6" textLength="61" clip-path="url(#terminal-line-9)">Type </text><text class="terminal-r3" x="61" y="239.6" textLength="61" clip-path="url(#terminal-line-9)">/help</text><text class="terminal-r1" x="122" y="239.6" textLength="256.2" clip-path="url(#terminal-line-9)"> for more information</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-r1" x="170.8" y="264" textLength="61" clip-path="url(#terminal-line-10)">Type </text><text class="terminal-r3" x="231.8" y="264" textLength="61" clip-path="url(#terminal-line-10)">/help</text><text class="terminal-r1" x="292.8" y="264" textLength="256.2" clip-path="url(#terminal-line-10)"> for more information</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-r4" x="24.4" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">⎣</text><text class="terminal-r4" x="48.8" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">▶</text><text class="terminal-r4" x="73.2" y="288.4" textLength="85.4" clip-path="url(#terminal-line-11)">6 lines</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="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="0" y="361.6" textLength="1464" clip-path="url(#terminal-line-14)">────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─</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="1464" clip-path="url(#terminal-line-14)">────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 9.1 KiB |
|
|
@ -148,21 +148,21 @@
|
||||||
</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="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="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="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="0" y="190.8" textLength="134.2" clip-path="url(#terminal-line-7)">  ⡠⣒⠄  ⡔⢄⠔⡄</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="0" y="215.2" textLength="134.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="0" y="239.6" textLength="134.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="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-r2" x="0" y="288.4" textLength="146.4" clip-path="url(#terminal-line-11)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="288.4" textLength="122" clip-path="url(#terminal-line-11)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="288.4" textLength="244" clip-path="url(#terminal-line-11)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="288.4" textLength="256.2" clip-path="url(#terminal-line-11)"> · [Subscription] Pro</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="414.8" clip-path="url(#terminal-line-12)">1 model · 0 MCP servers · 0 skills</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="414.8" clip-path="url(#terminal-line-13)">1 model · 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="61" clip-path="url(#terminal-line-13)">Type </text><text class="terminal-r3" x="61" y="337.2" textLength="61" clip-path="url(#terminal-line-13)">/help</text><text class="terminal-r1" x="122" y="337.2" textLength="256.2" clip-path="url(#terminal-line-13)"> for more information</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="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-r4" x="24.4" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">⎢</text><text class="terminal-r4" x="48.8" y="386" textLength="695.4" clip-path="url(#terminal-line-15)">What programming language are you currently working with?</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</text><text class="terminal-r4" x="24.4" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">⎢</text><text class="terminal-r4" x="48.8" y="410.4" textLength="695.4" clip-path="url(#terminal-line-16)">What programming language are you currently working with?</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
</text><text class="terminal-r4" x="24.4" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">⎢</text><text class="terminal-r1" x="48.8" y="410.4" textLength="48.8" clip-path="url(#terminal-line-16)">Rust</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||||
</text><text class="terminal-r4" x="24.4" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">⎢</text><text class="terminal-r1" x="48.8" y="434.8" textLength="48.8" clip-path="url(#terminal-line-17)">Rust</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
</text><text class="terminal-r4" x="24.4" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">⎢</text><text class="terminal-r4" x="48.8" y="434.8" textLength="463.6" clip-path="url(#terminal-line-17)">What type of project are you building?</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r4" x="24.4" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">⎢</text><text class="terminal-r4" x="48.8" y="459.2" textLength="463.6" clip-path="url(#terminal-line-18)">What type of project are you building?</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r4" x="24.4" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">⎢</text><text class="terminal-r1" x="48.8" y="459.2" textLength="183" clip-path="url(#terminal-line-18)">Web Application</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r4" x="24.4" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">⎢</text><text class="terminal-r1" x="48.8" y="483.6" textLength="183" clip-path="url(#terminal-line-19)">Web Application</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r4" x="24.4" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">⎢</text><text class="terminal-r4" x="48.8" y="483.6" textLength="402.6" clip-path="url(#terminal-line-19)">What editor or IDE do you prefer?</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
</text><text class="terminal-r4" x="24.4" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">⎢</text><text class="terminal-r4" x="48.8" y="508" textLength="402.6" clip-path="url(#terminal-line-20)">What editor or IDE do you prefer?</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r4" x="24.4" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">⎢</text><text class="terminal-r1" x="48.8" y="508" textLength="183" clip-path="url(#terminal-line-20)">(Other) VS Code</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r4" x="24.4" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">⎣</text><text class="terminal-r1" x="48.8" y="532.4" textLength="183" clip-path="url(#terminal-line-21)">(Other) VS Code</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r4" x="24.4" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">⎣</text><text class="terminal-r4" x="48.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">▼</text><text class="terminal-r4" x="73.2" y="532.4" textLength="109.8" clip-path="url(#terminal-line-21)">show less</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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="1464" clip-path="url(#terminal-line-24)">────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─</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="605.6" textLength="1464" clip-path="url(#terminal-line-24)">────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -174,13 +174,13 @@
|
||||||
</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="1464" 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="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="0" y="361.6" textLength="134.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="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</text><text class="terminal-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="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="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r2" x="0" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="0" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r1" x="0" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="61" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="122" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="1464" y="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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -178,13 +178,13 @@
|
||||||
</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="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="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="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||||
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
</text><text class="terminal-r1" x="0" y="337.2" textLength="134.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="0" y="361.6" textLength="134.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-r2" x="0" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="410.4" textLength="244" clip-path="url(#terminal-line-16)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||||
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1 model · 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="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="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="61" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="122" 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)">
|
||||||
</text><text class="terminal-r1" x="24.4" y="532.4" textLength="451.4" clip-path="url(#terminal-line-21)">Here's a very long print instruction:</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="24.4" y="532.4" textLength="451.4" clip-path="url(#terminal-line-21)">Here's a very long print instruction:</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -177,13 +177,13 @@
|
||||||
</text><text class="terminal-r1" x="1220" 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="1220" 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="1220" 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="1220" 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="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</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="483.6" textLength="134.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="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r1" x="0" y="508" textLength="134.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="1220" 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="134.2" clip-path="url(#terminal-line-22)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="556.8" textLength="244" clip-path="url(#terminal-line-22)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="556.8" textLength="256.2" clip-path="url(#terminal-line-22)"> · [Subscription] Pro</text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
</text><text class="terminal-r2" x="0" y="556.8" textLength="146.4" clip-path="url(#terminal-line-22)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="556.8" textLength="122" clip-path="url(#terminal-line-22)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="556.8" textLength="244" clip-path="url(#terminal-line-22)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="556.8" textLength="256.2" clip-path="url(#terminal-line-22)"> · [Subscription] Pro</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="134.2" clip-path="url(#terminal-line-23)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1 model · 0 MCP servers · 0 skills</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="581.2" textLength="414.8" clip-path="url(#terminal-line-23)">1 model · 0 MCP servers · 0 skills</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="134.2" clip-path="url(#terminal-line-24)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type </text><text class="terminal-r3" x="231.8" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">/help</text><text class="terminal-r1" x="292.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)"> for more information</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="605.6" textLength="61" clip-path="url(#terminal-line-24)">Type </text><text class="terminal-r3" x="61" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">/help</text><text class="terminal-r1" x="122" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)"> for more information</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="1220" 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="24.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">⎣</text><text class="terminal-r1" x="48.8" y="654.4" textLength="280.6" clip-path="url(#terminal-line-26)">Configuration opened...</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="24.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">⎣</text><text class="terminal-r1" x="48.8" y="654.4" textLength="280.6" clip-path="url(#terminal-line-26)">Configuration opened...</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="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">⎣</text><text class="terminal-r1" x="48.8" y="678.8" textLength="488" clip-path="url(#terminal-line-27)">Configuration closed (no changes saved).</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="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">⎣</text><text class="terminal-r1" x="48.8" y="678.8" textLength="488" clip-path="url(#terminal-line-27)">Configuration closed (no changes saved).</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -177,13 +177,13 @@
|
||||||
</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="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="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||||
</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="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||||
</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="361.6" textLength="134.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="1220" 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-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</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="410.4" textLength="134.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="1220" 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="134.2" clip-path="url(#terminal-line-18)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r2" x="0" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</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="134.2" clip-path="url(#terminal-line-19)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 skills</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="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 skills</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="134.2" clip-path="url(#terminal-line-20)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r1" x="0" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="61" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="122" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</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="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
</text><text class="terminal-r1" x="24.4" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">⎣</text><text class="terminal-r1" x="48.8" y="556.8" textLength="280.6" clip-path="url(#terminal-line-22)">Configuration opened...</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="24.4" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">⎣</text><text class="terminal-r1" x="48.8" y="556.8" textLength="280.6" clip-path="url(#terminal-line-22)">Configuration opened...</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="1220" 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)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -177,13 +177,13 @@
|
||||||
</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="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="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||||
</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="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||||
</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="361.6" textLength="134.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="1220" 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-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</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="410.4" textLength="134.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="1220" 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="134.2" clip-path="url(#terminal-line-18)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r2" x="0" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</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="134.2" clip-path="url(#terminal-line-19)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 skills</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="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 skills</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="134.2" clip-path="url(#terminal-line-20)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r1" x="0" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="61" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="122" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</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="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
</text><text class="terminal-r1" x="24.4" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">⎣</text><text class="terminal-r1" x="48.8" y="556.8" textLength="280.6" clip-path="url(#terminal-line-22)">Configuration opened...</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="24.4" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">⎣</text><text class="terminal-r1" x="48.8" y="556.8" textLength="280.6" clip-path="url(#terminal-line-22)">Configuration opened...</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="1220" 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)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -177,13 +177,13 @@
|
||||||
</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="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="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||||
</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="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||||
</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="361.6" textLength="134.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="1220" 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-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</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="410.4" textLength="134.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="1220" 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="134.2" clip-path="url(#terminal-line-18)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r2" x="0" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</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="134.2" clip-path="url(#terminal-line-19)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 skills</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="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 skills</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="134.2" clip-path="url(#terminal-line-20)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r1" x="0" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="61" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="122" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</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="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
</text><text class="terminal-r1" x="24.4" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">⎣</text><text class="terminal-r1" x="48.8" y="556.8" textLength="280.6" clip-path="url(#terminal-line-22)">Configuration opened...</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="24.4" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">⎣</text><text class="terminal-r1" x="48.8" y="556.8" textLength="280.6" clip-path="url(#terminal-line-22)">Configuration opened...</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="1220" 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)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -180,13 +180,13 @@
|
||||||
</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="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)">
|
||||||
</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="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="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="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-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-r2" x="0" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="605.6" textLength="244" clip-path="url(#terminal-line-24)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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-r1" x="0" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-r3" x="61" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="122" 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="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="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-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)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -180,13 +180,13 @@
|
||||||
</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="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)">
|
||||||
</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="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="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="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-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-r2" x="0" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="605.6" textLength="244" clip-path="url(#terminal-line-24)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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-r1" x="0" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-r3" x="61" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="122" 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="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="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-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)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -175,13 +175,13 @@
|
||||||
</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="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="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||||
</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="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||||
</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="361.6" textLength="134.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="1220" 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-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</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="410.4" textLength="134.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="1220" 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="134.2" clip-path="url(#terminal-line-18)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r2" x="0" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</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="134.2" clip-path="url(#terminal-line-19)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 skills</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="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 skills</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="134.2" clip-path="url(#terminal-line-20)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r1" x="0" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="61" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="122" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</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="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
</text><text class="terminal-r1" x="24.4" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">⎢</text><text class="terminal-r4" x="48.8" y="556.8" textLength="414.8" clip-path="url(#terminal-line-22)">Your Data Helps Improve Mistral AI</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="24.4" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">⎢</text><text class="terminal-r4" x="48.8" y="556.8" textLength="414.8" clip-path="url(#terminal-line-22)">Your Data Helps Improve Mistral AI</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="24.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">⎢</text><text class="terminal-r1" x="48.8" y="581.2" textLength="1159" clip-path="url(#terminal-line-23)">At Mistral AI, we're committed to delivering the best possible experience. When you use Mistral</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="24.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">⎢</text><text class="terminal-r1" x="48.8" y="581.2" textLength="1159" clip-path="url(#terminal-line-23)">At Mistral AI, we're committed to delivering the best possible experience. When you use Mistral</text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -179,13 +179,13 @@
|
||||||
</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="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)">
|
||||||
</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="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="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="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-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-r2" x="0" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="605.6" textLength="244" clip-path="url(#terminal-line-24)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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="0" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-r3" x="61" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="122" 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="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)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
|
@ -182,13 +182,13 @@
|
||||||
</text><text class="terminal-r1" x="878.4" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">│</text><text class="terminal-r1" x="902.8" y="434.8" textLength="268.4" clip-path="url(#terminal-line-17)">connection attempt 1/3</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
</text><text class="terminal-r1" x="878.4" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">│</text><text class="terminal-r1" x="902.8" y="434.8" textLength="268.4" clip-path="url(#terminal-line-17)">connection attempt 1/3</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">│</text><text class="terminal-r2" x="902.8" y="459.2" textLength="231.8" clip-path="url(#terminal-line-18)">2026-02-21 10:28:51</text><text class="terminal-r4" x="1146.8" y="459.2" textLength="97.6" clip-path="url(#terminal-line-18)">WARNING </text><text class="terminal-r1" x="1244.4" y="459.2" textLength="207.4" clip-path="url(#terminal-line-18)"> Rate limit      </text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r1" x="878.4" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">│</text><text class="terminal-r2" x="902.8" y="459.2" textLength="231.8" clip-path="url(#terminal-line-18)">2026-02-21 10:28:51</text><text class="terminal-r4" x="1146.8" y="459.2" textLength="97.6" clip-path="url(#terminal-line-18)">WARNING </text><text class="terminal-r1" x="1244.4" y="459.2" textLength="207.4" clip-path="url(#terminal-line-18)"> Rate limit      </text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">│</text><text class="terminal-r1" x="902.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">approaching for client api-key-abc</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="878.4" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">│</text><text class="terminal-r1" x="902.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">approaching for client api-key-abc</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">│</text><text class="terminal-r2" x="902.8" y="508" textLength="231.8" clip-path="url(#terminal-line-20)">2026-02-21 10:28:52</text><text class="terminal-r3" x="1146.8" y="508" textLength="97.6" clip-path="url(#terminal-line-20)">INFO    </text><text class="terminal-r1" x="1244.4" y="508" textLength="207.4" clip-path="url(#terminal-line-20)"> Health check    </text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r1" x="780.8" y="508" textLength="109.8" clip-path="url(#terminal-line-20)">        │</text><text class="terminal-r2" x="902.8" y="508" textLength="231.8" clip-path="url(#terminal-line-20)">2026-02-21 10:28:52</text><text class="terminal-r3" x="1146.8" y="508" textLength="97.6" clip-path="url(#terminal-line-20)">INFO    </text><text class="terminal-r1" x="1244.4" y="508" textLength="207.4" clip-path="url(#terminal-line-20)"> Health check    </text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">│</text><text class="terminal-r1" x="902.8" y="532.4" textLength="73.2" clip-path="url(#terminal-line-21)">passed</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="780.8" y="532.4" textLength="109.8" clip-path="url(#terminal-line-21)">        │</text><text class="terminal-r1" x="902.8" y="532.4" textLength="73.2" clip-path="url(#terminal-line-21)">passed</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">│</text><text class="terminal-r2" x="902.8" y="556.8" textLength="231.8" clip-path="url(#terminal-line-22)">2026-02-21 10:28:53</text><text class="terminal-r6" x="1146.8" y="556.8" textLength="97.6" clip-path="url(#terminal-line-22)">CRITICAL</text><text class="terminal-r1" x="1244.4" y="556.8" textLength="207.4" clip-path="url(#terminal-line-22)"> Out of memory   </text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
</text><text class="terminal-r1" x="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="780.8" y="556.8" textLength="109.8" clip-path="url(#terminal-line-22)">        │</text><text class="terminal-r2" x="902.8" y="556.8" textLength="231.8" clip-path="url(#terminal-line-22)">2026-02-21 10:28:53</text><text class="terminal-r6" x="1146.8" y="556.8" textLength="97.6" clip-path="url(#terminal-line-22)">CRITICAL</text><text class="terminal-r1" x="1244.4" y="556.8" textLength="207.4" clip-path="url(#terminal-line-22)"> Out of memory   </text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">│</text><text class="terminal-r1" x="902.8" y="581.2" textLength="170.8" clip-path="url(#terminal-line-23)">error detected</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="780.8" y="581.2" textLength="109.8" clip-path="url(#terminal-line-23)">        │</text><text class="terminal-r1" x="902.8" y="581.2" textLength="170.8" clip-path="url(#terminal-line-23)">error detected</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r7" x="170.8" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-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="195.2" clip-path="url(#terminal-line-24)"> · [Subscription</text><text class="terminal-r1" x="878.4" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">│</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
</text><text class="terminal-r7" x="0" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="605.6" textLength="244" clip-path="url(#terminal-line-24)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)"> · [Subscription] Pro</text><text class="terminal-r1" x="780.8" y="605.6" textLength="109.8" clip-path="url(#terminal-line-24)">        │</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||||
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="878.4" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">│</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
</text><text class="terminal-r1" x="0" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="780.8" y="630" textLength="109.8" clip-path="url(#terminal-line-25)">        │</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||||
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-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="878.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">│</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
</text><text class="terminal-r1" x="0" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-r3" x="61" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="122" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)"> for more information</text><text class="terminal-r1" x="780.8" y="654.4" textLength="109.8" 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="878.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">│</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
</text><text class="terminal-r1" x="878.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">│</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">│</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
</text><text class="terminal-r1" x="878.4" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">│</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">│</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
</text><text class="terminal-r1" x="878.4" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">│</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
|
@ -181,13 +181,13 @@
|
||||||
</text><text class="terminal-r1" x="878.4" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">│</text><text class="terminal-r1" x="902.8" y="434.8" textLength="268.4" clip-path="url(#terminal-line-17)">connection attempt 1/3</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
</text><text class="terminal-r1" x="878.4" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">│</text><text class="terminal-r1" x="902.8" y="434.8" textLength="268.4" clip-path="url(#terminal-line-17)">connection attempt 1/3</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">│</text><text class="terminal-r2" x="902.8" y="459.2" textLength="231.8" clip-path="url(#terminal-line-18)">2026-02-21 10:28:51</text><text class="terminal-r4" x="1146.8" y="459.2" textLength="97.6" clip-path="url(#terminal-line-18)">WARNING </text><text class="terminal-r1" x="1244.4" y="459.2" textLength="207.4" clip-path="url(#terminal-line-18)"> Rate limit      </text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r1" x="878.4" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">│</text><text class="terminal-r2" x="902.8" y="459.2" textLength="231.8" clip-path="url(#terminal-line-18)">2026-02-21 10:28:51</text><text class="terminal-r4" x="1146.8" y="459.2" textLength="97.6" clip-path="url(#terminal-line-18)">WARNING </text><text class="terminal-r1" x="1244.4" y="459.2" textLength="207.4" clip-path="url(#terminal-line-18)"> Rate limit      </text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">│</text><text class="terminal-r1" x="902.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">approaching for client api-key-abc</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="878.4" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">│</text><text class="terminal-r1" x="902.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">approaching for client api-key-abc</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">│</text><text class="terminal-r2" x="902.8" y="508" textLength="231.8" clip-path="url(#terminal-line-20)">2026-02-21 10:28:52</text><text class="terminal-r3" x="1146.8" y="508" textLength="97.6" clip-path="url(#terminal-line-20)">INFO    </text><text class="terminal-r1" x="1244.4" y="508" textLength="207.4" clip-path="url(#terminal-line-20)"> Health check    </text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r1" x="780.8" y="508" textLength="109.8" clip-path="url(#terminal-line-20)">        │</text><text class="terminal-r2" x="902.8" y="508" textLength="231.8" clip-path="url(#terminal-line-20)">2026-02-21 10:28:52</text><text class="terminal-r3" x="1146.8" y="508" textLength="97.6" clip-path="url(#terminal-line-20)">INFO    </text><text class="terminal-r1" x="1244.4" y="508" textLength="207.4" clip-path="url(#terminal-line-20)"> Health check    </text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">│</text><text class="terminal-r1" x="902.8" y="532.4" textLength="73.2" clip-path="url(#terminal-line-21)">passed</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="780.8" y="532.4" textLength="109.8" clip-path="url(#terminal-line-21)">        │</text><text class="terminal-r1" x="902.8" y="532.4" textLength="73.2" clip-path="url(#terminal-line-21)">passed</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">│</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
</text><text class="terminal-r1" x="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="780.8" y="556.8" textLength="109.8" 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="878.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">│</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="780.8" y="581.2" textLength="109.8" clip-path="url(#terminal-line-23)">        │</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r6" 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="195.2" clip-path="url(#terminal-line-24)"> · [Subscription</text><text class="terminal-r1" x="878.4" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">│</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
</text><text class="terminal-r6" x="0" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="605.6" textLength="244" clip-path="url(#terminal-line-24)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)"> · [Subscription] Pro</text><text class="terminal-r1" x="780.8" y="605.6" textLength="109.8" clip-path="url(#terminal-line-24)">        │</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||||
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="878.4" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">│</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
</text><text class="terminal-r1" x="0" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="780.8" y="630" textLength="109.8" clip-path="url(#terminal-line-25)">        │</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||||
</text><text class="terminal-r1" x="0" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-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="878.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">│</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
</text><text class="terminal-r1" x="0" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-r3" x="61" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="122" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)"> for more information</text><text class="terminal-r1" x="780.8" y="654.4" textLength="109.8" 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="878.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">│</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
</text><text class="terminal-r1" x="878.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">│</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">│</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
</text><text class="terminal-r1" x="878.4" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">│</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||||
</text><text class="terminal-r1" x="878.4" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">│</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
</text><text class="terminal-r1" x="878.4" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">│</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
|
|
@ -178,13 +178,13 @@
|
||||||
<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"/>
|
<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">
|
<g class="terminal-matrix">
|
||||||
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
<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="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||||
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||||
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
</text><text class="terminal-r1" x="0" y="93.2" textLength="134.2" clip-path="url(#terminal-line-3)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</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="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-r2" x="0" y="142" textLength="146.4" clip-path="url(#terminal-line-5)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="142" textLength="122" clip-path="url(#terminal-line-5)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="142" textLength="244" clip-path="url(#terminal-line-5)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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="0" y="190.8" textLength="61" clip-path="url(#terminal-line-7)">Type </text><text class="terminal-r3" x="61" y="190.8" textLength="61" clip-path="url(#terminal-line-7)">/help</text><text class="terminal-r1" x="122" 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="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="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)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
|
@ -35,9 +35,9 @@
|
||||||
.terminal-r1 { fill: #c5c8c6 }
|
.terminal-r1 { fill: #c5c8c6 }
|
||||||
.terminal-r2 { fill: #ff8205;font-weight: bold }
|
.terminal-r2 { fill: #ff8205;font-weight: bold }
|
||||||
.terminal-r3 { fill: #68a0b3 }
|
.terminal-r3 { fill: #68a0b3 }
|
||||||
.terminal-r4 { fill: #d0b344;font-weight: bold }
|
.terminal-r4 { fill: #292929 }
|
||||||
.terminal-r5 { fill: #d0b344 }
|
.terminal-r5 { fill: #d0b344;font-weight: bold }
|
||||||
.terminal-r6 { fill: #292929 }
|
.terminal-r6 { fill: #d0b344 }
|
||||||
.terminal-r7 { fill: #98a84b;font-weight: bold }
|
.terminal-r7 { fill: #98a84b;font-weight: bold }
|
||||||
.terminal-r8 { fill: #cc555a }
|
.terminal-r8 { fill: #cc555a }
|
||||||
.terminal-r9 { fill: #868887 }
|
.terminal-r9 { fill: #868887 }
|
||||||
|
|
@ -144,26 +144,26 @@
|
||||||
</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="#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"/>
|
<rect fill="#4b4e55" x="1207.8" y="1.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1207.8" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="1207.8" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="1207.8" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="1207.8" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#608ab1" x="1207.8" y="123.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><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">
|
<g class="terminal-matrix">
|
||||||
<text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><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="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-r2" x="0" y="68.8" textLength="146.4" clip-path="url(#terminal-line-2)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="68.8" textLength="122" clip-path="url(#terminal-line-2)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="68.8" textLength="244" clip-path="url(#terminal-line-2)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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="0" y="117.6" textLength="61" clip-path="url(#terminal-line-4)">Type </text><text class="terminal-r3" x="61" y="117.6" textLength="61" clip-path="url(#terminal-line-4)">/help</text><text class="terminal-r1" x="122" 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="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="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="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="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="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">│</text><text class="terminal-r5" 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="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-r6" 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="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-r6" 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="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-r6" 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="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-r6" 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="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-r6" 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="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-r6" 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="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-r6" 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="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-r6" 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="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="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="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)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
|
@ -148,13 +148,13 @@
|
||||||
<text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
<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="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="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="0" y="93.2" textLength="134.2" clip-path="url(#terminal-line-3)">  ⡠⣒⠄  ⡔⢄⠔⡄</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="0" y="117.6" textLength="134.2" clip-path="url(#terminal-line-4)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</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="0" y="142" textLength="134.2" clip-path="url(#terminal-line-5)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</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="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-r2" x="0" y="190.8" textLength="146.4" clip-path="url(#terminal-line-7)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="190.8" textLength="122" clip-path="url(#terminal-line-7)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="190.8" textLength="244" clip-path="url(#terminal-line-7)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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="0" y="239.6" textLength="61" clip-path="url(#terminal-line-9)">Type </text><text class="terminal-r3" x="61" y="239.6" textLength="61" clip-path="url(#terminal-line-9)">/help</text><text class="terminal-r1" x="122" 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="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="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="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
|
@ -37,7 +37,7 @@
|
||||||
.terminal-r3 { fill: #68a0b3 }
|
.terminal-r3 { fill: #68a0b3 }
|
||||||
.terminal-r4 { fill: #c5c8c6;font-weight: bold }
|
.terminal-r4 { fill: #c5c8c6;font-weight: bold }
|
||||||
.terminal-r5 { fill: #868887 }
|
.terminal-r5 { fill: #868887 }
|
||||||
.terminal-r6 { fill: #6b753d }
|
.terminal-r6 { fill: #98a84b }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<defs>
|
<defs>
|
||||||
|
|
@ -173,20 +173,20 @@
|
||||||
</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="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="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="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||||
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
</text><text class="terminal-r1" x="0" y="337.2" textLength="134.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="0" y="361.6" textLength="134.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-r2" x="0" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="410.4" textLength="244" clip-path="url(#terminal-line-16)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||||
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1 model · 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="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="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="61" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="122" 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)">
|
||||||
</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="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
</text><text class="terminal-r2" x="0" y="556.8" textLength="24.4" clip-path="url(#terminal-line-22)">> </text><text class="terminal-r4" x="24.4" y="556.8" textLength="231.8" clip-path="url(#terminal-line-22)">What is the answer?</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="556.8" textLength="24.4" clip-path="url(#terminal-line-22)">> </text><text class="terminal-r4" x="24.4" y="556.8" textLength="231.8" clip-path="url(#terminal-line-22)">What is the answer?</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r5" x="0" y="581.2" textLength="1464" clip-path="url(#terminal-line-23)">────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r5" x="0" y="581.2" textLength="1464" clip-path="url(#terminal-line-23)">────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
</text><text class="terminal-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-r6" x="0" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">✓</text><text class="terminal-r5" x="24.4" y="630" textLength="85.4" clip-path="url(#terminal-line-25)">Thought</text><text class="terminal-r5" x="122" 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-r6" x="0" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">■</text><text class="terminal-r5" x="24.4" y="630" textLength="85.4" clip-path="url(#terminal-line-25)">Thought</text><text class="terminal-r5" x="122" 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="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-r1" x="24.4" y="678.8" textLength="207.4" clip-path="url(#terminal-line-27)">The answer is 42.</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="24.4" y="678.8" textLength="207.4" clip-path="url(#terminal-line-27)">The answer is 42.</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)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -175,13 +175,13 @@
|
||||||
</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="1464" 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="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="0" y="361.6" textLength="134.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="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</text><text class="terminal-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="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="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r2" x="0" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="0" y="483.6" textLength="414.8" clip-path="url(#terminal-line-19)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r1" x="0" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="61" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="122" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="1464" y="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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -179,13 +179,13 @@
|
||||||
</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="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)">
|
||||||
</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="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="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="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
</text><text class="terminal-r1" x="0" y="605.6" textLength="134.2" clip-path="url(#terminal-line-24)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-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-r2" x="0" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="605.6" textLength="244" clip-path="url(#terminal-line-24)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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="0" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-r3" x="61" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="122" 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="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)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -173,13 +173,13 @@
|
||||||
</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="1464" 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="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="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="0" y="361.6" textLength="134.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="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</text><text class="terminal-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)">
|
||||||
</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-r2" x="0" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="434.8" textLength="244" clip-path="url(#terminal-line-17)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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="0" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="61" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="122" 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="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="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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -173,13 +173,13 @@
|
||||||
</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="1464" 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="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="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="0" y="361.6" textLength="134.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="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</text><text class="terminal-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)">
|
||||||
</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-r2" x="0" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="434.8" textLength="244" clip-path="url(#terminal-line-17)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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="0" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="61" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="122" 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="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="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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -175,13 +175,13 @@
|
||||||
</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="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="0" y="288.4" textLength="134.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="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="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||||
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
</text><text class="terminal-r1" x="0" y="337.2" textLength="134.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-r2" x="0" y="386" textLength="146.4" clip-path="url(#terminal-line-15)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="386" textLength="122" clip-path="url(#terminal-line-15)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="386" textLength="244" clip-path="url(#terminal-line-15)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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="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="61" clip-path="url(#terminal-line-17)">Type </text><text class="terminal-r3" x="61" y="434.8" textLength="61" clip-path="url(#terminal-line-17)">/help</text><text class="terminal-r1" x="122" 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)">
|
||||||
</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: 18 KiB After Width: | Height: | Size: 18 KiB |
|
|
@ -169,13 +169,13 @@
|
||||||
</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="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="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="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="0" y="215.2" textLength="134.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="0" y="239.6" textLength="134.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="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-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-r2" x="0" y="312.8" textLength="146.4" clip-path="url(#terminal-line-12)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="312.8" textLength="122" clip-path="url(#terminal-line-12)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="312.8" textLength="244" clip-path="url(#terminal-line-12)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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="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="61" clip-path="url(#terminal-line-14)">Type </text><text class="terminal-r3" x="61" y="361.6" textLength="61" clip-path="url(#terminal-line-14)">/help</text><text class="terminal-r1" x="122" 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)">
|
||||||
</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="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -167,13 +167,13 @@
|
||||||
</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="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="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="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="0" y="166.4" textLength="134.2" clip-path="url(#terminal-line-6)">  ⡠⣒⠄  ⡔⢄⠔⡄</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="0" y="190.8" textLength="134.2" clip-path="url(#terminal-line-7)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</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="0" y="215.2" textLength="134.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-r2" x="0" y="264" textLength="146.4" clip-path="url(#terminal-line-10)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="264" textLength="122" clip-path="url(#terminal-line-10)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="264" textLength="244" clip-path="url(#terminal-line-10)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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="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="61" clip-path="url(#terminal-line-12)">Type </text><text class="terminal-r3" x="61" y="312.8" textLength="61" clip-path="url(#terminal-line-12)">/help</text><text class="terminal-r1" x="122" 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)">
|
||||||
</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)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
|
@ -176,13 +176,13 @@
|
||||||
</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="1464" 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="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="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="0" y="361.6" textLength="134.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="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</text><text class="terminal-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)">
|
||||||
</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-r2" x="0" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="434.8" textLength="244" clip-path="url(#terminal-line-17)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r1" x="0" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> for more information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="0" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="61" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="122" 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="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="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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -175,13 +175,13 @@
|
||||||
</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="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="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="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||||
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
</text><text class="terminal-r1" x="0" y="337.2" textLength="134.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="0" y="361.6" textLength="134.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-r2" x="0" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="410.4" textLength="244" clip-path="url(#terminal-line-16)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||||
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="434.8" textLength="414.8" clip-path="url(#terminal-line-17)">1 model · 3 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="414.8" clip-path="url(#terminal-line-17)">1 model · 3 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="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="61" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="122" 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)">
|
||||||
</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="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -174,13 +174,13 @@
|
||||||
</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="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="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="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-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="0" y="288.4" textLength="134.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="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="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="0" y="361.6" textLength="134.2" clip-path="url(#terminal-line-14)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="361.6" textLength="146.4" clip-path="url(#terminal-line-14)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="361.6" textLength="122" clip-path="url(#terminal-line-14)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="361.6" textLength="244" clip-path="url(#terminal-line-14)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="361.6" textLength="256.2" clip-path="url(#terminal-line-14)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
</text><text class="terminal-r2" x="0" y="361.6" textLength="146.4" clip-path="url(#terminal-line-14)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="361.6" textLength="122" clip-path="url(#terminal-line-14)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="361.6" textLength="244" clip-path="url(#terminal-line-14)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="361.6" textLength="256.2" clip-path="url(#terminal-line-14)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||||
</text><text class="terminal-r1" x="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="386" textLength="585.6" clip-path="url(#terminal-line-15)">1 model · 2 connectors · 1 MCP server · 0 skills</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
</text><text class="terminal-r1" x="0" y="386" textLength="585.6" clip-path="url(#terminal-line-15)">1 model · 2 connectors · 1 MCP server · 0 skills</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">Type </text><text class="terminal-r3" x="231.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">/help</text><text class="terminal-r1" x="292.8" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)"> for more information</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
</text><text class="terminal-r1" x="0" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">Type </text><text class="terminal-r3" x="61" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">/help</text><text class="terminal-r1" x="122" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)"> for more information</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||||
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
</text><text class="terminal-r1" x="1464" y="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: 17 KiB After Width: | Height: | Size: 17 KiB |
|
|
@ -178,13 +178,13 @@
|
||||||
</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="1464" 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="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="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="0" y="361.6" textLength="134.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="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</text><text class="terminal-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)">
|
||||||
</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-r2" x="0" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="434.8" textLength="244" clip-path="url(#terminal-line-17)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="597.8" clip-path="url(#terminal-line-18)">1 model · 2 connectors · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r1" x="0" y="459.2" textLength="597.8" clip-path="url(#terminal-line-18)">1 model · 2 connectors · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> for more information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="0" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="61" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="122" 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="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="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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -176,13 +176,13 @@
|
||||||
</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="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="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="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||||
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
</text><text class="terminal-r1" x="0" y="337.2" textLength="134.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="0" y="361.6" textLength="134.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-r2" x="0" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="410.4" textLength="244" clip-path="url(#terminal-line-16)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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="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="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="61" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="122" 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)">
|
||||||
</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="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
|
@ -176,13 +176,13 @@
|
||||||
</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="1464" 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="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="0" y="361.6" textLength="134.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="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</text><text class="terminal-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="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="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r2" x="0" y="459.2" textLength="146.4" clip-path="url(#terminal-line-18)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="459.2" textLength="122" clip-path="url(#terminal-line-18)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="459.2" textLength="244" clip-path="url(#terminal-line-18)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="459.2" textLength="256.2" clip-path="url(#terminal-line-18)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="483.6" textLength="585.6" clip-path="url(#terminal-line-19)">1 model · 2 connectors · 1 MCP server · 0 skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="0" y="483.6" textLength="585.6" clip-path="url(#terminal-line-19)">1 model · 2 connectors · 1 MCP server · 0 skills</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="231.8" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="292.8" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r1" x="0" y="508" textLength="61" clip-path="url(#terminal-line-20)">Type </text><text class="terminal-r3" x="61" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="122" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> for more information</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="1464" y="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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
|
@ -176,13 +176,13 @@
|
||||||
</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="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)">
|
||||||
</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-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="410.4" textLength="134.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="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="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="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="483.6" textLength="244" clip-path="url(#terminal-line-19)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r2" x="0" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="483.6" textLength="244" clip-path="url(#terminal-line-19)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="508" textLength="414.8" clip-path="url(#terminal-line-20)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r1" x="0" y="508" textLength="414.8" clip-path="url(#terminal-line-20)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type </text><text class="terminal-r3" x="231.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/help</text><text class="terminal-r1" x="292.8" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)"> for more information</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="0" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type </text><text class="terminal-r3" x="61" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/help</text><text class="terminal-r1" x="122" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)"> for more information</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -174,13 +174,13 @@
|
||||||
</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="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)">
|
||||||
</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-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="410.4" textLength="134.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="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="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="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="483.6" textLength="244" clip-path="url(#terminal-line-19)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r2" x="0" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="483.6" textLength="244" clip-path="url(#terminal-line-19)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="508" textLength="414.8" clip-path="url(#terminal-line-20)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r1" x="0" y="508" textLength="414.8" clip-path="url(#terminal-line-20)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type </text><text class="terminal-r3" x="231.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/help</text><text class="terminal-r1" x="292.8" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)"> for more information</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="0" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type </text><text class="terminal-r3" x="61" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/help</text><text class="terminal-r1" x="122" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)"> for more information</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -176,13 +176,13 @@
|
||||||
</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="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="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="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||||
</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
</text><text class="terminal-r1" x="0" y="337.2" textLength="134.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="0" y="361.6" textLength="134.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-r2" x="0" y="410.4" textLength="146.4" clip-path="url(#terminal-line-16)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="410.4" textLength="122" clip-path="url(#terminal-line-16)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="410.4" textLength="244" clip-path="url(#terminal-line-16)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" 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="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="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="61" clip-path="url(#terminal-line-18)">Type </text><text class="terminal-r3" x="61" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="122" 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)">
|
||||||
</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="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
|
@ -175,13 +175,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="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="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="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||||
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-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="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="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="508" textLength="146.4" clip-path="url(#terminal-line-20)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="508" textLength="122" clip-path="url(#terminal-line-20)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="508" textLength="244" clip-path="url(#terminal-line-20)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r2" x="0" y="508" textLength="146.4" clip-path="url(#terminal-line-20)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="508" textLength="122" clip-path="url(#terminal-line-20)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="508" textLength="244" clip-path="url(#terminal-line-20)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="508" textLength="256.2" clip-path="url(#terminal-line-20)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="532.4" textLength="414.8" clip-path="url(#terminal-line-21)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="0" y="532.4" textLength="414.8" clip-path="url(#terminal-line-21)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
</text><text class="terminal-r1" x="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="556.8" textLength="61" clip-path="url(#terminal-line-22)">Type </text><text class="terminal-r3" x="231.8" y="556.8" textLength="61" clip-path="url(#terminal-line-22)">/help</text><text class="terminal-r1" x="292.8" y="556.8" textLength="256.2" clip-path="url(#terminal-line-22)"> for more information</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
</text><text class="terminal-r1" x="0" y="556.8" textLength="61" clip-path="url(#terminal-line-22)">Type </text><text class="terminal-r3" x="61" y="556.8" textLength="61" clip-path="url(#terminal-line-22)">/help</text><text class="terminal-r1" x="122" y="556.8" textLength="256.2" clip-path="url(#terminal-line-22)"> for more information</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||||
</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -176,13 +176,13 @@
|
||||||
</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="1464" 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="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="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="0" y="361.6" textLength="134.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="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</text><text class="terminal-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)">
|
||||||
</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-r2" x="0" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="434.8" textLength="244" clip-path="url(#terminal-line-17)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r1" x="0" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> for more information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="0" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="61" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="122" 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="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="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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -176,13 +176,13 @@
|
||||||
</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="1464" 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="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="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="0" y="361.6" textLength="134.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="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</text><text class="terminal-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)">
|
||||||
</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-r2" x="0" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="434.8" textLength="244" clip-path="url(#terminal-line-17)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r1" x="0" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> for more information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="0" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="61" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="122" 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="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="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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -176,13 +176,13 @@
|
||||||
</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="1464" 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="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="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="0" y="361.6" textLength="134.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="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</text><text class="terminal-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)">
|
||||||
</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-r2" x="0" y="434.8" textLength="146.4" clip-path="url(#terminal-line-17)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="434.8" textLength="122" clip-path="url(#terminal-line-17)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="434.8" textLength="244" clip-path="url(#terminal-line-17)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
</text><text class="terminal-r1" x="0" y="459.2" textLength="414.8" clip-path="url(#terminal-line-18)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="231.8" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="292.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> for more information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r1" x="0" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">Type </text><text class="terminal-r3" x="61" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="122" 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="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="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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
|
@ -176,13 +176,13 @@
|
||||||
</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="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)">
|
||||||
</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-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="410.4" textLength="134.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="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="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="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="483.6" textLength="244" clip-path="url(#terminal-line-19)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r2" x="0" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="483.6" textLength="244" clip-path="url(#terminal-line-19)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="508" textLength="414.8" clip-path="url(#terminal-line-20)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
</text><text class="terminal-r1" x="0" y="508" textLength="414.8" clip-path="url(#terminal-line-20)">1 model · 2 MCP servers · 0 skills</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||||
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type </text><text class="terminal-r3" x="231.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/help</text><text class="terminal-r1" x="292.8" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)"> for more information</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="0" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type </text><text class="terminal-r3" x="61" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/help</text><text class="terminal-r1" x="122" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)"> for more information</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||||
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||||
</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -174,13 +174,13 @@
|
||||||
</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="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="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="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-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="0" y="288.4" textLength="134.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="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="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="0" y="361.6" textLength="134.2" clip-path="url(#terminal-line-14)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="361.6" textLength="146.4" clip-path="url(#terminal-line-14)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="361.6" textLength="122" clip-path="url(#terminal-line-14)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="361.6" textLength="244" clip-path="url(#terminal-line-14)">devstral-latest[off]</text><text class="terminal-r1" x="683.2" y="361.6" textLength="256.2" clip-path="url(#terminal-line-14)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
</text><text class="terminal-r2" x="0" y="361.6" textLength="146.4" clip-path="url(#terminal-line-14)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="361.6" textLength="122" clip-path="url(#terminal-line-14)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="361.6" textLength="244" clip-path="url(#terminal-line-14)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="361.6" textLength="256.2" clip-path="url(#terminal-line-14)"> · [Subscription] Pro</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||||
</text><text class="terminal-r1" x="0" y="386" textLength="134.2" clip-path="url(#terminal-line-15)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="386" textLength="585.6" clip-path="url(#terminal-line-15)">1 model · 2 connectors · 1 MCP server · 0 skills</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
</text><text class="terminal-r1" x="0" y="386" textLength="585.6" clip-path="url(#terminal-line-15)">1 model · 2 connectors · 1 MCP server · 0 skills</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||||
</text><text class="terminal-r1" x="0" y="410.4" textLength="134.2" clip-path="url(#terminal-line-16)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">Type </text><text class="terminal-r3" x="231.8" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">/help</text><text class="terminal-r1" x="292.8" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)"> for more information</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
</text><text class="terminal-r1" x="0" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">Type </text><text class="terminal-r3" x="61" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">/help</text><text class="terminal-r1" x="122" y="410.4" textLength="256.2" clip-path="url(#terminal-line-16)"> for more information</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||||
</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
</text><text class="terminal-r1" x="1464" y="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: 17 KiB After Width: | Height: | Size: 17 KiB |
|
|
@ -179,13 +179,13 @@
|
||||||
</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="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||||
</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="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||||
</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="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||||
</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="508" textLength="134.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="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||||
</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="556.8" textLength="134.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="1220" 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="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="158.6" clip-path="url(#terminal-line-24)">devstral[off]</text><text class="terminal-r1" x="597.8" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)"> · [Subscription] Pro</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
</text><text class="terminal-r2" x="0" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="605.6" textLength="122" clip-path="url(#terminal-line-24)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="605.6" textLength="158.6" clip-path="url(#terminal-line-24)">devstral[off]</text><text class="terminal-r1" x="427" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)"> · [Subscription] Pro</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||||
</text><text class="terminal-r1" x="0" y="630" textLength="134.2" clip-path="url(#terminal-line-25)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="630" textLength="427" clip-path="url(#terminal-line-25)">5 models · 0 MCP servers · 0 skills</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="630" textLength="427" clip-path="url(#terminal-line-25)">5 models · 0 MCP servers · 0 skills</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="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="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
</text><text class="terminal-r1" x="0" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type </text><text class="terminal-r3" x="61" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="122" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)"> for more information</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="1220" 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="1220" y="703.2" textLength="12.2" 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-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
</text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
|
@ -177,13 +177,13 @@
|
||||||
</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="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||||
</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="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||||
</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="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||||
</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="386" textLength="134.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="1220" 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="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||||
</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="434.8" textLength="134.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="1220" 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="134.2" clip-path="url(#terminal-line-19)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="483.6" textLength="158.6" clip-path="url(#terminal-line-19)">devstral[off]</text><text class="terminal-r1" x="597.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> · [Subscription] Pro</text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r2" x="0" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="483.6" textLength="158.6" clip-path="url(#terminal-line-19)">devstral[off]</text><text class="terminal-r1" x="427" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> · [Subscription] Pro</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="134.2" clip-path="url(#terminal-line-20)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="508" textLength="427" clip-path="url(#terminal-line-20)">5 models · 0 MCP servers · 0 skills</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="508" textLength="427" clip-path="url(#terminal-line-20)">5 models · 0 MCP servers · 0 skills</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="134.2" clip-path="url(#terminal-line-21)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type </text><text class="terminal-r3" x="231.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/help</text><text class="terminal-r1" x="292.8" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)"> for more information</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="0" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type </text><text class="terminal-r3" x="61" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/help</text><text class="terminal-r1" x="122" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)"> for more information</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="1220" 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="1220" 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="1220" 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)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
|
@ -178,13 +178,13 @@
|
||||||
</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="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||||
</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="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||||
</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="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||||
</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="386" textLength="134.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="1220" 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="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||||
</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="434.8" textLength="134.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="1220" 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="134.2" clip-path="url(#terminal-line-19)">  ⡠⣒⠄  ⡔⢄⠔⡄</text><text class="terminal-r2" x="170.8" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="317.2" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </text><text class="terminal-r3" x="439.2" y="483.6" textLength="158.6" clip-path="url(#terminal-line-19)">devstral[off]</text><text class="terminal-r1" x="597.8" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> · [Subscription] Pro</text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
</text><text class="terminal-r2" x="0" y="483.6" textLength="146.4" clip-path="url(#terminal-line-19)">Mistral Vibe</text><text class="terminal-r1" x="146.4" y="483.6" textLength="122" clip-path="url(#terminal-line-19)"> v0.0.0 · </text><text class="terminal-r3" x="268.4" y="483.6" textLength="158.6" clip-path="url(#terminal-line-19)">devstral[off]</text><text class="terminal-r1" x="427" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)"> · [Subscription] Pro</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="134.2" clip-path="url(#terminal-line-20)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="508" textLength="427" clip-path="url(#terminal-line-20)">5 models · 0 MCP servers · 0 skills</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="508" textLength="427" clip-path="url(#terminal-line-20)">5 models · 0 MCP servers · 0 skills</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="134.2" clip-path="url(#terminal-line-21)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type </text><text class="terminal-r3" x="231.8" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/help</text><text class="terminal-r1" x="292.8" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)"> for more information</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
</text><text class="terminal-r1" x="0" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">Type </text><text class="terminal-r3" x="61" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/help</text><text class="terminal-r1" x="122" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)"> for more information</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="1220" 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="1220" 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="1220" 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)">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |