v2.9.4 (#669)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Jean-Baptiste Muscat <jeanbaptiste.muscatdupuis@mistral.ai> Co-authored-by: JeroenvdV <JeroenvdV@users.noreply.github.com> Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: allansimon-mistral <allan.simon@ext.mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
71f373c60c
commit
4972dd5694
50 changed files with 2892 additions and 842 deletions
26
.github/workflows/build-and-upload.yml
vendored
26
.github/workflows/build-and-upload.yml
vendored
|
|
@ -20,34 +20,12 @@ jobs:
|
|||
id: set-matrix
|
||||
run: |
|
||||
# Linux: manylinux_2_28 containers → binary works on glibc >= 2.28 (RHEL 8+, Ubuntu 20.04+)
|
||||
if [[ "${{ github.repository }}" == "mistralai/mistral-vibe" ]]; then
|
||||
matrix='{
|
||||
"include": [
|
||||
{"runner": "ubuntu-22.04", "os": "linux", "arch": "x86_64", "container": "quay.io/pypa/manylinux_2_28_x86_64"},
|
||||
{"runner": "ubuntu-22.04-arm", "os": "linux", "arch": "aarch64", "container": "quay.io/pypa/manylinux_2_28_aarch64"},
|
||||
{"runner": "macos-15-intel", "os": "darwin", "arch": "x86_64"},
|
||||
{"runner": "macos-14", "os": "darwin", "arch": "aarch64"},
|
||||
{"runner": "windows-2022", "os": "windows", "arch": "x86_64"},
|
||||
{"runner": "windows-11-arm", "os": "windows", "arch": "aarch64"}
|
||||
]
|
||||
}'
|
||||
smoke_matrix='{
|
||||
"include": [
|
||||
{"runner": "ubuntu-24.04", "os": "linux", "arch": "x86_64"},
|
||||
{"runner": "ubuntu-24.04", "os": "linux", "arch": "x86_64", "tag": "old-glibc", "container": "almalinux:8"},
|
||||
{"runner": "ubuntu-24.04-arm", "os": "linux", "arch": "aarch64"},
|
||||
{"runner": "macos-15", "os": "darwin", "arch": "aarch64"},
|
||||
{"runner": "macos-15-intel", "os": "darwin", "arch": "x86_64"},
|
||||
{"runner": "windows-latest", "os": "windows", "arch": "x86_64"},
|
||||
{"runner": "windows-11-arm", "os": "windows", "arch": "aarch64"}
|
||||
]
|
||||
}'
|
||||
else # skip ARM Linux/Windows (runners not available on non public repos)
|
||||
matrix='{
|
||||
"include": [
|
||||
{"runner": "ubuntu-22.04", "os": "linux", "arch": "x86_64", "container": "quay.io/pypa/manylinux_2_28_x86_64"},
|
||||
{"runner": "macos-15-intel", "os": "darwin", "arch": "x86_64"},
|
||||
{"runner": "macos-14", "os": "darwin", "arch": "aarch64"},
|
||||
{"runner": "windows-2022", "os": "windows", "arch": "x86_64"}
|
||||
]
|
||||
}'
|
||||
|
|
@ -55,12 +33,12 @@ jobs:
|
|||
"include": [
|
||||
{"runner": "ubuntu-24.04", "os": "linux", "arch": "x86_64"},
|
||||
{"runner": "ubuntu-24.04", "os": "linux", "arch": "x86_64", "tag": "old-glibc", "container": "almalinux:8"},
|
||||
{"runner": "ubuntu-24.04-arm", "os": "linux", "arch": "aarch64"},
|
||||
{"runner": "macos-15", "os": "darwin", "arch": "aarch64"},
|
||||
{"runner": "macos-15-intel", "os": "darwin", "arch": "x86_64"},
|
||||
{"runner": "windows-latest", "os": "windows", "arch": "x86_64"}
|
||||
]
|
||||
}'
|
||||
fi
|
||||
echo "matrix=$(echo $matrix | jq -c .)" >> $GITHUB_OUTPUT
|
||||
echo "smoke_matrix=$(echo $smoke_matrix | jq -c .)" >> $GITHUB_OUTPUT
|
||||
|
||||
|
|
@ -75,8 +53,6 @@ jobs:
|
|||
container: ${{ matrix.container || '' }}
|
||||
|
||||
steps:
|
||||
- name: echo
|
||||
run: echo github.repository=${{ github.repository }}
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ Always go through `uv` — never invoke bare `python` or `pip`.
|
|||
- Enums: `StrEnum` / `IntEnum` with `auto()` and UPPERCASE members. For type-mixing, the mix-in type comes before `Enum` in the bases. Add methods or `@property` rather than parallel lookup tables.
|
||||
- Write declarative, minimalist code: express intent, drop boilerplate.
|
||||
- Never call a private method from outside of it's class
|
||||
- Avoid comments and docstrings, except for when there's a hard to spot corner case
|
||||
|
||||
## Typing & imports
|
||||
|
||||
|
|
|
|||
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.9.4] - 2026-05-05
|
||||
|
||||
### Added
|
||||
|
||||
- `/rename` command to rename the current session
|
||||
- `feat: vibe.at_mention_inserted` telemetry event
|
||||
- "Always allow" tool permissions persist across sessions
|
||||
- Eager agent-loop warmup so `vibe.ready` telemetry fires sooner
|
||||
|
||||
### Changed
|
||||
|
||||
- `bash` (`!command`) bang commands run via async subprocess for better latency
|
||||
- Bumped `mistral` SDK to 2.4.4
|
||||
- Bumped `cryptography` to address upstream CVEs
|
||||
|
||||
### Fixed
|
||||
|
||||
- Preserve `non_retryable` flag on exceptions raised through `_chat` / `_chat_streaming`, so callers driving the agent loop from a Temporal activity can signal "do not retry"
|
||||
- `/clear` no longer chains `parent_session_id` to the previous session
|
||||
- `vibe.new_session` telemetry no longer fires when resuming a session
|
||||
|
||||
### Removed
|
||||
|
||||
- Windows ARM build artifacts (no longer published; required to bump `cryptography`)
|
||||
|
||||
|
||||
## [2.9.3] - 2026-04-30
|
||||
|
||||
### Added
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -219,7 +219,7 @@ Simply run `vibe` to enter the interactive chat loop.
|
|||
- **Tool Output Toggle**: Press `Ctrl+O` to toggle the tool output view.
|
||||
- **Todo View Toggle**: Press `Ctrl+T` to toggle the todo list view.
|
||||
- **Debug Console**: Press `Ctrl+\` to toggle the debug console.
|
||||
- **Auto-Approve Toggle**: Press `Shift+Tab` to toggle auto-approve mode on/off.
|
||||
- **Agent Selection**: Press `Shift+Tab` to cycle through agents (default, plan, ...).
|
||||
|
||||
You can start Vibe with a prompt using the following command:
|
||||
|
||||
|
|
@ -227,8 +227,6 @@ You can start Vibe with a prompt using the following command:
|
|||
vibe "Refactor the main function in cli/main.py to be more modular."
|
||||
```
|
||||
|
||||
**Note**: The `--auto-approve` flag automatically approves all tool executions without prompting. In interactive mode, you can also toggle auto-approve on/off using `Shift+Tab`.
|
||||
|
||||
### Trust Folder System
|
||||
|
||||
Vibe includes a trust folder system to ensure you only run the agent in directories you trust. When you first run Vibe in a new directory which contains a `.vibe` subfolder, it may ask you to confirm whether you trust the folder.
|
||||
|
|
@ -412,7 +410,15 @@ Vibe supports multiple ways to configure your API keys:
|
|||
|
||||
### Custom System Prompts
|
||||
|
||||
You can create custom system prompts to replace the default one (`prompts/cli.md`). Create a markdown file in the `~/.vibe/prompts/` directory with your custom prompt content.
|
||||
You can create `AGENTS.md` files to add custom instructions. You can also replace the entire system prompt.
|
||||
|
||||
Place `AGENTS.md` files in:
|
||||
- `~/.vibe/AGENTS.md` — user-level instructions for all projects
|
||||
- Project directories — project-specific instructions, loaded from cwd up to the trust root
|
||||
|
||||
Priority: closer directories override more distant ones. Instructions in `AGENTS.md` override the default system prompt. Files are only loaded for trusted folders.
|
||||
|
||||
Custom system prompts entirely replace the default one (`prompts/cli.md`). Create a markdown file in the `~/.vibe/prompts/` directory with your custom prompt content.
|
||||
|
||||
To use a custom system prompt, set the `system_prompt_id` in your configuration to match the filename (without the `.md` extension):
|
||||
|
||||
|
|
|
|||
|
|
@ -56,5 +56,4 @@ runs:
|
|||
run: |
|
||||
cd "${{ github.workspace }}"
|
||||
uv run --directory "${{ github.action_path }}" vibe \
|
||||
--auto-approve \
|
||||
-p "$PROMPT_INPUT"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
id = "mistral-vibe"
|
||||
name = "Mistral Vibe"
|
||||
description = "Mistral's open-source coding assistant"
|
||||
version = "2.9.3"
|
||||
version = "2.9.4"
|
||||
schema_version = 1
|
||||
authors = ["Mistral AI"]
|
||||
repository = "https://github.com/mistralai/mistral-vibe"
|
||||
|
|
@ -11,25 +11,21 @@ name = "Mistral Vibe"
|
|||
icon = "./icons/mistral_vibe.svg"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.darwin-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.3/vibe-acp-darwin-aarch64-2.9.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.4/vibe-acp-darwin-aarch64-2.9.4.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.darwin-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.3/vibe-acp-darwin-x86_64-2.9.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.4/vibe-acp-darwin-x86_64-2.9.4.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.linux-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.3/vibe-acp-linux-aarch64-2.9.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.4/vibe-acp-linux-aarch64-2.9.4.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.linux-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.3/vibe-acp-linux-x86_64-2.9.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.4/vibe-acp-linux-x86_64-2.9.4.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.windows-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.3/vibe-acp-windows-aarch64-2.9.3.zip"
|
||||
cmd = "./vibe-acp.exe"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.windows-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.3/vibe-acp-windows-x86_64-2.9.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.4/vibe-acp-windows-x86_64-2.9.4.zip"
|
||||
cmd = "./vibe-acp.exe"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "mistral-vibe"
|
||||
version = "2.9.3"
|
||||
version = "2.9.4"
|
||||
description = "Minimal CLI coding agent by Mistral"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
|
@ -31,7 +31,7 @@ dependencies = [
|
|||
"anyio>=4.12.0",
|
||||
"cachetools>=5.5.0",
|
||||
"charset-normalizer>=3.4.4",
|
||||
"cryptography>=44.0.0,<=46.0.3",
|
||||
"cryptography>=44.0.0",
|
||||
"gitpython>=3.1.46",
|
||||
"giturlparse>=0.14.0",
|
||||
"google-auth>=2.0.0",
|
||||
|
|
@ -40,7 +40,7 @@ dependencies = [
|
|||
"keyring>=25.6.0",
|
||||
"markdownify>=1.2.2",
|
||||
"mcp>=1.14.0",
|
||||
"mistralai==2.3.2",
|
||||
"mistralai==2.4.4",
|
||||
"opentelemetry-api>=1.39.1",
|
||||
"opentelemetry-exporter-otlp-proto-http>=1.39.1",
|
||||
"opentelemetry-sdk>=1.39.1",
|
||||
|
|
|
|||
|
|
@ -565,7 +565,7 @@ async def start_session_with_request_permission(
|
|||
|
||||
assert isinstance(last_response, RequestPermissionJsonRpcRequest)
|
||||
assert last_response.params is not None
|
||||
assert len(last_response.params.options) == 3
|
||||
assert len(last_response.params.options) == 4
|
||||
return last_response
|
||||
|
||||
|
||||
|
|
@ -778,13 +778,15 @@ class TestToolCallStructure:
|
|||
)
|
||||
assert permission_request.params is not None
|
||||
|
||||
# Verify "Allow always" option includes the pattern label
|
||||
# Verify granular permissions are passed in field_meta
|
||||
allow_always = next(
|
||||
o
|
||||
for o in permission_request.params.options
|
||||
if o.option_id == ToolOption.ALLOW_ALWAYS
|
||||
)
|
||||
assert "npm install *" in allow_always.name
|
||||
assert allow_always.name == "Allow for remainder of this session"
|
||||
assert allow_always.field_meta is not None
|
||||
assert "required_permissions" in allow_always.field_meta
|
||||
|
||||
@pytest.mark.skip(reason="Long running tool call updates are not implemented yet")
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.3"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.4"
|
||||
)
|
||||
|
||||
assert response.auth_methods == []
|
||||
|
|
@ -62,7 +62,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.3"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.4"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
363
tests/acp/test_session_set_title.py
Normal file
363
tests/acp/test_session_set_title.py
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from acp.schema import SessionInfoUpdate
|
||||
import pytest
|
||||
import tomli_w
|
||||
|
||||
from tests.conftest import build_test_vibe_config, get_base_config
|
||||
from tests.stubs.fake_backend import FakeBackend
|
||||
from tests.stubs.fake_client import FakeClient
|
||||
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
||||
from vibe.acp.exceptions import InternalError, InvalidRequestError, SessionNotFoundError
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
from vibe.core.config import ModelConfig, SessionLoggingConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acp_agent_with_session_config(
|
||||
backend: FakeBackend, temp_session_dir: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> tuple[VibeAcpAgentLoop, FakeClient]:
|
||||
session_config = SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="session", enabled=True
|
||||
)
|
||||
config = build_test_vibe_config(
|
||||
active_model="devstral-latest",
|
||||
models=[
|
||||
ModelConfig(
|
||||
name="devstral-latest", provider="mistral", alias="devstral-latest"
|
||||
)
|
||||
],
|
||||
session_logging=session_config,
|
||||
)
|
||||
|
||||
class PatchedAgentLoop(AgentLoop):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **{**kwargs, "backend": backend})
|
||||
self._base_config = config
|
||||
self.agent_manager.invalidate_config()
|
||||
|
||||
monkeypatch.setattr("vibe.acp.acp_agent_loop.AgentLoop", PatchedAgentLoop)
|
||||
monkeypatch.setattr(VibeAcpAgentLoop, "_load_config", lambda self: config)
|
||||
monkeypatch.setattr(
|
||||
"vibe.acp.acp_agent_loop.VibeConfig.load", lambda *args, **kwargs: config
|
||||
)
|
||||
|
||||
vibe_acp_agent = VibeAcpAgentLoop()
|
||||
client = FakeClient()
|
||||
vibe_acp_agent.on_connect(client)
|
||||
client.on_connect(vibe_acp_agent)
|
||||
|
||||
return vibe_acp_agent, client
|
||||
|
||||
|
||||
class TestSessionSetTitle:
|
||||
@pytest.mark.asyncio
|
||||
async def test_updates_live_unsaved_session_title(
|
||||
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
response = await acp_agent.new_session(cwd=str(Path.cwd()), mcp_servers=[])
|
||||
assert response is not None
|
||||
|
||||
result = await acp_agent.ext_method(
|
||||
"session/set_title",
|
||||
{"sessionId": response.session_id, "title": "Manual title"},
|
||||
)
|
||||
|
||||
assert result == {}
|
||||
|
||||
session = acp_agent.sessions[response.session_id]
|
||||
metadata = session.agent_loop.session_logger.session_metadata
|
||||
assert metadata is not None
|
||||
assert metadata.title == "Manual title"
|
||||
assert metadata.title_source == "manual"
|
||||
assert metadata.end_time is None
|
||||
assert not session.agent_loop.session_logger.metadata_filepath.exists()
|
||||
|
||||
info_updates = [
|
||||
notification.update
|
||||
for notification in client._session_updates
|
||||
if isinstance(notification.update, SessionInfoUpdate)
|
||||
]
|
||||
assert len(info_updates) == 1
|
||||
assert info_updates[0].title == "Manual title"
|
||||
assert info_updates[0].updated_at is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_updates_live_saved_session_title(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
saved_session_id = "saved-session-12345678"
|
||||
acp_session_id = saved_session_id[:8]
|
||||
cwd = str(Path.cwd())
|
||||
session_dir = create_test_session(
|
||||
temp_session_dir,
|
||||
saved_session_id,
|
||||
cwd,
|
||||
title="Old title",
|
||||
end_time="2024-01-01T12:05:00Z",
|
||||
)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=acp_session_id)
|
||||
client._session_updates.clear()
|
||||
|
||||
result = await acp_agent.ext_method(
|
||||
"session/set_title",
|
||||
{"sessionId": saved_session_id, "title": "Renamed session"},
|
||||
)
|
||||
|
||||
assert result == {}
|
||||
|
||||
session = acp_agent.sessions[acp_session_id]
|
||||
metadata = session.agent_loop.session_logger.session_metadata
|
||||
assert metadata is not None
|
||||
assert metadata.title == "Renamed session"
|
||||
assert metadata.title_source == "manual"
|
||||
assert metadata.end_time == "2024-01-01T12:05:00Z"
|
||||
assert session.agent_loop.session_id == saved_session_id
|
||||
|
||||
saved_metadata = json.loads((session_dir / "meta.json").read_text())
|
||||
assert saved_metadata["title"] == "Renamed session"
|
||||
assert saved_metadata["title_source"] == "manual"
|
||||
assert saved_metadata["end_time"] == "2024-01-01T12:05:00Z"
|
||||
|
||||
info_updates = [
|
||||
notification
|
||||
for notification in client._session_updates
|
||||
if isinstance(notification.update, SessionInfoUpdate)
|
||||
]
|
||||
assert len(info_updates) == 1
|
||||
assert info_updates[0].session_id == acp_session_id
|
||||
assert info_updates[0].update.title == "Renamed session"
|
||||
assert info_updates[0].update.updated_at == metadata.end_time
|
||||
assert saved_metadata["end_time"] == info_updates[0].update.updated_at
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_loaded_session_title_is_unchanged_when_persist_fails(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
saved_session_id = "saved-session-12345678"
|
||||
acp_session_id = saved_session_id[:8]
|
||||
cwd = str(Path.cwd())
|
||||
create_test_session(
|
||||
temp_session_dir,
|
||||
saved_session_id,
|
||||
cwd,
|
||||
title="Old title",
|
||||
end_time="2024-01-01T12:05:00Z",
|
||||
)
|
||||
|
||||
await acp_agent.load_session(cwd=cwd, mcp_servers=[], session_id=acp_session_id)
|
||||
client._session_updates.clear()
|
||||
|
||||
async def fail_persist(*args, **kwargs):
|
||||
raise ValueError("Cannot rewrite metadata")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vibe.acp.acp_agent_loop.update_saved_session_title_at_path", fail_persist
|
||||
)
|
||||
|
||||
with pytest.raises(InternalError):
|
||||
await acp_agent.ext_method(
|
||||
"session/set_title",
|
||||
{"sessionId": saved_session_id, "title": "Renamed session"},
|
||||
)
|
||||
|
||||
session = acp_agent.sessions[acp_session_id]
|
||||
metadata = session.agent_loop.session_logger.session_metadata
|
||||
assert metadata is not None
|
||||
assert metadata.title == "Old title"
|
||||
assert metadata.title_source == "auto"
|
||||
assert not [
|
||||
notification
|
||||
for notification in client._session_updates
|
||||
if isinstance(notification.update, SessionInfoUpdate)
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_updates_saved_but_not_loaded_session_title(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
session_id = "offline-session-12345678"
|
||||
cwd = str(Path.cwd())
|
||||
session_dir = create_test_session(
|
||||
temp_session_dir,
|
||||
session_id,
|
||||
cwd,
|
||||
title="Old title",
|
||||
end_time="2024-01-01T12:05:00Z",
|
||||
)
|
||||
|
||||
result = await acp_agent.ext_method(
|
||||
"session/set_title", {"sessionId": session_id, "title": "Renamed session"}
|
||||
)
|
||||
|
||||
assert result == {}
|
||||
|
||||
saved_metadata = json.loads((session_dir / "meta.json").read_text())
|
||||
assert saved_metadata["title"] == "Renamed session"
|
||||
assert saved_metadata["title_source"] == "manual"
|
||||
|
||||
info_updates = [
|
||||
notification
|
||||
for notification in client._session_updates
|
||||
if isinstance(notification.update, SessionInfoUpdate)
|
||||
]
|
||||
assert len(info_updates) == 1
|
||||
assert info_updates[0].session_id == session_id
|
||||
assert info_updates[0].update.title == "Renamed session"
|
||||
assert info_updates[0].update.updated_at == saved_metadata["end_time"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_updates_saved_session_with_configured_log_dir_without_api_key(
|
||||
self,
|
||||
config_dir: Path,
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
config = {
|
||||
**get_base_config(),
|
||||
"session_logging": {
|
||||
"enabled": True,
|
||||
"save_dir": str(temp_session_dir),
|
||||
"session_prefix": "session",
|
||||
},
|
||||
}
|
||||
monkeypatch.setenv("VIBE_HOME", str(config_dir))
|
||||
(config_dir / "config.toml").write_text(tomli_w.dumps(config), encoding="utf-8")
|
||||
monkeypatch.delenv("MISTRAL_API_KEY", raising=False)
|
||||
|
||||
session_id = "offline-session-12345678"
|
||||
session_dir = create_test_session(
|
||||
temp_session_dir,
|
||||
session_id,
|
||||
str(Path.cwd()),
|
||||
title="Old title",
|
||||
end_time="2024-01-01T12:05:00Z",
|
||||
)
|
||||
acp_agent = VibeAcpAgentLoop()
|
||||
client = FakeClient()
|
||||
acp_agent.on_connect(client)
|
||||
client.on_connect(acp_agent)
|
||||
|
||||
result = await acp_agent.ext_method(
|
||||
"session/set_title",
|
||||
{"sessionId": session_id, "title": "Renamed without key"},
|
||||
)
|
||||
|
||||
assert result == {}
|
||||
saved_metadata = json.loads((session_dir / "meta.json").read_text())
|
||||
assert saved_metadata["title"] == "Renamed without key"
|
||||
assert saved_metadata["title_source"] == "manual"
|
||||
|
||||
info_updates = [
|
||||
notification
|
||||
for notification in client._session_updates
|
||||
if isinstance(notification.update, SessionInfoUpdate)
|
||||
]
|
||||
assert len(info_updates) == 1
|
||||
assert info_updates[0].session_id == session_id
|
||||
assert info_updates[0].update.title == "Renamed without key"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_on_invalid_params(
|
||||
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
|
||||
) -> None:
|
||||
acp_agent, _client = acp_agent_with_session_config
|
||||
|
||||
with pytest.raises(InvalidRequestError):
|
||||
await acp_agent.ext_method(
|
||||
"session/set_title", {"title": "Missing session id"}
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidRequestError):
|
||||
await acp_agent.ext_method(
|
||||
"session/set_title", {"sessionId": "missing-title-session"}
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidRequestError):
|
||||
await acp_agent.ext_method(
|
||||
"session/set_title",
|
||||
{"sessionId": "blank-title-session", "title": " "},
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidRequestError):
|
||||
await acp_agent.ext_method(
|
||||
"session/set_title", {"sessionId": " ", "title": "Blank session id"}
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidRequestError):
|
||||
await acp_agent.ext_method(
|
||||
"session/set_title",
|
||||
{"savedSessionId": "saved-session", "title": "Unsupported target"},
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_id_falls_back_to_saved_session_lookup(
|
||||
self,
|
||||
acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient],
|
||||
temp_session_dir: Path,
|
||||
create_test_session,
|
||||
) -> None:
|
||||
acp_agent, client = acp_agent_with_session_config
|
||||
|
||||
session_id = "saved-session-12345678"
|
||||
cwd = str(Path.cwd())
|
||||
session_dir = create_test_session(
|
||||
temp_session_dir,
|
||||
session_id,
|
||||
cwd,
|
||||
title="Old title",
|
||||
end_time="2024-01-01T12:05:00Z",
|
||||
)
|
||||
|
||||
result = await acp_agent.ext_method(
|
||||
"session/set_title", {"sessionId": session_id, "title": "Renamed session"}
|
||||
)
|
||||
|
||||
assert result == {}
|
||||
saved_metadata = json.loads((session_dir / "meta.json").read_text())
|
||||
assert saved_metadata["title"] == "Renamed session"
|
||||
assert saved_metadata["title_source"] == "manual"
|
||||
|
||||
info_updates = [
|
||||
notification
|
||||
for notification in client._session_updates
|
||||
if isinstance(notification.update, SessionInfoUpdate)
|
||||
]
|
||||
assert len(info_updates) == 1
|
||||
assert info_updates[0].session_id == session_id
|
||||
assert info_updates[0].update.title == "Renamed session"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_when_session_cannot_be_found(
|
||||
self, acp_agent_with_session_config: tuple[VibeAcpAgentLoop, FakeClient]
|
||||
) -> None:
|
||||
acp_agent, _client = acp_agent_with_session_config
|
||||
|
||||
with pytest.raises(SessionNotFoundError):
|
||||
await acp_agent.ext_method(
|
||||
"session/set_title",
|
||||
{"sessionId": "missing-session", "title": "Renamed session"},
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -42,6 +43,37 @@ class TestTelemetryNotification:
|
|||
|
||||
assert telemetry_events == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_at_mention_inserted_dispatches_telemetry(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
session = await acp_agent_loop.new_session(cwd=str(Path.cwd()), mcp_servers=[])
|
||||
telemetry_events.clear()
|
||||
|
||||
await acp_agent_loop.ext_notification(
|
||||
"telemetry/send",
|
||||
{
|
||||
"event": "vibe.at_mention_inserted",
|
||||
"session_id": session.session_id,
|
||||
"properties": {
|
||||
"nb_mentions": 2,
|
||||
"context_types": {"file": 1, "folder": 1},
|
||||
"file_extensions": {".py": 1},
|
||||
"message_id": "msg-abc",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
at_mention_events = [
|
||||
e for e in telemetry_events if e["event_name"] == "vibe.at_mention_inserted"
|
||||
]
|
||||
assert len(at_mention_events) == 1
|
||||
props = at_mention_events[0]["properties"]
|
||||
assert props["nb_mentions"] == 2
|
||||
assert props["context_types"] == {"file": 1, "folder": 1}
|
||||
assert props["file_extensions"] == {".py": 1}
|
||||
assert props["message_id"] == "msg-abc"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_on_invalid_params(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
|
|
|
|||
|
|
@ -81,10 +81,13 @@ class TestBuildPermissionOptions:
|
|||
]
|
||||
result = build_permission_options(permissions)
|
||||
|
||||
assert len(result) == 3
|
||||
assert len(result) == 4
|
||||
allow_always = next(o for o in result if o.option_id == ToolOption.ALLOW_ALWAYS)
|
||||
assert "npm install *" in allow_always.name
|
||||
assert "session" in allow_always.name.lower()
|
||||
allow_permanent = next(
|
||||
o for o in result if o.option_id == ToolOption.ALLOW_ALWAYS_PERMANENT
|
||||
)
|
||||
assert "Always allow" == allow_permanent.name
|
||||
|
||||
def test_allow_always_has_field_meta(self) -> None:
|
||||
permissions = [
|
||||
|
|
@ -118,6 +121,6 @@ class TestBuildPermissionOptions:
|
|||
allow_once = next(o for o in result if o.option_id == ToolOption.ALLOW_ONCE)
|
||||
reject_once = next(o for o in result if o.option_id == ToolOption.REJECT_ONCE)
|
||||
assert allow_once.name == "Allow once"
|
||||
assert reject_once.name == "Reject once"
|
||||
assert reject_once.name == "Deny"
|
||||
assert allow_once.field_meta is None
|
||||
assert reject_once.field_meta is None
|
||||
|
|
|
|||
18
tests/autocompletion/test_path_prompt.py
Normal file
18
tests/autocompletion/test_path_prompt.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from vibe.core.autocompletion.path_prompt import build_path_prompt_payload
|
||||
|
||||
|
||||
def test_deduplicates_same_file_mentioned_twice(tmp_path: Path) -> None:
|
||||
readme = tmp_path / "README.md"
|
||||
readme.write_text("hello", encoding="utf-8")
|
||||
|
||||
payload = build_path_prompt_payload(
|
||||
"See @README.md and again @README.md", base_dir=tmp_path
|
||||
)
|
||||
|
||||
assert len(payload.resources) == 1
|
||||
assert payload.resources[0].path == readme
|
||||
assert len(payload.all_resources) == 2
|
||||
|
|
@ -102,6 +102,16 @@ class TestCommandRegistry:
|
|||
_, cmd, _ = result
|
||||
assert cmd.handler == "_show_session_picker"
|
||||
|
||||
def test_rename_command_registration(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
assert registry.get_command_name("/rename") == "rename"
|
||||
assert registry.get_command_name("/title") is None
|
||||
result = registry.parse_command("/rename Better title")
|
||||
assert result is not None
|
||||
_, cmd, cmd_args = result
|
||||
assert cmd.handler == "_rename_session"
|
||||
assert cmd_args == "Better title"
|
||||
|
||||
def test_parse_command_keeps_args_for_no_arg_commands(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
result = registry.parse_command("/help extra")
|
||||
|
|
|
|||
162
tests/cli/test_rename_command.py
Normal file
162
tests/cli/test_rename_command.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
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.core.config import SessionLoggingConfig
|
||||
|
||||
|
||||
def _enabled_session_config(save_dir: Path) -> SessionLoggingConfig:
|
||||
return SessionLoggingConfig(enabled=True, save_dir=str(save_dir))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_command_updates_live_unsaved_session_title(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
config = build_test_vibe_config(session_logging=_enabled_session_config(tmp_path))
|
||||
app = build_test_vibe_app(config=config)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
handled = await app._handle_command("/rename Manual title")
|
||||
await pilot.pause()
|
||||
messages = app.query(UserCommandMessage)
|
||||
assert any(
|
||||
message._content == 'Session renamed to "Manual title".'
|
||||
for message in messages
|
||||
)
|
||||
|
||||
assert handled is True
|
||||
|
||||
metadata = app.agent_loop.session_logger.session_metadata
|
||||
assert metadata is not None
|
||||
assert metadata.title == "Manual title"
|
||||
assert metadata.title_source == "manual"
|
||||
assert not app.agent_loop.session_logger.metadata_filepath.exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_command_persists_existing_session_metadata(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
config = build_test_vibe_config(session_logging=_enabled_session_config(tmp_path))
|
||||
app = build_test_vibe_app(config=config)
|
||||
logger = app.agent_loop.session_logger
|
||||
assert logger.session_dir is not None
|
||||
assert logger.session_metadata is not None
|
||||
|
||||
logger.session_dir.mkdir(parents=True)
|
||||
existing_metadata = {
|
||||
**logger.session_metadata.model_dump(),
|
||||
"end_time": "2024-01-01T12:05:00Z",
|
||||
"extra_field": "preserved",
|
||||
}
|
||||
logger.metadata_filepath.write_text(json.dumps(existing_metadata), encoding="utf-8")
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
handled = await app._handle_command("/rename Persisted title")
|
||||
await pilot.pause()
|
||||
|
||||
assert handled is True
|
||||
|
||||
metadata = logger.session_metadata
|
||||
assert metadata is not None
|
||||
assert metadata.title == "Persisted title"
|
||||
assert metadata.title_source == "manual"
|
||||
|
||||
saved_metadata = json.loads(logger.metadata_filepath.read_text())
|
||||
assert saved_metadata["title"] == "Persisted title"
|
||||
assert saved_metadata["title_source"] == "manual"
|
||||
assert saved_metadata["end_time"] == "2024-01-01T12:05:00Z"
|
||||
assert saved_metadata["extra_field"] == "preserved"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_picker_shows_renamed_session_title(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
config = build_test_vibe_config(
|
||||
session_logging=_enabled_session_config(tmp_path), vibe_code_enabled=False
|
||||
)
|
||||
app = build_test_vibe_app(config=config)
|
||||
logger = app.agent_loop.session_logger
|
||||
assert logger.session_dir is not None
|
||||
assert logger.session_metadata is not None
|
||||
|
||||
logger.session_dir.mkdir(parents=True)
|
||||
existing_metadata = {
|
||||
**logger.session_metadata.model_dump(),
|
||||
"end_time": "2024-01-01T12:05:00Z",
|
||||
"total_messages": 1,
|
||||
}
|
||||
logger.metadata_filepath.write_text(json.dumps(existing_metadata), encoding="utf-8")
|
||||
logger.messages_filepath.write_text(
|
||||
'{"role": "user", "content": "Original prompt"}\n', encoding="utf-8"
|
||||
)
|
||||
|
||||
captured_picker = None
|
||||
|
||||
async def capture_picker(picker):
|
||||
nonlocal captured_picker
|
||||
captured_picker = picker
|
||||
|
||||
monkeypatch.setattr(app, "_switch_from_input", capture_picker)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
handled = await app._handle_command("/rename New title")
|
||||
await app._show_session_picker()
|
||||
await pilot.pause()
|
||||
|
||||
assert handled is True
|
||||
assert captured_picker is not None
|
||||
assert captured_picker._latest_messages[f"local:{logger.session_id}"] == "New title"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_command_requires_title(tmp_path: Path) -> None:
|
||||
config = build_test_vibe_config(session_logging=_enabled_session_config(tmp_path))
|
||||
app = build_test_vibe_app(config=config)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
handled = await app._handle_command("/rename")
|
||||
await pilot.pause()
|
||||
errors = app.query(ErrorMessage)
|
||||
assert any(error._error == "Usage: /rename <title>" for error in errors)
|
||||
|
||||
assert handled is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_command_is_intercepted_for_remote_sessions(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
config = build_test_vibe_config(session_logging=_enabled_session_config(tmp_path))
|
||||
app = build_test_vibe_app(config=config)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
with patch(
|
||||
"vibe.cli.textual_ui.remote.remote_session_manager.RemoteEventsSource"
|
||||
) as MockSource:
|
||||
remote_source = MagicMock()
|
||||
remote_source.session_id = "remote-session-id"
|
||||
remote_source.is_terminated = False
|
||||
remote_source.is_waiting_for_input = False
|
||||
MockSource.return_value = remote_source
|
||||
|
||||
await app._remote_manager.attach(
|
||||
session_id="remote-session-id", config=config
|
||||
)
|
||||
handled = await app._handle_command("/rename Remote title")
|
||||
await pilot.pause()
|
||||
errors = app.query(ErrorMessage)
|
||||
assert any(
|
||||
error._error == "Renaming is only supported for local sessions."
|
||||
for error in errors
|
||||
)
|
||||
|
||||
assert handled is True
|
||||
|
|
@ -249,6 +249,81 @@ class TestTelemetryClient:
|
|||
assert telemetry_events[0]["event_name"] == "vibe.user_cancelled_action"
|
||||
assert telemetry_events[0]["properties"]["action"] == "interrupt_agent"
|
||||
|
||||
def test_send_at_mention_inserted_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_at_mention_inserted(
|
||||
nb_mentions=2,
|
||||
context_types={"file": 1, "folder": 1},
|
||||
file_extensions={".py": 1},
|
||||
message_id="msg-123",
|
||||
)
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
assert telemetry_events[0]["event_name"] == "vibe.at_mention_inserted"
|
||||
assert telemetry_events[0]["properties"] == {
|
||||
"nb_mentions": 2,
|
||||
"context_types": {"file": 1, "folder": 1},
|
||||
"file_extensions": {".py": 1},
|
||||
"message_id": "msg-123",
|
||||
}
|
||||
|
||||
def test_send_at_mention_inserted_null_file_extensions(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(enable_telemetry=True)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_at_mention_inserted(
|
||||
nb_mentions=1,
|
||||
context_types={"folder": 1},
|
||||
file_extensions=None,
|
||||
message_id=None,
|
||||
)
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
assert telemetry_events[0]["properties"]["file_extensions"] is None
|
||||
assert telemetry_events[0]["properties"]["message_id"] is None
|
||||
|
||||
def test_send_at_mention_inserted_multiple_files_counts_extensions(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(enable_telemetry=True)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_at_mention_inserted(
|
||||
nb_mentions=4,
|
||||
context_types={"file": 3, "folder": 1},
|
||||
file_extensions={".py": 2, ".ts": 1},
|
||||
message_id="msg-multi",
|
||||
)
|
||||
|
||||
props = telemetry_events[0]["properties"]
|
||||
assert props["nb_mentions"] == 4
|
||||
assert props["context_types"] == {"file": 3, "folder": 1}
|
||||
assert props["file_extensions"] == {".py": 2, ".ts": 1}
|
||||
|
||||
def test_send_at_mention_inserted_duplicate_references_reflected_in_counts(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
config = build_test_vibe_config(enable_telemetry=True)
|
||||
client = TelemetryClient(config_getter=lambda: config)
|
||||
|
||||
client.send_at_mention_inserted(
|
||||
nb_mentions=4,
|
||||
context_types={"file": 2, "folder": 2},
|
||||
file_extensions={".py": 2},
|
||||
message_id="msg-dupes",
|
||||
)
|
||||
|
||||
props = telemetry_events[0]["properties"]
|
||||
assert props["nb_mentions"] == 4
|
||||
assert props["context_types"] == {"file": 2, "folder": 2}
|
||||
assert props["file_extensions"] == {".py": 2}
|
||||
|
||||
def test_send_auto_compact_triggered_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
|
|
|
|||
110
tests/e2e/test_cli_tui_fresh_install.py
Normal file
110
tests/e2e/test_cli_tui_fresh_install.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pexpect
|
||||
import pytest
|
||||
|
||||
from tests import TESTS_ROOT
|
||||
from tests.e2e.common import (
|
||||
ansi_tolerant_pattern,
|
||||
send_ctrl_c_until_quit_confirmation,
|
||||
wait_for_main_screen,
|
||||
wait_for_request_count,
|
||||
)
|
||||
from tests.e2e.mock_server import StreamingMockServer
|
||||
|
||||
|
||||
def _venv_executable(venv_path: Path, name: str) -> Path:
|
||||
if os.name == "nt":
|
||||
return venv_path / "Scripts" / f"{name}.exe"
|
||||
return venv_path / "bin" / name
|
||||
|
||||
|
||||
def _build_wheel(dist_dir: Path) -> Path:
|
||||
subprocess.run(
|
||||
["uv", "build", "--wheel", "--out-dir", str(dist_dir)],
|
||||
cwd=TESTS_ROOT.parent,
|
||||
check=True,
|
||||
)
|
||||
wheels = sorted(dist_dir.glob("mistral_vibe-*.whl"))
|
||||
assert len(wheels) == 1
|
||||
return wheels[0]
|
||||
|
||||
|
||||
def _install_fresh_wheel(tmp_path: Path, wheel_path: Path) -> Path:
|
||||
venv_path = tmp_path / "fresh-install-venv"
|
||||
subprocess.run(
|
||||
["uv", "venv", "--no-config", "--python", sys.executable, str(venv_path)],
|
||||
cwd=tmp_path,
|
||||
check=True,
|
||||
)
|
||||
|
||||
python_path = _venv_executable(venv_path, "python")
|
||||
subprocess.run(
|
||||
[
|
||||
"uv",
|
||||
"pip",
|
||||
"install",
|
||||
"--no-config",
|
||||
"--refresh",
|
||||
"--python",
|
||||
str(python_path),
|
||||
str(wheel_path),
|
||||
],
|
||||
cwd=tmp_path,
|
||||
check=True,
|
||||
)
|
||||
return _venv_executable(venv_path, "vibe")
|
||||
|
||||
|
||||
@pytest.mark.timeout(90)
|
||||
def test_fresh_wheel_install_can_spawn_cli_and_complete_happy_path(
|
||||
streaming_mock_server: StreamingMockServer,
|
||||
setup_e2e_env: None,
|
||||
e2e_workdir: Path,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
wheel_path = _build_wheel(tmp_path / "dist")
|
||||
vibe_executable = _install_fresh_wheel(tmp_path, wheel_path)
|
||||
|
||||
monkeypatch.delenv("PYTHONPATH", raising=False)
|
||||
|
||||
captured = io.StringIO()
|
||||
child = pexpect.spawn(
|
||||
str(vibe_executable),
|
||||
["--workdir", str(e2e_workdir)],
|
||||
cwd=str(tmp_path),
|
||||
env=os.environ,
|
||||
encoding="utf-8",
|
||||
timeout=30,
|
||||
dimensions=(36, 120),
|
||||
)
|
||||
child.logfile_read = captured
|
||||
|
||||
try:
|
||||
wait_for_main_screen(child, timeout=20)
|
||||
child.send("Greet")
|
||||
child.send("\r")
|
||||
|
||||
wait_for_request_count(
|
||||
lambda: len(streaming_mock_server.requests), expected_count=1, timeout=10
|
||||
)
|
||||
child.expect(ansi_tolerant_pattern("Hello from mock server"), timeout=10)
|
||||
|
||||
send_ctrl_c_until_quit_confirmation(child, captured, timeout=5)
|
||||
child.expect(pexpect.EOF, timeout=10)
|
||||
finally:
|
||||
if child.isalive():
|
||||
child.terminate(force=True)
|
||||
if not child.closed:
|
||||
child.close()
|
||||
|
||||
output = captured.getvalue()
|
||||
assert "Welcome to Mistral Vibe" not in output
|
||||
assert streaming_mock_server.requests[-1].get("model") == "mock-model"
|
||||
|
|
@ -79,7 +79,9 @@ def test_spawn_cli_asks_bash_permission_and_shows_tool_output_after_approval(
|
|||
wait_for_request_count(
|
||||
lambda: len(streaming_mock_server.requests), expected_count=1, timeout=10
|
||||
)
|
||||
wait_for_rendered_text(child, captured, needle="bash command", timeout=10)
|
||||
wait_for_rendered_text(
|
||||
child, captured, needle="Permission for the bash tool", timeout=10
|
||||
)
|
||||
child.send("y")
|
||||
child.send("\r")
|
||||
wait_for_rendered_text(child, captured, needle=PREDICTABLE_OUTPUT, timeout=10)
|
||||
|
|
|
|||
178
tests/session/test_saved_sessions.py
Normal file
178
tests/session/test_saved_sessions.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.config import SessionLoggingConfig
|
||||
from vibe.core.session.saved_sessions import update_saved_session_title
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_session_dir(tmp_path: Path) -> Path:
|
||||
session_dir = tmp_path / "sessions"
|
||||
session_dir.mkdir()
|
||||
return session_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session_config(temp_session_dir: Path) -> SessionLoggingConfig:
|
||||
return SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="test", enabled=True
|
||||
)
|
||||
|
||||
|
||||
class TestUpdateSavedSessionTitle:
|
||||
@pytest.mark.asyncio
|
||||
async def test_updates_saved_session_title_without_losing_existing_metadata(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
saved_session_dir = session_dir / "test_20240101_120000_aaaaaaaa"
|
||||
saved_session_dir.mkdir()
|
||||
|
||||
(saved_session_dir / "messages.jsonl").write_text(
|
||||
'{"role": "user", "content": "Hello"}\n', encoding="utf-8"
|
||||
)
|
||||
|
||||
original_metadata = {
|
||||
"session_id": "aaaaaaaa-1111",
|
||||
"start_time": "2024-01-01T12:00:00Z",
|
||||
"end_time": "2024-01-01T12:05:00Z",
|
||||
"git_commit": None,
|
||||
"git_branch": None,
|
||||
"username": "test-user",
|
||||
"environment": {"working_directory": "/home/user/project"},
|
||||
"title": "Old title",
|
||||
"stats": {"steps": 2},
|
||||
"total_messages": 1,
|
||||
"tools_available": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {"name": "bash", "description": "Run shell commands"},
|
||||
}
|
||||
],
|
||||
"config": {"active_model": "test-model"},
|
||||
"system_prompt": {"role": "system", "content": "You are helpful"},
|
||||
}
|
||||
metadata_file = saved_session_dir / "meta.json"
|
||||
metadata_file.write_text(json.dumps(original_metadata), encoding="utf-8")
|
||||
|
||||
updated_metadata = await update_saved_session_title(
|
||||
"aaaaaaaa-1111", "Renamed session", session_config
|
||||
)
|
||||
|
||||
assert updated_metadata == {
|
||||
**original_metadata,
|
||||
"title": "Renamed session",
|
||||
"title_source": "manual",
|
||||
}
|
||||
assert json.loads(metadata_file.read_text(encoding="utf-8")) == updated_metadata
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejects_empty_title(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
saved_session_dir = session_dir / "test_20240101_120000_bbbbbbbb"
|
||||
saved_session_dir.mkdir()
|
||||
|
||||
(saved_session_dir / "messages.jsonl").write_text(
|
||||
'{"role": "user", "content": "Hello"}\n', encoding="utf-8"
|
||||
)
|
||||
|
||||
original_metadata = {
|
||||
"session_id": "bbbbbbbb-2222",
|
||||
"start_time": "2024-01-01T12:00:00Z",
|
||||
"end_time": "2024-01-01T12:05:00Z",
|
||||
"git_commit": None,
|
||||
"git_branch": None,
|
||||
"username": "test-user",
|
||||
"environment": {"working_directory": "/home/user/project"},
|
||||
"title": "Manual title",
|
||||
"title_source": "manual",
|
||||
"stats": {"steps": 2},
|
||||
}
|
||||
metadata_file = saved_session_dir / "meta.json"
|
||||
metadata_file.write_text(json.dumps(original_metadata), encoding="utf-8")
|
||||
|
||||
with pytest.raises(ValueError, match="Session title cannot be empty."):
|
||||
await update_saved_session_title("bbbbbbbb-2222", " ", session_config)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preserves_saved_session_end_time_when_updating_title(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
saved_session_dir = session_dir / "test_20240101_120000_cccccccc"
|
||||
saved_session_dir.mkdir()
|
||||
|
||||
(saved_session_dir / "messages.jsonl").write_text(
|
||||
'{"role": "user", "content": "Hello"}\n', encoding="utf-8"
|
||||
)
|
||||
|
||||
original_metadata = {
|
||||
"session_id": "cccccccc-3333",
|
||||
"start_time": "2024-01-01T12:00:00Z",
|
||||
"end_time": "2024-01-01T12:05:00Z",
|
||||
"git_commit": None,
|
||||
"git_branch": None,
|
||||
"username": "test-user",
|
||||
"environment": {"working_directory": "/home/user/project"},
|
||||
"title": "Old title",
|
||||
}
|
||||
metadata_file = saved_session_dir / "meta.json"
|
||||
metadata_file.write_text(json.dumps(original_metadata), encoding="utf-8")
|
||||
|
||||
updated_metadata = await update_saved_session_title(
|
||||
"cccccccc-3333", "Renamed session", session_config
|
||||
)
|
||||
|
||||
assert updated_metadata == {
|
||||
**original_metadata,
|
||||
"title": "Renamed session",
|
||||
"title_source": "manual",
|
||||
}
|
||||
assert json.loads(metadata_file.read_text(encoding="utf-8")) == updated_metadata
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_for_missing_saved_session(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
with pytest.raises(ValueError, match="Session not found: missing-session"):
|
||||
await update_saved_session_title(
|
||||
"missing-session", "Renamed", session_config
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_requires_exact_saved_session_id(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
saved_session_dir = session_dir / "test_20240101_120000_dddddddd"
|
||||
saved_session_dir.mkdir()
|
||||
|
||||
(saved_session_dir / "messages.jsonl").write_text(
|
||||
'{"role": "user", "content": "Hello"}\n', encoding="utf-8"
|
||||
)
|
||||
|
||||
original_metadata = {
|
||||
"session_id": "dddddddd-4444",
|
||||
"start_time": "2024-01-01T12:00:00Z",
|
||||
"end_time": "2024-01-01T12:05:00Z",
|
||||
"git_commit": None,
|
||||
"git_branch": None,
|
||||
"username": "test-user",
|
||||
"environment": {"working_directory": "/home/user/project"},
|
||||
"title": "Old title",
|
||||
}
|
||||
metadata_file = saved_session_dir / "meta.json"
|
||||
metadata_file.write_text(json.dumps(original_metadata), encoding="utf-8")
|
||||
|
||||
with pytest.raises(ValueError, match="Session not found: dddddddd"):
|
||||
await update_saved_session_title("dddddddd", "Renamed", session_config)
|
||||
|
||||
assert (
|
||||
json.loads(metadata_file.read_text(encoding="utf-8")) == original_metadata
|
||||
)
|
||||
|
|
@ -1049,6 +1049,36 @@ class TestSessionLoaderGetFirstUserMessage:
|
|||
|
||||
|
||||
class TestSessionLoaderUTF8Encoding:
|
||||
def test_load_metadata_defaults_title_source_for_existing_sessions(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = session_dir / "test_20230101_120000_legacyttl"
|
||||
session_folder.mkdir()
|
||||
|
||||
metadata_content = {
|
||||
"session_id": "legacy-title-test",
|
||||
"start_time": "2023-01-01T12:00:00Z",
|
||||
"end_time": "2023-01-01T12:05:00Z",
|
||||
"environment": {"working_directory": "/home/user/project"},
|
||||
"username": "testuser",
|
||||
"git_commit": None,
|
||||
"git_branch": None,
|
||||
"title": "Existing title",
|
||||
}
|
||||
|
||||
metadata_file = session_folder / "meta.json"
|
||||
with metadata_file.open("w", encoding="utf-8") as f:
|
||||
json.dump(metadata_content, f, indent=2, ensure_ascii=False)
|
||||
|
||||
messages_file = session_folder / "messages.jsonl"
|
||||
messages_file.write_text('{"role": "user", "content": "Hello"}\n')
|
||||
|
||||
metadata = SessionLoader.load_metadata(session_folder)
|
||||
|
||||
assert metadata.title == "Existing title"
|
||||
assert metadata.title_source == "auto"
|
||||
|
||||
def test_load_metadata_with_utf8_encoding(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -139,6 +139,8 @@ class TestSessionLoggerMetadata:
|
|||
assert metadata.username == "testuser"
|
||||
assert "working_directory" in metadata.environment
|
||||
assert metadata.environment["working_directory"] == str(Path.cwd())
|
||||
assert metadata.title is None
|
||||
assert metadata.title_source == "auto"
|
||||
|
||||
@patch("vibe.core.session.session_logger.subprocess.run")
|
||||
@patch("vibe.core.session.session_logger.getpass.getuser")
|
||||
|
|
@ -161,6 +163,50 @@ class TestSessionLoggerMetadata:
|
|||
assert metadata.username == "testuser"
|
||||
|
||||
|
||||
class TestSessionLoggerTitleManagement:
|
||||
def test_set_title_marks_live_session_title_as_manual(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
logger = SessionLogger(session_config, "test-session-123")
|
||||
|
||||
logger.set_title("Manual title")
|
||||
|
||||
assert logger.session_metadata is not None
|
||||
assert logger.session_metadata.title == "Manual title"
|
||||
assert logger.session_metadata.title_source == "manual"
|
||||
|
||||
def test_set_title_none_returns_live_session_to_auto_mode(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
logger = SessionLogger(session_config, "test-session-123")
|
||||
logger.set_title("Manual title")
|
||||
|
||||
logger.set_title(None)
|
||||
|
||||
assert logger.session_metadata is not None
|
||||
assert logger.session_metadata.title is None
|
||||
assert logger.session_metadata.title_source == "auto"
|
||||
|
||||
def test_set_title_rejects_empty_title(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
logger = SessionLogger(session_config, "test-session-123")
|
||||
|
||||
with pytest.raises(ValueError, match="Session title cannot be empty."):
|
||||
logger.set_title(" ")
|
||||
|
||||
def test_set_title_preserves_live_session_end_time(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
logger = SessionLogger(session_config, "test-session-123")
|
||||
assert logger.session_metadata is not None
|
||||
logger.session_metadata.end_time = "2026-01-01T10:00:00+00:00"
|
||||
|
||||
logger.set_title("Manual title")
|
||||
|
||||
assert logger.session_metadata.end_time == "2026-01-01T10:00:00+00:00"
|
||||
|
||||
|
||||
class TestSessionLoggerSaveInteraction:
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_interaction_disabled(
|
||||
|
|
@ -234,6 +280,7 @@ class TestSessionLoggerSaveInteraction:
|
|||
assert metadata["stats"]["steps"] == stats.steps
|
||||
assert "title" in metadata
|
||||
assert metadata["title"] == "Hello"
|
||||
assert metadata["title_source"] == "auto"
|
||||
assert "system_prompt" in metadata
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -447,6 +494,7 @@ class TestSessionLoggerSaveInteraction:
|
|||
assert metadata["total_messages"] == 1
|
||||
assert metadata["stats"]["steps"] == stats.steps
|
||||
assert metadata["title"] == "Untitled session"
|
||||
assert metadata["title_source"] == "auto"
|
||||
|
||||
messages_file = logger.session_dir / "messages.jsonl"
|
||||
assert messages_file.exists()
|
||||
|
|
@ -496,6 +544,46 @@ class TestSessionLoggerSaveInteraction:
|
|||
assert metadata["stats"]["steps"] == stats.steps
|
||||
expected_title = long_message[:50] + "…"
|
||||
assert metadata["title"] == expected_title
|
||||
assert metadata["title_source"] == "auto"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_interaction_preserves_manual_title(
|
||||
self,
|
||||
session_config: SessionLoggingConfig,
|
||||
mock_vibe_config: VibeConfig,
|
||||
mock_tool_manager: ToolManager,
|
||||
mock_agent_profile: AgentProfile,
|
||||
) -> None:
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
assert logger.session_metadata is not None
|
||||
|
||||
logger.set_title("Manual title")
|
||||
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content="Hello"),
|
||||
LLMMessage(role=Role.assistant, content="Hi there!"),
|
||||
]
|
||||
stats = AgentStats(
|
||||
steps=1, session_prompt_tokens=10, session_completion_tokens=20
|
||||
)
|
||||
|
||||
await logger.save_interaction(
|
||||
messages=messages,
|
||||
stats=stats,
|
||||
base_config=mock_vibe_config,
|
||||
tool_manager=mock_tool_manager,
|
||||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
|
||||
assert logger.session_dir is not None
|
||||
metadata_file = logger.session_dir / "meta.json"
|
||||
with open(metadata_file) as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
assert metadata["title"] == "Manual title"
|
||||
assert metadata["title_source"] == "manual"
|
||||
|
||||
messages_file = logger.session_dir / "messages.jsonl"
|
||||
assert messages_file.exists()
|
||||
|
|
|
|||
|
|
@ -33,11 +33,11 @@
|
|||
}
|
||||
|
||||
.terminal-r1 { fill: #c5c8c6 }
|
||||
.terminal-r2 { fill: #4b4e55 }
|
||||
.terminal-r3 { fill: #68a0b3 }
|
||||
.terminal-r4 { fill: #292929 }
|
||||
.terminal-r5 { fill: #9a9b99 }
|
||||
.terminal-r6 { fill: #608ab1;font-weight: bold }
|
||||
.terminal-r2 { fill: #68a0b3 }
|
||||
.terminal-r3 { fill: #4b4e55 }
|
||||
.terminal-r4 { fill: #9a9b99 }
|
||||
.terminal-r5 { fill: #608ab1;font-weight: bold }
|
||||
.terminal-r6 { fill: #292929 }
|
||||
.terminal-r7 { fill: #98a84b;font-weight: bold }
|
||||
.terminal-r8 { fill: #ff8205;font-weight: bold }
|
||||
</style>
|
||||
|
|
@ -197,56 +197,56 @@
|
|||
</g>
|
||||
|
||||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
<rect fill="#4b4e55" x="1451.8" y="25.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="123.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="196.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="221.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="245.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="269.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="294.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="318.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="367.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="391.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="416.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="440.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="465.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="489.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="513.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="538.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="562.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="587.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="611.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="635.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="660.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="684.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="709.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="733.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="757.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="782.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="806.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="831.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="855.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="879.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="904.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="928.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="953.1" width="12.2" height="24.65" shape-rendering="crispEdges"/>
|
||||
<rect fill="#4b4e55" x="1451.8" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="123.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="196.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="221.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="245.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="269.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="294.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="318.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="367.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="391.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="416.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="440.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="465.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="489.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="513.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="538.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="562.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="587.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="611.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="635.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="660.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="684.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="709.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="733.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="757.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="782.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="806.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="831.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="855.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="879.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="904.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="928.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="953.1" width="12.2" height="24.65" shape-rendering="crispEdges"/>
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)"> ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="170.8" y="20" textLength="414.8" clip-path="url(#terminal-line-0)">1 model · 0 MCP servers · 0 skills</text><text class="terminal-r2" x="1451.8" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">▁</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</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="170.8" y="44.4" textLength="61" clip-path="url(#terminal-line-1)">Type </text><text class="terminal-r3" x="231.8" y="44.4" textLength="61" clip-path="url(#terminal-line-1)">/help</text><text class="terminal-r1" x="292.8" y="44.4" textLength="256.2" clip-path="url(#terminal-line-1)"> for more information</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-r5" x="24.4" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">⎢</text><text class="terminal-r6" x="48.8" y="93.2" textLength="219.6" clip-path="url(#terminal-line-3)">Keyboard Shortcuts</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
</text><text class="terminal-r5" x="24.4" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">⎢</text><text class="terminal-r1" x="48.8" y="117.6" textLength="24.4" clip-path="url(#terminal-line-4)">• </text><text class="terminal-r7" x="73.2" y="117.6" textLength="61" clip-path="url(#terminal-line-4)">Enter</text><text class="terminal-r1" x="134.2" y="117.6" textLength="183" clip-path="url(#terminal-line-4)"> Submit message</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
|
||||
</text><text class="terminal-r5" x="24.4" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">⎢</text><text class="terminal-r1" x="48.8" y="142" textLength="24.4" clip-path="url(#terminal-line-5)">• </text><text class="terminal-r7" x="73.2" y="142" textLength="73.2" clip-path="url(#terminal-line-5)">Ctrl+J</text><text class="terminal-r1" x="146.4" y="142" textLength="36.6" clip-path="url(#terminal-line-5)"> / </text><text class="terminal-r7" x="183" y="142" textLength="134.2" clip-path="url(#terminal-line-5)">Shift+Enter</text><text class="terminal-r1" x="317.2" y="142" textLength="183" clip-path="url(#terminal-line-5)"> Insert newline</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
|
||||
</text><text class="terminal-r5" x="24.4" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">⎢</text><text class="terminal-r1" x="48.8" y="166.4" textLength="24.4" clip-path="url(#terminal-line-6)">• </text><text class="terminal-r7" x="73.2" y="166.4" textLength="73.2" clip-path="url(#terminal-line-6)">Escape</text><text class="terminal-r1" x="146.4" y="166.4" textLength="402.6" clip-path="url(#terminal-line-6)"> Interrupt agent or close dialogs</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
|
||||
</text><text class="terminal-r5" x="24.4" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">⎢</text><text class="terminal-r1" x="48.8" y="190.8" textLength="24.4" clip-path="url(#terminal-line-7)">• </text><text class="terminal-r7" x="73.2" y="190.8" textLength="73.2" clip-path="url(#terminal-line-7)">Ctrl+C</text><text class="terminal-r1" x="146.4" y="190.8" textLength="463.6" clip-path="url(#terminal-line-7)"> Quit (or clear input if text present)</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
|
||||
</text><text class="terminal-r5" x="24.4" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">⎢</text><text class="terminal-r1" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-line-8)">• </text><text class="terminal-r7" x="73.2" y="215.2" textLength="73.2" clip-path="url(#terminal-line-8)">Ctrl+G</text><text class="terminal-r1" x="146.4" y="215.2" textLength="366" clip-path="url(#terminal-line-8)"> Edit input in external editor</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
|
||||
</text><text class="terminal-r5" x="24.4" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">⎢</text><text class="terminal-r1" x="48.8" y="239.6" textLength="24.4" clip-path="url(#terminal-line-9)">• </text><text class="terminal-r7" x="73.2" y="239.6" textLength="73.2" clip-path="url(#terminal-line-9)">Ctrl+O</text><text class="terminal-r1" x="146.4" y="239.6" textLength="292.8" clip-path="url(#terminal-line-9)"> Toggle tool output view</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
|
||||
</text><text class="terminal-r5" x="24.4" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">⎢</text><text class="terminal-r1" x="48.8" y="264" textLength="24.4" clip-path="url(#terminal-line-10)">• </text><text class="terminal-r7" x="73.2" y="264" textLength="109.8" clip-path="url(#terminal-line-10)">Shift+Tab</text><text class="terminal-r1" x="183" y="264" textLength="305" clip-path="url(#terminal-line-10)"> Toggle auto-approve mode</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
|
||||
</text><text class="terminal-r5" x="24.4" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">⎢</text><text class="terminal-r1" x="48.8" y="288.4" textLength="24.4" clip-path="url(#terminal-line-11)">• </text><text class="terminal-r7" x="73.2" y="288.4" textLength="73.2" clip-path="url(#terminal-line-11)">Alt+↑↓</text><text class="terminal-r1" x="146.4" y="288.4" textLength="36.6" clip-path="url(#terminal-line-11)"> / </text><text class="terminal-r7" x="183" y="288.4" textLength="97.6" clip-path="url(#terminal-line-11)">Ctrl+P/N</text><text class="terminal-r1" x="280.6" y="288.4" textLength="390.4" clip-path="url(#terminal-line-11)"> Rewind to previous/next message</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
|
||||
</text><text class="terminal-r5" x="24.4" 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-r5" x="24.4" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">⎢</text><text class="terminal-r6" x="48.8" y="337.2" textLength="195.2" clip-path="url(#terminal-line-13)">Special Features</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||
</text><text class="terminal-r5" x="24.4" 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-r5" x="24.4" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">⎢</text><text class="terminal-r1" x="48.8" y="386" textLength="24.4" clip-path="url(#terminal-line-15)">• </text><text class="terminal-r7" x="73.2" y="386" textLength="122" clip-path="url(#terminal-line-15)">!<command></text><text class="terminal-r1" x="195.2" y="386" textLength="366" clip-path="url(#terminal-line-15)"> Execute bash command directly</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||
</text><text class="terminal-r5" 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="24.4" clip-path="url(#terminal-line-16)">• </text><text class="terminal-r7" x="73.2" y="410.4" textLength="170.8" clip-path="url(#terminal-line-16)">@path/to/file/</text><text class="terminal-r1" x="244" y="410.4" textLength="305" clip-path="url(#terminal-line-16)"> Autocompletes file paths</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||
</text><text class="terminal-r5" x="24.4" 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-r5" x="24.4" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">⎢</text><text class="terminal-r6" x="48.8" y="459.2" textLength="97.6" clip-path="url(#terminal-line-18)">Commands</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r5" x="24.4" 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-r5" 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="24.4" clip-path="url(#terminal-line-20)">• </text><text class="terminal-r7" x="73.2" y="508" textLength="61" clip-path="url(#terminal-line-20)">/help</text><text class="terminal-r1" x="134.2" y="508" textLength="231.8" clip-path="url(#terminal-line-20)">: Show help message</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r5" 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="24.4" clip-path="url(#terminal-line-21)">• </text><text class="terminal-r7" x="73.2" y="532.4" textLength="85.4" clip-path="url(#terminal-line-21)">/config</text><text class="terminal-r1" x="158.6" y="532.4" textLength="268.4" clip-path="url(#terminal-line-21)">: Edit config settings</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r5" 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="24.4" clip-path="url(#terminal-line-22)">• </text><text class="terminal-r7" x="73.2" y="556.8" textLength="73.2" clip-path="url(#terminal-line-22)">/model</text><text class="terminal-r1" x="146.4" y="556.8" textLength="256.2" clip-path="url(#terminal-line-22)">: Select active model</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r5" x="24.4" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">⎢</text><text class="terminal-r1" x="48.8" y="581.2" textLength="24.4" clip-path="url(#terminal-line-23)">• </text><text class="terminal-r7" x="73.2" y="581.2" textLength="109.8" clip-path="url(#terminal-line-23)">/thinking</text><text class="terminal-r1" x="183" y="581.2" textLength="280.6" clip-path="url(#terminal-line-23)">: Select thinking level</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r5" x="24.4" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">⎢</text><text class="terminal-r1" x="48.8" y="605.6" textLength="24.4" clip-path="url(#terminal-line-24)">• </text><text class="terminal-r7" x="73.2" y="605.6" textLength="85.4" clip-path="url(#terminal-line-24)">/reload</text><text class="terminal-r1" x="158.6" y="605.6" textLength="780.8" clip-path="url(#terminal-line-24)">: Reload configuration, agent instructions, and skills from disk</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||
</text><text class="terminal-r5" x="24.4" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">⎢</text><text class="terminal-r1" x="48.8" y="630" textLength="24.4" clip-path="url(#terminal-line-25)">• </text><text class="terminal-r7" x="73.2" y="630" textLength="73.2" clip-path="url(#terminal-line-25)">/clear</text><text class="terminal-r1" x="146.4" y="630" textLength="341.6" clip-path="url(#terminal-line-25)">: Clear conversation history</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r5" x="24.4" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">⎢</text><text class="terminal-r1" x="48.8" y="654.4" textLength="24.4" clip-path="url(#terminal-line-26)">• </text><text class="terminal-r7" x="73.2" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/copy</text><text class="terminal-r1" x="134.2" y="654.4" textLength="561.2" clip-path="url(#terminal-line-26)">: Copy the last agent message to the clipboard</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r5" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">⎢</text><text class="terminal-r1" x="48.8" y="678.8" textLength="24.4" clip-path="url(#terminal-line-27)">• </text><text class="terminal-r7" x="73.2" y="678.8" textLength="48.8" clip-path="url(#terminal-line-27)">/log</text><text class="terminal-r1" x="122" y="678.8" textLength="524.6" clip-path="url(#terminal-line-27)">: Show path to current interaction log file</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r5" x="24.4" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">⎢</text><text class="terminal-r1" x="48.8" y="703.2" textLength="24.4" clip-path="url(#terminal-line-28)">• </text><text class="terminal-r7" x="73.2" y="703.2" textLength="73.2" clip-path="url(#terminal-line-28)">/debug</text><text class="terminal-r1" x="146.4" y="703.2" textLength="268.4" clip-path="url(#terminal-line-28)">: Toggle debug console</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||
</text><text class="terminal-r5" x="24.4" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">⎢</text><text class="terminal-r1" x="48.8" y="727.6" textLength="24.4" clip-path="url(#terminal-line-29)">• </text><text class="terminal-r7" x="73.2" y="727.6" textLength="97.6" clip-path="url(#terminal-line-29)">/compact</text><text class="terminal-r1" x="170.8" y="727.6" textLength="1171.2" clip-path="url(#terminal-line-29)">: Compact conversation history by summarizing. Optionally pass instructions to guide the summary</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r5" x="24.4" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">⎢</text><text class="terminal-r1" x="48.8" y="752" textLength="24.4" clip-path="url(#terminal-line-30)">• </text><text class="terminal-r7" x="73.2" y="752" textLength="61" clip-path="url(#terminal-line-30)">/exit</text><text class="terminal-r1" x="134.2" y="752" textLength="268.4" clip-path="url(#terminal-line-30)">: Exit the application</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r5" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">⎢</text><text class="terminal-r1" x="48.8" y="776.4" textLength="24.4" clip-path="url(#terminal-line-31)">• </text><text class="terminal-r7" x="73.2" y="776.4" textLength="85.4" clip-path="url(#terminal-line-31)">/status</text><text class="terminal-r1" x="158.6" y="776.4" textLength="317.2" clip-path="url(#terminal-line-31)">: Display agent statistics</text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r5" x="24.4" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">⎢</text><text class="terminal-r1" x="48.8" y="800.8" textLength="24.4" clip-path="url(#terminal-line-32)">• </text><text class="terminal-r7" x="73.2" y="800.8" textLength="146.4" clip-path="url(#terminal-line-32)">/proxy-setup</text><text class="terminal-r1" x="219.6" y="800.8" textLength="561.2" clip-path="url(#terminal-line-32)">: Configure proxy and SSL certificate settings</text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
</text><text class="terminal-r5" x="24.4" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">⎢</text><text class="terminal-r1" x="48.8" y="825.2" textLength="24.4" clip-path="url(#terminal-line-33)">• </text><text class="terminal-r7" x="73.2" y="825.2" textLength="109.8" clip-path="url(#terminal-line-33)">/continue</text><text class="terminal-r1" x="183" y="825.2" textLength="24.4" clip-path="url(#terminal-line-33)">, </text><text class="terminal-r7" x="207.4" y="825.2" textLength="85.4" clip-path="url(#terminal-line-33)">/resume</text><text class="terminal-r1" x="292.8" y="825.2" textLength="402.6" clip-path="url(#terminal-line-33)">: Browse and resume past sessions</text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
|
||||
</text><text class="terminal-r5" x="24.4" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">⎢</text><text class="terminal-r1" x="48.8" y="849.6" textLength="24.4" clip-path="url(#terminal-line-34)">• </text><text class="terminal-r7" x="73.2" y="849.6" textLength="134.2" clip-path="url(#terminal-line-34)">/connectors</text><text class="terminal-r1" x="207.4" y="849.6" textLength="24.4" clip-path="url(#terminal-line-34)">, </text><text class="terminal-r7" x="231.8" y="849.6" textLength="48.8" clip-path="url(#terminal-line-34)">/mcp</text><text class="terminal-r1" x="280.6" y="849.6" textLength="939.4" clip-path="url(#terminal-line-34)">: Display available MCP servers and connectors. Pass a name to list its tools</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
|
||||
</text><text class="terminal-r5" x="24.4" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">⎢</text><text class="terminal-r1" x="48.8" y="874" textLength="24.4" clip-path="url(#terminal-line-35)">• </text><text class="terminal-r7" x="73.2" y="874" textLength="73.2" clip-path="url(#terminal-line-35)">/voice</text><text class="terminal-r1" x="146.4" y="874" textLength="317.2" clip-path="url(#terminal-line-35)">: Configure voice settings</text><text class="terminal-r1" x="1464" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">
|
||||
</text><text class="terminal-r5" x="24.4" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">⎢</text><text class="terminal-r1" x="48.8" y="898.4" textLength="24.4" clip-path="url(#terminal-line-36)">• </text><text class="terminal-r7" x="73.2" y="898.4" textLength="122" clip-path="url(#terminal-line-36)">/leanstall</text><text class="terminal-r1" x="195.2" y="898.4" textLength="463.6" clip-path="url(#terminal-line-36)">: Install the Lean 4 agent (leanstral)</text><text class="terminal-r1" x="1464" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">
|
||||
</text><text class="terminal-r5" x="24.4" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">⎢</text><text class="terminal-r1" x="48.8" y="922.8" textLength="24.4" clip-path="url(#terminal-line-37)">• </text><text class="terminal-r7" x="73.2" y="922.8" textLength="146.4" clip-path="url(#terminal-line-37)">/unleanstall</text><text class="terminal-r1" x="219.6" y="922.8" textLength="341.6" clip-path="url(#terminal-line-37)">: Uninstall the Lean 4 agent</text><text class="terminal-r1" x="1464" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">
|
||||
</text><text class="terminal-r5" x="24.4" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">⎢</text><text class="terminal-r1" x="48.8" y="947.2" textLength="24.4" clip-path="url(#terminal-line-38)">• </text><text class="terminal-r7" x="73.2" y="947.2" textLength="85.4" clip-path="url(#terminal-line-38)">/rewind</text><text class="terminal-r1" x="158.6" y="947.2" textLength="366" clip-path="url(#terminal-line-38)">: Rewind to a previous message</text><text class="terminal-r1" x="1464" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">
|
||||
</text><text class="terminal-r5" x="24.4" y="971.6" textLength="12.2" clip-path="url(#terminal-line-39)">⎣</text><text class="terminal-r1" x="48.8" y="971.6" textLength="24.4" clip-path="url(#terminal-line-39)">• </text><text class="terminal-r7" x="73.2" y="971.6" textLength="183" clip-path="url(#terminal-line-39)">/data-retention</text><text class="terminal-r1" x="256.2" y="971.6" textLength="402.6" clip-path="url(#terminal-line-39)">: Show data retention information</text><text class="terminal-r1" x="1464" y="971.6" textLength="12.2" clip-path="url(#terminal-line-39)">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="20" textLength="61" clip-path="url(#terminal-line-0)">Type </text><text class="terminal-r2" x="231.8" y="20" textLength="61" clip-path="url(#terminal-line-0)">/help</text><text class="terminal-r1" x="292.8" y="20" textLength="256.2" clip-path="url(#terminal-line-0)"> for more information</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r3" x="1451.8" 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-r4" x="24.4" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">⎢</text><text class="terminal-r5" x="48.8" y="68.8" textLength="219.6" clip-path="url(#terminal-line-2)">Keyboard Shortcuts</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r4" x="24.4" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">⎢</text><text class="terminal-r1" x="48.8" y="93.2" textLength="24.4" clip-path="url(#terminal-line-3)">• </text><text class="terminal-r7" x="73.2" y="93.2" textLength="61" clip-path="url(#terminal-line-3)">Enter</text><text class="terminal-r1" x="134.2" y="93.2" textLength="183" clip-path="url(#terminal-line-3)"> Submit message</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
</text><text class="terminal-r4" x="24.4" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">⎢</text><text class="terminal-r1" x="48.8" y="117.6" textLength="24.4" clip-path="url(#terminal-line-4)">• </text><text class="terminal-r7" x="73.2" y="117.6" textLength="73.2" clip-path="url(#terminal-line-4)">Ctrl+J</text><text class="terminal-r1" x="146.4" y="117.6" textLength="36.6" clip-path="url(#terminal-line-4)"> / </text><text class="terminal-r7" x="183" y="117.6" textLength="134.2" clip-path="url(#terminal-line-4)">Shift+Enter</text><text class="terminal-r1" x="317.2" y="117.6" textLength="183" clip-path="url(#terminal-line-4)"> Insert newline</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
|
||||
</text><text class="terminal-r4" x="24.4" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">⎢</text><text class="terminal-r1" x="48.8" y="142" textLength="24.4" clip-path="url(#terminal-line-5)">• </text><text class="terminal-r7" x="73.2" y="142" textLength="73.2" clip-path="url(#terminal-line-5)">Escape</text><text class="terminal-r1" x="146.4" y="142" textLength="402.6" clip-path="url(#terminal-line-5)"> Interrupt agent or close dialogs</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
|
||||
</text><text class="terminal-r4" x="24.4" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">⎢</text><text class="terminal-r1" x="48.8" y="166.4" textLength="24.4" clip-path="url(#terminal-line-6)">• </text><text class="terminal-r7" x="73.2" y="166.4" textLength="73.2" clip-path="url(#terminal-line-6)">Ctrl+C</text><text class="terminal-r1" x="146.4" y="166.4" textLength="463.6" clip-path="url(#terminal-line-6)"> Quit (or clear input if text present)</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
|
||||
</text><text class="terminal-r4" x="24.4" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">⎢</text><text class="terminal-r1" x="48.8" y="190.8" textLength="24.4" clip-path="url(#terminal-line-7)">• </text><text class="terminal-r7" x="73.2" y="190.8" textLength="73.2" clip-path="url(#terminal-line-7)">Ctrl+G</text><text class="terminal-r1" x="146.4" y="190.8" textLength="366" clip-path="url(#terminal-line-7)"> Edit input in external editor</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
|
||||
</text><text class="terminal-r4" x="24.4" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">⎢</text><text class="terminal-r1" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-line-8)">• </text><text class="terminal-r7" x="73.2" y="215.2" textLength="73.2" clip-path="url(#terminal-line-8)">Ctrl+O</text><text class="terminal-r1" x="146.4" y="215.2" textLength="292.8" clip-path="url(#terminal-line-8)"> Toggle tool output view</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
|
||||
</text><text class="terminal-r4" x="24.4" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">⎢</text><text class="terminal-r1" x="48.8" y="239.6" textLength="24.4" clip-path="url(#terminal-line-9)">• </text><text class="terminal-r7" x="73.2" y="239.6" textLength="109.8" clip-path="url(#terminal-line-9)">Shift+Tab</text><text class="terminal-r1" x="183" y="239.6" textLength="512.4" clip-path="url(#terminal-line-9)"> Cycle through agents (default, plan, ...)</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
|
||||
</text><text class="terminal-r4" x="24.4" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">⎢</text><text class="terminal-r1" x="48.8" y="264" textLength="24.4" clip-path="url(#terminal-line-10)">• </text><text class="terminal-r7" x="73.2" y="264" textLength="73.2" clip-path="url(#terminal-line-10)">Alt+↑↓</text><text class="terminal-r1" x="146.4" y="264" textLength="36.6" clip-path="url(#terminal-line-10)"> / </text><text class="terminal-r7" x="183" y="264" textLength="97.6" clip-path="url(#terminal-line-10)">Ctrl+P/N</text><text class="terminal-r1" x="280.6" y="264" textLength="390.4" clip-path="url(#terminal-line-10)"> Rewind to previous/next message</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
|
||||
</text><text class="terminal-r4" x="24.4" 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-r4" x="24.4" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">⎢</text><text class="terminal-r5" x="48.8" y="312.8" textLength="195.2" clip-path="url(#terminal-line-12)">Special Features</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||
</text><text class="terminal-r4" x="24.4" 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-r4" x="24.4" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">⎢</text><text class="terminal-r1" x="48.8" y="361.6" textLength="24.4" clip-path="url(#terminal-line-14)">• </text><text class="terminal-r7" x="73.2" y="361.6" textLength="122" clip-path="url(#terminal-line-14)">!<command></text><text class="terminal-r1" x="195.2" y="361.6" textLength="366" clip-path="url(#terminal-line-14)"> Execute bash command directly</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||
</text><text class="terminal-r4" x="24.4" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">⎢</text><text class="terminal-r1" x="48.8" y="386" textLength="24.4" clip-path="url(#terminal-line-15)">• </text><text class="terminal-r7" x="73.2" y="386" textLength="170.8" clip-path="url(#terminal-line-15)">@path/to/file/</text><text class="terminal-r1" x="244" y="386" textLength="305" clip-path="url(#terminal-line-15)"> Autocompletes file paths</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-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-r5" x="48.8" y="434.8" textLength="97.6" clip-path="url(#terminal-line-17)">Commands</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-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="24.4" clip-path="url(#terminal-line-19)">• </text><text class="terminal-r7" x="73.2" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="134.2" y="483.6" textLength="231.8" clip-path="url(#terminal-line-19)">: Show help message</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-r1" x="48.8" y="508" textLength="24.4" clip-path="url(#terminal-line-20)">• </text><text class="terminal-r7" x="73.2" y="508" textLength="85.4" clip-path="url(#terminal-line-20)">/config</text><text class="terminal-r1" x="158.6" y="508" textLength="268.4" clip-path="url(#terminal-line-20)">: Edit config settings</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="24.4" clip-path="url(#terminal-line-21)">• </text><text class="terminal-r7" x="73.2" y="532.4" textLength="73.2" clip-path="url(#terminal-line-21)">/model</text><text class="terminal-r1" x="146.4" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)">: Select active model</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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">⎢</text><text class="terminal-r1" x="48.8" y="556.8" textLength="24.4" clip-path="url(#terminal-line-22)">• </text><text class="terminal-r7" x="73.2" y="556.8" textLength="109.8" clip-path="url(#terminal-line-22)">/thinking</text><text class="terminal-r1" x="183" y="556.8" textLength="280.6" clip-path="url(#terminal-line-22)">: Select thinking level</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r4" x="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="24.4" clip-path="url(#terminal-line-23)">• </text><text class="terminal-r7" x="73.2" y="581.2" textLength="85.4" clip-path="url(#terminal-line-23)">/reload</text><text class="terminal-r1" x="158.6" y="581.2" textLength="780.8" clip-path="url(#terminal-line-23)">: Reload configuration, agent instructions, and skills from disk</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r4" x="24.4" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">⎢</text><text class="terminal-r1" x="48.8" y="605.6" textLength="24.4" clip-path="url(#terminal-line-24)">• </text><text class="terminal-r7" x="73.2" y="605.6" textLength="73.2" clip-path="url(#terminal-line-24)">/clear</text><text class="terminal-r1" x="146.4" y="605.6" textLength="341.6" clip-path="url(#terminal-line-24)">: Clear conversation history</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||
</text><text class="terminal-r4" x="24.4" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">⎢</text><text class="terminal-r1" x="48.8" y="630" textLength="24.4" clip-path="url(#terminal-line-25)">• </text><text class="terminal-r7" x="73.2" y="630" textLength="61" clip-path="url(#terminal-line-25)">/copy</text><text class="terminal-r1" x="134.2" y="630" textLength="561.2" clip-path="url(#terminal-line-25)">: Copy the last agent message to the clipboard</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r4" 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="24.4" clip-path="url(#terminal-line-26)">• </text><text class="terminal-r7" x="73.2" y="654.4" textLength="48.8" clip-path="url(#terminal-line-26)">/log</text><text class="terminal-r1" x="122" y="654.4" textLength="524.6" clip-path="url(#terminal-line-26)">: Show path to current interaction log file</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="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="24.4" clip-path="url(#terminal-line-27)">• </text><text class="terminal-r7" x="73.2" y="678.8" textLength="73.2" clip-path="url(#terminal-line-27)">/debug</text><text class="terminal-r1" x="146.4" y="678.8" textLength="268.4" clip-path="url(#terminal-line-27)">: Toggle debug console</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="24.4" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">⎢</text><text class="terminal-r1" x="48.8" y="703.2" textLength="24.4" clip-path="url(#terminal-line-28)">• </text><text class="terminal-r7" x="73.2" y="703.2" textLength="97.6" clip-path="url(#terminal-line-28)">/compact</text><text class="terminal-r1" x="170.8" y="703.2" textLength="1171.2" clip-path="url(#terminal-line-28)">: Compact conversation history by summarizing. Optionally pass instructions to guide the summary</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="24.4" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">⎢</text><text class="terminal-r1" x="48.8" y="727.6" textLength="24.4" clip-path="url(#terminal-line-29)">• </text><text class="terminal-r7" x="73.2" y="727.6" textLength="61" clip-path="url(#terminal-line-29)">/exit</text><text class="terminal-r1" x="134.2" y="727.6" textLength="268.4" clip-path="url(#terminal-line-29)">: Exit the application</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="24.4" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">⎢</text><text class="terminal-r1" x="48.8" y="752" textLength="24.4" clip-path="url(#terminal-line-30)">• </text><text class="terminal-r7" x="73.2" y="752" textLength="85.4" clip-path="url(#terminal-line-30)">/status</text><text class="terminal-r1" x="158.6" y="752" textLength="317.2" clip-path="url(#terminal-line-30)">: Display agent statistics</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r4" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">⎢</text><text class="terminal-r1" x="48.8" y="776.4" textLength="24.4" clip-path="url(#terminal-line-31)">• </text><text class="terminal-r7" x="73.2" y="776.4" textLength="146.4" clip-path="url(#terminal-line-31)">/proxy-setup</text><text class="terminal-r1" x="219.6" y="776.4" textLength="561.2" clip-path="url(#terminal-line-31)">: Configure proxy and SSL certificate settings</text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r4" x="24.4" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">⎢</text><text class="terminal-r1" x="48.8" y="800.8" textLength="24.4" clip-path="url(#terminal-line-32)">• </text><text class="terminal-r7" x="73.2" y="800.8" textLength="109.8" clip-path="url(#terminal-line-32)">/continue</text><text class="terminal-r1" x="183" y="800.8" textLength="24.4" clip-path="url(#terminal-line-32)">, </text><text class="terminal-r7" x="207.4" y="800.8" textLength="85.4" clip-path="url(#terminal-line-32)">/resume</text><text class="terminal-r1" x="292.8" y="800.8" textLength="402.6" clip-path="url(#terminal-line-32)">: Browse and resume past sessions</text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
</text><text class="terminal-r4" x="24.4" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">⎢</text><text class="terminal-r1" x="48.8" y="825.2" textLength="24.4" clip-path="url(#terminal-line-33)">• </text><text class="terminal-r7" x="73.2" y="825.2" textLength="85.4" clip-path="url(#terminal-line-33)">/rename</text><text class="terminal-r1" x="158.6" y="825.2" textLength="341.6" clip-path="url(#terminal-line-33)">: Rename the current session</text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
|
||||
</text><text class="terminal-r4" x="24.4" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">⎢</text><text class="terminal-r1" x="48.8" y="849.6" textLength="24.4" clip-path="url(#terminal-line-34)">• </text><text class="terminal-r7" x="73.2" y="849.6" textLength="134.2" clip-path="url(#terminal-line-34)">/connectors</text><text class="terminal-r1" x="207.4" y="849.6" textLength="24.4" clip-path="url(#terminal-line-34)">, </text><text class="terminal-r7" x="231.8" y="849.6" textLength="48.8" clip-path="url(#terminal-line-34)">/mcp</text><text class="terminal-r1" x="280.6" y="849.6" textLength="939.4" clip-path="url(#terminal-line-34)">: Display available MCP servers and connectors. Pass a name to list its tools</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
|
||||
</text><text class="terminal-r4" x="24.4" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">⎢</text><text class="terminal-r1" x="48.8" y="874" textLength="24.4" clip-path="url(#terminal-line-35)">• </text><text class="terminal-r7" x="73.2" y="874" textLength="73.2" clip-path="url(#terminal-line-35)">/voice</text><text class="terminal-r1" x="146.4" y="874" textLength="317.2" clip-path="url(#terminal-line-35)">: Configure voice settings</text><text class="terminal-r1" x="1464" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">
|
||||
</text><text class="terminal-r4" x="24.4" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">⎢</text><text class="terminal-r1" x="48.8" y="898.4" textLength="24.4" clip-path="url(#terminal-line-36)">• </text><text class="terminal-r7" x="73.2" y="898.4" textLength="122" clip-path="url(#terminal-line-36)">/leanstall</text><text class="terminal-r1" x="195.2" y="898.4" textLength="463.6" clip-path="url(#terminal-line-36)">: Install the Lean 4 agent (leanstral)</text><text class="terminal-r1" x="1464" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">
|
||||
</text><text class="terminal-r4" x="24.4" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">⎢</text><text class="terminal-r1" x="48.8" y="922.8" textLength="24.4" clip-path="url(#terminal-line-37)">• </text><text class="terminal-r7" x="73.2" y="922.8" textLength="146.4" clip-path="url(#terminal-line-37)">/unleanstall</text><text class="terminal-r1" x="219.6" y="922.8" textLength="341.6" clip-path="url(#terminal-line-37)">: Uninstall the Lean 4 agent</text><text class="terminal-r1" x="1464" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">
|
||||
</text><text class="terminal-r4" x="24.4" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">⎢</text><text class="terminal-r1" x="48.8" y="947.2" textLength="24.4" clip-path="url(#terminal-line-38)">• </text><text class="terminal-r7" x="73.2" y="947.2" textLength="85.4" clip-path="url(#terminal-line-38)">/rewind</text><text class="terminal-r1" x="158.6" y="947.2" textLength="366" clip-path="url(#terminal-line-38)">: Rewind to a previous message</text><text class="terminal-r1" x="1464" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">
|
||||
</text><text class="terminal-r4" x="24.4" y="971.6" textLength="12.2" clip-path="url(#terminal-line-39)">⎣</text><text class="terminal-r1" x="48.8" y="971.6" textLength="24.4" clip-path="url(#terminal-line-39)">• </text><text class="terminal-r7" x="73.2" y="971.6" textLength="183" clip-path="url(#terminal-line-39)">/data-retention</text><text class="terminal-r1" x="256.2" y="971.6" textLength="402.6" clip-path="url(#terminal-line-39)">: Show data retention information</text><text class="terminal-r1" x="1464" y="971.6" textLength="12.2" clip-path="url(#terminal-line-39)">
|
||||
</text><text class="terminal-r1" x="1464" y="996" textLength="12.2" clip-path="url(#terminal-line-40)">
|
||||
</text><text class="terminal-r1" x="1464" y="1020.4" textLength="12.2" clip-path="url(#terminal-line-41)">
|
||||
</text><text class="terminal-r5" x="0" y="1044.8" textLength="1464" clip-path="url(#terminal-line-42)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐</text><text class="terminal-r1" x="1464" y="1044.8" textLength="12.2" clip-path="url(#terminal-line-42)">
|
||||
</text><text class="terminal-r5" x="0" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">│</text><text class="terminal-r8" x="24.4" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">></text><text class="terminal-r5" x="1451.8" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">│</text><text class="terminal-r1" x="1464" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">
|
||||
</text><text class="terminal-r5" x="0" y="1093.6" textLength="12.2" clip-path="url(#terminal-line-44)">│</text><text class="terminal-r5" x="1451.8" y="1093.6" textLength="12.2" clip-path="url(#terminal-line-44)">│</text><text class="terminal-r1" x="1464" y="1093.6" textLength="12.2" clip-path="url(#terminal-line-44)">
|
||||
</text><text class="terminal-r5" x="0" y="1118" textLength="12.2" clip-path="url(#terminal-line-45)">│</text><text class="terminal-r5" x="1451.8" y="1118" textLength="12.2" clip-path="url(#terminal-line-45)">│</text><text class="terminal-r1" x="1464" y="1118" textLength="12.2" clip-path="url(#terminal-line-45)">
|
||||
</text><text class="terminal-r5" x="0" y="1142.4" textLength="1464" clip-path="url(#terminal-line-46)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="1142.4" textLength="12.2" clip-path="url(#terminal-line-46)">
|
||||
</text><text class="terminal-r5" x="0" y="1166.8" textLength="158.6" clip-path="url(#terminal-line-47)">/test/workdir</text><text class="terminal-r5" x="1256.6" y="1166.8" textLength="207.4" clip-path="url(#terminal-line-47)">0% of 200k tokens</text>
|
||||
</text><text class="terminal-r4" x="0" y="1044.8" textLength="1464" clip-path="url(#terminal-line-42)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐</text><text class="terminal-r1" x="1464" y="1044.8" textLength="12.2" clip-path="url(#terminal-line-42)">
|
||||
</text><text class="terminal-r4" x="0" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">│</text><text class="terminal-r8" x="24.4" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">></text><text class="terminal-r4" x="1451.8" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">│</text><text class="terminal-r1" x="1464" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">
|
||||
</text><text class="terminal-r4" x="0" y="1093.6" textLength="12.2" clip-path="url(#terminal-line-44)">│</text><text class="terminal-r4" x="1451.8" y="1093.6" textLength="12.2" clip-path="url(#terminal-line-44)">│</text><text class="terminal-r1" x="1464" y="1093.6" textLength="12.2" clip-path="url(#terminal-line-44)">
|
||||
</text><text class="terminal-r4" x="0" y="1118" textLength="12.2" clip-path="url(#terminal-line-45)">│</text><text class="terminal-r4" x="1451.8" y="1118" textLength="12.2" clip-path="url(#terminal-line-45)">│</text><text class="terminal-r1" x="1464" y="1118" textLength="12.2" clip-path="url(#terminal-line-45)">
|
||||
</text><text class="terminal-r4" x="0" y="1142.4" textLength="1464" clip-path="url(#terminal-line-46)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="1142.4" textLength="12.2" clip-path="url(#terminal-line-46)">
|
||||
</text><text class="terminal-r4" x="0" y="1166.8" textLength="158.6" clip-path="url(#terminal-line-47)">/test/workdir</text><text class="terminal-r4" x="1256.6" y="1166.8" textLength="207.4" clip-path="url(#terminal-line-47)">0% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
|
|
@ -33,13 +33,12 @@
|
|||
}
|
||||
|
||||
.terminal-r1 { fill: #c5c8c6 }
|
||||
.terminal-r2 { fill: #68a0b3 }
|
||||
.terminal-r3 { fill: #4b4e55 }
|
||||
.terminal-r4 { fill: #9a9b99 }
|
||||
.terminal-r5 { fill: #608ab1;font-weight: bold }
|
||||
.terminal-r2 { fill: #9a9b99 }
|
||||
.terminal-r3 { fill: #608ab1;font-weight: bold }
|
||||
.terminal-r4 { fill: #98a84b;font-weight: bold }
|
||||
.terminal-r5 { fill: #4b4e55 }
|
||||
.terminal-r6 { fill: #292929 }
|
||||
.terminal-r7 { fill: #98a84b;font-weight: bold }
|
||||
.terminal-r8 { fill: #ff8205;font-weight: bold }
|
||||
.terminal-r7 { fill: #ff8205;font-weight: bold }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
|
|
@ -197,56 +196,56 @@
|
|||
</g>
|
||||
|
||||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
<rect fill="#4b4e55" x="1451.8" y="50.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="123.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="196.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="221.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="245.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="269.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="294.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="318.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="367.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="391.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="416.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="440.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="465.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="489.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="513.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="538.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="562.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="587.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="611.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="635.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="660.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="684.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="709.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="733.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="757.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="782.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="806.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="831.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="855.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="879.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="904.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="928.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="953.1" width="12.2" height="24.65" shape-rendering="crispEdges"/>
|
||||
<rect fill="#4b4e55" x="1451.8" y="74.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="99.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="123.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="147.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="172.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="196.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="221.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="245.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="269.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="294.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="318.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="343.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="367.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="391.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="416.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="440.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="465.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="489.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="513.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="538.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="562.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="587.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="611.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="635.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="660.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="684.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="709.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="733.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="757.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="782.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="806.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="831.1" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="855.5" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="879.9" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="904.3" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="928.7" width="12.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#4b4e55" x="1451.8" y="953.1" width="12.2" height="24.65" shape-rendering="crispEdges"/>
|
||||
<g class="terminal-matrix">
|
||||
<text class="terminal-r1" x="0" y="20" textLength="134.2" clip-path="url(#terminal-line-0)">  ⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="170.8" y="20" textLength="61" clip-path="url(#terminal-line-0)">Type </text><text class="terminal-r2" x="231.8" y="20" textLength="61" clip-path="url(#terminal-line-0)">/help</text><text class="terminal-r1" x="292.8" y="20" textLength="256.2" clip-path="url(#terminal-line-0)"> for more information</text><text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r3" x="1451.8" 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-r4" x="24.4" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">⎢</text><text class="terminal-r5" x="48.8" y="68.8" textLength="219.6" clip-path="url(#terminal-line-2)">Keyboard Shortcuts</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
|
||||
</text><text class="terminal-r4" x="24.4" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">⎢</text><text class="terminal-r1" x="48.8" y="93.2" textLength="24.4" clip-path="url(#terminal-line-3)">• </text><text class="terminal-r7" x="73.2" y="93.2" textLength="61" clip-path="url(#terminal-line-3)">Enter</text><text class="terminal-r1" x="134.2" y="93.2" textLength="183" clip-path="url(#terminal-line-3)"> Submit message</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
</text><text class="terminal-r4" x="24.4" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">⎢</text><text class="terminal-r1" x="48.8" y="117.6" textLength="24.4" clip-path="url(#terminal-line-4)">• </text><text class="terminal-r7" x="73.2" y="117.6" textLength="73.2" clip-path="url(#terminal-line-4)">Ctrl+J</text><text class="terminal-r1" x="146.4" y="117.6" textLength="36.6" clip-path="url(#terminal-line-4)"> / </text><text class="terminal-r7" x="183" y="117.6" textLength="134.2" clip-path="url(#terminal-line-4)">Shift+Enter</text><text class="terminal-r1" x="317.2" y="117.6" textLength="183" clip-path="url(#terminal-line-4)"> Insert newline</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
|
||||
</text><text class="terminal-r4" x="24.4" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">⎢</text><text class="terminal-r1" x="48.8" y="142" textLength="24.4" clip-path="url(#terminal-line-5)">• </text><text class="terminal-r7" x="73.2" y="142" textLength="73.2" clip-path="url(#terminal-line-5)">Escape</text><text class="terminal-r1" x="146.4" y="142" textLength="402.6" clip-path="url(#terminal-line-5)"> Interrupt agent or close dialogs</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
|
||||
</text><text class="terminal-r4" x="24.4" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">⎢</text><text class="terminal-r1" x="48.8" y="166.4" textLength="24.4" clip-path="url(#terminal-line-6)">• </text><text class="terminal-r7" x="73.2" y="166.4" textLength="73.2" clip-path="url(#terminal-line-6)">Ctrl+C</text><text class="terminal-r1" x="146.4" y="166.4" textLength="463.6" clip-path="url(#terminal-line-6)"> Quit (or clear input if text present)</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
|
||||
</text><text class="terminal-r4" x="24.4" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">⎢</text><text class="terminal-r1" x="48.8" y="190.8" textLength="24.4" clip-path="url(#terminal-line-7)">• </text><text class="terminal-r7" x="73.2" y="190.8" textLength="73.2" clip-path="url(#terminal-line-7)">Ctrl+G</text><text class="terminal-r1" x="146.4" y="190.8" textLength="366" clip-path="url(#terminal-line-7)"> Edit input in external editor</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
|
||||
</text><text class="terminal-r4" x="24.4" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">⎢</text><text class="terminal-r1" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-line-8)">• </text><text class="terminal-r7" x="73.2" y="215.2" textLength="73.2" clip-path="url(#terminal-line-8)">Ctrl+O</text><text class="terminal-r1" x="146.4" y="215.2" textLength="292.8" clip-path="url(#terminal-line-8)"> Toggle tool output view</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
|
||||
</text><text class="terminal-r4" x="24.4" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">⎢</text><text class="terminal-r1" x="48.8" y="239.6" textLength="24.4" clip-path="url(#terminal-line-9)">• </text><text class="terminal-r7" x="73.2" y="239.6" textLength="109.8" clip-path="url(#terminal-line-9)">Shift+Tab</text><text class="terminal-r1" x="183" y="239.6" textLength="305" clip-path="url(#terminal-line-9)"> Toggle auto-approve mode</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
|
||||
</text><text class="terminal-r4" x="24.4" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">⎢</text><text class="terminal-r1" x="48.8" y="264" textLength="24.4" clip-path="url(#terminal-line-10)">• </text><text class="terminal-r7" x="73.2" y="264" textLength="73.2" clip-path="url(#terminal-line-10)">Alt+↑↓</text><text class="terminal-r1" x="146.4" y="264" textLength="36.6" clip-path="url(#terminal-line-10)"> / </text><text class="terminal-r7" x="183" y="264" textLength="97.6" clip-path="url(#terminal-line-10)">Ctrl+P/N</text><text class="terminal-r1" x="280.6" y="264" textLength="390.4" clip-path="url(#terminal-line-10)"> Rewind to previous/next message</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
|
||||
</text><text class="terminal-r4" x="24.4" 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-r4" x="24.4" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">⎢</text><text class="terminal-r5" x="48.8" y="312.8" textLength="195.2" clip-path="url(#terminal-line-12)">Special Features</text><text class="terminal-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
||||
</text><text class="terminal-r4" x="24.4" 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-r4" x="24.4" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">⎢</text><text class="terminal-r1" x="48.8" y="361.6" textLength="24.4" clip-path="url(#terminal-line-14)">• </text><text class="terminal-r7" x="73.2" y="361.6" textLength="122" clip-path="url(#terminal-line-14)">!<command></text><text class="terminal-r1" x="195.2" y="361.6" textLength="366" clip-path="url(#terminal-line-14)"> Execute bash command directly</text><text class="terminal-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
|
||||
</text><text class="terminal-r4" x="24.4" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">⎢</text><text class="terminal-r1" x="48.8" y="386" textLength="24.4" clip-path="url(#terminal-line-15)">• </text><text class="terminal-r7" x="73.2" y="386" textLength="170.8" clip-path="url(#terminal-line-15)">@path/to/file/</text><text class="terminal-r1" x="244" y="386" textLength="305" clip-path="url(#terminal-line-15)"> Autocompletes file paths</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-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-r5" x="48.8" y="434.8" textLength="97.6" clip-path="url(#terminal-line-17)">Commands</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-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="24.4" clip-path="url(#terminal-line-19)">• </text><text class="terminal-r7" x="73.2" y="483.6" textLength="61" clip-path="url(#terminal-line-19)">/help</text><text class="terminal-r1" x="134.2" y="483.6" textLength="231.8" clip-path="url(#terminal-line-19)">: Show help message</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-r1" x="48.8" y="508" textLength="24.4" clip-path="url(#terminal-line-20)">• </text><text class="terminal-r7" x="73.2" y="508" textLength="85.4" clip-path="url(#terminal-line-20)">/config</text><text class="terminal-r1" x="158.6" y="508" textLength="268.4" clip-path="url(#terminal-line-20)">: Edit config settings</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="24.4" clip-path="url(#terminal-line-21)">• </text><text class="terminal-r7" x="73.2" y="532.4" textLength="73.2" clip-path="url(#terminal-line-21)">/model</text><text class="terminal-r1" x="146.4" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)">: Select active model</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="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">⎢</text><text class="terminal-r1" x="48.8" y="556.8" textLength="24.4" clip-path="url(#terminal-line-22)">• </text><text class="terminal-r7" x="73.2" y="556.8" textLength="109.8" clip-path="url(#terminal-line-22)">/thinking</text><text class="terminal-r1" x="183" y="556.8" textLength="280.6" clip-path="url(#terminal-line-22)">: Select thinking level</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r4" x="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="24.4" clip-path="url(#terminal-line-23)">• </text><text class="terminal-r7" x="73.2" y="581.2" textLength="85.4" clip-path="url(#terminal-line-23)">/reload</text><text class="terminal-r1" x="158.6" y="581.2" textLength="780.8" clip-path="url(#terminal-line-23)">: Reload configuration, agent instructions, and skills from disk</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r4" x="24.4" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">⎢</text><text class="terminal-r1" x="48.8" y="605.6" textLength="24.4" clip-path="url(#terminal-line-24)">• </text><text class="terminal-r7" x="73.2" y="605.6" textLength="73.2" clip-path="url(#terminal-line-24)">/clear</text><text class="terminal-r1" x="146.4" y="605.6" textLength="341.6" clip-path="url(#terminal-line-24)">: Clear conversation history</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||
</text><text class="terminal-r4" x="24.4" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">⎢</text><text class="terminal-r1" x="48.8" y="630" textLength="24.4" clip-path="url(#terminal-line-25)">• </text><text class="terminal-r7" x="73.2" y="630" textLength="61" clip-path="url(#terminal-line-25)">/copy</text><text class="terminal-r1" x="134.2" y="630" textLength="561.2" clip-path="url(#terminal-line-25)">: Copy the last agent message to the clipboard</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r4" 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="24.4" clip-path="url(#terminal-line-26)">• </text><text class="terminal-r7" x="73.2" y="654.4" textLength="48.8" clip-path="url(#terminal-line-26)">/log</text><text class="terminal-r1" x="122" y="654.4" textLength="524.6" clip-path="url(#terminal-line-26)">: Show path to current interaction log file</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="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="24.4" clip-path="url(#terminal-line-27)">• </text><text class="terminal-r7" x="73.2" y="678.8" textLength="73.2" clip-path="url(#terminal-line-27)">/debug</text><text class="terminal-r1" x="146.4" y="678.8" textLength="268.4" clip-path="url(#terminal-line-27)">: Toggle debug console</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="24.4" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">⎢</text><text class="terminal-r1" x="48.8" y="703.2" textLength="24.4" clip-path="url(#terminal-line-28)">• </text><text class="terminal-r7" x="73.2" y="703.2" textLength="97.6" clip-path="url(#terminal-line-28)">/compact</text><text class="terminal-r1" x="170.8" y="703.2" textLength="1171.2" clip-path="url(#terminal-line-28)">: Compact conversation history by summarizing. Optionally pass instructions to guide the summary</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="24.4" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">⎢</text><text class="terminal-r1" x="48.8" y="727.6" textLength="24.4" clip-path="url(#terminal-line-29)">• </text><text class="terminal-r7" x="73.2" y="727.6" textLength="61" clip-path="url(#terminal-line-29)">/exit</text><text class="terminal-r1" x="134.2" y="727.6" textLength="268.4" clip-path="url(#terminal-line-29)">: Exit the application</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="24.4" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">⎢</text><text class="terminal-r1" x="48.8" y="752" textLength="24.4" clip-path="url(#terminal-line-30)">• </text><text class="terminal-r7" x="73.2" y="752" textLength="85.4" clip-path="url(#terminal-line-30)">/status</text><text class="terminal-r1" x="158.6" y="752" textLength="317.2" clip-path="url(#terminal-line-30)">: Display agent statistics</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r4" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">⎢</text><text class="terminal-r1" x="48.8" y="776.4" textLength="24.4" clip-path="url(#terminal-line-31)">• </text><text class="terminal-r7" x="73.2" y="776.4" textLength="109.8" clip-path="url(#terminal-line-31)">/teleport</text><text class="terminal-r1" x="183" y="776.4" textLength="378.2" clip-path="url(#terminal-line-31)">: Teleport session to Vibe Code</text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r4" x="24.4" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">⎢</text><text class="terminal-r1" x="48.8" y="800.8" textLength="24.4" clip-path="url(#terminal-line-32)">• </text><text class="terminal-r7" x="73.2" y="800.8" textLength="146.4" clip-path="url(#terminal-line-32)">/proxy-setup</text><text class="terminal-r1" x="219.6" y="800.8" textLength="561.2" clip-path="url(#terminal-line-32)">: Configure proxy and SSL certificate settings</text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
</text><text class="terminal-r4" x="24.4" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">⎢</text><text class="terminal-r1" x="48.8" y="825.2" textLength="24.4" clip-path="url(#terminal-line-33)">• </text><text class="terminal-r7" x="73.2" y="825.2" textLength="109.8" clip-path="url(#terminal-line-33)">/continue</text><text class="terminal-r1" x="183" y="825.2" textLength="24.4" clip-path="url(#terminal-line-33)">, </text><text class="terminal-r7" x="207.4" y="825.2" textLength="85.4" clip-path="url(#terminal-line-33)">/resume</text><text class="terminal-r1" x="292.8" y="825.2" textLength="402.6" clip-path="url(#terminal-line-33)">: Browse and resume past sessions</text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
|
||||
</text><text class="terminal-r4" x="24.4" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">⎢</text><text class="terminal-r1" x="48.8" y="849.6" textLength="24.4" clip-path="url(#terminal-line-34)">• </text><text class="terminal-r7" x="73.2" y="849.6" textLength="134.2" clip-path="url(#terminal-line-34)">/connectors</text><text class="terminal-r1" x="207.4" y="849.6" textLength="24.4" clip-path="url(#terminal-line-34)">, </text><text class="terminal-r7" x="231.8" y="849.6" textLength="48.8" clip-path="url(#terminal-line-34)">/mcp</text><text class="terminal-r1" x="280.6" y="849.6" textLength="939.4" clip-path="url(#terminal-line-34)">: Display available MCP servers and connectors. Pass a name to list its tools</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
|
||||
</text><text class="terminal-r4" x="24.4" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">⎢</text><text class="terminal-r1" x="48.8" y="874" textLength="24.4" clip-path="url(#terminal-line-35)">• </text><text class="terminal-r7" x="73.2" y="874" textLength="73.2" clip-path="url(#terminal-line-35)">/voice</text><text class="terminal-r1" x="146.4" y="874" textLength="317.2" clip-path="url(#terminal-line-35)">: Configure voice settings</text><text class="terminal-r1" x="1464" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">
|
||||
</text><text class="terminal-r4" x="24.4" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">⎢</text><text class="terminal-r1" x="48.8" y="898.4" textLength="24.4" clip-path="url(#terminal-line-36)">• </text><text class="terminal-r7" x="73.2" y="898.4" textLength="122" clip-path="url(#terminal-line-36)">/leanstall</text><text class="terminal-r1" x="195.2" y="898.4" textLength="463.6" clip-path="url(#terminal-line-36)">: Install the Lean 4 agent (leanstral)</text><text class="terminal-r1" x="1464" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">
|
||||
</text><text class="terminal-r4" x="24.4" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">⎢</text><text class="terminal-r1" x="48.8" y="922.8" textLength="24.4" clip-path="url(#terminal-line-37)">• </text><text class="terminal-r7" x="73.2" y="922.8" textLength="146.4" clip-path="url(#terminal-line-37)">/unleanstall</text><text class="terminal-r1" x="219.6" y="922.8" textLength="341.6" clip-path="url(#terminal-line-37)">: Uninstall the Lean 4 agent</text><text class="terminal-r1" x="1464" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">
|
||||
</text><text class="terminal-r4" x="24.4" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">⎢</text><text class="terminal-r1" x="48.8" y="947.2" textLength="24.4" clip-path="url(#terminal-line-38)">• </text><text class="terminal-r7" x="73.2" y="947.2" textLength="85.4" clip-path="url(#terminal-line-38)">/rewind</text><text class="terminal-r1" x="158.6" y="947.2" textLength="366" clip-path="url(#terminal-line-38)">: Rewind to a previous message</text><text class="terminal-r1" x="1464" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">
|
||||
</text><text class="terminal-r4" x="24.4" y="971.6" textLength="12.2" clip-path="url(#terminal-line-39)">⎣</text><text class="terminal-r1" x="48.8" y="971.6" textLength="24.4" clip-path="url(#terminal-line-39)">• </text><text class="terminal-r7" x="73.2" y="971.6" textLength="183" clip-path="url(#terminal-line-39)">/data-retention</text><text class="terminal-r1" x="256.2" y="971.6" textLength="402.6" clip-path="url(#terminal-line-39)">: Show data retention information</text><text class="terminal-r1" x="1464" y="971.6" textLength="12.2" clip-path="url(#terminal-line-39)">
|
||||
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
||||
</text><text class="terminal-r2" x="24.4" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">⎢</text><text class="terminal-r3" x="48.8" y="44.4" textLength="219.6" clip-path="url(#terminal-line-1)">Keyboard Shortcuts</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
|
||||
</text><text class="terminal-r2" x="24.4" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">⎢</text><text class="terminal-r1" x="48.8" y="68.8" textLength="24.4" clip-path="url(#terminal-line-2)">• </text><text class="terminal-r4" x="73.2" y="68.8" textLength="61" clip-path="url(#terminal-line-2)">Enter</text><text class="terminal-r1" x="134.2" y="68.8" textLength="183" clip-path="url(#terminal-line-2)"> Submit message</text><text class="terminal-r5" x="1451.8" 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-r2" x="24.4" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">⎢</text><text class="terminal-r1" x="48.8" y="93.2" textLength="24.4" clip-path="url(#terminal-line-3)">• </text><text class="terminal-r4" x="73.2" y="93.2" textLength="73.2" clip-path="url(#terminal-line-3)">Ctrl+J</text><text class="terminal-r1" x="146.4" y="93.2" textLength="36.6" clip-path="url(#terminal-line-3)"> / </text><text class="terminal-r4" x="183" y="93.2" textLength="134.2" clip-path="url(#terminal-line-3)">Shift+Enter</text><text class="terminal-r1" x="317.2" y="93.2" textLength="183" clip-path="url(#terminal-line-3)"> Insert newline</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
</text><text class="terminal-r2" x="24.4" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">⎢</text><text class="terminal-r1" x="48.8" y="117.6" textLength="24.4" clip-path="url(#terminal-line-4)">• </text><text class="terminal-r4" x="73.2" y="117.6" textLength="73.2" clip-path="url(#terminal-line-4)">Escape</text><text class="terminal-r1" x="146.4" y="117.6" textLength="402.6" clip-path="url(#terminal-line-4)"> Interrupt agent or close dialogs</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
|
||||
</text><text class="terminal-r2" x="24.4" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">⎢</text><text class="terminal-r1" x="48.8" y="142" textLength="24.4" clip-path="url(#terminal-line-5)">• </text><text class="terminal-r4" x="73.2" y="142" textLength="73.2" clip-path="url(#terminal-line-5)">Ctrl+C</text><text class="terminal-r1" x="146.4" y="142" textLength="463.6" clip-path="url(#terminal-line-5)"> Quit (or clear input if text present)</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
|
||||
</text><text class="terminal-r2" x="24.4" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">⎢</text><text class="terminal-r1" x="48.8" y="166.4" textLength="24.4" clip-path="url(#terminal-line-6)">• </text><text class="terminal-r4" x="73.2" y="166.4" textLength="73.2" clip-path="url(#terminal-line-6)">Ctrl+G</text><text class="terminal-r1" x="146.4" y="166.4" textLength="366" clip-path="url(#terminal-line-6)"> Edit input in external editor</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
|
||||
</text><text class="terminal-r2" x="24.4" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">⎢</text><text class="terminal-r1" x="48.8" y="190.8" textLength="24.4" clip-path="url(#terminal-line-7)">• </text><text class="terminal-r4" x="73.2" y="190.8" textLength="73.2" clip-path="url(#terminal-line-7)">Ctrl+O</text><text class="terminal-r1" x="146.4" y="190.8" textLength="292.8" clip-path="url(#terminal-line-7)"> Toggle tool output view</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="24.4" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">⎢</text><text class="terminal-r1" x="48.8" y="215.2" textLength="24.4" clip-path="url(#terminal-line-8)">• </text><text class="terminal-r4" x="73.2" y="215.2" textLength="109.8" clip-path="url(#terminal-line-8)">Shift+Tab</text><text class="terminal-r1" x="183" y="215.2" textLength="512.4" clip-path="url(#terminal-line-8)"> Cycle through agents (default, plan, ...)</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
|
||||
</text><text class="terminal-r2" x="24.4" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">⎢</text><text class="terminal-r1" x="48.8" y="239.6" textLength="24.4" clip-path="url(#terminal-line-9)">• </text><text class="terminal-r4" x="73.2" y="239.6" textLength="73.2" clip-path="url(#terminal-line-9)">Alt+↑↓</text><text class="terminal-r1" x="146.4" y="239.6" textLength="36.6" clip-path="url(#terminal-line-9)"> / </text><text class="terminal-r4" x="183" y="239.6" textLength="97.6" clip-path="url(#terminal-line-9)">Ctrl+P/N</text><text class="terminal-r1" x="280.6" y="239.6" textLength="390.4" clip-path="url(#terminal-line-9)"> Rewind to previous/next message</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
|
||||
</text><text class="terminal-r2" x="24.4" 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-r2" x="24.4" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">⎢</text><text class="terminal-r3" x="48.8" y="288.4" textLength="195.2" clip-path="url(#terminal-line-11)">Special Features</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="24.4" 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-r2" x="24.4" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">⎢</text><text class="terminal-r1" x="48.8" y="337.2" textLength="24.4" clip-path="url(#terminal-line-13)">• </text><text class="terminal-r4" x="73.2" y="337.2" textLength="122" clip-path="url(#terminal-line-13)">!<command></text><text class="terminal-r1" x="195.2" y="337.2" textLength="366" clip-path="url(#terminal-line-13)"> Execute bash command directly</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||
</text><text class="terminal-r2" x="24.4" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">⎢</text><text class="terminal-r1" x="48.8" y="361.6" textLength="24.4" clip-path="url(#terminal-line-14)">• </text><text class="terminal-r4" x="73.2" y="361.6" textLength="170.8" clip-path="url(#terminal-line-14)">@path/to/file/</text><text class="terminal-r1" x="244" y="361.6" textLength="305" clip-path="url(#terminal-line-14)"> Autocompletes file paths</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="24.4" 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-r2" x="24.4" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">⎢</text><text class="terminal-r3" x="48.8" y="410.4" textLength="97.6" clip-path="url(#terminal-line-16)">Commands</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="24.4" 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-r2" 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="24.4" clip-path="url(#terminal-line-18)">• </text><text class="terminal-r4" x="73.2" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/help</text><text class="terminal-r1" x="134.2" y="459.2" textLength="231.8" clip-path="url(#terminal-line-18)">: Show help message</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="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="24.4" clip-path="url(#terminal-line-19)">• </text><text class="terminal-r4" x="73.2" y="483.6" textLength="85.4" clip-path="url(#terminal-line-19)">/config</text><text class="terminal-r1" x="158.6" y="483.6" textLength="268.4" clip-path="url(#terminal-line-19)">: Edit config settings</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="24.4" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">⎢</text><text class="terminal-r1" x="48.8" y="508" textLength="24.4" clip-path="url(#terminal-line-20)">• </text><text class="terminal-r4" x="73.2" y="508" textLength="73.2" clip-path="url(#terminal-line-20)">/model</text><text class="terminal-r1" x="146.4" y="508" textLength="256.2" clip-path="url(#terminal-line-20)">: Select active model</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r2" 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="24.4" 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)">/thinking</text><text class="terminal-r1" x="183" y="532.4" textLength="280.6" clip-path="url(#terminal-line-21)">: Select thinking level</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="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="24.4" clip-path="url(#terminal-line-22)">• </text><text class="terminal-r4" x="73.2" y="556.8" textLength="85.4" clip-path="url(#terminal-line-22)">/reload</text><text class="terminal-r1" x="158.6" y="556.8" textLength="780.8" clip-path="url(#terminal-line-22)">: Reload configuration, agent instructions, and skills from disk</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="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="24.4" clip-path="url(#terminal-line-23)">• </text><text class="terminal-r4" x="73.2" y="581.2" textLength="73.2" clip-path="url(#terminal-line-23)">/clear</text><text class="terminal-r1" x="146.4" y="581.2" textLength="341.6" clip-path="url(#terminal-line-23)">: Clear conversation history</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r2" x="24.4" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">⎢</text><text class="terminal-r1" x="48.8" y="605.6" textLength="24.4" clip-path="url(#terminal-line-24)">• </text><text class="terminal-r4" x="73.2" y="605.6" textLength="61" clip-path="url(#terminal-line-24)">/copy</text><text class="terminal-r1" x="134.2" y="605.6" textLength="561.2" clip-path="url(#terminal-line-24)">: Copy the last agent message to the clipboard</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="24.4" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">⎢</text><text class="terminal-r1" x="48.8" y="630" textLength="24.4" clip-path="url(#terminal-line-25)">• </text><text class="terminal-r4" x="73.2" y="630" textLength="48.8" clip-path="url(#terminal-line-25)">/log</text><text class="terminal-r1" x="122" y="630" textLength="524.6" clip-path="url(#terminal-line-25)">: Show path to current interaction log file</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r2" 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="24.4" clip-path="url(#terminal-line-26)">• </text><text class="terminal-r4" x="73.2" y="654.4" textLength="73.2" clip-path="url(#terminal-line-26)">/debug</text><text class="terminal-r1" x="146.4" y="654.4" textLength="268.4" clip-path="url(#terminal-line-26)">: Toggle debug console</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r2" x="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="24.4" clip-path="url(#terminal-line-27)">• </text><text class="terminal-r4" x="73.2" y="678.8" textLength="97.6" clip-path="url(#terminal-line-27)">/compact</text><text class="terminal-r1" x="170.8" y="678.8" textLength="1171.2" clip-path="url(#terminal-line-27)">: Compact conversation history by summarizing. Optionally pass instructions to guide the summary</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r2" x="24.4" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">⎢</text><text class="terminal-r1" x="48.8" y="703.2" textLength="24.4" clip-path="url(#terminal-line-28)">• </text><text class="terminal-r4" x="73.2" y="703.2" textLength="61" clip-path="url(#terminal-line-28)">/exit</text><text class="terminal-r1" x="134.2" y="703.2" textLength="268.4" clip-path="url(#terminal-line-28)">: Exit the application</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||
</text><text class="terminal-r2" x="24.4" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">⎢</text><text class="terminal-r1" x="48.8" y="727.6" textLength="24.4" clip-path="url(#terminal-line-29)">• </text><text class="terminal-r4" x="73.2" y="727.6" textLength="85.4" clip-path="url(#terminal-line-29)">/status</text><text class="terminal-r1" x="158.6" y="727.6" textLength="317.2" clip-path="url(#terminal-line-29)">: Display agent statistics</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r2" x="24.4" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">⎢</text><text class="terminal-r1" x="48.8" y="752" textLength="24.4" clip-path="url(#terminal-line-30)">• </text><text class="terminal-r4" x="73.2" y="752" textLength="109.8" clip-path="url(#terminal-line-30)">/teleport</text><text class="terminal-r1" x="183" y="752" textLength="378.2" clip-path="url(#terminal-line-30)">: Teleport session to Vibe Code</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r2" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">⎢</text><text class="terminal-r1" x="48.8" y="776.4" textLength="24.4" clip-path="url(#terminal-line-31)">• </text><text class="terminal-r4" x="73.2" y="776.4" textLength="146.4" clip-path="url(#terminal-line-31)">/proxy-setup</text><text class="terminal-r1" x="219.6" y="776.4" textLength="561.2" clip-path="url(#terminal-line-31)">: Configure proxy and SSL certificate settings</text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r2" x="24.4" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">⎢</text><text class="terminal-r1" x="48.8" y="800.8" textLength="24.4" clip-path="url(#terminal-line-32)">• </text><text class="terminal-r4" x="73.2" y="800.8" textLength="109.8" clip-path="url(#terminal-line-32)">/continue</text><text class="terminal-r1" x="183" y="800.8" textLength="24.4" clip-path="url(#terminal-line-32)">, </text><text class="terminal-r4" x="207.4" y="800.8" textLength="85.4" clip-path="url(#terminal-line-32)">/resume</text><text class="terminal-r1" x="292.8" y="800.8" textLength="402.6" clip-path="url(#terminal-line-32)">: Browse and resume past sessions</text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
</text><text class="terminal-r2" x="24.4" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">⎢</text><text class="terminal-r1" x="48.8" y="825.2" textLength="24.4" clip-path="url(#terminal-line-33)">• </text><text class="terminal-r4" x="73.2" y="825.2" textLength="85.4" clip-path="url(#terminal-line-33)">/rename</text><text class="terminal-r1" x="158.6" y="825.2" textLength="341.6" clip-path="url(#terminal-line-33)">: Rename the current session</text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
|
||||
</text><text class="terminal-r2" x="24.4" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">⎢</text><text class="terminal-r1" x="48.8" y="849.6" textLength="24.4" clip-path="url(#terminal-line-34)">• </text><text class="terminal-r4" x="73.2" y="849.6" textLength="134.2" clip-path="url(#terminal-line-34)">/connectors</text><text class="terminal-r1" x="207.4" y="849.6" textLength="24.4" clip-path="url(#terminal-line-34)">, </text><text class="terminal-r4" x="231.8" y="849.6" textLength="48.8" clip-path="url(#terminal-line-34)">/mcp</text><text class="terminal-r1" x="280.6" y="849.6" textLength="939.4" clip-path="url(#terminal-line-34)">: Display available MCP servers and connectors. Pass a name to list its tools</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
|
||||
</text><text class="terminal-r2" x="24.4" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">⎢</text><text class="terminal-r1" x="48.8" y="874" textLength="24.4" clip-path="url(#terminal-line-35)">• </text><text class="terminal-r4" x="73.2" y="874" textLength="73.2" clip-path="url(#terminal-line-35)">/voice</text><text class="terminal-r1" x="146.4" y="874" textLength="317.2" clip-path="url(#terminal-line-35)">: Configure voice settings</text><text class="terminal-r1" x="1464" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">
|
||||
</text><text class="terminal-r2" x="24.4" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">⎢</text><text class="terminal-r1" x="48.8" y="898.4" textLength="24.4" clip-path="url(#terminal-line-36)">• </text><text class="terminal-r4" x="73.2" y="898.4" textLength="122" clip-path="url(#terminal-line-36)">/leanstall</text><text class="terminal-r1" x="195.2" y="898.4" textLength="463.6" clip-path="url(#terminal-line-36)">: Install the Lean 4 agent (leanstral)</text><text class="terminal-r1" x="1464" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">
|
||||
</text><text class="terminal-r2" x="24.4" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">⎢</text><text class="terminal-r1" x="48.8" y="922.8" textLength="24.4" clip-path="url(#terminal-line-37)">• </text><text class="terminal-r4" x="73.2" y="922.8" textLength="146.4" clip-path="url(#terminal-line-37)">/unleanstall</text><text class="terminal-r1" x="219.6" y="922.8" textLength="341.6" clip-path="url(#terminal-line-37)">: Uninstall the Lean 4 agent</text><text class="terminal-r1" x="1464" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">
|
||||
</text><text class="terminal-r2" x="24.4" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">⎢</text><text class="terminal-r1" x="48.8" y="947.2" textLength="24.4" clip-path="url(#terminal-line-38)">• </text><text class="terminal-r4" x="73.2" y="947.2" textLength="85.4" clip-path="url(#terminal-line-38)">/rewind</text><text class="terminal-r1" x="158.6" y="947.2" textLength="366" clip-path="url(#terminal-line-38)">: Rewind to a previous message</text><text class="terminal-r1" x="1464" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">
|
||||
</text><text class="terminal-r2" x="24.4" y="971.6" textLength="12.2" clip-path="url(#terminal-line-39)">⎣</text><text class="terminal-r1" x="48.8" y="971.6" textLength="24.4" clip-path="url(#terminal-line-39)">• </text><text class="terminal-r4" x="73.2" y="971.6" textLength="183" clip-path="url(#terminal-line-39)">/data-retention</text><text class="terminal-r1" x="256.2" y="971.6" textLength="402.6" clip-path="url(#terminal-line-39)">: Show data retention information</text><text class="terminal-r1" x="1464" y="971.6" textLength="12.2" clip-path="url(#terminal-line-39)">
|
||||
</text><text class="terminal-r1" x="1464" y="996" textLength="12.2" clip-path="url(#terminal-line-40)">
|
||||
</text><text class="terminal-r1" x="1464" y="1020.4" textLength="12.2" clip-path="url(#terminal-line-41)">
|
||||
</text><text class="terminal-r4" x="0" y="1044.8" textLength="1464" clip-path="url(#terminal-line-42)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐</text><text class="terminal-r1" x="1464" y="1044.8" textLength="12.2" clip-path="url(#terminal-line-42)">
|
||||
</text><text class="terminal-r4" x="0" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">│</text><text class="terminal-r8" x="24.4" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">></text><text class="terminal-r4" x="1451.8" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">│</text><text class="terminal-r1" x="1464" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">
|
||||
</text><text class="terminal-r4" x="0" y="1093.6" textLength="12.2" clip-path="url(#terminal-line-44)">│</text><text class="terminal-r4" x="1451.8" y="1093.6" textLength="12.2" clip-path="url(#terminal-line-44)">│</text><text class="terminal-r1" x="1464" y="1093.6" textLength="12.2" clip-path="url(#terminal-line-44)">
|
||||
</text><text class="terminal-r4" x="0" y="1118" textLength="12.2" clip-path="url(#terminal-line-45)">│</text><text class="terminal-r4" x="1451.8" y="1118" textLength="12.2" clip-path="url(#terminal-line-45)">│</text><text class="terminal-r1" x="1464" y="1118" textLength="12.2" clip-path="url(#terminal-line-45)">
|
||||
</text><text class="terminal-r4" x="0" y="1142.4" textLength="1464" clip-path="url(#terminal-line-46)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="1142.4" textLength="12.2" clip-path="url(#terminal-line-46)">
|
||||
</text><text class="terminal-r4" x="0" y="1166.8" textLength="158.6" clip-path="url(#terminal-line-47)">/test/workdir</text><text class="terminal-r4" x="1256.6" y="1166.8" textLength="207.4" clip-path="url(#terminal-line-47)">0% of 200k tokens</text>
|
||||
</text><text class="terminal-r2" x="0" y="1044.8" textLength="1464" clip-path="url(#terminal-line-42)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐</text><text class="terminal-r1" x="1464" y="1044.8" textLength="12.2" clip-path="url(#terminal-line-42)">
|
||||
</text><text class="terminal-r2" x="0" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">│</text><text class="terminal-r7" x="24.4" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">></text><text class="terminal-r2" x="1451.8" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">│</text><text class="terminal-r1" x="1464" y="1069.2" textLength="12.2" clip-path="url(#terminal-line-43)">
|
||||
</text><text class="terminal-r2" x="0" y="1093.6" textLength="12.2" clip-path="url(#terminal-line-44)">│</text><text class="terminal-r2" x="1451.8" y="1093.6" textLength="12.2" clip-path="url(#terminal-line-44)">│</text><text class="terminal-r1" x="1464" y="1093.6" textLength="12.2" clip-path="url(#terminal-line-44)">
|
||||
</text><text class="terminal-r2" x="0" y="1118" textLength="12.2" clip-path="url(#terminal-line-45)">│</text><text class="terminal-r2" x="1451.8" y="1118" textLength="12.2" clip-path="url(#terminal-line-45)">│</text><text class="terminal-r1" x="1464" y="1118" textLength="12.2" clip-path="url(#terminal-line-45)">
|
||||
</text><text class="terminal-r2" x="0" y="1142.4" textLength="1464" clip-path="url(#terminal-line-46)">└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1464" y="1142.4" textLength="12.2" clip-path="url(#terminal-line-46)">
|
||||
</text><text class="terminal-r2" x="0" y="1166.8" textLength="158.6" clip-path="url(#terminal-line-47)">/test/workdir</text><text class="terminal-r2" x="1256.6" y="1166.8" textLength="207.4" clip-path="url(#terminal-line-47)">0% of 200k tokens</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
|
|
@ -536,6 +536,51 @@ async def test_context_too_long_non_streaming(observer_capture) -> None:
|
|||
assert agent.session_logger.save_interaction.await_count == 1
|
||||
|
||||
|
||||
class _NonRetryableError(Exception):
|
||||
# Mimics Temporal's ``ApplicationError(non_retryable=True)`` without
|
||||
# pulling temporalio into vibe's test deps. The wrap-site check relies
|
||||
# on the truthy ``non_retryable`` attribute, not the concrete type.
|
||||
non_retryable = True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_retryable_passes_through_streaming(observer_capture) -> None:
|
||||
observed, observer = observer_capture
|
||||
backend = FakeBackend(exception_to_raise=_NonRetryableError("auth failed"))
|
||||
agent = build_test_agent_loop(
|
||||
config=make_config(),
|
||||
backend=backend,
|
||||
message_observer=observer,
|
||||
enable_streaming=True,
|
||||
)
|
||||
agent.session_logger.save_interaction = AsyncMock(return_value=None)
|
||||
|
||||
with pytest.raises(_NonRetryableError, match="auth failed"):
|
||||
[_ async for _ in agent.act("Trigger non-retryable failure while streaming")]
|
||||
|
||||
assert [role for role, _ in observed] == [Role.system, Role.user]
|
||||
assert agent.session_logger.save_interaction.await_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_retryable_passes_through_non_streaming(observer_capture) -> None:
|
||||
observed, observer = observer_capture
|
||||
backend = FakeBackend(exception_to_raise=_NonRetryableError("auth failed"))
|
||||
agent = build_test_agent_loop(
|
||||
config=make_config(),
|
||||
backend=backend,
|
||||
message_observer=observer,
|
||||
enable_streaming=False,
|
||||
)
|
||||
agent.session_logger.save_interaction = AsyncMock(return_value=None)
|
||||
|
||||
with pytest.raises(_NonRetryableError, match="auth failed"):
|
||||
[_ async for _ in agent.act("Trigger non-retryable failure without streaming")]
|
||||
|
||||
assert [role for role, _ in observed] == [Role.system, Role.user]
|
||||
assert agent.session_logger.save_interaction.await_count == 1
|
||||
|
||||
|
||||
def _snapshot_events(events: list) -> list[tuple[str, str]]:
|
||||
return [
|
||||
(type(e).__name__, e.content)
|
||||
|
|
|
|||
|
|
@ -627,6 +627,7 @@ class TestClearHistoryFullReset:
|
|||
|
||||
assert agent.session_id != original_session_id
|
||||
assert agent.session_id == agent.session_logger.session_id
|
||||
assert agent.parent_session_id is None
|
||||
|
||||
|
||||
class TestClearHistoryObserverBugfix:
|
||||
|
|
|
|||
120
tests/test_approve_always_permanent.py
Normal file
120
tests/test_approve_always_permanent.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import tomllib
|
||||
|
||||
from tests.conftest import build_test_agent_loop, build_test_vibe_config
|
||||
from vibe.core.tools.permissions import PermissionScope, RequiredPermission
|
||||
|
||||
|
||||
def _read_persisted_config(config_dir: Path) -> dict:
|
||||
config_file = config_dir / "config.toml"
|
||||
with config_file.open("rb") as f:
|
||||
return tomllib.load(f)
|
||||
|
||||
|
||||
class TestApproveAlwaysPermanentNoGranularPermissions:
|
||||
def test_sets_tool_permission_always_in_config(self, config_dir: Path):
|
||||
agent = build_test_agent_loop()
|
||||
|
||||
agent.approve_always("bash", None, save_permanently=True)
|
||||
|
||||
persisted = _read_persisted_config(config_dir)
|
||||
assert persisted["tools"]["bash"]["permission"] == "always"
|
||||
|
||||
def test_session_only_does_not_persist(self, config_dir: Path):
|
||||
agent = build_test_agent_loop()
|
||||
|
||||
agent.approve_always("bash", None, save_permanently=False)
|
||||
|
||||
assert agent.config.tools["bash"]["permission"] == "always"
|
||||
persisted = _read_persisted_config(config_dir)
|
||||
assert "bash" not in persisted.get("tools", {})
|
||||
|
||||
|
||||
class TestApproveAlwaysPermanentWithGranularPermissions:
|
||||
def _make_permissions(self) -> list[RequiredPermission]:
|
||||
return [
|
||||
RequiredPermission(
|
||||
scope=PermissionScope.COMMAND_PATTERN,
|
||||
invocation_pattern="npm install foo",
|
||||
session_pattern="npm install *",
|
||||
label="npm install *",
|
||||
)
|
||||
]
|
||||
|
||||
def test_persists_allowlist_to_config(self, config_dir: Path):
|
||||
agent = build_test_agent_loop()
|
||||
perms = self._make_permissions()
|
||||
|
||||
agent.approve_always("bash", perms, save_permanently=True)
|
||||
|
||||
persisted = _read_persisted_config(config_dir)
|
||||
assert persisted["tools"]["bash"]["allowlist"] == ["npm install *"]
|
||||
|
||||
def test_also_adds_session_rules(self, config_dir: Path):
|
||||
agent = build_test_agent_loop()
|
||||
perms = self._make_permissions()
|
||||
|
||||
agent.approve_always("bash", perms, save_permanently=True)
|
||||
|
||||
assert len(agent._session_rules) == 1
|
||||
rule = agent._session_rules[0]
|
||||
assert rule.tool_name == "bash"
|
||||
assert rule.scope == PermissionScope.COMMAND_PATTERN
|
||||
assert rule.session_pattern == "npm install *"
|
||||
|
||||
def test_session_only_does_not_persist_allowlist(self, config_dir: Path):
|
||||
agent = build_test_agent_loop()
|
||||
perms = self._make_permissions()
|
||||
|
||||
agent.approve_always("bash", perms, save_permanently=False)
|
||||
|
||||
assert len(agent._session_rules) == 1
|
||||
persisted = _read_persisted_config(config_dir)
|
||||
assert "bash" not in persisted.get("tools", {})
|
||||
|
||||
def test_does_not_duplicate_existing_allowlist_entries(self, config_dir: Path):
|
||||
config = build_test_vibe_config(
|
||||
tools={"bash": {"allowlist": ["npm install *"]}}
|
||||
)
|
||||
agent = build_test_agent_loop(config=config)
|
||||
perms = self._make_permissions()
|
||||
|
||||
agent.approve_always("bash", perms, save_permanently=True)
|
||||
|
||||
persisted = _read_persisted_config(config_dir)
|
||||
# Pattern already existed -- nothing new should be written
|
||||
assert persisted.get("tools", {}).get("bash", {}).get("allowlist") is None
|
||||
|
||||
def test_appends_new_patterns_to_existing_allowlist(self, config_dir: Path):
|
||||
config = build_test_vibe_config(tools={"bash": {"allowlist": ["git *"]}})
|
||||
agent = build_test_agent_loop(config=config)
|
||||
perms = self._make_permissions()
|
||||
|
||||
agent.approve_always("bash", perms, save_permanently=True)
|
||||
|
||||
persisted = _read_persisted_config(config_dir)
|
||||
assert persisted["tools"]["bash"]["allowlist"] == ["git *", "npm install *"]
|
||||
|
||||
def test_multiple_permissions_persisted(self, config_dir: Path):
|
||||
agent = build_test_agent_loop()
|
||||
perms = [
|
||||
RequiredPermission(
|
||||
scope=PermissionScope.COMMAND_PATTERN,
|
||||
invocation_pattern="npm install foo",
|
||||
session_pattern="npm install *",
|
||||
label="npm install *",
|
||||
),
|
||||
RequiredPermission(
|
||||
scope=PermissionScope.OUTSIDE_DIRECTORY,
|
||||
invocation_pattern="/tmp/newdir",
|
||||
session_pattern="/tmp/*",
|
||||
label="/tmp/*",
|
||||
),
|
||||
]
|
||||
|
||||
agent.approve_always("bash", perms, save_permanently=True)
|
||||
|
||||
persisted = _read_persisted_config(config_dir)
|
||||
assert persisted["tools"]["bash"]["allowlist"] == ["/tmp/*", "npm install *"]
|
||||
|
|
@ -80,9 +80,7 @@ def test_run_programmatic_preload_streaming_is_batched(
|
|||
new_session = [
|
||||
e for e in telemetry_events if e.get("event_name") == "vibe.new_session"
|
||||
]
|
||||
assert len(new_session) == 1
|
||||
assert new_session[0]["properties"]["entrypoint"] == "programmatic"
|
||||
assert "version" in new_session[0]["properties"]
|
||||
assert len(new_session) == 0
|
||||
|
||||
assert (
|
||||
spy.emitted[0][1] == "You are Vibe, a super useful programming assistant."
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
from tests.stubs.fake_connector_registry import FakeConnectorRegistry
|
||||
from tests.stubs.fake_mcp_registry import FakeMCPRegistry
|
||||
|
|
@ -13,6 +14,7 @@ from vibe.core.config import ConnectorConfig, VibeConfig
|
|||
from vibe.core.tools.base import BaseToolConfig, ToolError
|
||||
from vibe.core.tools.connectors import CONNECTORS_ENV_VAR
|
||||
from vibe.core.tools.connectors.connector_registry import (
|
||||
ConnectorRegistry,
|
||||
RemoteTool,
|
||||
_connector_error_message,
|
||||
_normalize_name,
|
||||
|
|
@ -497,3 +499,206 @@ class TestConnectorDisableFiltering:
|
|||
)
|
||||
assert "connector_wiki_search" in tm.available_tools
|
||||
assert "connector_mail_send" not in tm.available_tools
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bootstrap-based discovery (ConnectorRegistry._discover_all via httpx)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_BOOTSTRAP_URL = "https://api.mistral.ai/v1/connectors/bootstrap"
|
||||
|
||||
|
||||
def _make_bootstrap_response(
|
||||
connectors: list[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return {"connectors": connectors or [], "errors": None}
|
||||
|
||||
|
||||
def _make_connector_payload(
|
||||
*,
|
||||
connector_id: str = "conn-1",
|
||||
name: str = "wiki",
|
||||
is_ready: bool = True,
|
||||
tools: list[dict[str, Any]] | None = None,
|
||||
bootstrap_errors: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"id": connector_id,
|
||||
"name": name,
|
||||
"display_name": name,
|
||||
"description": name,
|
||||
"status": {"is_ready": is_ready},
|
||||
"tools": tools or [],
|
||||
"bootstrap_errors": bootstrap_errors,
|
||||
}
|
||||
|
||||
|
||||
def _make_tool_payload(
|
||||
name: str = "search", description: str = "Search docs"
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"inputSchema": {"type": "object", "properties": {"query": {"type": "string"}}},
|
||||
}
|
||||
|
||||
|
||||
class TestBootstrapDiscovery:
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_discovers_tools_from_bootstrap(self) -> None:
|
||||
payload = _make_bootstrap_response([
|
||||
_make_connector_payload(
|
||||
name="wiki",
|
||||
tools=[_make_tool_payload("search"), _make_tool_payload("read")],
|
||||
)
|
||||
])
|
||||
respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload))
|
||||
|
||||
registry = ConnectorRegistry(api_key="test-key")
|
||||
tools = await registry.get_tools_async()
|
||||
|
||||
assert "connector_wiki_search" in tools
|
||||
assert "connector_wiki_read" in tools
|
||||
assert registry.connector_count == 1
|
||||
assert registry.is_connected("wiki")
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_skips_not_ready_connectors(self) -> None:
|
||||
payload = _make_bootstrap_response([
|
||||
_make_connector_payload(
|
||||
name="broken", is_ready=False, bootstrap_errors=["auth failed"]
|
||||
),
|
||||
_make_connector_payload(name="healthy", tools=[_make_tool_payload("ping")]),
|
||||
])
|
||||
respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload))
|
||||
|
||||
registry = ConnectorRegistry(api_key="test-key")
|
||||
tools = await registry.get_tools_async()
|
||||
|
||||
assert "connector_healthy_ping" in tools
|
||||
assert not any("broken" in name for name in tools)
|
||||
assert not registry.is_connected("broken")
|
||||
assert registry.is_connected("healthy")
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_handles_bootstrap_http_error(self) -> None:
|
||||
respx.get(_BOOTSTRAP_URL).mock(
|
||||
return_value=httpx.Response(500, text="Internal Server Error")
|
||||
)
|
||||
|
||||
registry = ConnectorRegistry(api_key="test-key")
|
||||
tools = await registry.get_tools_async()
|
||||
|
||||
assert tools == {}
|
||||
assert registry.connector_count == 0
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_deduplicates_connector_aliases(self) -> None:
|
||||
payload = _make_bootstrap_response([
|
||||
_make_connector_payload(
|
||||
connector_id="c-1", name="mcp", tools=[_make_tool_payload("a")]
|
||||
),
|
||||
_make_connector_payload(
|
||||
connector_id="c-2", name="mcp", tools=[_make_tool_payload("b")]
|
||||
),
|
||||
])
|
||||
respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload))
|
||||
|
||||
registry = ConnectorRegistry(api_key="test-key")
|
||||
tools = await registry.get_tools_async()
|
||||
|
||||
assert "connector_mcp_a" in tools
|
||||
assert "connector_mcp_2_b" in tools
|
||||
assert registry.connector_count == 2
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_skips_connector_without_id(self) -> None:
|
||||
payload = _make_bootstrap_response([
|
||||
{"name": "broken", "status": {"is_ready": True}, "tools": []},
|
||||
_make_connector_payload(name="valid", tools=[_make_tool_payload("ping")]),
|
||||
])
|
||||
respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload))
|
||||
|
||||
registry = ConnectorRegistry(api_key="test-key")
|
||||
tools = await registry.get_tools_async()
|
||||
|
||||
assert "connector_valid_ping" in tools
|
||||
assert registry.connector_count == 1
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_connectors_list(self) -> None:
|
||||
payload = _make_bootstrap_response([])
|
||||
respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload))
|
||||
|
||||
registry = ConnectorRegistry(api_key="test-key")
|
||||
tools = await registry.get_tools_async()
|
||||
|
||||
assert tools == {}
|
||||
assert registry.connector_count == 0
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_uses_custom_server_url(self) -> None:
|
||||
custom_url = "https://custom.api.example.com/v1/connectors/bootstrap"
|
||||
payload = _make_bootstrap_response([
|
||||
_make_connector_payload(tools=[_make_tool_payload("ping")])
|
||||
])
|
||||
respx.get(custom_url).mock(return_value=httpx.Response(200, json=payload))
|
||||
|
||||
registry = ConnectorRegistry(
|
||||
api_key="test-key", server_url="https://custom.api.example.com"
|
||||
)
|
||||
tools = await registry.get_tools_async()
|
||||
|
||||
assert "connector_wiki_ping" in tools
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_caches_after_first_call(self) -> None:
|
||||
payload = _make_bootstrap_response([
|
||||
_make_connector_payload(tools=[_make_tool_payload("search")])
|
||||
])
|
||||
route = respx.get(_BOOTSTRAP_URL).mock(
|
||||
return_value=httpx.Response(200, json=payload)
|
||||
)
|
||||
|
||||
registry = ConnectorRegistry(api_key="test-key")
|
||||
await registry.get_tools_async()
|
||||
await registry.get_tools_async()
|
||||
|
||||
assert route.call_count == 1
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_connector_updates_cache(self) -> None:
|
||||
payload = _make_bootstrap_response([
|
||||
_make_connector_payload(
|
||||
connector_id="c-1", name="wiki", tools=[_make_tool_payload("search")]
|
||||
)
|
||||
])
|
||||
respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload))
|
||||
|
||||
registry = ConnectorRegistry(api_key="test-key")
|
||||
await registry.get_tools_async()
|
||||
|
||||
# Refresh returns updated tools from a second bootstrap call
|
||||
refresh_payload = _make_bootstrap_response([
|
||||
_make_connector_payload(
|
||||
connector_id="c-1",
|
||||
name="wiki",
|
||||
tools=[_make_tool_payload("search"), _make_tool_payload("write")],
|
||||
)
|
||||
])
|
||||
respx.get(_BOOTSTRAP_URL).mock(
|
||||
return_value=httpx.Response(200, json=refresh_payload)
|
||||
)
|
||||
|
||||
refreshed = await registry.refresh_connector_async("wiki")
|
||||
assert "connector_wiki_search" in refreshed
|
||||
assert "connector_wiki_write" in refreshed
|
||||
|
|
|
|||
|
|
@ -16,6 +16,18 @@ from vibe.core.types import Role
|
|||
|
||||
async def _wait_for_bash_output_message(
|
||||
vibe_app: VibeApp, pilot, timeout: float = 1.0
|
||||
) -> BashOutputMessage:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if message := next(iter(vibe_app.query(BashOutputMessage)), None):
|
||||
if not message._pending:
|
||||
return message
|
||||
await pilot.pause(0.05)
|
||||
raise TimeoutError(f"BashOutputMessage did not appear within {timeout}s")
|
||||
|
||||
|
||||
async def _wait_for_pending_bash_message(
|
||||
vibe_app: VibeApp, pilot, timeout: float = 1.0
|
||||
) -> BashOutputMessage:
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
|
|
@ -157,3 +169,71 @@ async def test_ui_sends_manual_command_output_to_next_agent_turn() -> None:
|
|||
assert user_messages[-2].content == injected_message.content
|
||||
assert user_messages[-2].injected is True
|
||||
assert user_messages[-1].content == "what did the command print?"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_shows_command_immediately_in_pending_state(vibe_app: VibeApp) -> None:
|
||||
"""The command line should appear before the process finishes."""
|
||||
async with vibe_app.run_test() as pilot:
|
||||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||
chat_input.value = "!sleep 10"
|
||||
|
||||
await pilot.press("enter")
|
||||
message = await _wait_for_pending_bash_message(vibe_app, pilot)
|
||||
assert message._pending is True
|
||||
# command line is rendered
|
||||
cmd_widget = message.query_one(".bash-command", Static)
|
||||
assert str(cmd_widget.render()) == "sleep 10"
|
||||
# no output container yet
|
||||
assert not list(message.query(".bash-output"))
|
||||
|
||||
# clean up: cancel the background task
|
||||
if vibe_app._bash_task and not vibe_app._bash_task.done():
|
||||
vibe_app._bash_task.cancel()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_streams_output_incrementally(vibe_app: VibeApp) -> None:
|
||||
"""Output should appear as the command produces it, not all at once."""
|
||||
async with vibe_app.run_test() as pilot:
|
||||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||
# print lines with a small delay so streaming has a chance to show partial output
|
||||
chat_input.value = "!bash -lc 'echo first; echo second'"
|
||||
|
||||
await pilot.press("enter")
|
||||
message = await _wait_for_bash_output_message(vibe_app, pilot)
|
||||
output_widget = message.query_one(".bash-output", Static)
|
||||
rendered = str(output_widget.render())
|
||||
assert "first" in rendered
|
||||
assert "second" in rendered
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_cancels_running_command_on_new_submit(vibe_app: VibeApp) -> None:
|
||||
"""Submitting new input while a bang command is running should cancel it."""
|
||||
async with vibe_app.run_test() as pilot:
|
||||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||
chat_input.value = "!sleep 30"
|
||||
|
||||
await pilot.press("enter")
|
||||
await _wait_for_pending_bash_message(vibe_app, pilot)
|
||||
assert vibe_app._bash_task is not None
|
||||
assert not vibe_app._bash_task.done()
|
||||
|
||||
# submit a new command which should cancel the first one
|
||||
chat_input.value = "!echo done"
|
||||
await pilot.press("enter")
|
||||
|
||||
# wait until we have two messages and the second is finished
|
||||
deadline = time.monotonic() + 2.0
|
||||
while time.monotonic() < deadline:
|
||||
all_msgs = list(vibe_app.query(BashOutputMessage))
|
||||
if len(all_msgs) == 2 and not all_msgs[1]._pending:
|
||||
break
|
||||
await pilot.pause(0.05)
|
||||
|
||||
all_msgs = list(vibe_app.query(BashOutputMessage))
|
||||
assert len(all_msgs) == 2
|
||||
second = all_msgs[1]
|
||||
output_widget = second.query_one(".bash-output", Static)
|
||||
assert str(output_widget.render()) == "done"
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ from __future__ import annotations
|
|||
from pathlib import Path
|
||||
|
||||
VIBE_ROOT = Path(__file__).parent
|
||||
__version__ = "2.9.3"
|
||||
__version__ = "2.9.4"
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ from acp.schema import (
|
|||
SessionConfigOptionSelect,
|
||||
SessionForkCapabilities,
|
||||
SessionInfo,
|
||||
SessionInfoUpdate,
|
||||
SessionListCapabilities,
|
||||
SetSessionConfigOptionResponse,
|
||||
SseMcpServer,
|
||||
|
|
@ -122,6 +123,10 @@ from vibe.core.proxy_setup import (
|
|||
set_proxy_var,
|
||||
unset_proxy_var,
|
||||
)
|
||||
from vibe.core.session.saved_sessions import (
|
||||
update_saved_session_title,
|
||||
update_saved_session_title_at_path,
|
||||
)
|
||||
from vibe.core.session.session_loader import SessionLoader
|
||||
from vibe.core.skills.manager import SkillManager
|
||||
from vibe.core.telemetry.build_metadata import build_entrypoint_metadata
|
||||
|
|
@ -159,6 +164,15 @@ class ForkSessionParams(BaseModel):
|
|||
message_id: str | None = Field(default=None, alias="messageId")
|
||||
|
||||
|
||||
class SessionSetTitleRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
session_id: str = Field(
|
||||
validation_alias=AliasChoices("session_id", "sessionId"), min_length=1
|
||||
)
|
||||
title: str = Field(min_length=1)
|
||||
|
||||
|
||||
class TelemetrySendNotification(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
|
|
@ -167,7 +181,20 @@ class TelemetrySendNotification(BaseModel):
|
|||
session_id: str = Field(validation_alias=AliasChoices("session_id", "sessionId"))
|
||||
|
||||
|
||||
_EVENT_DISPATCHERS: dict[str, Callable[[TelemetryClient, dict[str, Any]], None]] = {}
|
||||
def _dispatch_at_mention_inserted(
|
||||
client: TelemetryClient, properties: dict[str, Any]
|
||||
) -> None:
|
||||
client.send_at_mention_inserted(
|
||||
nb_mentions=properties.get("nb_mentions", 0),
|
||||
context_types=properties.get("context_types", {}),
|
||||
file_extensions=properties.get("file_extensions"),
|
||||
message_id=properties.get("message_id"),
|
||||
)
|
||||
|
||||
|
||||
_EVENT_DISPATCHERS: dict[str, Callable[[TelemetryClient, dict[str, Any]], None]] = {
|
||||
"vibe.at_mention_inserted": _dispatch_at_mention_inserted
|
||||
}
|
||||
|
||||
|
||||
def _resolved_user_message_id(client_message_id: str | None) -> str:
|
||||
|
|
@ -301,9 +328,20 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
agent_loop.set_approval_callback(self._create_approval_callback(session.id))
|
||||
|
||||
session.spawn(self._send_available_commands(session))
|
||||
session.spawn(self._warm_up_agent_loop(agent_loop))
|
||||
|
||||
return session
|
||||
|
||||
async def _warm_up_agent_loop(self, agent_loop: AgentLoop) -> None:
|
||||
"""Proactively await deferred init so `vibe.ready` telemetry is emitted
|
||||
without waiting for the user's first prompt. Errors are swallowed here
|
||||
and will resurface on the first `act()` call via `requires_init`.
|
||||
"""
|
||||
try:
|
||||
await agent_loop.wait_until_ready()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _create_agent_loop(
|
||||
self, config: VibeConfig, agent_name: str, hook_config_result: Any = None
|
||||
) -> AgentLoop:
|
||||
|
|
@ -398,6 +436,11 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
case ToolOption.ALLOW_ALWAYS:
|
||||
session.agent_loop.approve_always(tool_name, required_permissions)
|
||||
return (ApprovalResponse.YES, None)
|
||||
case ToolOption.ALLOW_ALWAYS_PERMANENT:
|
||||
session.agent_loop.approve_always(
|
||||
tool_name, required_permissions, save_permanently=True
|
||||
)
|
||||
return (ApprovalResponse.YES, None)
|
||||
case ToolOption.REJECT_ONCE:
|
||||
session.agent_loop.telemetry_client.send_user_cancelled_action(
|
||||
"reject_approval"
|
||||
|
|
@ -454,6 +497,29 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
raise SessionNotFoundError(session_id)
|
||||
return self.sessions[session_id]
|
||||
|
||||
def _find_acp_session_by_vibe_session_id(
|
||||
self, session_id: str
|
||||
) -> AcpSessionLoop | None:
|
||||
for candidate in self.sessions.values():
|
||||
if candidate.agent_loop.session_id == session_id:
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
def _load_session_logging_config(self) -> SessionLoggingConfig:
|
||||
try:
|
||||
return VibeConfig.load().session_logging
|
||||
except MissingAPIKeyError:
|
||||
try:
|
||||
persisted_config = VibeConfig.get_persisted_config()
|
||||
return SessionLoggingConfig.model_validate(
|
||||
persisted_config.get("session_logging", {})
|
||||
)
|
||||
except Exception as e:
|
||||
raise ConfigurationError(str(e)) from e
|
||||
except Exception as e:
|
||||
raise ConfigurationError(str(e)) from e
|
||||
|
||||
def _build_usage(self, session: AcpSessionLoop) -> Usage:
|
||||
stats = session.agent_loop.stats
|
||||
return Usage(
|
||||
|
|
@ -1008,9 +1074,97 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
) -> ResumeSessionResponse:
|
||||
raise NotImplementedMethodError("resume_session")
|
||||
|
||||
async def _emit_session_info_update(
|
||||
self, session_id: str, *, title: str, updated_at: str | None
|
||||
) -> None:
|
||||
update_kwargs: dict[str, Any] = {
|
||||
"session_update": "session_info_update",
|
||||
"title": title,
|
||||
}
|
||||
if updated_at is not None:
|
||||
update_kwargs["updated_at"] = updated_at
|
||||
|
||||
await self.client.session_update(
|
||||
session_id=session_id, update=SessionInfoUpdate(**update_kwargs)
|
||||
)
|
||||
|
||||
async def _persist_live_session_title(
|
||||
self, session: AcpSessionLoop, title: str
|
||||
) -> dict[str, Any] | None:
|
||||
logger = session.agent_loop.session_logger
|
||||
if not logger.enabled or logger.session_dir is None:
|
||||
return None
|
||||
if not logger.metadata_filepath.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
return await update_saved_session_title_at_path(logger.session_dir, title)
|
||||
except ValueError as exc:
|
||||
raise InternalError(
|
||||
f"Failed to persist title update for session {logger.session_id}: {exc}"
|
||||
) from exc
|
||||
|
||||
def _set_live_session_title(self, session: AcpSessionLoop, title: str) -> None:
|
||||
try:
|
||||
session.agent_loop.session_logger.set_title(title)
|
||||
except ValueError as exc:
|
||||
raise InvalidRequestError(
|
||||
f"Invalid ACP session title request: {exc}"
|
||||
) from exc
|
||||
|
||||
async def _handle_session_set_title(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
try:
|
||||
request = SessionSetTitleRequest.model_validate(params)
|
||||
except ValidationError as exc:
|
||||
raise InvalidRequestError(
|
||||
f"Invalid ACP session title request: {exc}"
|
||||
) from exc
|
||||
|
||||
live_session = self.sessions.get(
|
||||
request.session_id
|
||||
) or self._find_acp_session_by_vibe_session_id(request.session_id)
|
||||
if live_session is None:
|
||||
try:
|
||||
metadata = await update_saved_session_title(
|
||||
request.session_id,
|
||||
request.title,
|
||||
self._load_session_logging_config(),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise SessionNotFoundError(request.session_id) from exc
|
||||
|
||||
await self._emit_session_info_update(
|
||||
request.session_id,
|
||||
title=request.title,
|
||||
updated_at=metadata.get("end_time"),
|
||||
)
|
||||
return {}
|
||||
|
||||
persisted_metadata = await self._persist_live_session_title(
|
||||
live_session, request.title
|
||||
)
|
||||
self._set_live_session_title(live_session, request.title)
|
||||
updated_at = (
|
||||
persisted_metadata.get("end_time")
|
||||
if persisted_metadata is not None
|
||||
else (
|
||||
live_session.agent_loop.session_logger.session_metadata.end_time
|
||||
if live_session.agent_loop.session_logger.session_metadata is not None
|
||||
else None
|
||||
)
|
||||
)
|
||||
|
||||
await self._emit_session_info_update(
|
||||
live_session.id, title=request.title, updated_at=updated_at
|
||||
)
|
||||
return {}
|
||||
|
||||
@override
|
||||
async def ext_method(self, method: str, params: dict) -> dict:
|
||||
raise NotImplementedMethodError("ext_method")
|
||||
if method == "session/set_title":
|
||||
return await self._handle_session_set_title(params)
|
||||
|
||||
raise NotImplementedMethodError(method)
|
||||
|
||||
@override
|
||||
async def ext_notification(self, method: str, params: dict) -> None:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from acp.schema import (
|
||||
AgentMessageChunk,
|
||||
|
|
@ -9,6 +9,7 @@ from acp.schema import (
|
|||
ContentToolCallContent,
|
||||
ModelInfo,
|
||||
PermissionOption,
|
||||
PermissionOptionKind,
|
||||
SessionConfigOptionSelect,
|
||||
SessionConfigSelectOption,
|
||||
SessionMode,
|
||||
|
|
@ -34,25 +35,31 @@ if TYPE_CHECKING:
|
|||
class ToolOption(StrEnum):
|
||||
ALLOW_ONCE = "allow_once"
|
||||
ALLOW_ALWAYS = "allow_always"
|
||||
ALLOW_ALWAYS_PERMANENT = "allow_always_permanent"
|
||||
REJECT_ONCE = "reject_once"
|
||||
REJECT_ALWAYS = "reject_always"
|
||||
|
||||
|
||||
_KIND_ALLOW_ONCE: PermissionOptionKind = "allow_once"
|
||||
_KIND_ALLOW_ALWAYS: PermissionOptionKind = "allow_always"
|
||||
_KIND_REJECT_ONCE: PermissionOptionKind = "reject_once"
|
||||
|
||||
TOOL_OPTIONS = [
|
||||
PermissionOption(
|
||||
option_id=ToolOption.ALLOW_ONCE,
|
||||
name="Allow once",
|
||||
kind=cast(Literal["allow_once"], ToolOption.ALLOW_ONCE),
|
||||
option_id=ToolOption.ALLOW_ONCE, name="Allow once", kind=_KIND_ALLOW_ONCE
|
||||
),
|
||||
PermissionOption(
|
||||
option_id=ToolOption.ALLOW_ALWAYS,
|
||||
name="Allow for this session",
|
||||
kind=cast(Literal["allow_always"], ToolOption.ALLOW_ALWAYS),
|
||||
name="Allow for remainder of this session",
|
||||
kind=_KIND_ALLOW_ALWAYS,
|
||||
),
|
||||
PermissionOption(
|
||||
option_id=ToolOption.REJECT_ONCE,
|
||||
name="Reject once",
|
||||
kind=cast(Literal["reject_once"], ToolOption.REJECT_ONCE),
|
||||
option_id=ToolOption.ALLOW_ALWAYS_PERMANENT,
|
||||
name="Always allow",
|
||||
kind=_KIND_ALLOW_ALWAYS,
|
||||
),
|
||||
PermissionOption(
|
||||
option_id=ToolOption.REJECT_ONCE, name="Deny", kind=_KIND_REJECT_ONCE
|
||||
),
|
||||
]
|
||||
|
||||
|
|
@ -64,7 +71,6 @@ def build_permission_options(
|
|||
if not required_permissions:
|
||||
return TOOL_OPTIONS
|
||||
|
||||
labels = ", ".join(rp.label for rp in required_permissions)
|
||||
permissions_meta = [
|
||||
{
|
||||
"scope": rp.scope,
|
||||
|
|
@ -77,20 +83,22 @@ def build_permission_options(
|
|||
|
||||
return [
|
||||
PermissionOption(
|
||||
option_id=ToolOption.ALLOW_ONCE,
|
||||
name="Allow once",
|
||||
kind=cast(Literal["allow_once"], ToolOption.ALLOW_ONCE),
|
||||
option_id=ToolOption.ALLOW_ONCE, name="Allow once", kind=_KIND_ALLOW_ONCE
|
||||
),
|
||||
PermissionOption(
|
||||
option_id=ToolOption.ALLOW_ALWAYS,
|
||||
name=f"Allow for this session: {labels}",
|
||||
kind=cast(Literal["allow_always"], ToolOption.ALLOW_ALWAYS),
|
||||
name="Allow for remainder of this session",
|
||||
kind=_KIND_ALLOW_ALWAYS,
|
||||
field_meta={"required_permissions": permissions_meta},
|
||||
),
|
||||
PermissionOption(
|
||||
option_id=ToolOption.REJECT_ONCE,
|
||||
name="Reject once",
|
||||
kind=cast(Literal["reject_once"], ToolOption.REJECT_ONCE),
|
||||
option_id=ToolOption.ALLOW_ALWAYS_PERMANENT,
|
||||
name="Always allow",
|
||||
kind=_KIND_ALLOW_ALWAYS,
|
||||
field_meta={"required_permissions": permissions_meta},
|
||||
),
|
||||
PermissionOption(
|
||||
option_id=ToolOption.REJECT_ONCE, name="Deny", kind=_KIND_REJECT_ONCE
|
||||
),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -265,6 +265,7 @@ def run_cli(args: argparse.Namespace) -> None:
|
|||
initial_prompt=args.initial_prompt or stdin_prompt,
|
||||
teleport_on_start=args.teleport,
|
||||
show_resume_picker=args.resume is True,
|
||||
is_resuming_session=loaded_session is not None,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -128,6 +128,11 @@ class CommandRegistry:
|
|||
description="Browse and resume past sessions",
|
||||
handler="_show_session_picker",
|
||||
),
|
||||
"rename": Command(
|
||||
aliases=frozenset(["/rename"]),
|
||||
description="Rename the current session",
|
||||
handler="_rename_session",
|
||||
),
|
||||
"mcp": Command(
|
||||
aliases=frozenset(["/mcp", "/connectors"]),
|
||||
description=(
|
||||
|
|
@ -225,7 +230,7 @@ class CommandRegistry:
|
|||
"- `Ctrl+C` Quit (or clear input if text present)",
|
||||
"- `Ctrl+G` Edit input in external editor",
|
||||
"- `Ctrl+O` Toggle tool output view",
|
||||
"- `Shift+Tab` Toggle auto-approve mode",
|
||||
"- `Shift+Tab` Cycle through agents (default, plan, ...)",
|
||||
f"- `{ALT_KEY}+↑↓` / `Ctrl+P/N` Rewind to previous/next message",
|
||||
"",
|
||||
"### Special Features",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import codecs
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import aclosing
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -9,9 +10,9 @@ import gc
|
|||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Any, ClassVar, assert_never, cast
|
||||
from uuid import uuid4
|
||||
from weakref import WeakKeyDictionary
|
||||
import webbrowser
|
||||
|
||||
|
|
@ -121,6 +122,7 @@ from vibe.core.agent_loop import AgentLoop, TeleportError
|
|||
from vibe.core.agents import AgentProfile
|
||||
from vibe.core.audio_player.audio_player import AudioPlayer
|
||||
from vibe.core.audio_recorder import AudioRecorder
|
||||
from vibe.core.autocompletion.path_prompt import build_path_prompt_payload
|
||||
from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.data_retention import DATA_RETENTION_MESSAGE
|
||||
|
|
@ -135,6 +137,7 @@ from vibe.core.session.resume_sessions import (
|
|||
list_remote_resume_sessions,
|
||||
short_session_id,
|
||||
)
|
||||
from vibe.core.session.saved_sessions import update_saved_session_title_at_path
|
||||
from vibe.core.session.session_loader import SessionLoader
|
||||
from vibe.core.skills.manager import SkillManager
|
||||
from vibe.core.teleport.types import (
|
||||
|
|
@ -294,6 +297,7 @@ class StartupOptions:
|
|||
initial_prompt: str | None = None
|
||||
teleport_on_start: bool = False
|
||||
show_resume_picker: bool = False
|
||||
is_resuming_session: bool = False
|
||||
|
||||
|
||||
class VibeApp(App): # noqa: PLR0904
|
||||
|
|
@ -349,6 +353,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self._agent_running = False
|
||||
self._interrupt_requested = False
|
||||
self._agent_task: asyncio.Task | None = None
|
||||
self._bash_task: asyncio.Task | None = None
|
||||
self._remote_manager = RemoteSessionManager()
|
||||
|
||||
self._loading_widget: LoadingWidget | None = None
|
||||
|
|
@ -374,12 +379,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self._update_cache_repository = update_cache_repository
|
||||
self._current_version = current_version
|
||||
self._plan_offer_gateway = plan_offer_gateway
|
||||
opts = startup or StartupOptions()
|
||||
self._initial_prompt = opts.initial_prompt
|
||||
self._teleport_on_start = (
|
||||
opts.teleport_on_start and self.agent_loop.base_config.vibe_code_enabled
|
||||
)
|
||||
self._show_resume_picker = opts.show_resume_picker
|
||||
self._configure_startup_options(startup)
|
||||
self._last_escape_time: float | None = None
|
||||
self._quit_manager = QuitManager(self)
|
||||
self._banner: Banner | None = None
|
||||
|
|
@ -399,6 +399,15 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self._fatal_init_error = False
|
||||
self.commands = self._build_command_registry()
|
||||
|
||||
def _configure_startup_options(self, startup: StartupOptions | None) -> None:
|
||||
opts = startup or StartupOptions()
|
||||
self._initial_prompt = opts.initial_prompt
|
||||
self._teleport_on_start = (
|
||||
opts.teleport_on_start and self.agent_loop.base_config.vibe_code_enabled
|
||||
)
|
||||
self._show_resume_picker = opts.show_resume_picker
|
||||
self._is_resuming_session = opts.is_resuming_session
|
||||
|
||||
@property
|
||||
def config(self) -> VibeConfig:
|
||||
return self.agent_loop.config
|
||||
|
|
@ -497,6 +506,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
await self._resume_history_from_messages()
|
||||
await self._check_and_show_whats_new()
|
||||
self._schedule_update_notification()
|
||||
if not self._is_resuming_session:
|
||||
self.agent_loop.emit_new_session_telemetry()
|
||||
|
||||
self.call_after_refresh(self._refresh_banner)
|
||||
|
|
@ -586,11 +596,15 @@ class VibeApp(App): # noqa: PLR0904
|
|||
input_widget = self.query_one(ChatInputContainer)
|
||||
input_widget.value = ""
|
||||
|
||||
if self._bash_task and not self._bash_task.done():
|
||||
self._bash_task.cancel()
|
||||
self._bash_task = None
|
||||
|
||||
if self._agent_running:
|
||||
await self._interrupt_agent_loop()
|
||||
|
||||
if value.startswith("!"):
|
||||
await self._handle_bash_command(value[1:])
|
||||
self._bash_task = asyncio.create_task(self._handle_bash_command(value[1:]))
|
||||
return
|
||||
|
||||
if value.startswith("&") and self.commands.has_command("teleport"):
|
||||
|
|
@ -619,6 +633,16 @@ class VibeApp(App): # noqa: PLR0904
|
|||
if self._pending_approval and not self._pending_approval.done():
|
||||
self._pending_approval.set_result((ApprovalResponse.YES, None))
|
||||
|
||||
async def on_approval_app_approval_granted_always_permanent(
|
||||
self, message: ApprovalApp.ApprovalGrantedAlwaysPermanent
|
||||
) -> None:
|
||||
self.agent_loop.approve_always(
|
||||
message.tool_name, message.required_permissions, save_permanently=True
|
||||
)
|
||||
|
||||
if self._pending_approval and not self._pending_approval.done():
|
||||
self._pending_approval.set_result((ApprovalResponse.YES, None))
|
||||
|
||||
async def on_approval_app_approval_rejected(
|
||||
self, message: ApprovalApp.ApprovalRejected
|
||||
) -> None:
|
||||
|
|
@ -905,6 +929,40 @@ class VibeApp(App): # noqa: PLR0904
|
|||
await self._handle_user_message(prompt)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def _bash_read_stream(
|
||||
stream: asyncio.StreamReader | None,
|
||||
parts: list[str],
|
||||
bash_msg: BashOutputMessage,
|
||||
) -> None:
|
||||
if not stream:
|
||||
return
|
||||
decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
||||
while True:
|
||||
chunk = await stream.read(4096)
|
||||
if not chunk:
|
||||
break
|
||||
text = decoder.decode(chunk)
|
||||
if not text:
|
||||
continue
|
||||
parts.append(text)
|
||||
await bash_msg.append_output(text)
|
||||
final_text = decoder.decode(b"", final=True)
|
||||
if not final_text:
|
||||
return
|
||||
parts.append(final_text)
|
||||
await bash_msg.append_output(final_text)
|
||||
|
||||
@staticmethod
|
||||
async def _kill_running_process(proc: asyncio.subprocess.Process | None) -> None:
|
||||
if proc is None or proc.returncode is not None:
|
||||
return
|
||||
try:
|
||||
proc.kill()
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
await proc.wait()
|
||||
|
||||
async def _handle_bash_command(self, command: str) -> None:
|
||||
if not command:
|
||||
await self._mount_and_scroll(
|
||||
|
|
@ -914,41 +972,31 @@ class VibeApp(App): # noqa: PLR0904
|
|||
)
|
||||
return
|
||||
|
||||
bash_msg = BashOutputMessage(command, str(Path.cwd()), pending=True)
|
||||
await self._mount_and_scroll(bash_msg)
|
||||
|
||||
proc: asyncio.subprocess.Process | None = None
|
||||
stdout_parts: list[str] = []
|
||||
stderr_parts: list[str] = []
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command, shell=True, capture_output=True, text=False, timeout=30
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
stdout = (
|
||||
result.stdout.decode("utf-8", errors="replace") if result.stdout else ""
|
||||
)
|
||||
stderr = (
|
||||
result.stderr.decode("utf-8", errors="replace") if result.stderr else ""
|
||||
)
|
||||
output = stdout or stderr or "(no output)"
|
||||
exit_code = result.returncode
|
||||
await self._mount_and_scroll(
|
||||
BashOutputMessage(command, str(Path.cwd()), output, exit_code)
|
||||
)
|
||||
await self.agent_loop.inject_user_context(
|
||||
self._format_manual_command_context(
|
||||
command=command,
|
||||
cwd=str(Path.cwd()),
|
||||
exit_code=exit_code,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
)
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
stdout = (
|
||||
error.stdout.decode("utf-8", errors="replace")
|
||||
if isinstance(error.stdout, bytes)
|
||||
else (error.stdout or "")
|
||||
)
|
||||
stderr = (
|
||||
error.stderr.decode("utf-8", errors="replace")
|
||||
if isinstance(error.stderr, bytes)
|
||||
else (error.stderr or "")
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(
|
||||
self._bash_read_stream(proc.stdout, stdout_parts, bash_msg),
|
||||
self._bash_read_stream(proc.stderr, stderr_parts, bash_msg),
|
||||
proc.wait(),
|
||||
),
|
||||
timeout=30,
|
||||
)
|
||||
except TimeoutError:
|
||||
await self._kill_running_process(proc)
|
||||
stdout = "".join(stdout_parts)
|
||||
stderr = "".join(stderr_parts)
|
||||
await bash_msg.finish(1)
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
"Command timed out after 30 seconds",
|
||||
|
|
@ -964,7 +1012,40 @@ class VibeApp(App): # noqa: PLR0904
|
|||
status="timed out after 30 seconds",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
stdout = "".join(stdout_parts)
|
||||
stderr = "".join(stderr_parts)
|
||||
exit_code = proc.returncode or 0
|
||||
await bash_msg.finish(exit_code)
|
||||
await self.agent_loop.inject_user_context(
|
||||
self._format_manual_command_context(
|
||||
command=command,
|
||||
cwd=str(Path.cwd()),
|
||||
exit_code=exit_code,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
)
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
await self._kill_running_process(proc)
|
||||
await bash_msg.finish(1, interrupted=True)
|
||||
stdout = "".join(stdout_parts)
|
||||
stderr = "".join(stderr_parts)
|
||||
await self.agent_loop.inject_user_context(
|
||||
self._format_manual_command_context(
|
||||
command=command,
|
||||
cwd=str(Path.cwd()),
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
status="interrupted by user",
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
await self._kill_running_process(proc)
|
||||
await bash_msg.finish(1)
|
||||
stdout = "".join(stdout_parts)
|
||||
stderr = "".join(stderr_parts)
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(f"Command failed: {e}", collapsed=self._tools_collapsed)
|
||||
)
|
||||
|
|
@ -972,6 +1053,8 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self._format_manual_command_context(
|
||||
command=command,
|
||||
cwd=str(Path.cwd()),
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
status=f"failed before completion: {e}",
|
||||
)
|
||||
)
|
||||
|
|
@ -1230,10 +1313,30 @@ class VibeApp(App): # noqa: PLR0904
|
|||
try:
|
||||
await self._handle_agent_loop_init()
|
||||
await self._ensure_loading_widget()
|
||||
message_id = str(uuid4())
|
||||
prompt_payload = build_path_prompt_payload(prompt, base_dir=Path.cwd())
|
||||
if prompt_payload.all_resources:
|
||||
context_types: dict[str, int] = {}
|
||||
for r in prompt_payload.all_resources:
|
||||
context_types[r.kind] = context_types.get(r.kind, 0) + 1
|
||||
file_ext_counts: dict[str, int] = {}
|
||||
for r in prompt_payload.all_resources:
|
||||
if r.kind == "file" and r.path.suffix:
|
||||
file_ext_counts[r.path.suffix] = (
|
||||
file_ext_counts.get(r.path.suffix, 0) + 1
|
||||
)
|
||||
self.agent_loop.telemetry_client.send_at_mention_inserted(
|
||||
nb_mentions=len(prompt_payload.all_resources),
|
||||
context_types=context_types,
|
||||
file_extensions=file_ext_counts or None,
|
||||
message_id=message_id,
|
||||
)
|
||||
rendered_prompt = render_path_prompt(prompt, base_dir=Path.cwd())
|
||||
self._narrator_manager.cancel()
|
||||
self._narrator_manager.on_turn_start(rendered_prompt)
|
||||
async with aclosing(self.agent_loop.act(rendered_prompt)) as events:
|
||||
async with aclosing(
|
||||
self.agent_loop.act(rendered_prompt, client_message_id=message_id)
|
||||
) as events:
|
||||
await self._handle_agent_loop_events(events)
|
||||
except asyncio.CancelledError:
|
||||
await self._handle_turn_error()
|
||||
|
|
@ -1566,6 +1669,53 @@ class VibeApp(App): # noqa: PLR0904
|
|||
async def _show_data_retention(self, **kwargs: Any) -> None:
|
||||
await self._mount_and_scroll(UserCommandMessage(DATA_RETENTION_MESSAGE))
|
||||
|
||||
async def _rename_local_session(self, title: str) -> str:
|
||||
session_logger = self.agent_loop.session_logger
|
||||
if not session_logger.enabled or session_logger.session_metadata is None:
|
||||
raise ValueError("Session logging is disabled in configuration.")
|
||||
|
||||
if (
|
||||
session_logger.session_dir is not None
|
||||
and session_logger.metadata_filepath.exists()
|
||||
):
|
||||
await update_saved_session_title_at_path(session_logger.session_dir, title)
|
||||
|
||||
session_logger.set_title(title)
|
||||
renamed_title = session_logger.session_metadata.title
|
||||
assert renamed_title is not None
|
||||
return renamed_title
|
||||
|
||||
async def _rename_session(self, cmd_args: str = "", **kwargs: Any) -> None:
|
||||
if self._remote_manager.is_active:
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
"Renaming is only supported for local sessions.",
|
||||
collapsed=self._tools_collapsed,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
title = cmd_args.strip()
|
||||
if not title:
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage("Usage: /rename <title>", collapsed=self._tools_collapsed)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
renamed_title = await self._rename_local_session(title)
|
||||
except Exception as e:
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
f"Failed to rename session: {e}", collapsed=self._tools_collapsed
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
await self._mount_and_scroll(
|
||||
UserCommandMessage(f'Session renamed to "{renamed_title}".')
|
||||
)
|
||||
|
||||
async def _show_session_picker(self, **kwargs: Any) -> None:
|
||||
cwd = str(Path.cwd())
|
||||
local_sessions = (
|
||||
|
|
@ -1608,7 +1758,8 @@ class VibeApp(App): # noqa: PLR0904
|
|||
sessions = sorted(raw_sessions, key=lambda s: s.end_time or "", reverse=True)
|
||||
|
||||
latest_messages = {
|
||||
s.option_id: SessionLoader.get_first_user_message(
|
||||
s.option_id: s.title
|
||||
or SessionLoader.get_first_user_message(
|
||||
s.session_id, self.config.session_logging
|
||||
)
|
||||
for s in sessions
|
||||
|
|
@ -2598,6 +2749,8 @@ class VibeApp(App): # noqa: PLR0904
|
|||
def _force_quit(self) -> None:
|
||||
if self._agent_task and not self._agent_task.done():
|
||||
self._agent_task.cancel()
|
||||
if self._bash_task and not self._bash_task.done():
|
||||
self._bash_task.cancel()
|
||||
self._remote_manager.cancel_stream_task()
|
||||
|
||||
self._log_reader.shutdown()
|
||||
|
|
|
|||
|
|
@ -413,6 +413,10 @@ Markdown {
|
|||
&.bash-error {
|
||||
color: ansi_red;
|
||||
}
|
||||
|
||||
&.bash-interrupted {
|
||||
color: ansi_yellow;
|
||||
}
|
||||
}
|
||||
|
||||
.bash-command {
|
||||
|
|
@ -786,7 +790,7 @@ StatusMessage {
|
|||
.approval-title {
|
||||
height: auto;
|
||||
text-style: bold;
|
||||
color: ansi_yellow;
|
||||
color: ansi_bright_yellow;
|
||||
}
|
||||
|
||||
.approval-tool-info-container {
|
||||
|
|
@ -808,6 +812,11 @@ StatusMessage {
|
|||
.approval-option {
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
margin-bottom: 1;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.approval-cursor-selected {
|
||||
|
|
@ -817,14 +826,14 @@ StatusMessage {
|
|||
}
|
||||
|
||||
&.approval-option-no {
|
||||
color: ansi_red;
|
||||
color: ansi_bright_red;
|
||||
text-style: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.approval-option-selected {
|
||||
&.approval-option-yes {
|
||||
color: ansi_green;
|
||||
color: ansi_white;
|
||||
}
|
||||
|
||||
&.approval-option-no {
|
||||
|
|
@ -839,7 +848,7 @@ StatusMessage {
|
|||
|
||||
.approval-description {
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
color: ansi_bright_black;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ class ApprovalApp(Container):
|
|||
can_focus = True
|
||||
can_focus_children = False
|
||||
|
||||
NUM_OPTIONS = 4
|
||||
|
||||
BINDINGS: ClassVar[list[BindingType]] = [
|
||||
Binding("up", "move_up", "Up", show=False),
|
||||
Binding("down", "move_down", "Down", show=False),
|
||||
|
|
@ -27,8 +29,9 @@ class ApprovalApp(Container):
|
|||
Binding("1", "select_1", "Yes", show=False),
|
||||
Binding("y", "select_1", "Yes", show=False),
|
||||
Binding("2", "select_2", "Always Tool Session", show=False),
|
||||
Binding("3", "select_3", "No", show=False),
|
||||
Binding("n", "select_3", "No", show=False),
|
||||
Binding("3", "select_3", "Always Permanent", show=False),
|
||||
Binding("4", "select_4", "No", show=False),
|
||||
Binding("n", "select_4", "No", show=False),
|
||||
]
|
||||
|
||||
class ApprovalGranted(Message):
|
||||
|
|
@ -49,6 +52,18 @@ class ApprovalApp(Container):
|
|||
self.tool_args = tool_args
|
||||
self.required_permissions = required_permissions
|
||||
|
||||
class ApprovalGrantedAlwaysPermanent(Message):
|
||||
def __init__(
|
||||
self,
|
||||
tool_name: str,
|
||||
tool_args: BaseModel,
|
||||
required_permissions: list[RequiredPermission],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.tool_name = tool_name
|
||||
self.tool_args = tool_args
|
||||
self.required_permissions = required_permissions
|
||||
|
||||
class ApprovalRejected(Message):
|
||||
def __init__(self, tool_name: str, tool_args: BaseModel) -> None:
|
||||
super().__init__()
|
||||
|
|
@ -77,20 +92,18 @@ class ApprovalApp(Container):
|
|||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="approval-options"):
|
||||
yield NoMarkupStatic("")
|
||||
for _ in range(3):
|
||||
for _ in range(self.NUM_OPTIONS):
|
||||
widget = NoMarkupStatic("", classes="approval-option")
|
||||
self.option_widgets.append(widget)
|
||||
yield widget
|
||||
yield NoMarkupStatic("")
|
||||
self.help_widget = NoMarkupStatic(
|
||||
"↑↓ navigate Enter select ESC reject", classes="approval-help"
|
||||
)
|
||||
yield self.help_widget
|
||||
|
||||
with Vertical(id="approval-content"):
|
||||
self.title_widget = NoMarkupStatic(
|
||||
f"⚠ {self.tool_name} command", classes="approval-title"
|
||||
)
|
||||
title = self._build_title()
|
||||
self.title_widget = NoMarkupStatic(title, classes="approval-title")
|
||||
yield self.title_widget
|
||||
|
||||
with VerticalScroll(classes="approval-tool-info-scroll"):
|
||||
|
|
@ -99,6 +112,12 @@ class ApprovalApp(Container):
|
|||
)
|
||||
yield self.tool_info_container
|
||||
|
||||
def _build_title(self) -> str:
|
||||
if self.required_permissions:
|
||||
labels = ", ".join(rp.label for rp in self.required_permissions)
|
||||
return f"Permission for the {self.tool_name} tool ({labels})"
|
||||
return f"Permission for the {self.tool_name} tool"
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
await self._update_tool_info()
|
||||
self._update_options()
|
||||
|
|
@ -113,16 +132,11 @@ class ApprovalApp(Container):
|
|||
await self.tool_info_container.mount(approval_widget)
|
||||
|
||||
def _update_options(self) -> None:
|
||||
if self.required_permissions:
|
||||
labels = ", ".join(rp.label for rp in self.required_permissions)
|
||||
always_text = f"Yes and always allow for this session: {labels}"
|
||||
else:
|
||||
always_text = f"Yes and always allow {self.tool_name} for this session"
|
||||
|
||||
options = [
|
||||
("Yes", "yes"),
|
||||
(always_text, "yes"),
|
||||
("No and tell the agent what to do instead", "no"),
|
||||
("Allow once", "yes"),
|
||||
("Allow for remainder of this session", "yes"),
|
||||
("Always allow", "yes"),
|
||||
("Deny", "no"),
|
||||
]
|
||||
|
||||
for idx, ((text, color_type), widget) in enumerate(
|
||||
|
|
@ -154,11 +168,11 @@ class ApprovalApp(Container):
|
|||
widget.add_class("approval-option-no")
|
||||
|
||||
def action_move_up(self) -> None:
|
||||
self.selected_option = (self.selected_option - 1) % 3
|
||||
self.selected_option = (self.selected_option - 1) % self.NUM_OPTIONS
|
||||
self._update_options()
|
||||
|
||||
def action_move_down(self) -> None:
|
||||
self.selected_option = (self.selected_option + 1) % 3
|
||||
self.selected_option = (self.selected_option + 1) % self.NUM_OPTIONS
|
||||
self._update_options()
|
||||
|
||||
def action_select(self) -> None:
|
||||
|
|
@ -176,9 +190,13 @@ class ApprovalApp(Container):
|
|||
self.selected_option = 2
|
||||
self._handle_selection(2)
|
||||
|
||||
def action_select_4(self) -> None:
|
||||
self.selected_option = 3
|
||||
self._handle_selection(3)
|
||||
|
||||
def action_reject(self) -> None:
|
||||
self.selected_option = 2
|
||||
self._handle_selection(2)
|
||||
self.selected_option = 3
|
||||
self._handle_selection(3)
|
||||
|
||||
def _handle_selection(self, option: int) -> None:
|
||||
match option:
|
||||
|
|
@ -197,6 +215,14 @@ class ApprovalApp(Container):
|
|||
)
|
||||
)
|
||||
case 2:
|
||||
self.post_message(
|
||||
self.ApprovalGrantedAlwaysPermanent(
|
||||
tool_name=self.tool_name,
|
||||
tool_args=self.tool_args,
|
||||
required_permissions=self.required_permissions,
|
||||
)
|
||||
)
|
||||
case 3:
|
||||
self.post_message(
|
||||
self.ApprovalRejected(
|
||||
tool_name=self.tool_name, tool_args=self.tool_args
|
||||
|
|
|
|||
|
|
@ -264,23 +264,92 @@ class InterruptMessage(Static):
|
|||
|
||||
|
||||
class BashOutputMessage(Static):
|
||||
def __init__(self, command: str, cwd: str, output: str, exit_code: int) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
command: str,
|
||||
cwd: str,
|
||||
output: str = "",
|
||||
exit_code: int = 0,
|
||||
*,
|
||||
pending: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.add_class("bash-output-message")
|
||||
self._command = command
|
||||
self._cwd = cwd
|
||||
self._output = output.rstrip("\n")
|
||||
self._exit_code = exit_code
|
||||
self._pending = pending
|
||||
self._output_widget: NoMarkupStatic | None = None
|
||||
self._output_container: Horizontal | None = None
|
||||
self._prompt_widget: NonSelectableStatic | None = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
status_class = "bash-success" if self._exit_code == 0 else "bash-error"
|
||||
status_class = (
|
||||
"bash-error"
|
||||
if not self._pending and self._exit_code != 0
|
||||
else "bash-success"
|
||||
)
|
||||
self.add_class(status_class)
|
||||
with Horizontal(classes="bash-command-line"):
|
||||
yield NonSelectableStatic("$ ", classes=f"bash-prompt {status_class}")
|
||||
self._prompt_widget = NonSelectableStatic(
|
||||
"$ ", classes=f"bash-prompt {status_class}"
|
||||
)
|
||||
yield self._prompt_widget
|
||||
yield NoMarkupStatic(self._command, classes="bash-command")
|
||||
with Horizontal(classes="bash-output-container"):
|
||||
if not self._pending:
|
||||
self._output_container = Horizontal(classes="bash-output-container")
|
||||
with self._output_container:
|
||||
yield ExpandingBorder(classes="bash-output-border")
|
||||
yield NoMarkupStatic(self._output, classes="bash-output")
|
||||
self._output_widget = NoMarkupStatic(
|
||||
self._output, classes="bash-output"
|
||||
)
|
||||
yield self._output_widget
|
||||
|
||||
async def _ensure_output_container(self) -> None:
|
||||
if self._output_container is not None:
|
||||
return
|
||||
self._output_widget = NoMarkupStatic("", classes="bash-output")
|
||||
self._output_container = Horizontal(
|
||||
ExpandingBorder(classes="bash-output-border"),
|
||||
self._output_widget,
|
||||
classes="bash-output-container",
|
||||
)
|
||||
await self.mount(self._output_container)
|
||||
|
||||
async def append_output(self, text: str) -> None:
|
||||
await self._ensure_output_container()
|
||||
self._output += text
|
||||
if self._output_widget:
|
||||
self._output_widget.update(self._output.rstrip("\n"))
|
||||
|
||||
async def finish(self, exit_code: int, *, interrupted: bool = False) -> None:
|
||||
self._exit_code = exit_code
|
||||
self._pending = False
|
||||
if interrupted:
|
||||
self.remove_class("bash-success")
|
||||
self.add_class("bash-interrupted")
|
||||
if self._prompt_widget:
|
||||
self._prompt_widget.remove_class("bash-success")
|
||||
self._prompt_widget.add_class("bash-interrupted")
|
||||
elif exit_code != 0:
|
||||
self.remove_class("bash-success")
|
||||
self.add_class("bash-error")
|
||||
if self._prompt_widget:
|
||||
self._prompt_widget.remove_class("bash-success")
|
||||
self._prompt_widget.add_class("bash-error")
|
||||
if interrupted:
|
||||
suffix = (
|
||||
"\n(interrupted)"
|
||||
if self._output and not self._output.endswith("\n")
|
||||
else "(interrupted)"
|
||||
)
|
||||
self._output += suffix
|
||||
if not self._output:
|
||||
self._output = "(no output)"
|
||||
await self._ensure_output_container()
|
||||
if self._output_widget:
|
||||
self._output_widget.update(self._output.rstrip("\n"))
|
||||
|
||||
|
||||
class ErrorMessage(Static):
|
||||
|
|
|
|||
|
|
@ -172,6 +172,13 @@ def _is_context_too_long_error(e: Exception) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def _is_non_retryable_error(e: BaseException) -> bool:
|
||||
# Detect Temporal-style ``non_retryable`` flag without importing temporalio.
|
||||
# Wrapping such an exception in a plain RuntimeError strips the flag, so
|
||||
# Temporal's activity retry policy will retry the call until exhaustion.
|
||||
return bool(getattr(e, "non_retryable", False))
|
||||
|
||||
|
||||
def requires_init(fn: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""Decorator that awaits deferred initialization before executing the method."""
|
||||
if inspect.isasyncgenfunction(fn):
|
||||
|
|
@ -451,6 +458,10 @@ class AgentLoop:
|
|||
session_pattern=rp.session_pattern,
|
||||
)
|
||||
)
|
||||
if save_permanently:
|
||||
self.config.add_tool_allowlist_patterns(
|
||||
tool_name, [rp.session_pattern for rp in required_permissions]
|
||||
)
|
||||
else:
|
||||
self.set_tool_permission(
|
||||
tool_name, ToolPermission.ALWAYS, save_permanently=save_permanently
|
||||
|
|
@ -1199,6 +1210,8 @@ class AgentLoop:
|
|||
raise RateLimitError(provider.name, active_model.name) from e
|
||||
if _is_context_too_long_error(e):
|
||||
raise ContextTooLongError(provider.name, active_model.name) from e
|
||||
if _is_non_retryable_error(e):
|
||||
raise
|
||||
|
||||
raise RuntimeError(
|
||||
f"API error from {provider.name} (model: {active_model.name}): {e}"
|
||||
|
|
@ -1275,6 +1288,8 @@ class AgentLoop:
|
|||
raise RateLimitError(provider.name, active_model.name) from e
|
||||
if _is_context_too_long_error(e):
|
||||
raise ContextTooLongError(provider.name, active_model.name) from e
|
||||
if _is_non_retryable_error(e):
|
||||
raise
|
||||
|
||||
raise RuntimeError(
|
||||
f"API error from {provider.name} (model: {active_model.name}): {e}"
|
||||
|
|
@ -1414,13 +1429,14 @@ class AgentLoop:
|
|||
|
||||
i += 1
|
||||
|
||||
def _reset_session(self) -> None:
|
||||
def _reset_session(self, keep_parent: bool = True) -> None:
|
||||
old_session_id = self.session_id
|
||||
suffix = extract_suffix(self.session_id)
|
||||
self.session_id = generate_session_id(suffix=suffix)
|
||||
self.parent_session_id = old_session_id
|
||||
parent_session_id = old_session_id if keep_parent else None
|
||||
self.parent_session_id = parent_session_id
|
||||
self.session_logger.reset_session(
|
||||
self.session_id, parent_session_id=old_session_id
|
||||
self.session_id, parent_session_id=parent_session_id
|
||||
)
|
||||
self.emit_new_session_telemetry()
|
||||
|
||||
|
|
@ -1500,7 +1516,7 @@ class AgentLoop:
|
|||
|
||||
self.middleware_pipeline.reset()
|
||||
self.tool_manager.reset_all()
|
||||
self._reset_session()
|
||||
self._reset_session(keep_parent=False)
|
||||
|
||||
@requires_init
|
||||
async def compact(self, extra_instructions: str = "") -> str:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from typing import Literal
|
|||
class PathResource:
|
||||
path: Path
|
||||
alias: str
|
||||
kind: Literal["file", "directory"]
|
||||
kind: Literal["file", "folder"]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -17,13 +17,14 @@ class PathPromptPayload:
|
|||
display_text: str
|
||||
prompt_text: str
|
||||
resources: list[PathResource]
|
||||
all_resources: list[PathResource]
|
||||
|
||||
|
||||
def build_path_prompt_payload(
|
||||
message: str, *, base_dir: Path | None = None
|
||||
) -> PathPromptPayload:
|
||||
if not message:
|
||||
return PathPromptPayload(message, message, [])
|
||||
return PathPromptPayload(message, message, [], [])
|
||||
|
||||
resolved_base = (base_dir or Path.cwd()).resolve()
|
||||
prompt_parts: list[str] = []
|
||||
|
|
@ -44,7 +45,7 @@ def build_path_prompt_payload(
|
|||
|
||||
prompt_text = "".join(prompt_parts)
|
||||
unique_resources = _dedupe_resources(resources)
|
||||
return PathPromptPayload(message, prompt_text, unique_resources)
|
||||
return PathPromptPayload(message, prompt_text, unique_resources, resources)
|
||||
|
||||
|
||||
def _is_path_anchor(message: str, pos: int) -> bool:
|
||||
|
|
@ -93,7 +94,7 @@ def _to_resource(candidate: str, base_dir: Path) -> PathResource | None:
|
|||
if not resolved.exists():
|
||||
return None
|
||||
|
||||
kind = "directory" if resolved.is_dir() else "file"
|
||||
kind = "folder" if resolved.is_dir() else "file"
|
||||
return PathResource(path=resolved, alias=candidate, kind=kind)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ def _path_prompt_to_content_blocks(
|
|||
"uri": resource.path.as_uri(),
|
||||
"name": resource.alias,
|
||||
})
|
||||
case "directory":
|
||||
case "folder":
|
||||
blocks.append({
|
||||
"type": "resource_link",
|
||||
"uri": resource.path.as_uri(),
|
||||
|
|
|
|||
|
|
@ -332,8 +332,6 @@ MCPServer = Annotated[
|
|||
|
||||
|
||||
class ConnectorConfig(BaseModel):
|
||||
"""Per-connector settings persisted in config.toml under ``[[connectors]]``."""
|
||||
|
||||
name: str = Field(description="Normalized connector alias to match against.")
|
||||
disabled: bool = Field(
|
||||
default=False,
|
||||
|
|
@ -937,9 +935,21 @@ class VibeConfig(BaseSettings):
|
|||
]
|
||||
type(self).save_updates({"models": models})
|
||||
|
||||
def add_tool_allowlist_patterns(self, tool_name: str, patterns: list[str]) -> None:
|
||||
current_allowlist: list[str] = list(
|
||||
self.tools.get(tool_name, {}).get("allowlist", [])
|
||||
)
|
||||
new_patterns = [p for p in patterns if p not in current_allowlist]
|
||||
if not new_patterns:
|
||||
return
|
||||
merged = sorted(current_allowlist + new_patterns)
|
||||
self.save_updates({"tools": {tool_name: {"allowlist": merged}}})
|
||||
if tool_name not in self.tools:
|
||||
self.tools[tool_name] = {}
|
||||
self.tools[tool_name]["allowlist"] = merged
|
||||
|
||||
@classmethod
|
||||
def get_persisted_config(cls) -> dict[str, Any]:
|
||||
"""Return the raw config as persisted in the TOML file (no profile merging)."""
|
||||
return TomlFileSettingsSource(cls).toml_data
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ def run_programmatic( # noqa: PLR0913, PLR0917
|
|||
logger.info(
|
||||
"Loaded %d messages from previous session", len(non_system_messages)
|
||||
)
|
||||
|
||||
else:
|
||||
agent_loop.emit_new_session_telemetry()
|
||||
|
||||
if teleport and config.vibe_code_enabled:
|
||||
|
|
|
|||
58
vibe/core/session/saved_sessions.py
Normal file
58
vibe/core/session/saved_sessions.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from vibe.core.config import SessionLoggingConfig
|
||||
from vibe.core.session.session_loader import METADATA_FILENAME, SessionLoader
|
||||
from vibe.core.session.session_logger import SessionLogger
|
||||
from vibe.core.utils.io import read_safe
|
||||
|
||||
|
||||
def _normalize_session_title(title: str) -> str:
|
||||
normalized_title = title.strip()
|
||||
if not normalized_title:
|
||||
raise ValueError("Session title cannot be empty.")
|
||||
|
||||
return normalized_title
|
||||
|
||||
|
||||
def _resolve_saved_session_dir(
|
||||
session_id: str, session_config: SessionLoggingConfig
|
||||
) -> Path:
|
||||
for session_dir in SessionLoader._find_session_dirs_by_short_id(
|
||||
session_id, session_config
|
||||
):
|
||||
try:
|
||||
metadata = _load_raw_metadata(session_dir)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
continue
|
||||
|
||||
if metadata.get("session_id") == session_id:
|
||||
return session_dir
|
||||
|
||||
raise ValueError(f"Session not found: {session_id}")
|
||||
|
||||
|
||||
def _load_raw_metadata(session_dir: Path) -> dict[str, Any]:
|
||||
metadata_path = session_dir / METADATA_FILENAME
|
||||
return json.loads(read_safe(metadata_path).text)
|
||||
|
||||
|
||||
async def update_saved_session_title_at_path(
|
||||
session_dir: Path, title: str
|
||||
) -> dict[str, Any]:
|
||||
normalized_title = _normalize_session_title(title)
|
||||
metadata = _load_raw_metadata(session_dir)
|
||||
|
||||
updated_metadata = {**metadata, "title": normalized_title, "title_source": "manual"}
|
||||
await SessionLogger.persist_metadata(updated_metadata, session_dir)
|
||||
return updated_metadata
|
||||
|
||||
|
||||
async def update_saved_session_title(
|
||||
session_id: str, title: str, session_config: SessionLoggingConfig
|
||||
) -> dict[str, Any]:
|
||||
session_dir = _resolve_saved_session_dir(session_id, session_config)
|
||||
return await update_saved_session_title_at_path(session_dir, title)
|
||||
|
|
@ -8,7 +8,7 @@ import os
|
|||
from pathlib import Path
|
||||
import subprocess
|
||||
from threading import Lock
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
from anyio import NamedTemporaryFile, Path as AsyncPath
|
||||
|
||||
|
|
@ -134,6 +134,8 @@ class SessionLogger:
|
|||
git_branch=git_branch,
|
||||
username=user_name,
|
||||
environment={"working_directory": str(Path.cwd())},
|
||||
title=None,
|
||||
title_source="auto",
|
||||
)
|
||||
|
||||
def _get_title(self, messages: Sequence[LLMMessage]) -> str:
|
||||
|
|
@ -154,6 +156,37 @@ class SessionLogger:
|
|||
|
||||
return title
|
||||
|
||||
def _set_title_state(
|
||||
self, title: str | None, *, source: Literal["auto", "manual"]
|
||||
) -> None:
|
||||
if self.session_metadata is None:
|
||||
return
|
||||
|
||||
self.session_metadata.title = title
|
||||
self.session_metadata.title_source = source
|
||||
|
||||
def set_title(self, title: str | None) -> None:
|
||||
if title is None:
|
||||
self._set_title_state(None, source="auto")
|
||||
return
|
||||
|
||||
normalized_title = title.strip()
|
||||
if not normalized_title:
|
||||
raise ValueError("Session title cannot be empty.")
|
||||
|
||||
self._set_title_state(normalized_title, source="manual")
|
||||
|
||||
def _resolve_title(self, messages: Sequence[LLMMessage]) -> str | None:
|
||||
if self.session_metadata is None:
|
||||
return self._get_title(messages)
|
||||
|
||||
if self.session_metadata.title_source == "manual":
|
||||
return self.session_metadata.title
|
||||
|
||||
title = self._get_title(messages)
|
||||
self._set_title_state(title, source="auto")
|
||||
return title
|
||||
|
||||
@staticmethod
|
||||
async def persist_metadata(metadata: Any, session_dir: Path) -> None:
|
||||
temp_metadata_filepath = None
|
||||
|
|
@ -265,7 +298,7 @@ class SessionLogger:
|
|||
for tool_class in tool_manager.available_tools.values()
|
||||
]
|
||||
|
||||
title = self._get_title(messages)
|
||||
title = self._resolve_title(messages)
|
||||
system_prompt = (
|
||||
messages[0].model_dump()
|
||||
if len(messages) > 0 and messages[0].role == Role.system
|
||||
|
|
|
|||
|
|
@ -300,6 +300,22 @@ class TelemetryClient:
|
|||
payload = {"init_duration_ms": init_duration_ms}
|
||||
self.send_telemetry_event("vibe.ready", payload)
|
||||
|
||||
def send_at_mention_inserted(
|
||||
self,
|
||||
*,
|
||||
nb_mentions: int,
|
||||
context_types: dict[str, int],
|
||||
file_extensions: dict[str, int] | None,
|
||||
message_id: str | None,
|
||||
) -> None:
|
||||
payload: dict[str, Any] = {
|
||||
"nb_mentions": nb_mentions,
|
||||
"context_types": context_types,
|
||||
"file_extensions": file_extensions,
|
||||
"message_id": message_id,
|
||||
}
|
||||
self.send_telemetry_event("vibe.at_mention_inserted", payload)
|
||||
|
||||
def send_user_rating_feedback(self, rating: int, model: str) -> None:
|
||||
self.send_telemetry_event(
|
||||
"vibe.user_rating_feedback",
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@ from typing import TYPE_CHECKING, Any, ClassVar
|
|||
|
||||
import httpx
|
||||
from mistralai.client import Mistral
|
||||
from mistralai.client.models.connectorsqueryfilters import (
|
||||
ConnectorsQueryFiltersTypedDict,
|
||||
)
|
||||
|
||||
from vibe.core.logger import logger
|
||||
from vibe.core.tools.base import (
|
||||
|
|
@ -33,9 +30,7 @@ from vibe.core.utils import run_sync
|
|||
if TYPE_CHECKING:
|
||||
from vibe.core.types import ToolResultEvent
|
||||
|
||||
_LIST_PAGE_SIZE = 100
|
||||
_LIST_QUERY_FILTERS: ConnectorsQueryFiltersTypedDict = {"active": True}
|
||||
_TOOL_FETCH_TIMEOUT = 8.0
|
||||
_BOOTSTRAP_TIMEOUT = 30.0
|
||||
|
||||
|
||||
def _normalize_name(name: str) -> str:
|
||||
|
|
@ -44,25 +39,15 @@ def _normalize_name(name: str) -> str:
|
|||
return result or "unnamed"
|
||||
|
||||
|
||||
def _connector_tool_to_remote(tool: dict[str, Any] | Any) -> RemoteTool | None:
|
||||
"""Convert a ConnectorTool (SDK object or raw dict) to a RemoteTool."""
|
||||
if isinstance(tool, dict):
|
||||
def _connector_tool_to_remote(tool: dict[str, Any]) -> RemoteTool | None:
|
||||
"""Convert a bootstrap tool dict to a RemoteTool."""
|
||||
name = tool.get("name")
|
||||
description = tool.get("description")
|
||||
schema = tool.get("jsonschema") or tool.get("json_schema")
|
||||
else:
|
||||
name = getattr(tool, "name", None)
|
||||
description = getattr(tool, "description", None)
|
||||
schema = getattr(tool, "jsonschema", None) or getattr(tool, "json_schema", None)
|
||||
if not name:
|
||||
return None
|
||||
if schema is not None and not isinstance(schema, dict):
|
||||
dump = getattr(schema, "model_dump", None)
|
||||
schema = dump() if callable(dump) else None
|
||||
return RemoteTool.model_validate({
|
||||
"name": name,
|
||||
"description": description,
|
||||
"inputSchema": schema or {"type": "object", "properties": {}},
|
||||
"description": tool.get("description"),
|
||||
"inputSchema": tool.get("inputSchema") or {"type": "object", "properties": {}},
|
||||
})
|
||||
|
||||
|
||||
|
|
@ -208,6 +193,36 @@ def create_connector_proxy_tool_class(
|
|||
return ConnectorProxyTool
|
||||
|
||||
|
||||
def _deduplicate_connectors(
|
||||
connectors: list[dict[str, Any]],
|
||||
) -> list[tuple[str, str, dict[str, Any]]]:
|
||||
"""Deduplicate connectors by normalized alias, preserving order.
|
||||
|
||||
When two connectors share the same alias, disambiguate with a
|
||||
numeric suffix rather than silently dropping.
|
||||
"""
|
||||
seen_names: set[str] = set()
|
||||
result: list[tuple[str, str, dict[str, Any]]] = []
|
||||
for connector in connectors:
|
||||
connector_id = str(connector.get("id", ""))
|
||||
if not connector_id:
|
||||
continue
|
||||
raw_name = connector.get("name") or connector_id
|
||||
alias = _normalize_name(raw_name)
|
||||
if alias in seen_names:
|
||||
original = alias
|
||||
suffix = 2
|
||||
while f"{original}_{suffix}" in seen_names:
|
||||
suffix += 1
|
||||
alias = f"{original}_{suffix}"
|
||||
logger.warning(
|
||||
"Connector %r alias %r collides, using %r", raw_name, original, alias
|
||||
)
|
||||
seen_names.add(alias)
|
||||
result.append((connector_id, alias, connector))
|
||||
return result
|
||||
|
||||
|
||||
class ConnectorRegistry:
|
||||
"""Discovers connector tools from the Mistral API.
|
||||
|
||||
|
|
@ -240,6 +255,47 @@ class ConnectorRegistry:
|
|||
|
||||
return await self._discover_all()
|
||||
|
||||
async def _fetch_bootstrap(self) -> dict[str, Any]:
|
||||
base_url = self._server_url or _DEFAULT_BASE_URL
|
||||
url = f"{base_url}/v1/connectors/bootstrap"
|
||||
headers = {"Authorization": f"Bearer {self._api_key}"}
|
||||
async with httpx.AsyncClient(timeout=_BOOTSTRAP_TIMEOUT) as http:
|
||||
response = await http.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def _build_tools_for_connector(
|
||||
self,
|
||||
*,
|
||||
connector_id: str,
|
||||
alias: str,
|
||||
name: str,
|
||||
raw_tools: list[dict[str, Any]],
|
||||
) -> dict[str, type[BaseTool]]:
|
||||
tools_map: dict[str, type[BaseTool]] = {}
|
||||
for tool in raw_tools:
|
||||
remote = _connector_tool_to_remote(tool)
|
||||
if remote is None:
|
||||
continue
|
||||
try:
|
||||
proxy_cls = create_connector_proxy_tool_class(
|
||||
connector_name=name,
|
||||
connector_alias=alias,
|
||||
connector_id=connector_id,
|
||||
remote=remote,
|
||||
api_key=self._api_key,
|
||||
server_url=self._server_url,
|
||||
)
|
||||
tools_map[proxy_cls.get_name()] = proxy_cls
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to register connector tool %s for %s",
|
||||
tool.get("name", "<unknown>"),
|
||||
name,
|
||||
exc_info=True,
|
||||
)
|
||||
return tools_map
|
||||
|
||||
async def _discover_all(self) -> dict[str, type[BaseTool]]:
|
||||
async with self._discover_lock:
|
||||
# Re-check under lock — another coroutine may have finished
|
||||
|
|
@ -250,76 +306,45 @@ class ConnectorRegistry:
|
|||
result.update(tools)
|
||||
return result
|
||||
|
||||
async with self._get_client() as client:
|
||||
connectors: list[Any] = []
|
||||
|
||||
try:
|
||||
page = await client.beta.connectors.list_async(
|
||||
page_size=_LIST_PAGE_SIZE, query_filters=_LIST_QUERY_FILTERS
|
||||
)
|
||||
connectors.extend(page.items or [])
|
||||
while page.pagination and page.pagination.next_cursor:
|
||||
page = await client.beta.connectors.list_async(
|
||||
page_size=_LIST_PAGE_SIZE,
|
||||
cursor=page.pagination.next_cursor,
|
||||
query_filters=_LIST_QUERY_FILTERS,
|
||||
)
|
||||
connectors.extend(page.items or [])
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to list connectors: {exc}")
|
||||
data = await self._fetch_bootstrap()
|
||||
except Exception:
|
||||
logger.warning("Failed to bootstrap connectors", exc_info=True)
|
||||
self._cache = {}
|
||||
self._connector_names = []
|
||||
self._connector_connected = {}
|
||||
self._alias_to_id = {}
|
||||
return {}
|
||||
|
||||
# Build results locally to avoid publishing incomplete state.
|
||||
unique_connectors = _deduplicate_connectors(data.get("connectors") or [])
|
||||
|
||||
cache: dict[str, dict[str, type[BaseTool]]] = {}
|
||||
all_tools: dict[str, type[BaseTool]] = {}
|
||||
connector_names: list[str] = []
|
||||
connector_connected: dict[str, bool] = {}
|
||||
|
||||
# Deduplicate by normalized name, preserving order.
|
||||
# When two connectors share the same alias, disambiguate
|
||||
# with a numeric suffix rather than silently dropping.
|
||||
seen_names: set[str] = set()
|
||||
unique_connectors: list[tuple[str, str, Any]] = []
|
||||
for connector in connectors:
|
||||
connector_id = str(connector.id)
|
||||
raw_name = connector.name or connector_id
|
||||
alias = _normalize_name(raw_name)
|
||||
if alias in seen_names:
|
||||
original = alias
|
||||
suffix = 2
|
||||
while f"{alias}_{suffix}" in seen_names:
|
||||
suffix += 1
|
||||
alias = f"{alias}_{suffix}"
|
||||
logger.warning(
|
||||
f"Connector {raw_name!r} alias {original!r} collides, "
|
||||
f"using {alias!r}"
|
||||
)
|
||||
seen_names.add(alias)
|
||||
unique_connectors.append((connector_id, alias, connector))
|
||||
|
||||
# Fetch tools for all connectors in parallel.
|
||||
tasks = [
|
||||
self._fetch_connector_tools(client, c, alias)
|
||||
for _, alias, c in unique_connectors
|
||||
]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
for (connector_id, alias, _), tools_map in zip(
|
||||
unique_connectors, results, strict=True
|
||||
):
|
||||
for connector_id, alias, connector in unique_connectors:
|
||||
connector_names.append(alias)
|
||||
if tools_map is None:
|
||||
# Timeout or error — show in UI but don't cache so
|
||||
# a refresh will retry discovery for this connector.
|
||||
name = connector.get("name") or connector_id
|
||||
|
||||
if bootstrap_errors := connector.get("bootstrap_errors"):
|
||||
logger.warning(
|
||||
"Connector %r bootstrap errors: %s", name, bootstrap_errors
|
||||
)
|
||||
|
||||
status = connector.get("status") or {}
|
||||
if not status.get("is_ready", False):
|
||||
connector_connected[alias] = False
|
||||
continue
|
||||
|
||||
tools_map = self._build_tools_for_connector(
|
||||
connector_id=connector_id,
|
||||
alias=alias,
|
||||
name=name,
|
||||
raw_tools=connector.get("tools") or [],
|
||||
)
|
||||
cache[connector_id] = tools_map
|
||||
all_tools.update(tools_map)
|
||||
# TODO: replace with actual API field when available
|
||||
connector_connected[alias] = bool(tools_map)
|
||||
|
||||
# Publish atomically — concurrent callers waiting on the
|
||||
|
|
@ -331,77 +356,6 @@ class ConnectorRegistry:
|
|||
|
||||
return all_tools
|
||||
|
||||
async def _fetch_connector_tools(
|
||||
self, client: Mistral, connector: Any, connector_alias: str
|
||||
) -> dict[str, type[BaseTool]] | None:
|
||||
"""Fetch tools for a single connector via ``list_tools_async``.
|
||||
|
||||
The list endpoint does not always include tools inline, so we
|
||||
call ``list_tools_async`` per connector to get the full set.
|
||||
"""
|
||||
connector_id = str(connector.id)
|
||||
name = connector.name or connector_id
|
||||
|
||||
try:
|
||||
response = await asyncio.wait_for(
|
||||
client.beta.connectors.list_tools_async(
|
||||
connector_id_or_name=connector_id
|
||||
),
|
||||
timeout=_TOOL_FETCH_TIMEOUT,
|
||||
)
|
||||
except TimeoutError:
|
||||
logger.warning(
|
||||
f"Timeout fetching tools for connector {name} (>{_TOOL_FETCH_TIMEOUT}s)"
|
||||
)
|
||||
return None
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to list tools for connector {name}: {exc}")
|
||||
return None
|
||||
|
||||
# The SDK may return a plain list, an iterable wrapper, or an
|
||||
# object with a `data`/`items` attribute. Handle all cases.
|
||||
if isinstance(response, list):
|
||||
raw_tools: list[Any] = response
|
||||
elif hasattr(response, "data"):
|
||||
raw_tools = list(response.data or [])
|
||||
elif hasattr(response, "items"):
|
||||
raw_tools = list(response.items or [])
|
||||
else:
|
||||
try:
|
||||
raw_tools = list(response)
|
||||
except TypeError:
|
||||
logger.warning(
|
||||
f"Unexpected response type from list_tools_async for {name}: "
|
||||
f"{type(response).__name__}"
|
||||
)
|
||||
raw_tools = []
|
||||
|
||||
result: dict[str, type[BaseTool]] = {}
|
||||
for tool in raw_tools:
|
||||
remote = _connector_tool_to_remote(tool)
|
||||
if remote is None:
|
||||
continue
|
||||
try:
|
||||
proxy_cls = create_connector_proxy_tool_class(
|
||||
connector_name=name,
|
||||
connector_alias=connector_alias,
|
||||
connector_id=connector_id,
|
||||
remote=remote,
|
||||
api_key=self._api_key,
|
||||
server_url=self._server_url,
|
||||
)
|
||||
result[proxy_cls.get_name()] = proxy_cls
|
||||
except Exception as exc:
|
||||
tool_name = (
|
||||
tool.get("name")
|
||||
if isinstance(tool, dict)
|
||||
else getattr(tool, "name", "<unknown>")
|
||||
)
|
||||
logger.warning(
|
||||
f"Failed to register connector tool {tool_name} for {name}: {exc}"
|
||||
)
|
||||
return result
|
||||
|
||||
@property
|
||||
def connector_count(self) -> int:
|
||||
if self._cache is None:
|
||||
|
|
@ -421,6 +375,7 @@ class ConnectorRegistry:
|
|||
async def refresh_connector_async(self, alias: str) -> dict[str, type[BaseTool]]:
|
||||
"""Re-fetch tools for a single connector by alias.
|
||||
|
||||
Calls the bootstrap endpoint and extracts the matching connector.
|
||||
Updates the internal cache for that connector only. Returns
|
||||
the new tool map (empty dict on failure).
|
||||
"""
|
||||
|
|
@ -428,15 +383,27 @@ class ConnectorRegistry:
|
|||
if connector_id is None:
|
||||
return {}
|
||||
|
||||
tools_map: dict[str, type[BaseTool]] | None = None
|
||||
try:
|
||||
async with self._get_client() as client:
|
||||
connector = await client.beta.connectors.get_async(
|
||||
connector_id_or_name=connector_id
|
||||
data = await self._fetch_bootstrap()
|
||||
for connector in data.get("connectors") or []:
|
||||
if str(connector.get("id")) != connector_id:
|
||||
continue
|
||||
|
||||
name = connector.get("name") or connector_id
|
||||
status = connector.get("status") or {}
|
||||
if not status.get("is_ready", False):
|
||||
break
|
||||
|
||||
tools_map = self._build_tools_for_connector(
|
||||
connector_id=connector_id,
|
||||
alias=alias,
|
||||
name=name,
|
||||
raw_tools=connector.get("tools") or [],
|
||||
)
|
||||
tools_map = await self._fetch_connector_tools(client, connector, alias)
|
||||
break
|
||||
except Exception:
|
||||
logger.debug("Failed to refresh connector %s", alias)
|
||||
tools_map = None
|
||||
|
||||
if self._cache is None:
|
||||
self._cache = {}
|
||||
|
|
|
|||
|
|
@ -142,6 +142,8 @@ class SessionMetadata(BaseModel):
|
|||
git_branch: str | None
|
||||
environment: dict[str, str | None]
|
||||
username: str
|
||||
title: str | None = None
|
||||
title_source: Literal["auto", "manual"] = "auto"
|
||||
|
||||
|
||||
StrToolChoice = Literal["auto", "none", "any", "required"]
|
||||
|
|
|
|||
|
|
@ -1,12 +1,5 @@
|
|||
# What's new in v2.9.1
|
||||
# What's new in v2.9.4
|
||||
|
||||
- **Default model**: Migrated to `mistral-medium-3.5`.
|
||||
- **Connector OAuth**: Authenticate connectors via OAuth directly from the `/mcp` menu.
|
||||
|
||||
# What's new in v2.9.0
|
||||
- **Scratchpad directory**: Model's autonomous workspace for temporary files, intermediate results, and drafts shared with subagents
|
||||
- **`/copy` command**: New slash command to copy content directly from the conversation
|
||||
- **`--trust` flag**: Grant session-only tool permissions
|
||||
- **MCP toggle**: Enable/disable MCP servers and individual tools from the `/mcp` menu
|
||||
- **Custom compaction**: Provide custom instructions when compacting context via `/compact`
|
||||
- **Thinking level picker**: Choose thinking level with `/thinking`
|
||||
- **`/rename` command**: Rename the current session from the slash menu.
|
||||
- **Persistent "always allow"**: Tool permissions granted with "always allow" now stick across sessions.
|
||||
- **Faster bash bang commands**: `!command` runs via async subprocess.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue