v2.9.5 (#676)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: allansimon-mistral <allan.simon@ext.mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
3
.gitignore
vendored
|
|
@ -203,3 +203,6 @@ tests/playground/*
|
|||
# Profiler HTML/TXT reports (generated by vibe.cli.profiler)
|
||||
*-profile.html
|
||||
*-profile.txt
|
||||
|
||||
# Snapshot test report (generated by pytest-textual-snapshot)
|
||||
snapshot_report.html
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ Always go through `uv` — never invoke bare `python` or `pip`.
|
|||
## Logging & errors
|
||||
|
||||
- Use `from vibe.core.logger import logger` — stdlib `logging` with `StructuredLogFormatter`, not `structlog`.
|
||||
- Configure via env: `LOG_LEVEL` (default `WARNING`), `DEBUG_MODE`, `LOG_MAX_BYTES`. Logs land in `~/.vibe/logs/vibe.log`.
|
||||
- Configure via env: `LOG_LEVEL` (default `WARNING`), `LOG_MAX_BYTES`. Logs land in `~/.vibe/logs/vibe.log`.
|
||||
- Pass variables as keyword args, not interpolated into the message: prefer `logger.error("Failed to fetch", url=url)` over `logger.error(f"Failed to fetch {url}")`.
|
||||
- Define module-local exception hierarchies. Always chain with `raise NewError(...) from e`. Rich exceptions expose a `_fmt()` helper for human-readable output.
|
||||
|
||||
|
|
|
|||
25
CHANGELOG.md
|
|
@ -5,6 +5,31 @@ 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.5] - 2026-05-06
|
||||
|
||||
### Added
|
||||
|
||||
- `/loop` command to run a prompt or slash command on a recurring interval
|
||||
- `default_agent` config setting
|
||||
- Telemetry instrumentation for the `teleport` command
|
||||
- Logging environment variables documented in `--help`
|
||||
|
||||
### Changed
|
||||
|
||||
- `enable_telemetry` now takes precedence over `enable_otel`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Non-retryable errors raised by sub-activities now correctly stop the retry when the agent loop runs inside a Temporal activity (deepens the 2.9.4 fix to walk the exception cause chain)
|
||||
- Compacted session IDs displayed correctly
|
||||
- Reload the history file before writing so parallel instances don't clobber each other
|
||||
- `read_file` flagged as truncated when the limit is reached
|
||||
- CLI loader left-alignment
|
||||
- Default scroll sensitivity in the TUI restored
|
||||
- Loosened the "no git commit" constraint
|
||||
- Ensure `enable_telemetry` takes precedence over `enable_otel`
|
||||
|
||||
|
||||
## [2.9.4] - 2026-05-05
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ Logs are written to `~/.vibe/logs/vibe.log` by default. Control logging via envi
|
|||
|----------|-------------|---------|
|
||||
| `LOG_LEVEL` | Logging level (`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`) | `WARNING` |
|
||||
| `LOG_MAX_BYTES` | Max log file size in bytes before rotation | `10485760` (10 MB) |
|
||||
| `DEBUG_MODE` | When `true`, forces `DEBUG` level | - |
|
||||
| `DEBUG_MODE` | When `true`, forces `DEBUG` logging (and attaches `debugpy` on `localhost:5678` under `vibe-acp`) | - |
|
||||
|
||||
Example:
|
||||
|
||||
|
|
|
|||
16
README.md
|
|
@ -123,6 +123,22 @@ Use the `--agent` flag to select a different agent:
|
|||
vibe --agent plan
|
||||
```
|
||||
|
||||
To change the default agent used when `--agent` is not passed, set
|
||||
`default_agent` in your `config.toml`:
|
||||
|
||||
```toml
|
||||
default_agent = "plan"
|
||||
```
|
||||
|
||||
Valid values are `default`, `plan`, `accept-edits`, `auto-approve`,
|
||||
`lean` (only when listed in `installed_agents`), or the name of any
|
||||
custom agent file in `~/.vibe/agents/` or the project's `.vibe/agents/`
|
||||
directory. Subagents such as `explore` are not accepted.
|
||||
|
||||
> Note: `default_agent` only applies to interactive sessions. In
|
||||
> programmatic mode (`-p` / `--prompt`), Vibe falls back to `auto-approve`
|
||||
> when `--agent` is not provided, so `default_agent` is ignored.
|
||||
|
||||
### Subagents and Task Delegation
|
||||
|
||||
Vibe supports subagents for delegating tasks. Subagents run independently and can perform specialized work without user interaction, preventing the context from being overloaded.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
id = "mistral-vibe"
|
||||
name = "Mistral Vibe"
|
||||
description = "Mistral's open-source coding assistant"
|
||||
version = "2.9.4"
|
||||
version = "2.9.5"
|
||||
schema_version = 1
|
||||
authors = ["Mistral AI"]
|
||||
repository = "https://github.com/mistralai/mistral-vibe"
|
||||
|
|
@ -11,21 +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.4/vibe-acp-darwin-aarch64-2.9.4.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.5/vibe-acp-darwin-aarch64-2.9.5.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.darwin-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.4/vibe-acp-darwin-x86_64-2.9.4.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.5/vibe-acp-darwin-x86_64-2.9.5.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.linux-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.4/vibe-acp-linux-aarch64-2.9.4.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.5/vibe-acp-linux-aarch64-2.9.5.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.linux-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.4/vibe-acp-linux-x86_64-2.9.4.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.5/vibe-acp-linux-x86_64-2.9.5.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.windows-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.4/vibe-acp-windows-x86_64-2.9.4.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.9.5/vibe-acp-windows-x86_64-2.9.5.zip"
|
||||
cmd = "./vibe-acp.exe"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "mistral-vibe"
|
||||
version = "2.9.4"
|
||||
version = "2.9.5"
|
||||
description = "Minimal CLI coding agent by Mistral"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
|
|
|||
|
|
@ -101,6 +101,11 @@ def _build_env(vibe_home_dir: Path, *, include_api_key: bool) -> dict[str, str]:
|
|||
env["PYTHONUNBUFFERED"] = "1"
|
||||
env["VIBE_HOME"] = str(vibe_home_dir)
|
||||
|
||||
vibe_home_dir.mkdir(parents=True, exist_ok=True)
|
||||
config_file = vibe_home_dir / "config.toml"
|
||||
if not config_file.exists():
|
||||
config_file.write_text("enable_telemetry = false\n")
|
||||
|
||||
if include_api_key:
|
||||
env["MISTRAL_API_KEY"] = "mock"
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
|||
class TestCloseSession:
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_session_removes_session_and_closes_resources(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
self, acp_agent_loop: VibeAcpAgentLoop, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
session_response = await acp_agent_loop.new_session(cwd=".", mcp_servers=[])
|
||||
session = acp_agent_loop.sessions[session_response.session_id]
|
||||
|
|
@ -30,6 +30,17 @@ class TestCloseSession:
|
|||
assert session_response.session_id not in acp_agent_loop.sessions
|
||||
backend_close.assert_awaited_once()
|
||||
telemetry_close.assert_awaited_once()
|
||||
session_closed_events = [
|
||||
event
|
||||
for event in telemetry_events
|
||||
if event["event_name"] == "vibe.session_closed"
|
||||
]
|
||||
assert len(session_closed_events) == 1
|
||||
assert (
|
||||
session_closed_events[0]["properties"]["session_id"]
|
||||
== session_response.session_id
|
||||
)
|
||||
assert session_closed_events[0]["properties"]["agent_entrypoint"] == "acp"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_session_cancels_active_prompt(
|
||||
|
|
@ -69,6 +80,23 @@ class TestCloseSession:
|
|||
|
||||
assert bg_task.cancelled()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_emit_session_closed_for_active_sessions(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
session1 = await acp_agent_loop.new_session(cwd=".", mcp_servers=[])
|
||||
session2 = await acp_agent_loop.new_session(cwd=".", mcp_servers=[])
|
||||
|
||||
await acp_agent_loop.emit_session_closed_for_active_sessions()
|
||||
|
||||
session_closed_events = [
|
||||
e for e in telemetry_events if e["event_name"] == "vibe.session_closed"
|
||||
]
|
||||
emitted_ids = {e["properties"]["session_id"] for e in session_closed_events}
|
||||
assert len(session_closed_events) == 2
|
||||
assert session1.session_id in emitted_ids
|
||||
assert session2.session_id in emitted_ids
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_session_rejects_new_background_tasks(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from tests.stubs.fake_backend import FakeBackend
|
|||
from tests.stubs.fake_client import FakeClient
|
||||
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
from vibe.core.session.session_id import shorten_session_id
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -79,3 +80,10 @@ class TestCompactEventHandling:
|
|||
assert compact_end.status == "completed"
|
||||
|
||||
assert compact_start.tool_call_id == compact_end.tool_call_id
|
||||
assert compact_end.content is not None
|
||||
compact_end_text = compact_end.content[0].content
|
||||
assert isinstance(compact_end_text, TextContentBlock)
|
||||
assert shorten_session_id(session_response.session_id) in compact_end_text.text
|
||||
assert (
|
||||
shorten_session_id(session.agent_loop.session_id) in compact_end_text.text
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.4"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.5"
|
||||
)
|
||||
|
||||
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.4"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.5"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
|
|
@ -92,20 +92,26 @@ class TestMultiSessionCore:
|
|||
)
|
||||
assert user_message1 is not None
|
||||
assert user_message1.content == "Prompt for session 1"
|
||||
assistant_message1 = next(
|
||||
(msg for msg in session1.agent_loop.messages if msg.role == Role.assistant),
|
||||
None,
|
||||
)
|
||||
assert assistant_message1 is not None
|
||||
assert assistant_message1.content == "Response 1"
|
||||
user_message2 = next(
|
||||
(msg for msg in session2.agent_loop.messages if msg.role == Role.user), None
|
||||
)
|
||||
assert user_message2 is not None
|
||||
assert user_message2.content == "Prompt for session 2"
|
||||
|
||||
# Backend stream order is non-deterministic under asyncio.gather, so
|
||||
# assert that both sessions received distinct responses from the
|
||||
# expected set rather than pinning a specific assignment.
|
||||
assistant_message1 = next(
|
||||
(msg for msg in session1.agent_loop.messages if msg.role == Role.assistant),
|
||||
None,
|
||||
)
|
||||
assistant_message2 = next(
|
||||
(msg for msg in session2.agent_loop.messages if msg.role == Role.assistant),
|
||||
None,
|
||||
)
|
||||
assert assistant_message1 is not None
|
||||
assert assistant_message2 is not None
|
||||
assert assistant_message2.content == "Response 2"
|
||||
assert {assistant_message1.content, assistant_message2.content} == {
|
||||
"Response 1",
|
||||
"Response 2",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,6 +199,28 @@ def test_callable_entries_updates_completions_dynamically() -> None:
|
|||
assert suggestions[0].description == "Summarize the conversation"
|
||||
|
||||
|
||||
def test_tab_on_slash_command_with_args_replaces_only_head() -> None:
|
||||
controller, view = make_controller()
|
||||
text = "/compact some args"
|
||||
controller.on_text_changed(text, cursor_index=len(text))
|
||||
|
||||
result = controller.on_key(key_event("tab"), text=text, cursor_index=len(text))
|
||||
|
||||
assert result is CompletionResult.HANDLED
|
||||
assert view.replacements == [Replacement(0, 8, "/compact")]
|
||||
|
||||
|
||||
def test_enter_on_slash_command_with_args_submits_with_head_only_replacement() -> None:
|
||||
controller, view = make_controller()
|
||||
text = "/compact some args"
|
||||
controller.on_text_changed(text, cursor_index=len(text))
|
||||
|
||||
result = controller.on_key(key_event("enter"), text=text, cursor_index=len(text))
|
||||
|
||||
assert result is CompletionResult.SUBMIT
|
||||
assert view.replacements == [Replacement(0, 8, "/compact")]
|
||||
|
||||
|
||||
def test_callable_entries_reflects_enabled_disabled_skills() -> None:
|
||||
"""Test that skill enable/disable changes are reflected in completions.
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ async def test_popup_hides_when_input_cleared(vibe_app: VibeApp) -> None:
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pressing_tab_writes_selected_command_and_hides_popup(
|
||||
async def test_pressing_tab_writes_selected_command_and_leaves_popup_visible(
|
||||
vibe_app: VibeApp,
|
||||
) -> None:
|
||||
async with vibe_app.run_test() as pilot:
|
||||
|
|
@ -53,7 +53,7 @@ async def test_pressing_tab_writes_selected_command_and_hides_popup(
|
|||
await pilot.press("tab")
|
||||
|
||||
assert chat_input.value == "/config"
|
||||
assert popup.styles.display == "none"
|
||||
assert popup.styles.display == "block"
|
||||
|
||||
|
||||
def ensure_selected_command(popup: CompletionPopup, expected_alias: str) -> None:
|
||||
|
|
|
|||
|
|
@ -133,3 +133,13 @@ class TestCommandRegistry:
|
|||
assert result is not None
|
||||
_, cmd, _ = result
|
||||
assert cmd.handler == "_show_data_retention"
|
||||
|
||||
def test_loop_command_registration(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
assert registry.get_command_name("/loop") == "loop"
|
||||
result = registry.parse_command("/loop 30s ping")
|
||||
assert result is not None
|
||||
cmd_name, cmd, cmd_args = result
|
||||
assert cmd_name == "loop"
|
||||
assert cmd.handler == "_loop_command"
|
||||
assert cmd_args == "30s ping"
|
||||
|
|
|
|||
28
tests/cli/test_compact_message.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from vibe.cli.textual_ui.widgets.compact import CompactMessage
|
||||
from vibe.core.session.session_id import shorten_session_id
|
||||
|
||||
|
||||
class TestCompactMessage:
|
||||
def test_get_content_includes_session_ids_after_compaction(self) -> None:
|
||||
message = CompactMessage()
|
||||
message.post_message = MagicMock()
|
||||
|
||||
message.set_complete(
|
||||
old_tokens=177_017,
|
||||
new_tokens=23_263,
|
||||
old_session_id="11111111-1111-1111-1111-111111111111",
|
||||
new_session_id="22222222-2222-2222-2222-222222222222",
|
||||
)
|
||||
|
||||
assert message.get_content() == (
|
||||
"Compaction complete: 177,017 → 23,263 tokens (-87.%)\n"
|
||||
"session: "
|
||||
f"{shorten_session_id('11111111-1111-1111-1111-111111111111')} "
|
||||
"(before compaction) → "
|
||||
f"{shorten_session_id('22222222-2222-2222-2222-222222222222')} "
|
||||
"(after compaction)"
|
||||
)
|
||||
55
tests/cli/test_initial_agent_name.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
from vibe.cli.cli import get_initial_agent_name
|
||||
from vibe.core.agents.models import BuiltinAgentName
|
||||
from vibe.core.config import VibeConfig
|
||||
|
||||
|
||||
def _make_args(*, agent: str | None, prompt: str | None) -> argparse.Namespace:
|
||||
return argparse.Namespace(agent=agent, prompt=prompt)
|
||||
|
||||
|
||||
def test_uses_args_agent_when_provided() -> None:
|
||||
config = VibeConfig.model_construct(default_agent=BuiltinAgentName.PLAN)
|
||||
args = _make_args(agent="accept-edits", prompt=None)
|
||||
|
||||
assert get_initial_agent_name(args, config) == "accept-edits"
|
||||
|
||||
|
||||
def test_falls_back_to_config_default_agent_when_args_agent_is_none() -> None:
|
||||
config = VibeConfig.model_construct(default_agent=BuiltinAgentName.PLAN)
|
||||
args = _make_args(agent=None, prompt=None)
|
||||
|
||||
assert get_initial_agent_name(args, config) == BuiltinAgentName.PLAN
|
||||
|
||||
|
||||
def test_defaults_to_default_when_unset_in_config_and_args() -> None:
|
||||
config = VibeConfig.model_construct()
|
||||
args = _make_args(agent=None, prompt=None)
|
||||
|
||||
assert get_initial_agent_name(args, config) == BuiltinAgentName.DEFAULT
|
||||
|
||||
|
||||
def test_programmatic_mode_promotes_default_to_auto_approve() -> None:
|
||||
config = VibeConfig.model_construct(default_agent=BuiltinAgentName.DEFAULT)
|
||||
args = _make_args(agent=None, prompt="hello")
|
||||
|
||||
assert get_initial_agent_name(args, config) == BuiltinAgentName.AUTO_APPROVE
|
||||
|
||||
|
||||
def test_programmatic_mode_ignores_config_default_agent_when_args_agent_is_none() -> (
|
||||
None
|
||||
):
|
||||
config = VibeConfig.model_construct(default_agent=BuiltinAgentName.PLAN)
|
||||
args = _make_args(agent=None, prompt="hello")
|
||||
|
||||
assert get_initial_agent_name(args, config) == BuiltinAgentName.AUTO_APPROVE
|
||||
|
||||
|
||||
def test_programmatic_mode_keeps_explicit_agent_arg() -> None:
|
||||
config = VibeConfig.model_construct(default_agent=BuiltinAgentName.DEFAULT)
|
||||
args = _make_args(agent="accept-edits", prompt="hello")
|
||||
|
||||
assert get_initial_agent_name(args, config) == "accept-edits"
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -10,7 +11,7 @@ from tests.conftest import build_test_vibe_app, build_test_vibe_config
|
|||
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIPlanType, WhoAmIResponse
|
||||
from vibe.cli.textual_ui.widgets.chat_input import ChatInputContainer
|
||||
from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig
|
||||
from vibe.core.types import Backend
|
||||
from vibe.core.types import Backend, LLMMessage, Role
|
||||
|
||||
|
||||
def _chat_plan_gateway(*, prompt_switching_to_pro_plan: bool) -> FakeWhoAmIGateway:
|
||||
|
|
@ -36,6 +37,16 @@ async def _wait_until(pause, predicate, timeout: float = 2.0) -> None:
|
|||
raise AssertionError("Condition was not met within the timeout")
|
||||
|
||||
|
||||
def _teleport_failed_events(
|
||||
telemetry_events: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
return [
|
||||
event
|
||||
for event in telemetry_events
|
||||
if event["event_name"] == "vibe.teleport_failed"
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_command_visible_for_paid_chat_users() -> None:
|
||||
app = build_test_vibe_app(
|
||||
|
|
@ -56,6 +67,81 @@ async def test_teleport_command_visible_for_paid_chat_users() -> None:
|
|||
assert "&" in input_widget.mode_characters
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_command_without_history_sends_early_failure_telemetry(
|
||||
telemetry_events: list[dict[str, Any]],
|
||||
) -> None:
|
||||
app = build_test_vibe_app(
|
||||
config=_vibe_code_enabled_config(),
|
||||
plan_offer_gateway=_chat_plan_gateway(prompt_switching_to_pro_plan=False),
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: app.commands.get_command_name("/teleport") == "teleport",
|
||||
)
|
||||
|
||||
await app.on_chat_input_container_submitted(
|
||||
ChatInputContainer.Submitted("/teleport")
|
||||
)
|
||||
|
||||
assert _teleport_failed_events(telemetry_events) == [
|
||||
{
|
||||
"event_name": "vibe.teleport_failed",
|
||||
"properties": {
|
||||
"stage": "no_history",
|
||||
"error_class": "TeleportNoHistoryError",
|
||||
"push_required": False,
|
||||
"github_auth_required": False,
|
||||
"nb_session_messages": 0,
|
||||
"session_id": app.agent_loop.session_id,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_command_in_remote_session_sends_early_failure_telemetry(
|
||||
telemetry_events: list[dict[str, Any]],
|
||||
) -> None:
|
||||
app = build_test_vibe_app(
|
||||
config=_vibe_code_enabled_config(),
|
||||
plan_offer_gateway=_chat_plan_gateway(prompt_switching_to_pro_plan=False),
|
||||
)
|
||||
app.agent_loop.messages.append(LLMMessage(role=Role.user, content="hello"))
|
||||
app.agent_loop.messages.append(LLMMessage(role=Role.assistant, content="hi"))
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: app.commands.get_command_name("/teleport") == "teleport",
|
||||
)
|
||||
|
||||
await app._remote_manager.attach(session_id="remote-session", config=app.config)
|
||||
await app.on_chat_input_container_submitted(
|
||||
ChatInputContainer.Submitted("/teleport")
|
||||
)
|
||||
await _wait_until(
|
||||
pilot.pause, lambda: len(_teleport_failed_events(telemetry_events)) == 1
|
||||
)
|
||||
await app._remote_manager.detach()
|
||||
|
||||
assert _teleport_failed_events(telemetry_events) == [
|
||||
{
|
||||
"event_name": "vibe.teleport_failed",
|
||||
"properties": {
|
||||
"stage": "remote_session",
|
||||
"error_class": "TeleportRemoteSessionError",
|
||||
"push_required": False,
|
||||
"github_auth_required": False,
|
||||
"nb_session_messages": 2,
|
||||
"session_id": app.agent_loop.session_id,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_command_hidden_when_current_key_is_not_eligible() -> None:
|
||||
app = build_test_vibe_app(
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ def get_base_config() -> dict[str, Any]:
|
|||
}
|
||||
],
|
||||
"enable_auto_update": False,
|
||||
"enable_telemetry": False,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -174,7 +175,8 @@ def telemetry_events(monkeypatch: pytest.MonkeyPatch) -> list[dict[str, Any]]:
|
|||
*,
|
||||
correlation_id: str | None = None,
|
||||
) -> None:
|
||||
event: dict[str, Any] = {"event_name": event_name, "properties": properties}
|
||||
merged = self.build_client_event_metadata() | properties
|
||||
event: dict[str, Any] = {"event_name": event_name, "properties": merged}
|
||||
if correlation_id is not None:
|
||||
event["correlation_id"] = correlation_id
|
||||
events.append(event)
|
||||
|
|
|
|||
|
|
@ -104,3 +104,19 @@ class TestAgentManager:
|
|||
)
|
||||
manager = AgentManager(lambda: config, initial_agent="plan")
|
||||
assert manager.active_profile.name == "plan"
|
||||
|
||||
def test_initial_agent_raises_when_agent_is_disabled(self) -> None:
|
||||
config = build_test_vibe_config(
|
||||
include_project_context=False,
|
||||
include_prompt_detail=False,
|
||||
disabled_agents=["plan"],
|
||||
)
|
||||
with pytest.raises(ValueError, match="not available"):
|
||||
AgentManager(lambda: config, initial_agent="plan")
|
||||
|
||||
def test_initial_agent_raises_when_agent_does_not_exist(self) -> None:
|
||||
config = build_test_vibe_config(
|
||||
include_project_context=False, include_prompt_detail=False
|
||||
)
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
AgentManager(lambda: config, initial_agent="nonexistent-agent")
|
||||
|
|
|
|||
304
tests/core/test_loop.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.loop import (
|
||||
MAX_LOOPS_PER_SESSION,
|
||||
MIN_INTERVAL_SECONDS,
|
||||
LoopError,
|
||||
LoopErrorResult,
|
||||
LoopListResult,
|
||||
LoopManager,
|
||||
LoopOkResult,
|
||||
ScheduledLoop,
|
||||
format_duration,
|
||||
parse_interval,
|
||||
)
|
||||
from vibe.core.session.session_logger import SessionLogger
|
||||
|
||||
|
||||
class FakeMetadata:
|
||||
def __init__(self) -> None:
|
||||
self.loops: list[ScheduledLoop] = []
|
||||
|
||||
|
||||
class FakeSessionLogger:
|
||||
def __init__(self) -> None:
|
||||
self.session_metadata = FakeMetadata()
|
||||
self.persisted: list[list[ScheduledLoop]] = []
|
||||
|
||||
async def persist_loops(self) -> None:
|
||||
self.persisted.append([*self.session_metadata.loops])
|
||||
|
||||
|
||||
class RaisingSessionLogger:
|
||||
def __init__(self) -> None:
|
||||
self.session_metadata = FakeMetadata()
|
||||
|
||||
async def persist_loops(self) -> None:
|
||||
raise RuntimeError("disk on fire")
|
||||
|
||||
|
||||
def _build_manager() -> tuple[LoopManager, FakeSessionLogger]:
|
||||
fake = FakeSessionLogger()
|
||||
return LoopManager(cast(SessionLogger, fake)), fake
|
||||
|
||||
|
||||
class TestParseInterval:
|
||||
def test_parses_supported_units(self) -> None:
|
||||
assert parse_interval("30s") == 30
|
||||
assert parse_interval("5m") == 300
|
||||
assert parse_interval("2h") == 7200
|
||||
assert parse_interval("1d") == 86400
|
||||
|
||||
def test_parses_case_insensitively(self) -> None:
|
||||
assert parse_interval("30S") == 30
|
||||
assert parse_interval("2H") == 7200
|
||||
|
||||
def test_strips_whitespace(self) -> None:
|
||||
assert parse_interval(" 30s ") == 30
|
||||
|
||||
@pytest.mark.parametrize("bad", ["", "5", "5x", "-5m", "5 m", "1.5m", "abc"])
|
||||
def test_rejects_invalid_inputs(self, bad: str) -> None:
|
||||
with pytest.raises(LoopError):
|
||||
parse_interval(bad)
|
||||
|
||||
def test_rejects_below_minimum(self) -> None:
|
||||
with pytest.raises(LoopError, match=f"at least {MIN_INTERVAL_SECONDS}"):
|
||||
parse_interval("29s")
|
||||
|
||||
|
||||
class TestFormatInterval:
|
||||
@pytest.mark.parametrize("text", ["30s", "5m", "2h", "1d"])
|
||||
def test_round_trip_with_parse(self, text: str) -> None:
|
||||
assert format_duration(parse_interval(text)) == text
|
||||
|
||||
def test_formats_non_round_values(self) -> None:
|
||||
assert format_duration(90) == "1m30s"
|
||||
assert format_duration(180) == "3m"
|
||||
assert format_duration(7200) == "2h"
|
||||
assert format_duration(259200) == "3d"
|
||||
|
||||
def test_formats_short_version(self) -> None:
|
||||
assert format_duration(90, short=True) == "1m"
|
||||
assert format_duration(180, short=True) == "3m"
|
||||
assert format_duration(190, short=True) == "3m"
|
||||
assert format_duration(8000, short=True) == "2h"
|
||||
assert format_duration(260000, short=True) == "3d"
|
||||
|
||||
|
||||
class TestLoopManagerHandleCommand:
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_returns_loops_list(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command("")
|
||||
assert isinstance(result, LoopListResult)
|
||||
assert result.loops == []
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("verb", ["list", "ls"])
|
||||
async def test_list_alias_returns_loops_list(self, verb: str) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command(verb)
|
||||
assert isinstance(result, LoopListResult)
|
||||
assert result.loops == []
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_success_persists_and_returns_id(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command("1m hello world")
|
||||
assert isinstance(result, LoopOkResult)
|
||||
assert "every 1m" in result.message
|
||||
assert "hello world" in result.message
|
||||
assert len(manager.loops) == 1
|
||||
loop = manager.loops[0]
|
||||
assert loop.interval_seconds == 60
|
||||
assert loop.prompt == "hello world"
|
||||
assert result.message.count(loop.id) >= 1
|
||||
assert len(fake.persisted) == 1
|
||||
assert fake.persisted[0][0].id == loop.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_bad_interval_returns_error_no_persist(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command("nope hello")
|
||||
assert isinstance(result, LoopErrorResult)
|
||||
assert "Invalid interval" in result.message
|
||||
assert "Usage:" not in result.message
|
||||
assert manager.loops == []
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_empty_prompt_returns_error_no_persist(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command("30s ")
|
||||
assert isinstance(result, LoopErrorResult)
|
||||
assert "Missing prompt" in result.message
|
||||
assert manager.loops == []
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_prompt_starting_with_slash_returns_error(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command("30s /config")
|
||||
assert isinstance(result, LoopErrorResult)
|
||||
assert "cannot start with '/'" in result.message
|
||||
assert manager.loops == []
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_over_limit_returns_error(self) -> None:
|
||||
manager, _ = _build_manager()
|
||||
for i in range(MAX_LOOPS_PER_SESSION):
|
||||
result = await manager.handle_command(f"30s prompt {i}")
|
||||
assert isinstance(result, LoopOkResult)
|
||||
result = await manager.handle_command("30s overflow")
|
||||
assert isinstance(result, LoopErrorResult)
|
||||
assert "limit" in result.message.lower()
|
||||
assert len(manager.loops) == MAX_LOOPS_PER_SESSION
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_missing_target_is_error_no_persist(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
result = await manager.handle_command("cancel")
|
||||
assert isinstance(result, LoopErrorResult)
|
||||
assert "Missing loop id" in result.message
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_unknown_id_is_error_no_persist(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
fake.persisted.clear()
|
||||
result = await manager.handle_command("cancel deadbeef")
|
||||
assert isinstance(result, LoopErrorResult)
|
||||
assert "deadbeef" in result.message
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_known_id_persists(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
loop_id = manager.loops[0].id
|
||||
fake.persisted.clear()
|
||||
result = await manager.handle_command(f"cancel {loop_id}")
|
||||
assert isinstance(result, LoopOkResult)
|
||||
assert loop_id in result.message
|
||||
assert "ping" in result.message
|
||||
assert manager.loops == []
|
||||
assert fake.persisted == [[]]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_all_persists(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
await manager.handle_command("30s a")
|
||||
await manager.handle_command("30s b")
|
||||
fake.persisted.clear()
|
||||
result = await manager.handle_command("cancel all")
|
||||
assert isinstance(result, LoopOkResult)
|
||||
assert "2 scheduled loop(s)" in result.message
|
||||
assert manager.loops == []
|
||||
assert fake.persisted == [[]]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("verb", ["rm", "stop", "delete"])
|
||||
async def test_cancel_alias_verbs_work(self, verb: str) -> None:
|
||||
manager, _ = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
loop_id = manager.loops[0].id
|
||||
result = await manager.handle_command(f"{verb} {loop_id}")
|
||||
assert isinstance(result, LoopOkResult)
|
||||
assert manager.loops == []
|
||||
|
||||
|
||||
class TestLoopManagerPopDue:
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_none_when_empty(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
assert await manager.pop_due() is None
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_none_when_nothing_due(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
fake.persisted.clear()
|
||||
result = await manager.pop_due(now=0.0)
|
||||
assert result is None
|
||||
assert fake.persisted == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_due_loop_advances_and_persists(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
fake.persisted.clear()
|
||||
loop = manager.loops[0]
|
||||
original_next = loop.next_fire_at
|
||||
due = await manager.pop_due(now=original_next + 100.0)
|
||||
assert due is not None
|
||||
assert due.id == loop.id
|
||||
assert manager.loops[0].next_fire_at == original_next + 100.0 + 30.0
|
||||
assert len(fake.persisted) == 1
|
||||
|
||||
|
||||
class TestLoopManagerNextDueIn:
|
||||
def test_inf_when_empty(self) -> None:
|
||||
manager, _ = _build_manager()
|
||||
assert manager.next_due_in() == math.inf
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_correct_delta_when_populated(self) -> None:
|
||||
manager, _ = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
loop = manager.loops[0]
|
||||
delta = manager.next_due_in(now=loop.next_fire_at - 7.0)
|
||||
assert delta == pytest.approx(7.0)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_zero_when_overdue(self) -> None:
|
||||
manager, _ = _build_manager()
|
||||
await manager.handle_command("30s ping")
|
||||
loop = manager.loops[0]
|
||||
delta = manager.next_due_in(now=loop.next_fire_at + 100.0)
|
||||
assert delta == 0.0
|
||||
|
||||
|
||||
class TestLoopManagerRestore:
|
||||
def test_replaces_in_memory_list_without_persist(self) -> None:
|
||||
manager, fake = _build_manager()
|
||||
loops = [
|
||||
ScheduledLoop(
|
||||
id="aabbccdd",
|
||||
interval_seconds=30,
|
||||
prompt="x",
|
||||
next_fire_at=1.0,
|
||||
created_at=0.0,
|
||||
),
|
||||
ScheduledLoop(
|
||||
id="11223344",
|
||||
interval_seconds=60,
|
||||
prompt="y",
|
||||
next_fire_at=2.0,
|
||||
created_at=0.0,
|
||||
),
|
||||
]
|
||||
manager.restore(loops)
|
||||
assert [loop.id for loop in manager.loops] == ["aabbccdd", "11223344"]
|
||||
assert fake.persisted == []
|
||||
|
||||
|
||||
class TestLoopManagerPersisterErrors:
|
||||
@pytest.mark.asyncio
|
||||
async def test_persister_exception_does_not_propagate(self) -> None:
|
||||
manager = LoopManager(cast(SessionLogger, RaisingSessionLogger()))
|
||||
result = await manager.handle_command("30s ping")
|
||||
assert isinstance(result, LoopOkResult)
|
||||
assert len(manager.loops) == 1
|
||||
loop = manager.loops[0]
|
||||
due = await manager.pop_due(now=loop.next_fire_at + 1.0)
|
||||
assert due is not None
|
||||
|
|
@ -362,6 +362,48 @@ class TestTelemetryClient:
|
|||
assert telemetry_events[1]["properties"]["command"] == "my_skill"
|
||||
assert telemetry_events[1]["properties"]["command_type"] == "skill"
|
||||
|
||||
def test_send_teleport_completed_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_teleport_completed(
|
||||
push_required=True, github_auth_required=False, nb_session_messages=4
|
||||
)
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
assert telemetry_events[0]["event_name"] == "vibe.teleport_completed"
|
||||
assert telemetry_events[0]["properties"] == {
|
||||
"push_required": True,
|
||||
"github_auth_required": False,
|
||||
"nb_session_messages": 4,
|
||||
}
|
||||
|
||||
def test_send_teleport_failed_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_teleport_failed(
|
||||
stage="push",
|
||||
error_class="ServiceTeleportError",
|
||||
push_required=True,
|
||||
github_auth_required=False,
|
||||
nb_session_messages=4,
|
||||
)
|
||||
|
||||
assert len(telemetry_events) == 1
|
||||
assert telemetry_events[0]["event_name"] == "vibe.teleport_failed"
|
||||
assert telemetry_events[0]["properties"] == {
|
||||
"stage": "push",
|
||||
"error_class": "ServiceTeleportError",
|
||||
"push_required": True,
|
||||
"github_auth_required": False,
|
||||
"nb_session_messages": 4,
|
||||
}
|
||||
|
||||
def test_send_new_session_payload(
|
||||
self, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
|
|
@ -393,6 +435,55 @@ class TestTelemetryClient:
|
|||
assert properties["terminal_emulator"] == "vscode"
|
||||
assert "version" in properties
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_session_closed_payload(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
TelemetryClient, "send_telemetry_event", _original_send_telemetry_event
|
||||
)
|
||||
config = build_test_vibe_config(enable_telemetry=True)
|
||||
env_key = config.get_active_provider().api_key_env_var
|
||||
monkeypatch.setenv(env_key, "sk-test")
|
||||
client = TelemetryClient(
|
||||
config_getter=lambda: config,
|
||||
session_id_getter=lambda: "current-session",
|
||||
parent_session_id_getter=lambda: "current-parent-session",
|
||||
entrypoint_metadata_getter=lambda: EntrypointMetadata(
|
||||
agent_entrypoint="cli",
|
||||
agent_version="1.0.0",
|
||||
client_name="vibe_cli",
|
||||
client_version="1.0.0",
|
||||
),
|
||||
)
|
||||
mock_post = AsyncMock(return_value=MagicMock(status_code=204))
|
||||
client._client = MagicMock()
|
||||
client._client.post = mock_post
|
||||
client._client.aclose = AsyncMock()
|
||||
|
||||
client.send_session_closed()
|
||||
await client.aclose()
|
||||
|
||||
mock_post.assert_called_once_with(
|
||||
"https://api.mistral.ai/v1/datalake/events",
|
||||
json={
|
||||
"event": "vibe.session_closed",
|
||||
"properties": {
|
||||
"agent_entrypoint": "cli",
|
||||
"agent_version": "1.0.0",
|
||||
"client_name": "vibe_cli",
|
||||
"client_version": "1.0.0",
|
||||
"session_id": "current-session",
|
||||
"parent_session_id": "current-parent-session",
|
||||
},
|
||||
},
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer sk-test",
|
||||
"User-Agent": get_user_agent(Backend.MISTRAL),
|
||||
},
|
||||
)
|
||||
|
||||
def test_build_base_metadata_includes_entrypoint_and_session(self) -> None:
|
||||
metadata = build_base_metadata(
|
||||
entrypoint_metadata=EntrypointMetadata(
|
||||
|
|
|
|||
231
tests/core/test_teleport_telemetry.py
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.agent_loop import AgentLoop, TeleportError
|
||||
from vibe.core.teleport.errors import ServiceTeleportError
|
||||
from vibe.core.teleport.teleport import TeleportService
|
||||
from vibe.core.teleport.types import (
|
||||
TeleportAuthRequiredEvent,
|
||||
TeleportCheckingGitEvent,
|
||||
TeleportCompleteEvent,
|
||||
TeleportFetchingUrlEvent,
|
||||
TeleportPushingEvent,
|
||||
TeleportPushRequiredEvent,
|
||||
TeleportPushResponseEvent,
|
||||
TeleportStartingWorkflowEvent,
|
||||
TeleportWaitingForGitHubEvent,
|
||||
)
|
||||
from vibe.core.types import LLMMessage, Role
|
||||
|
||||
|
||||
def _set_teleport_service(agent_loop: AgentLoop, service: object) -> None:
|
||||
agent_loop._teleport_service = cast(TeleportService, service)
|
||||
|
||||
|
||||
class TestTeleportAgentLoopTelemetry:
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_to_vibe_code_sends_completed_success(
|
||||
self, agent_loop: AgentLoop, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
class FakeTeleportService:
|
||||
async def __aenter__(self) -> FakeTeleportService:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args: object) -> None:
|
||||
return None
|
||||
|
||||
async def execute(
|
||||
self, *_args: object, **_kwargs: object
|
||||
) -> AsyncGenerator[object, object]:
|
||||
yield TeleportCheckingGitEvent()
|
||||
yield TeleportPushRequiredEvent()
|
||||
yield TeleportPushingEvent()
|
||||
yield TeleportStartingWorkflowEvent()
|
||||
yield TeleportWaitingForGitHubEvent()
|
||||
yield TeleportAuthRequiredEvent(oauth_url="https://github.com/auth")
|
||||
yield TeleportFetchingUrlEvent()
|
||||
yield TeleportCompleteEvent(url="https://chat.example.com/123")
|
||||
|
||||
agent_loop.messages.append(LLMMessage(role=Role.user, content="hello"))
|
||||
_set_teleport_service(agent_loop, FakeTeleportService())
|
||||
|
||||
gen = agent_loop.teleport_to_vibe_code(None)
|
||||
response = None
|
||||
events = []
|
||||
while True:
|
||||
try:
|
||||
event = await gen.asend(response)
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
events.append(event)
|
||||
response = (
|
||||
TeleportPushResponseEvent(approved=True)
|
||||
if isinstance(event, TeleportPushRequiredEvent)
|
||||
else None
|
||||
)
|
||||
|
||||
assert isinstance(events[-1], TeleportCompleteEvent)
|
||||
assert telemetry_events[-1]["event_name"] == "vibe.teleport_completed"
|
||||
assert telemetry_events[-1]["properties"] == {
|
||||
"push_required": True,
|
||||
"github_auth_required": True,
|
||||
"nb_session_messages": 1,
|
||||
"session_id": agent_loop.session_id,
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_to_vibe_code_sends_failed_stage(
|
||||
self, agent_loop: AgentLoop, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
class FakeTeleportService:
|
||||
async def __aenter__(self) -> FakeTeleportService:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args: object) -> None:
|
||||
return None
|
||||
|
||||
async def execute(
|
||||
self, *_args: object, **_kwargs: object
|
||||
) -> AsyncGenerator[object, object]:
|
||||
yield TeleportCheckingGitEvent()
|
||||
yield TeleportStartingWorkflowEvent()
|
||||
raise ServiceTeleportError("Workflow api-key-123 could not be started.")
|
||||
|
||||
agent_loop.messages.append(LLMMessage(role=Role.user, content="hello"))
|
||||
_set_teleport_service(agent_loop, FakeTeleportService())
|
||||
|
||||
gen = agent_loop.teleport_to_vibe_code(None)
|
||||
with pytest.raises(TeleportError, match="api-key-123"):
|
||||
async for _ in gen:
|
||||
pass
|
||||
|
||||
assert telemetry_events[-1]["event_name"] == "vibe.teleport_failed"
|
||||
assert telemetry_events[-1]["properties"] == {
|
||||
"stage": "workflow_start",
|
||||
"error_class": "ServiceTeleportError",
|
||||
"push_required": False,
|
||||
"github_auth_required": False,
|
||||
"nb_session_messages": 1,
|
||||
"session_id": agent_loop.session_id,
|
||||
}
|
||||
assert "api-key-123" not in str(telemetry_events[-1]["properties"])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_to_vibe_code_sends_failed_cancelled(
|
||||
self, agent_loop: AgentLoop, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
class FakeTeleportService:
|
||||
async def __aenter__(self) -> FakeTeleportService:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args: object) -> None:
|
||||
return None
|
||||
|
||||
async def execute(
|
||||
self, *_args: object, **_kwargs: object
|
||||
) -> AsyncGenerator[object, object]:
|
||||
yield TeleportCheckingGitEvent()
|
||||
response = yield TeleportPushRequiredEvent()
|
||||
if (
|
||||
not isinstance(response, TeleportPushResponseEvent)
|
||||
or not response.approved
|
||||
):
|
||||
raise ServiceTeleportError(
|
||||
"Teleport cancelled: changes not pushed."
|
||||
)
|
||||
|
||||
agent_loop.messages.append(LLMMessage(role=Role.user, content="hello"))
|
||||
_set_teleport_service(agent_loop, FakeTeleportService())
|
||||
|
||||
gen = agent_loop.teleport_to_vibe_code(None)
|
||||
assert isinstance(await gen.asend(None), TeleportCheckingGitEvent)
|
||||
assert isinstance(await gen.asend(None), TeleportPushRequiredEvent)
|
||||
|
||||
with pytest.raises(TeleportError, match="Teleport cancelled"):
|
||||
await gen.asend(TeleportPushResponseEvent(approved=False))
|
||||
|
||||
assert telemetry_events[-1]["event_name"] == "vibe.teleport_failed"
|
||||
assert telemetry_events[-1]["properties"] == {
|
||||
"stage": "cancelled",
|
||||
"error_class": "ServiceTeleportError",
|
||||
"push_required": True,
|
||||
"github_auth_required": False,
|
||||
"nb_session_messages": 1,
|
||||
"session_id": agent_loop.session_id,
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_to_vibe_code_sends_failed_when_task_cancelled(
|
||||
self, agent_loop: AgentLoop, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
class FakeTeleportService:
|
||||
async def __aenter__(self) -> FakeTeleportService:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args: object) -> None:
|
||||
return None
|
||||
|
||||
async def execute(
|
||||
self, *_args: object, **_kwargs: object
|
||||
) -> AsyncGenerator[object, object]:
|
||||
yield TeleportCheckingGitEvent()
|
||||
raise asyncio.CancelledError
|
||||
|
||||
agent_loop.messages.append(LLMMessage(role=Role.user, content="hello"))
|
||||
_set_teleport_service(agent_loop, FakeTeleportService())
|
||||
|
||||
gen = agent_loop.teleport_to_vibe_code(None)
|
||||
assert isinstance(await gen.asend(None), TeleportCheckingGitEvent)
|
||||
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await gen.asend(None)
|
||||
|
||||
assert telemetry_events[-1]["event_name"] == "vibe.teleport_failed"
|
||||
assert telemetry_events[-1]["properties"] == {
|
||||
"stage": "cancelled",
|
||||
"error_class": "CancelledError",
|
||||
"push_required": False,
|
||||
"github_auth_required": False,
|
||||
"nb_session_messages": 1,
|
||||
"session_id": agent_loop.session_id,
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_to_vibe_code_sends_failed_when_consumer_closes_generator(
|
||||
self, agent_loop: AgentLoop, telemetry_events: list[dict[str, Any]]
|
||||
) -> None:
|
||||
class FakeTeleportService:
|
||||
async def __aenter__(self) -> FakeTeleportService:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args: object) -> None:
|
||||
return None
|
||||
|
||||
async def execute(
|
||||
self, *_args: object, **_kwargs: object
|
||||
) -> AsyncGenerator[object, object]:
|
||||
yield TeleportCheckingGitEvent()
|
||||
yield TeleportPushRequiredEvent()
|
||||
|
||||
agent_loop.messages.append(LLMMessage(role=Role.user, content="hello"))
|
||||
_set_teleport_service(agent_loop, FakeTeleportService())
|
||||
|
||||
gen = agent_loop.teleport_to_vibe_code(None)
|
||||
assert isinstance(await gen.asend(None), TeleportCheckingGitEvent)
|
||||
|
||||
await gen.aclose()
|
||||
|
||||
assert telemetry_events[-1]["event_name"] == "vibe.teleport_failed"
|
||||
assert telemetry_events[-1]["properties"] == {
|
||||
"stage": "cancelled",
|
||||
"error_class": "CancelledError",
|
||||
"push_required": False,
|
||||
"github_auth_required": False,
|
||||
"nb_session_messages": 1,
|
||||
"session_id": agent_loop.session_id,
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from vibe.core.utils import get_server_url_from_api_base
|
||||
from vibe.core.utils import compact_reduction_display, get_server_url_from_api_base
|
||||
import vibe.core.utils.io as io_utils
|
||||
from vibe.core.utils.io import decode_safe, read_safe, read_safe_async
|
||||
|
||||
|
|
@ -23,6 +23,24 @@ def test_get_server_url_from_api_base(api_base, expected):
|
|||
assert get_server_url_from_api_base(api_base) == expected
|
||||
|
||||
|
||||
class TestCompactReductionDisplay:
|
||||
def test_includes_session_ids_when_available(self) -> None:
|
||||
assert compact_reduction_display(
|
||||
177_017,
|
||||
23_263,
|
||||
old_session_id="11111111-1111-1111-1111-111111111111",
|
||||
new_session_id="22222222-2222-2222-2222-222222222222",
|
||||
) == (
|
||||
"Compaction complete: 177,017 → 23,263 tokens (-87.%)\n"
|
||||
"session: 11111111 (before compaction) → 22222222 (after compaction)"
|
||||
)
|
||||
|
||||
def test_returns_base_message_without_session_ids(self) -> None:
|
||||
assert compact_reduction_display(177_017, 23_263) == (
|
||||
"Compaction complete: 177,017 → 23,263 tokens (-87.%)"
|
||||
)
|
||||
|
||||
|
||||
class TestReadSafe:
|
||||
def test_reads_utf8(self, tmp_path: Path) -> None:
|
||||
f = tmp_path / "hello.txt"
|
||||
|
|
|
|||
|
|
@ -60,7 +60,62 @@ class TestReadFileExecution:
|
|||
|
||||
assert result.content == "line 50\nline 51\n"
|
||||
assert result.lines_read == 2
|
||||
assert result.was_truncated
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_marks_truncated_when_max_read_bytes_exceeded(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "big.txt"
|
||||
test_file.write_text(
|
||||
"".join(f"line {i}\n" for i in range(100)), encoding="utf-8"
|
||||
)
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(max_read_bytes=20),
|
||||
state=ReadFileState(),
|
||||
)
|
||||
|
||||
result = await collect_result(tool.run(ReadFileArgs(path=str(test_file))))
|
||||
|
||||
assert result.was_truncated
|
||||
assert result.lines_read > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_not_truncated_when_eof_reached(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "small.txt"
|
||||
test_file.write_text("a\nb\nc\n", encoding="utf-8")
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(max_read_bytes=1024),
|
||||
state=ReadFileState(),
|
||||
)
|
||||
|
||||
result = await collect_result(tool.run(ReadFileArgs(path=str(test_file))))
|
||||
|
||||
assert not result.was_truncated
|
||||
assert result.lines_read == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_not_truncated_when_limit_matches_remaining_lines(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
test_file = tmp_path / "exact.txt"
|
||||
test_file.write_text("a\nb\nc\n", encoding="utf-8")
|
||||
tool = ReadFile(
|
||||
config_getter=lambda: ReadFileToolConfig(max_read_bytes=1024),
|
||||
state=ReadFileState(),
|
||||
)
|
||||
|
||||
result = await collect_result(
|
||||
tool.run(ReadFileArgs(path=str(test_file), limit=10))
|
||||
)
|
||||
|
||||
assert not result.was_truncated
|
||||
assert result.lines_read == 3
|
||||
|
||||
|
||||
class TestGetResultExtra:
|
||||
|
|
|
|||
|
|
@ -5,33 +5,41 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||
import pytest
|
||||
|
||||
from vibe.core.session.resume_sessions import (
|
||||
SHORT_SESSION_ID_LEN,
|
||||
list_remote_resume_sessions,
|
||||
short_session_id,
|
||||
)
|
||||
from vibe.core.session.session_id import shorten_session_id
|
||||
|
||||
|
||||
class TestShortenSessionId:
|
||||
def test_shortens_to_first_8_chars(self) -> None:
|
||||
sid = "abcdef1234567890"
|
||||
assert shorten_session_id(sid) == "abcdef12"
|
||||
|
||||
def test_from_end_shortens_to_last_8_chars(self) -> None:
|
||||
sid = "abcdef1234567890"
|
||||
assert shorten_session_id(sid, from_end=True) == "34567890"
|
||||
|
||||
def test_returns_full_id_when_shorter_than_limit(self) -> None:
|
||||
sid = "abc"
|
||||
assert shorten_session_id(sid) == "abc"
|
||||
assert shorten_session_id(sid, from_end=True) == "abc"
|
||||
|
||||
|
||||
class TestShortSessionId:
|
||||
def test_local_shortens_to_first_chars(self) -> None:
|
||||
def test_local_delegates_to_shorten(self) -> None:
|
||||
sid = "abcdef1234567890"
|
||||
result = short_session_id(sid)
|
||||
assert result == sid[:SHORT_SESSION_ID_LEN]
|
||||
assert len(result) == SHORT_SESSION_ID_LEN
|
||||
assert short_session_id(sid) == shorten_session_id(sid)
|
||||
|
||||
def test_local_is_default(self) -> None:
|
||||
sid = "abcdef1234567890"
|
||||
assert short_session_id(sid) == short_session_id(sid, source="local")
|
||||
|
||||
def test_remote_shortens_to_last_chars(self) -> None:
|
||||
def test_remote_delegates_to_shorten_from_end(self) -> None:
|
||||
sid = "abcdef1234567890"
|
||||
result = short_session_id(sid, source="remote")
|
||||
assert result == sid[-SHORT_SESSION_ID_LEN:]
|
||||
assert len(result) == SHORT_SESSION_ID_LEN
|
||||
|
||||
def test_returns_full_id_when_shorter_than_limit(self) -> None:
|
||||
sid = "abc"
|
||||
assert short_session_id(sid) == "abc"
|
||||
assert short_session_id(sid, source="remote") == "abc"
|
||||
assert short_session_id(sid, source="remote") == shorten_session_id(
|
||||
sid, from_end=True
|
||||
)
|
||||
|
||||
def test_empty_string(self) -> None:
|
||||
assert short_session_id("") == ""
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import pytest
|
|||
from tests.conftest import build_test_vibe_config
|
||||
from vibe.core.agents.models import AgentProfile, AgentSafety
|
||||
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
||||
from vibe.core.loop import ScheduledLoop
|
||||
from vibe.core.session.session_logger import SessionLogger
|
||||
from vibe.core.tools.manager import ToolManager
|
||||
from vibe.core.types import AgentStats, LLMMessage, Role, SessionMetadata
|
||||
|
|
@ -868,3 +869,136 @@ class TestSessionLoggerCleanupTmpFiles:
|
|||
logger.maybe_cleanup_tmp_files()
|
||||
|
||||
assert cleanup_spy.call_count == 2
|
||||
|
||||
|
||||
class TestPersistLoops:
|
||||
@pytest.mark.asyncio
|
||||
async def test_writes_into_existing_metadata(
|
||||
self,
|
||||
session_config: SessionLoggingConfig,
|
||||
mock_vibe_config: VibeConfig,
|
||||
mock_tool_manager: ToolManager,
|
||||
mock_agent_profile: AgentProfile,
|
||||
) -> None:
|
||||
logger = SessionLogger(session_config, "test-session-loops")
|
||||
await logger.save_interaction(
|
||||
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),
|
||||
base_config=mock_vibe_config,
|
||||
tool_manager=mock_tool_manager,
|
||||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
|
||||
assert logger.session_metadata is not None
|
||||
logger.session_metadata.loops = [
|
||||
ScheduledLoop(
|
||||
id="aabbccdd",
|
||||
interval_seconds=30,
|
||||
prompt="ping",
|
||||
next_fire_at=12345.0,
|
||||
created_at=12000.0,
|
||||
)
|
||||
]
|
||||
await logger.persist_loops()
|
||||
|
||||
assert logger.session_dir is not None
|
||||
with open(logger.session_dir / "meta.json") as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
assert metadata["session_id"] == "test-session-loops"
|
||||
assert metadata["total_messages"] == 2
|
||||
assert metadata["loops"] == [
|
||||
{
|
||||
"id": "aabbccdd",
|
||||
"interval_seconds": 30,
|
||||
"prompt": "ping",
|
||||
"next_fire_at": 12345.0,
|
||||
"created_at": 12000.0,
|
||||
}
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_noop_when_metadata_file_missing(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
logger = SessionLogger(session_config, "no-meta-session")
|
||||
assert logger.session_dir is not None
|
||||
# No save_interaction was called -> meta.json does not exist
|
||||
assert not (logger.session_dir / "meta.json").exists()
|
||||
|
||||
assert logger.session_metadata is not None
|
||||
logger.session_metadata.loops = [
|
||||
ScheduledLoop(
|
||||
id="aabbccdd",
|
||||
interval_seconds=30,
|
||||
prompt="ping",
|
||||
next_fire_at=1.0,
|
||||
created_at=0.0,
|
||||
)
|
||||
]
|
||||
await logger.persist_loops()
|
||||
|
||||
assert not (logger.session_dir / "meta.json").exists()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_noop_when_logging_disabled(
|
||||
self, disabled_session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
logger = SessionLogger(disabled_session_config, "ignored")
|
||||
# Should not raise even though session_metadata is None
|
||||
await logger.persist_loops()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subsequent_save_interaction_preserves_loops(
|
||||
self,
|
||||
session_config: SessionLoggingConfig,
|
||||
mock_vibe_config: VibeConfig,
|
||||
mock_tool_manager: ToolManager,
|
||||
mock_agent_profile: AgentProfile,
|
||||
) -> None:
|
||||
logger = SessionLogger(session_config, "loops-vs-save")
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content="Hello"),
|
||||
LLMMessage(role=Role.assistant, content="Hi there!"),
|
||||
]
|
||||
await logger.save_interaction(
|
||||
messages=messages,
|
||||
stats=AgentStats(steps=1),
|
||||
base_config=mock_vibe_config,
|
||||
tool_manager=mock_tool_manager,
|
||||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
|
||||
assert logger.session_metadata is not None
|
||||
logger.session_metadata.loops = [
|
||||
ScheduledLoop(
|
||||
id="aabbccdd",
|
||||
interval_seconds=30,
|
||||
prompt="ping",
|
||||
next_fire_at=12345.0,
|
||||
created_at=12000.0,
|
||||
)
|
||||
]
|
||||
await logger.persist_loops()
|
||||
|
||||
# A subsequent save (e.g. user sends another message) must not
|
||||
# overwrite the on-disk loops with the stale in-memory value.
|
||||
more_messages = [*messages, LLMMessage(role=Role.user, content="Again")]
|
||||
await logger.save_interaction(
|
||||
messages=more_messages,
|
||||
stats=AgentStats(steps=2),
|
||||
base_config=mock_vibe_config,
|
||||
tool_manager=mock_tool_manager,
|
||||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
|
||||
assert logger.session_dir is not None
|
||||
with open(logger.session_dir / "meta.json") as f:
|
||||
metadata = json.load(f)
|
||||
assert len(metadata["loops"]) == 1
|
||||
assert metadata["loops"][0]["id"] == "aabbccdd"
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@
|
|||
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r1" x="0" y="678.8" textLength="268.4" clip-path="url(#terminal-line-27)">Hello! I can help you.</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||
</text><text class="terminal-r5" x="12.2" y="727.6" textLength="36.6" clip-path="url(#terminal-line-29)">▂▅▇</text><text class="terminal-r1" x="48.8" y="727.6" textLength="122" clip-path="url(#terminal-line-29)"> speaking </text><text class="terminal-r6" x="170.8" y="727.6" textLength="219.6" clip-path="url(#terminal-line-29)">Esc/Ctrl+C to stop</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r5" x="0" y="727.6" textLength="36.6" clip-path="url(#terminal-line-29)">▂▅▇</text><text class="terminal-r1" x="36.6" y="727.6" textLength="122" clip-path="url(#terminal-line-29)"> speaking </text><text class="terminal-r6" x="158.6" y="727.6" textLength="219.6" clip-path="url(#terminal-line-29)">Esc/Ctrl+C to stop</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r7" x="0" y="752" textLength="1464" clip-path="url(#terminal-line-30)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r7" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r2" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">></text><text class="terminal-r7" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r7" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r7" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
|
@ -191,7 +191,7 @@
|
|||
</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r1" x="0" y="678.8" textLength="268.4" clip-path="url(#terminal-line-27)">Hello! I can help you.</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
|
||||
</text><text class="terminal-r5" x="12.2" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">█</text><text class="terminal-r1" x="24.4" y="727.6" textLength="158.6" clip-path="url(#terminal-line-29)"> summarizing </text><text class="terminal-r6" x="183" y="727.6" textLength="219.6" clip-path="url(#terminal-line-29)">Esc/Ctrl+C to stop</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r5" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">█</text><text class="terminal-r1" x="12.2" y="727.6" textLength="158.6" clip-path="url(#terminal-line-29)"> summarizing </text><text class="terminal-r6" x="170.8" y="727.6" textLength="219.6" clip-path="url(#terminal-line-29)">Esc/Ctrl+C to stop</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r7" x="0" y="752" textLength="1464" clip-path="url(#terminal-line-30)">┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r7" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r2" x="24.4" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">></text><text class="terminal-r7" x="1451.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">│</text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r7" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r7" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">│</text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 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="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 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="146.4" clip-path="url(#terminal-line-30)">/proxy-setup</text><text class="terminal-r1" x="219.6" y="752" textLength="561.2" clip-path="url(#terminal-line-30)">: Configure proxy and SSL certificate settings</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="109.8" clip-path="url(#terminal-line-31)">/continue</text><text class="terminal-r1" x="183" y="776.4" textLength="24.4" clip-path="url(#terminal-line-31)">, </text><text class="terminal-r4" x="207.4" y="776.4" textLength="85.4" clip-path="url(#terminal-line-31)">/resume</text><text class="terminal-r1" x="292.8" y="776.4" textLength="402.6" clip-path="url(#terminal-line-31)">: Browse and resume past sessions</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="85.4" clip-path="url(#terminal-line-32)">/rename</text><text class="terminal-r1" x="158.6" y="800.8" textLength="341.6" clip-path="url(#terminal-line-32)">: Rename the current session</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="134.2" clip-path="url(#terminal-line-33)">/connectors</text><text class="terminal-r1" x="207.4" y="825.2" textLength="24.4" clip-path="url(#terminal-line-33)">, </text><text class="terminal-r4" x="231.8" y="825.2" textLength="48.8" clip-path="url(#terminal-line-33)">/mcp</text><text class="terminal-r1" x="280.6" y="825.2" textLength="939.4" clip-path="url(#terminal-line-33)">: Display available MCP servers and connectors. Pass a name to list its tools</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="73.2" clip-path="url(#terminal-line-34)">/voice</text><text class="terminal-r1" x="146.4" y="849.6" textLength="317.2" clip-path="url(#terminal-line-34)">: Configure voice settings</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="122" clip-path="url(#terminal-line-35)">/leanstall</text><text class="terminal-r1" x="195.2" y="874" textLength="463.6" clip-path="url(#terminal-line-35)">: Install the Lean 4 agent (leanstral)</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="146.4" clip-path="url(#terminal-line-36)">/unleanstall</text><text class="terminal-r1" x="219.6" y="898.4" textLength="341.6" clip-path="url(#terminal-line-36)">: Uninstall the Lean 4 agent</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="85.4" clip-path="url(#terminal-line-37)">/rewind</text><text class="terminal-r1" x="158.6" y="922.8" textLength="366" clip-path="url(#terminal-line-37)">: Rewind to a previous message</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="61" clip-path="url(#terminal-line-38)">/loop</text><text class="terminal-r1" x="134.2" y="947.2" textLength="427" clip-path="url(#terminal-line-38)">: Schedule a recurring prompt. Use </text><text class="terminal-r4" x="561.2" y="947.2" textLength="305" clip-path="url(#terminal-line-38)">/loop <interval> <prompt></text><text class="terminal-r1" x="866.2" y="947.2" textLength="24.4" clip-path="url(#terminal-line-38)">, </text><text class="terminal-r4" x="890.6" y="947.2" textLength="122" clip-path="url(#terminal-line-38)">/loop list</text><text class="terminal-r1" x="1012.6" y="947.2" textLength="61" clip-path="url(#terminal-line-38)">, or </text><text class="terminal-r4" x="1073.6" y="947.2" textLength="256.2" clip-path="url(#terminal-line-38)">/loop cancel <id|all></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: 36 KiB |
|
|
@ -196,47 +196,47 @@
|
|||
</g>
|
||||
|
||||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||
<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="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="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 class="terminal-r2" x="24.4" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">⎢</text><text class="terminal-r3" x="48.8" y="20" textLength="219.6" clip-path="url(#terminal-line-0)">Keyboard Shortcuts</text><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-r1" x="48.8" y="44.4" textLength="24.4" clip-path="url(#terminal-line-1)">• </text><text class="terminal-r4" x="73.2" y="44.4" textLength="61" clip-path="url(#terminal-line-1)">Enter</text><text class="terminal-r1" x="134.2" y="44.4" textLength="183" clip-path="url(#terminal-line-1)"> Submit message</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="73.2" clip-path="url(#terminal-line-2)">Ctrl+J</text><text class="terminal-r1" x="146.4" y="68.8" textLength="36.6" clip-path="url(#terminal-line-2)"> / </text><text class="terminal-r4" x="183" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">Shift+Enter</text><text class="terminal-r1" x="317.2" y="68.8" textLength="183" clip-path="url(#terminal-line-2)"> Insert newline</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)">Escape</text><text class="terminal-r1" x="146.4" y="93.2" textLength="402.6" clip-path="url(#terminal-line-3)"> Interrupt agent or close dialogs</text><text class="terminal-r5" x="1451.8" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">▃</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
|
||||
</text><text class="terminal-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)">Ctrl+C</text><text class="terminal-r1" x="146.4" y="117.6" textLength="463.6" clip-path="url(#terminal-line-4)"> Quit (or clear input if text present)</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+G</text><text class="terminal-r1" x="146.4" y="142" textLength="366" clip-path="url(#terminal-line-5)"> Edit input in external editor</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+O</text><text class="terminal-r1" x="146.4" y="166.4" textLength="292.8" clip-path="url(#terminal-line-6)"> Toggle tool output view</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="109.8" clip-path="url(#terminal-line-7)">Shift+Tab</text><text class="terminal-r1" x="183" y="190.8" textLength="512.4" clip-path="url(#terminal-line-7)"> Cycle through agents (default, plan, ...)</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="73.2" clip-path="url(#terminal-line-8)">Alt+↑↓</text><text class="terminal-r1" x="146.4" y="215.2" textLength="36.6" clip-path="url(#terminal-line-8)"> / </text><text class="terminal-r4" x="183" y="215.2" textLength="97.6" clip-path="url(#terminal-line-8)">Ctrl+P/N</text><text class="terminal-r1" x="280.6" y="215.2" textLength="390.4" clip-path="url(#terminal-line-8)"> Rewind to previous/next message</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="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-r3" x="48.8" y="264" textLength="195.2" clip-path="url(#terminal-line-10)">Special Features</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-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="48.8" y="312.8" textLength="24.4" clip-path="url(#terminal-line-12)">• </text><text class="terminal-r4" x="73.2" y="312.8" textLength="122" clip-path="url(#terminal-line-12)">!<command></text><text class="terminal-r1" x="195.2" y="312.8" textLength="366" clip-path="url(#terminal-line-12)"> Execute bash command directly</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="170.8" clip-path="url(#terminal-line-13)">@path/to/file/</text><text class="terminal-r1" x="244" y="337.2" textLength="305" clip-path="url(#terminal-line-13)"> Autocompletes file paths</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="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-r3" x="48.8" y="386" textLength="97.6" clip-path="url(#terminal-line-15)">Commands</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-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="48.8" y="434.8" textLength="24.4" clip-path="url(#terminal-line-17)">• </text><text class="terminal-r4" x="73.2" y="434.8" textLength="61" clip-path="url(#terminal-line-17)">/help</text><text class="terminal-r1" x="134.2" y="434.8" textLength="231.8" clip-path="url(#terminal-line-17)">: Show help message</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="85.4" clip-path="url(#terminal-line-18)">/config</text><text class="terminal-r1" x="158.6" y="459.2" textLength="268.4" clip-path="url(#terminal-line-18)">: Edit config settings</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="73.2" clip-path="url(#terminal-line-19)">/model</text><text class="terminal-r1" x="146.4" y="483.6" textLength="256.2" clip-path="url(#terminal-line-19)">: Select active model</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="109.8" clip-path="url(#terminal-line-20)">/thinking</text><text class="terminal-r1" x="183" y="508" textLength="280.6" clip-path="url(#terminal-line-20)">: Select thinking level</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="85.4" clip-path="url(#terminal-line-21)">/reload</text><text class="terminal-r1" x="158.6" y="532.4" textLength="780.8" clip-path="url(#terminal-line-21)">: Reload configuration, agent instructions, and skills from disk</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="73.2" clip-path="url(#terminal-line-22)">/clear</text><text class="terminal-r1" x="146.4" y="556.8" textLength="341.6" clip-path="url(#terminal-line-22)">: Clear conversation history</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="61" clip-path="url(#terminal-line-23)">/copy</text><text class="terminal-r1" x="134.2" y="581.2" textLength="561.2" clip-path="url(#terminal-line-23)">: Copy the last agent message to the clipboard</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="48.8" clip-path="url(#terminal-line-24)">/log</text><text class="terminal-r1" x="122" y="605.6" textLength="524.6" clip-path="url(#terminal-line-24)">: Show path to current interaction log file</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="73.2" clip-path="url(#terminal-line-25)">/debug</text><text class="terminal-r1" x="146.4" y="630" textLength="268.4" clip-path="url(#terminal-line-25)">: Toggle debug console</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="97.6" clip-path="url(#terminal-line-26)">/compact</text><text class="terminal-r1" x="170.8" y="654.4" textLength="1171.2" clip-path="url(#terminal-line-26)">: Compact conversation history by summarizing. Optionally pass instructions to guide the summary</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="61" clip-path="url(#terminal-line-27)">/exit</text><text class="terminal-r1" x="134.2" y="678.8" textLength="268.4" clip-path="url(#terminal-line-27)">: Exit the application</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="85.4" clip-path="url(#terminal-line-28)">/status</text><text class="terminal-r1" x="158.6" y="703.2" textLength="317.2" clip-path="url(#terminal-line-28)">: Display agent statistics</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="109.8" clip-path="url(#terminal-line-29)">/teleport</text><text class="terminal-r1" x="183" y="727.6" textLength="378.2" clip-path="url(#terminal-line-29)">: Teleport session to Vibe Code</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="146.4" clip-path="url(#terminal-line-30)">/proxy-setup</text><text class="terminal-r1" x="219.6" y="752" textLength="561.2" clip-path="url(#terminal-line-30)">: Configure proxy and SSL certificate settings</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="109.8" clip-path="url(#terminal-line-31)">/continue</text><text class="terminal-r1" x="183" y="776.4" textLength="24.4" clip-path="url(#terminal-line-31)">, </text><text class="terminal-r4" x="207.4" y="776.4" textLength="85.4" clip-path="url(#terminal-line-31)">/resume</text><text class="terminal-r1" x="292.8" y="776.4" textLength="402.6" clip-path="url(#terminal-line-31)">: Browse and resume past sessions</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="85.4" clip-path="url(#terminal-line-32)">/rename</text><text class="terminal-r1" x="158.6" y="800.8" textLength="341.6" clip-path="url(#terminal-line-32)">: Rename the current session</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="134.2" clip-path="url(#terminal-line-33)">/connectors</text><text class="terminal-r1" x="207.4" y="825.2" textLength="24.4" clip-path="url(#terminal-line-33)">, </text><text class="terminal-r4" x="231.8" y="825.2" textLength="48.8" clip-path="url(#terminal-line-33)">/mcp</text><text class="terminal-r1" x="280.6" y="825.2" textLength="939.4" clip-path="url(#terminal-line-33)">: Display available MCP servers and connectors. Pass a name to list its tools</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="73.2" clip-path="url(#terminal-line-34)">/voice</text><text class="terminal-r1" x="146.4" y="849.6" textLength="317.2" clip-path="url(#terminal-line-34)">: Configure voice settings</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="122" clip-path="url(#terminal-line-35)">/leanstall</text><text class="terminal-r1" x="195.2" y="874" textLength="463.6" clip-path="url(#terminal-line-35)">: Install the Lean 4 agent (leanstral)</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="146.4" clip-path="url(#terminal-line-36)">/unleanstall</text><text class="terminal-r1" x="219.6" y="898.4" textLength="341.6" clip-path="url(#terminal-line-36)">: Uninstall the Lean 4 agent</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="85.4" clip-path="url(#terminal-line-37)">/rewind</text><text class="terminal-r1" x="158.6" y="922.8" textLength="366" clip-path="url(#terminal-line-37)">: Rewind to a previous message</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="61" clip-path="url(#terminal-line-38)">/loop</text><text class="terminal-r1" x="134.2" y="947.2" textLength="427" clip-path="url(#terminal-line-38)">: Schedule a recurring prompt. Use </text><text class="terminal-r4" x="561.2" y="947.2" textLength="305" clip-path="url(#terminal-line-38)">/loop <interval> <prompt></text><text class="terminal-r1" x="866.2" y="947.2" textLength="24.4" clip-path="url(#terminal-line-38)">, </text><text class="terminal-r4" x="890.6" y="947.2" textLength="122" clip-path="url(#terminal-line-38)">/loop list</text><text class="terminal-r1" x="1012.6" y="947.2" textLength="61" clip-path="url(#terminal-line-38)">, or </text><text class="terminal-r4" x="1073.6" y="947.2" textLength="256.2" clip-path="url(#terminal-line-38)">/loop cancel <id|all></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)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
|
|
@ -11,6 +11,11 @@ def _pin_timezone(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
time.tzset()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _pin_snapshot_colors(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("NO_COLOR", raising=False)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _pin_banner_version(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from tests.mock.utils import mock_llm_chunk
|
|||
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp
|
||||
from tests.snapshots.snap_compare import SnapCompare
|
||||
from tests.stubs.fake_backend import FakeBackend
|
||||
from vibe.core.telemetry.send import TelemetryClient
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
|
@ -20,6 +21,7 @@ def _enable_feedback_bar(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
"vibe.cli.textual_ui.widgets.feedback_bar_manager.MIN_USER_MESSAGES_FOR_FEEDBACK",
|
||||
1,
|
||||
)
|
||||
monkeypatch.setattr(TelemetryClient, "is_active", lambda self: True)
|
||||
|
||||
|
||||
class FeedbackBarSnapshotApp(BaseSnapshotTestApp):
|
||||
|
|
|
|||
|
|
@ -581,6 +581,60 @@ async def test_non_retryable_passes_through_non_streaming(observer_capture) -> N
|
|||
assert agent.session_logger.save_interaction.await_count == 1
|
||||
|
||||
|
||||
def _wrap_with_cause(message: str, cause: BaseException) -> RuntimeError:
|
||||
# Mirrors how Temporal raises ``ActivityError`` on the workflow side with
|
||||
# the original ``ApplicationError`` chained as ``__cause__`` — except we
|
||||
# don't import temporalio. The wrap-site check has to traverse the chain
|
||||
# to find ``non_retryable`` regardless of how deep it is.
|
||||
error = RuntimeError(message)
|
||||
error.__cause__ = cause
|
||||
return error
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_retryable_via_cause_chain_streaming(observer_capture) -> None:
|
||||
observed, observer = observer_capture
|
||||
wrapped = _wrap_with_cause(
|
||||
"Activity task failed", _NonRetryableError("auth failed")
|
||||
)
|
||||
backend = FakeBackend(exception_to_raise=wrapped)
|
||||
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(RuntimeError, match="Activity task failed"):
|
||||
[_ async for _ in agent.act("Trigger non-retryable via cause chain")]
|
||||
|
||||
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_via_cause_chain_non_streaming(observer_capture) -> None:
|
||||
observed, observer = observer_capture
|
||||
wrapped = _wrap_with_cause(
|
||||
"Activity task failed", _NonRetryableError("auth failed")
|
||||
)
|
||||
backend = FakeBackend(exception_to_raise=wrapped)
|
||||
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(RuntimeError, match="Activity task failed"):
|
||||
[_ async for _ in agent.act("Trigger non-retryable via cause chain")]
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -80,8 +80,15 @@ 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) == 0
|
||||
|
||||
session_closed = [
|
||||
e for e in telemetry_events if e.get("event_name") == "vibe.session_closed"
|
||||
]
|
||||
assert len(session_closed) == 1
|
||||
assert session_closed[0]["properties"]["agent_entrypoint"] == "programmatic"
|
||||
|
||||
assert (
|
||||
spy.emitted[0][1] == "You are Vibe, a super useful programming assistant."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ from __future__ import annotations
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.cli.history_manager import HistoryManager
|
||||
|
||||
|
||||
|
|
@ -74,6 +76,45 @@ def test_history_manager_stores_slash_prefixed_entries(tmp_path: Path) -> None:
|
|||
assert reloaded.get_previous(current_input="") is None
|
||||
|
||||
|
||||
def test_history_manager_keeps_entries_when_reload_read_fails(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
history_file = tmp_path / "history.jsonl"
|
||||
manager = HistoryManager(history_file)
|
||||
manager.add("first")
|
||||
|
||||
def raise_os_error(*args: object, **kwargs: object) -> None:
|
||||
raise OSError
|
||||
|
||||
monkeypatch.setattr("vibe.cli.history_manager.read_safe", raise_os_error)
|
||||
|
||||
manager.add("second")
|
||||
|
||||
assert manager.get_previous(current_input="") == "second"
|
||||
assert manager.get_previous(current_input="") == "first"
|
||||
|
||||
|
||||
def test_history_manager_resets_navigation_when_add_is_duplicate(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
history_file = tmp_path / "history.jsonl"
|
||||
manager = HistoryManager(history_file, max_entries=2)
|
||||
manager.add("a")
|
||||
manager.add("b")
|
||||
|
||||
other = HistoryManager(history_file, max_entries=10)
|
||||
other.add("c")
|
||||
other.add("d")
|
||||
other.add("e")
|
||||
|
||||
assert manager.get_previous(current_input="") == "b"
|
||||
manager.add("e")
|
||||
|
||||
assert manager.get_previous(current_input="") == "e"
|
||||
assert manager.get_previous(current_input="") == "d"
|
||||
assert manager.get_previous(current_input="") is None
|
||||
|
||||
|
||||
def test_history_manager_allows_navigation_round_trip(tmp_path: Path) -> None:
|
||||
history_file = tmp_path / "history.jsonl"
|
||||
manager = HistoryManager(history_file)
|
||||
|
|
|
|||
|
|
@ -48,19 +48,28 @@ def _otel_provider(monkeypatch: pytest.MonkeyPatch):
|
|||
|
||||
class TestSetupTracing:
|
||||
def test_noop_when_disabled(self) -> None:
|
||||
config = MagicMock(enable_otel=False)
|
||||
config = MagicMock(enable_telemetry=True, enable_otel=False)
|
||||
with patch("vibe.core.tracing.trace.set_tracer_provider") as mock_set:
|
||||
setup_tracing(config)
|
||||
mock_set.assert_not_called()
|
||||
|
||||
def test_noop_when_telemetry_disabled(self) -> None:
|
||||
config = MagicMock(enable_telemetry=False, enable_otel=True)
|
||||
with patch("vibe.core.tracing.trace.set_tracer_provider") as mock_set:
|
||||
setup_tracing(config)
|
||||
mock_set.assert_not_called()
|
||||
|
||||
def test_noop_when_exporter_config_is_none(self) -> None:
|
||||
config = MagicMock(enable_otel=True, otel_span_exporter_config=None)
|
||||
config = MagicMock(
|
||||
enable_telemetry=True, enable_otel=True, otel_span_exporter_config=None
|
||||
)
|
||||
with patch("vibe.core.tracing.trace.set_tracer_provider") as mock_set:
|
||||
setup_tracing(config)
|
||||
mock_set.assert_not_called()
|
||||
|
||||
def test_configures_provider_from_exporter_config(self) -> None:
|
||||
config = MagicMock(
|
||||
enable_telemetry=True,
|
||||
enable_otel=True,
|
||||
otel_span_exporter_config=OtelSpanExporterConfig(
|
||||
endpoint="https://customer.mistral.ai/telemetry/v1/traces",
|
||||
|
|
@ -85,6 +94,7 @@ class TestSetupTracing:
|
|||
|
||||
def test_custom_endpoint_has_no_auth_headers(self) -> None:
|
||||
config = MagicMock(
|
||||
enable_telemetry=True,
|
||||
enable_otel=True,
|
||||
otel_span_exporter_config=OtelSpanExporterConfig(
|
||||
endpoint="https://my-collector:4318/v1/traces"
|
||||
|
|
|
|||
4
uv.lock
generated
|
|
@ -3,7 +3,7 @@ revision = 3
|
|||
requires-python = ">=3.12"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-04-28T08:28:58.401523Z"
|
||||
exclude-newer = "2026-04-29T14:28:22.645142Z"
|
||||
exclude-newer-span = "P7D"
|
||||
|
||||
[options.exclude-newer-package]
|
||||
|
|
@ -820,7 +820,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "mistral-vibe"
|
||||
version = "2.9.4"
|
||||
version = "2.9.5"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "agent-client-protocol" },
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ from __future__ import annotations
|
|||
from pathlib import Path
|
||||
|
||||
VIBE_ROOT = Path(__file__).parent
|
||||
__version__ = "2.9.4"
|
||||
__version__ = "2.9.5"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import inspect
|
|||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import sys
|
||||
from typing import Any, cast, override
|
||||
from uuid import uuid4
|
||||
|
|
@ -994,11 +995,22 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
session = self._get_session(session_id)
|
||||
self.sessions.pop(session_id, None)
|
||||
|
||||
session.agent_loop.emit_session_closed_telemetry()
|
||||
await session.close()
|
||||
await self._close_agent_loop(session.agent_loop)
|
||||
|
||||
return CloseSessionResponse()
|
||||
|
||||
async def emit_session_closed_for_active_sessions(self) -> None:
|
||||
agent_loops = [session.agent_loop for session in self.sessions.values()]
|
||||
for agent_loop in agent_loops:
|
||||
agent_loop.telemetry_client._client = None
|
||||
agent_loop.emit_session_closed_telemetry()
|
||||
await asyncio.gather(
|
||||
*(agent_loop.telemetry_client.aclose() for agent_loop in agent_loops),
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
async def _close_agent_loop(self, agent_loop: AgentLoop) -> None:
|
||||
deferred_init_thread = agent_loop._deferred_init_thread
|
||||
if deferred_init_thread is not None and deferred_init_thread.is_alive():
|
||||
|
|
@ -1248,6 +1260,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
|
||||
tool_call_id = str(uuid4())
|
||||
old_tokens = session.agent_loop.stats.context_tokens
|
||||
old_session_id = session.agent_loop.session_id
|
||||
parts = text_prompt.strip().split(None, 1)
|
||||
cmd_args = parts[1] if len(parts) > 1 else ""
|
||||
|
||||
|
|
@ -1268,6 +1281,8 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
old_context_tokens=old_tokens or 0,
|
||||
new_context_tokens=new_tokens or 0,
|
||||
summary_length=0,
|
||||
old_session_id=old_session_id,
|
||||
new_session_id=session.agent_loop.session_id,
|
||||
tool_call_id=tool_call_id,
|
||||
)
|
||||
await self.client.session_update(
|
||||
|
|
@ -1419,19 +1434,48 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
return await self._command_reply(session, DATA_RETENTION_MESSAGE, message_id)
|
||||
|
||||
|
||||
SESSION_CLOSED_FLUSH_TIMEOUT_SECONDS = 1.0
|
||||
|
||||
|
||||
def run_acp_server() -> None:
|
||||
agent = VibeAcpAgentLoop()
|
||||
install_sigterm_flush = TelemetryClient(config_getter=VibeConfig.load).is_active()
|
||||
received_sigterm = False
|
||||
previous_sigterm_handler = signal.getsignal(signal.SIGTERM)
|
||||
|
||||
def _handle_sigterm(_signum: int, _frame: Any) -> None:
|
||||
nonlocal received_sigterm
|
||||
received_sigterm = True
|
||||
raise KeyboardInterrupt
|
||||
|
||||
if install_sigterm_flush:
|
||||
signal.signal(signal.SIGTERM, _handle_sigterm)
|
||||
try:
|
||||
asyncio.run(
|
||||
run_agent(
|
||||
agent=VibeAcpAgentLoop(),
|
||||
agent=agent,
|
||||
use_unstable_protocol=True,
|
||||
observers=[acp_message_observer],
|
||||
)
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
if received_sigterm:
|
||||
signal.signal(signal.SIGTERM, previous_sigterm_handler)
|
||||
try:
|
||||
asyncio.run(
|
||||
asyncio.wait_for(
|
||||
agent.emit_session_closed_for_active_sessions(),
|
||||
timeout=SESSION_CLOSED_FLUSH_TIMEOUT_SECONDS,
|
||||
)
|
||||
)
|
||||
except (TimeoutError, Exception):
|
||||
pass
|
||||
# This is expected when the server is terminated
|
||||
pass
|
||||
except Exception as e:
|
||||
# Log any unexpected errors
|
||||
print(f"ACP Agent Server error: {e}", file=sys.stderr)
|
||||
raise
|
||||
finally:
|
||||
if install_sigterm_flush:
|
||||
signal.signal(signal.SIGTERM, previous_sigterm_handler)
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ def bootstrap_config_files() -> None:
|
|||
raise
|
||||
|
||||
|
||||
# When DEBUG_MODE=true, attaches debugpy on localhost:5678.
|
||||
def handle_debug_mode() -> None:
|
||||
if os.environ.get("DEBUG_MODE") != "true":
|
||||
return
|
||||
|
|
|
|||
|
|
@ -231,7 +231,10 @@ def create_compact_end_session_update(event: CompactEndEvent) -> ToolCallProgres
|
|||
type="text",
|
||||
text=(
|
||||
compact_reduction_display(
|
||||
event.old_context_tokens, event.new_context_tokens
|
||||
event.old_context_tokens,
|
||||
event.new_context_tokens,
|
||||
old_session_id=event.old_session_id,
|
||||
new_session_id=event.new_session_id,
|
||||
)
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class SlashCommandController:
|
|||
return
|
||||
|
||||
suggestions = self._completer.get_completion_items(text, cursor_index)
|
||||
if suggestions and all(alias != text for alias, _ in suggestions):
|
||||
if suggestions:
|
||||
self._suggestions = suggestions
|
||||
self._selected_index = 0
|
||||
self._view.render_completion_suggestions(
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@ def _build_cli_entrypoint_metadata() -> EntrypointMetadata:
|
|||
)
|
||||
|
||||
|
||||
def get_initial_agent_name(args: argparse.Namespace) -> str:
|
||||
if args.prompt is not None and args.agent == BuiltinAgentName.DEFAULT:
|
||||
def get_initial_agent_name(args: argparse.Namespace, config: VibeConfig) -> str:
|
||||
if args.prompt is not None and args.agent is None:
|
||||
return BuiltinAgentName.AUTO_APPROVE
|
||||
return args.agent
|
||||
return args.agent or config.default_agent
|
||||
|
||||
|
||||
def get_prompt_from_stdin() -> str | None:
|
||||
|
|
@ -196,9 +196,9 @@ def run_cli(args: argparse.Namespace) -> None:
|
|||
sys.exit(0)
|
||||
|
||||
try:
|
||||
initial_agent_name = get_initial_agent_name(args)
|
||||
is_interactive = args.prompt is None
|
||||
config = load_config_or_exit(interactive=is_interactive)
|
||||
initial_agent_name = get_initial_agent_name(args, config)
|
||||
hook_config_result = load_hooks_from_fs(config)
|
||||
setup_tracing(config)
|
||||
|
||||
|
|
@ -243,10 +243,11 @@ def run_cli(args: argparse.Namespace) -> None:
|
|||
except TeleportError as e:
|
||||
print(f"Teleport error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except RuntimeError as e:
|
||||
except (RuntimeError, ValueError) as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
try:
|
||||
agent_loop = AgentLoop(
|
||||
config,
|
||||
agent_name=initial_agent_name,
|
||||
|
|
@ -255,6 +256,9 @@ def run_cli(args: argparse.Namespace) -> None:
|
|||
defer_heavy_init=True,
|
||||
hook_config_result=hook_config_result,
|
||||
)
|
||||
except ValueError as e:
|
||||
rprint(f"[red]Error:[/] {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if loaded_session:
|
||||
_resume_previous_session(agent_loop, *loaded_session)
|
||||
|
|
|
|||
|
|
@ -161,6 +161,14 @@ class CommandRegistry:
|
|||
description="Rewind to a previous message",
|
||||
handler="_start_rewind_mode",
|
||||
),
|
||||
"loop": Command(
|
||||
aliases=frozenset(["/loop"]),
|
||||
description=(
|
||||
"Schedule a recurring prompt. "
|
||||
"Use `/loop <interval> <prompt>`, `/loop list`, or `/loop cancel <id|all>`"
|
||||
),
|
||||
handler="_loop_command",
|
||||
),
|
||||
"data-retention": Command(
|
||||
aliases=frozenset(["/data-retention"]),
|
||||
description="Show data retention information",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import sys
|
|||
from rich import print as rprint
|
||||
|
||||
from vibe import __version__
|
||||
from vibe.core.agents.models import BuiltinAgentName
|
||||
from vibe.core.config.harness_files import init_harness_files_manager
|
||||
from vibe.core.trusted_folders import find_trustable_files, trusted_folders_manager
|
||||
from vibe.setup.trusted_folders.trust_folder_dialog import (
|
||||
|
|
@ -18,7 +17,18 @@ from vibe.setup.trusted_folders.trust_folder_dialog import (
|
|||
|
||||
|
||||
def parse_arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Run the Mistral Vibe interactive CLI")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run the Mistral Vibe interactive CLI",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=(
|
||||
"Environment variables:\n"
|
||||
" VIBE_HOME Override the Vibe home directory (default: ~/.vibe)\n"
|
||||
" LOG_LEVEL Logging level: DEBUG, INFO, WARNING (default), ERROR, CRITICAL.\n"
|
||||
" Logs are written to $VIBE_HOME/logs/vibe.log.\n"
|
||||
" LOG_MAX_BYTES Max size of vibe.log before rotation (default: 10485760).\n"
|
||||
" VIBE_* Override any config field (e.g. VIBE_ACTIVE_MODEL=local)."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--version", action="version", version=f"%(prog)s {__version__}"
|
||||
)
|
||||
|
|
@ -72,9 +82,12 @@ def parse_arguments() -> argparse.Namespace:
|
|||
parser.add_argument(
|
||||
"--agent",
|
||||
metavar="NAME",
|
||||
default=BuiltinAgentName.DEFAULT,
|
||||
default=None,
|
||||
help="Agent to use (builtin: default, plan, accept-edits, auto-approve, "
|
||||
"or custom from ~/.vibe/agents/NAME.toml)",
|
||||
"or custom from ~/.vibe/agents/NAME.toml). In interactive mode, "
|
||||
"defaults to the 'default_agent' config setting. In programmatic "
|
||||
"mode (-p/--prompt), defaults to auto-approve and 'default_agent' "
|
||||
"is ignored.",
|
||||
)
|
||||
parser.add_argument("--setup", action="store_true", help="Setup API key and exit")
|
||||
parser.add_argument(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
from vibe.core.utils.io import read_safe
|
||||
|
||||
|
|
@ -22,12 +24,10 @@ class HistoryManager:
|
|||
try:
|
||||
text = read_safe(self.history_file).text
|
||||
except OSError:
|
||||
self._entries = []
|
||||
return
|
||||
|
||||
entries = []
|
||||
for raw_line in text.splitlines():
|
||||
raw_line = raw_line.rstrip("\n\r")
|
||||
if not raw_line:
|
||||
continue
|
||||
try:
|
||||
|
|
@ -36,13 +36,24 @@ class HistoryManager:
|
|||
entry = raw_line
|
||||
entries.append(entry if isinstance(entry, str) else str(entry))
|
||||
self._entries = entries[-self.max_entries :]
|
||||
self.reset_navigation()
|
||||
|
||||
def _save_history(self) -> None:
|
||||
try:
|
||||
self.history_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self.history_file.open("w", encoding="utf-8") as f:
|
||||
fd, tmp_path = tempfile.mkstemp(
|
||||
prefix=f".{self.history_file.name}.",
|
||||
suffix=".tmp",
|
||||
dir=self.history_file.parent,
|
||||
)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
for entry in self._entries:
|
||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||
os.replace(tmp_path, self.history_file)
|
||||
except OSError:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
raise
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
|
@ -51,6 +62,8 @@ class HistoryManager:
|
|||
if not text:
|
||||
return
|
||||
|
||||
self._load_history()
|
||||
|
||||
if self._entries and self._entries[-1] == text:
|
||||
return
|
||||
|
||||
|
|
@ -60,7 +73,6 @@ class HistoryManager:
|
|||
self._entries = self._entries[-self.max_entries :]
|
||||
|
||||
self._save_history()
|
||||
self.reset_navigation()
|
||||
|
||||
def get_previous(self, current_input: str) -> str | None:
|
||||
if not self._entries:
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ from vibe.cli.textual_ui.notifications import (
|
|||
)
|
||||
from vibe.cli.textual_ui.quit_manager import QuitManager
|
||||
from vibe.cli.textual_ui.remote import RemoteSessionManager, is_progress_event
|
||||
from vibe.cli.textual_ui.scheduled_loop_runner import ScheduledLoopRunner
|
||||
from vibe.cli.textual_ui.session_exit import print_session_resume_message
|
||||
from vibe.cli.textual_ui.widgets.approval_app import ApprovalApp
|
||||
from vibe.cli.textual_ui.widgets.banner.banner import Banner
|
||||
|
|
@ -140,6 +141,7 @@ from vibe.core.session.resume_sessions import (
|
|||
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.telemetry import send_teleport_early_failure_telemetry
|
||||
from vibe.core.teleport.types import (
|
||||
TeleportAuthCompleteEvent,
|
||||
TeleportAuthRequiredEvent,
|
||||
|
|
@ -339,7 +341,6 @@ class VibeApp(App): # noqa: PLR0904
|
|||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.scroll_sensitivity_y = 1.0
|
||||
self.agent_loop = agent_loop
|
||||
self._plan_info: PlanInfo | None = None
|
||||
self._voice_manager: VoiceManagerPort = (
|
||||
|
|
@ -397,7 +398,17 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self._rewind_mode = False
|
||||
self._rewind_highlighted_widget: UserMessage | None = None
|
||||
self._fatal_init_error = False
|
||||
self._force_quit_task: asyncio.Task[None] | None = None
|
||||
self.commands = self._build_command_registry()
|
||||
self._loop_runner = ScheduledLoopRunner(
|
||||
self.agent_loop.session_logger,
|
||||
can_fire=lambda: (
|
||||
not self._agent_running and self._current_bottom_app == BottomApp.Input
|
||||
),
|
||||
fire=self._handle_user_message,
|
||||
mount=self._mount_and_scroll,
|
||||
tools_collapsed=lambda: self._tools_collapsed,
|
||||
)
|
||||
|
||||
def _configure_startup_options(self, startup: StartupOptions | None) -> None:
|
||||
opts = startup or StartupOptions()
|
||||
|
|
@ -504,6 +515,8 @@ class VibeApp(App): # noqa: PLR0904
|
|||
await self._resolve_plan()
|
||||
await self._show_dangerous_directory_warning()
|
||||
await self._resume_history_from_messages()
|
||||
self._loop_runner.restore_from_session()
|
||||
self._loop_runner.start()
|
||||
await self._check_and_show_whats_new()
|
||||
self._schedule_update_notification()
|
||||
if not self._is_resuming_session:
|
||||
|
|
@ -1407,6 +1420,12 @@ class VibeApp(App): # noqa: PLR0904
|
|||
if show_message:
|
||||
await self._mount_and_scroll(UserMessage("/teleport"))
|
||||
if not has_history:
|
||||
send_teleport_early_failure_telemetry(
|
||||
self.agent_loop.telemetry_client,
|
||||
stage="no_history",
|
||||
error_class="TeleportNoHistoryError",
|
||||
nb_session_messages=len(self.agent_loop.messages[1:]),
|
||||
)
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
"No conversation history to teleport.",
|
||||
|
|
@ -1429,6 +1448,12 @@ class VibeApp(App): # noqa: PLR0904
|
|||
await self._mount_and_scroll(teleport_msg)
|
||||
|
||||
if self._remote_manager.is_active:
|
||||
send_teleport_early_failure_telemetry(
|
||||
self.agent_loop.telemetry_client,
|
||||
stage="remote_session",
|
||||
error_class="TeleportRemoteSessionError",
|
||||
nb_session_messages=len(self.agent_loop.messages[1:]),
|
||||
)
|
||||
await loading.remove()
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
|
|
@ -1818,6 +1843,8 @@ class VibeApp(App): # noqa: PLR0904
|
|||
f"Session `{short_session_id(session.session_id)}` not found."
|
||||
)
|
||||
|
||||
self._emit_session_closed_for_active_session()
|
||||
|
||||
loaded_messages, metadata = SessionLoader.load_session(session_path)
|
||||
if self._chat_input_container:
|
||||
self._chat_input_container.set_custom_border(None)
|
||||
|
|
@ -1846,6 +1873,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
if self.event_handler:
|
||||
self.event_handler.is_remote = False
|
||||
await self._resume_history_from_messages()
|
||||
self._loop_runner.restore_from_session()
|
||||
await self._mount_and_scroll(
|
||||
UserCommandMessage(
|
||||
f"Resumed session `{short_session_id(session.session_id)}`"
|
||||
|
|
@ -1856,6 +1884,8 @@ class VibeApp(App): # noqa: PLR0904
|
|||
await self._remote_manager.attach(
|
||||
session_id=session.session_id, config=self.config
|
||||
)
|
||||
self._emit_session_closed_for_active_session()
|
||||
self.agent_loop.session_id = session.session_id
|
||||
self._refresh_profile_widgets()
|
||||
if self._chat_input_container:
|
||||
self._chat_input_container.set_custom_border(None)
|
||||
|
|
@ -2018,6 +2048,10 @@ class VibeApp(App): # noqa: PLR0904
|
|||
)
|
||||
)
|
||||
|
||||
async def _loop_command(self, cmd_args: str = "", **kwargs: Any) -> None:
|
||||
widget = await self._loop_runner.handle_command(cmd_args)
|
||||
await self._mount_and_scroll(widget)
|
||||
|
||||
async def _compact_history(self, cmd_args: str = "", **kwargs: Any) -> None:
|
||||
if self._agent_running:
|
||||
await self._mount_and_scroll(
|
||||
|
|
@ -2041,22 +2075,32 @@ class VibeApp(App): # noqa: PLR0904
|
|||
return
|
||||
|
||||
old_tokens = self.agent_loop.stats.context_tokens
|
||||
old_session_id = self.agent_loop.session_id
|
||||
compact_msg = CompactMessage()
|
||||
self.event_handler.current_compact = compact_msg
|
||||
await self._mount_and_scroll(compact_msg)
|
||||
|
||||
self._agent_task = asyncio.create_task(
|
||||
self._run_compact(compact_msg, old_tokens, cmd_args.strip())
|
||||
self._run_compact(compact_msg, old_tokens, old_session_id, cmd_args.strip())
|
||||
)
|
||||
|
||||
async def _run_compact(
|
||||
self, compact_msg: CompactMessage, old_tokens: int, extra_instructions: str = ""
|
||||
self,
|
||||
compact_msg: CompactMessage,
|
||||
old_tokens: int,
|
||||
old_session_id: str,
|
||||
extra_instructions: str = "",
|
||||
) -> None:
|
||||
self._agent_running = True
|
||||
try:
|
||||
await self.agent_loop.compact(extra_instructions=extra_instructions)
|
||||
new_tokens = self.agent_loop.stats.context_tokens
|
||||
compact_msg.set_complete(old_tokens=old_tokens, new_tokens=new_tokens)
|
||||
compact_msg.set_complete(
|
||||
old_tokens=old_tokens,
|
||||
new_tokens=new_tokens,
|
||||
old_session_id=old_session_id,
|
||||
new_session_id=self.agent_loop.session_id,
|
||||
)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
compact_msg.set_error("Compaction interrupted")
|
||||
|
|
@ -2085,8 +2129,15 @@ class VibeApp(App): # noqa: PLR0904
|
|||
return short_session_id(self.agent_loop.session_logger.session_id)
|
||||
|
||||
async def _exit_app(self, **kwargs: Any) -> None:
|
||||
self._emit_session_closed_for_active_session()
|
||||
await self._loop_runner.stop()
|
||||
self._log_reader.shutdown()
|
||||
await self._narrator_manager.close()
|
||||
try:
|
||||
await self.agent_loop.telemetry_client.aclose()
|
||||
except Exception as exc:
|
||||
logger.error("Failed to close telemetry client during exit", exc_info=exc)
|
||||
finally:
|
||||
self.exit(result=self._get_session_resume_info())
|
||||
|
||||
def _make_default_voice_manager(self) -> VoiceManager:
|
||||
|
|
@ -2746,7 +2797,16 @@ class VibeApp(App): # noqa: PLR0904
|
|||
return
|
||||
self._quit_manager.request_confirmation("Ctrl+D")
|
||||
|
||||
def _emit_session_closed_for_active_session(self) -> None:
|
||||
self.agent_loop.emit_session_closed_telemetry()
|
||||
|
||||
def _force_quit(self) -> None:
|
||||
if self._force_quit_task is not None and not self._force_quit_task.done():
|
||||
return
|
||||
self._force_quit_task = asyncio.create_task(self._force_quit_async())
|
||||
|
||||
async def _force_quit_async(self) -> None:
|
||||
self._emit_session_closed_for_active_session()
|
||||
if self._agent_task and not self._agent_task.done():
|
||||
self._agent_task.cancel()
|
||||
if self._bash_task and not self._bash_task.done():
|
||||
|
|
@ -2755,6 +2815,13 @@ class VibeApp(App): # noqa: PLR0904
|
|||
|
||||
self._log_reader.shutdown()
|
||||
self._narrator_manager.cancel()
|
||||
try:
|
||||
await self.agent_loop.telemetry_client.aclose()
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to close telemetry client during force quit", exc_info=exc
|
||||
)
|
||||
finally:
|
||||
self.exit(result=self._get_session_resume_info())
|
||||
|
||||
def action_scroll_chat_up(self) -> None:
|
||||
|
|
|
|||
|
|
@ -980,7 +980,7 @@ NarratorStatus {
|
|||
height: auto;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin: 0 0 0 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#banner-container {
|
||||
|
|
|
|||
|
|
@ -217,7 +217,10 @@ class EventHandler:
|
|||
async def _handle_compact_end(self, event: CompactEndEvent) -> None:
|
||||
if self.current_compact:
|
||||
self.current_compact.set_complete(
|
||||
old_tokens=event.old_context_tokens, new_tokens=event.new_context_tokens
|
||||
old_tokens=event.old_context_tokens,
|
||||
new_tokens=event.new_context_tokens,
|
||||
old_session_id=event.old_session_id,
|
||||
new_session_id=event.new_session_id,
|
||||
)
|
||||
self.current_compact = None
|
||||
|
||||
|
|
|
|||
99
vibe/cli/textual_ui/scheduled_loop_runner.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
import time
|
||||
|
||||
from textual.widget import Widget
|
||||
|
||||
from vibe.cli.textual_ui.widgets.messages import ErrorMessage, UserCommandMessage
|
||||
from vibe.core.logger import logger
|
||||
from vibe.core.loop import (
|
||||
USAGE_HINT,
|
||||
LoopErrorResult,
|
||||
LoopListResult,
|
||||
LoopManager,
|
||||
LoopOkResult,
|
||||
ScheduledLoop,
|
||||
format_duration,
|
||||
)
|
||||
from vibe.core.session.session_logger import SessionLogger
|
||||
|
||||
|
||||
def _format_loop_list(loops: list[ScheduledLoop]) -> str:
|
||||
if not loops:
|
||||
return "No scheduled loops."
|
||||
now = time.time()
|
||||
rows = ["| Prompt | Next in | Every | ID |", "|--------|------|-------|----|"]
|
||||
for loop in loops:
|
||||
remaining = format_duration(max(0, int(loop.next_fire_at - now)), short=True)
|
||||
interval = format_duration(loop.interval_seconds)
|
||||
prompt = loop.prompt.replace("|", "\\|").replace("\n", " ")
|
||||
rows.append(f"| {prompt} | {remaining} | {interval} | `{loop.id}` |")
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
class ScheduledLoopRunner:
|
||||
def __init__(
|
||||
self,
|
||||
session_logger: SessionLogger,
|
||||
*,
|
||||
can_fire: Callable[[], bool],
|
||||
fire: Callable[[str], Awaitable[None]],
|
||||
mount: Callable[[Widget], Awaitable[None]],
|
||||
tools_collapsed: Callable[[], bool],
|
||||
) -> None:
|
||||
self._session_logger = session_logger
|
||||
self._manager = LoopManager(session_logger)
|
||||
self._can_fire = can_fire
|
||||
self._fire = fire
|
||||
self._mount = mount
|
||||
self._tools_collapsed = tools_collapsed
|
||||
self._task: asyncio.Task[None] | None = None
|
||||
|
||||
def restore_from_session(self) -> None:
|
||||
metadata = self._session_logger.session_metadata
|
||||
self._manager.restore(list(metadata.loops) if metadata is not None else [])
|
||||
|
||||
def start(self) -> None:
|
||||
if self._task is None or self._task.done():
|
||||
self._task = asyncio.create_task(self._poll())
|
||||
|
||||
async def stop(self) -> None:
|
||||
if self._task is None:
|
||||
return
|
||||
self._task.cancel()
|
||||
try:
|
||||
await self._task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._task = None
|
||||
|
||||
async def handle_command(self, cmd_args: str) -> Widget:
|
||||
result = await self._manager.handle_command(cmd_args)
|
||||
match result:
|
||||
case LoopListResult(loops=loops):
|
||||
return UserCommandMessage(_format_loop_list(loops))
|
||||
case LoopErrorResult(message=message):
|
||||
return ErrorMessage(
|
||||
f"{message}\n{USAGE_HINT}", collapsed=self._tools_collapsed()
|
||||
)
|
||||
case LoopOkResult(message=message):
|
||||
return UserCommandMessage(message)
|
||||
|
||||
async def _poll(self) -> None:
|
||||
while True:
|
||||
try:
|
||||
sleep_for = min(self._manager.next_due_in(), 1.0)
|
||||
await asyncio.sleep(max(0.05, sleep_for))
|
||||
if not self._can_fire():
|
||||
continue
|
||||
due = await self._manager.pop_due()
|
||||
if due is None:
|
||||
continue
|
||||
await self._fire(due.prompt)
|
||||
await self._mount(UserCommandMessage(f"Loop `{due.id}` fired"))
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error("Error polling scheduled loops", exc_info=e)
|
||||
|
|
@ -17,6 +17,8 @@ class CompactMessage(StatusMessage):
|
|||
self.add_class("compact-message")
|
||||
self.old_tokens: int | None = None
|
||||
self.new_tokens: int | None = None
|
||||
self.old_session_id: str | None = None
|
||||
self.new_session_id: str | None = None
|
||||
self.error_message: str | None = None
|
||||
|
||||
def get_content(self) -> str:
|
||||
|
|
@ -26,13 +28,25 @@ class CompactMessage(StatusMessage):
|
|||
if self.error_message:
|
||||
return f"Error: {self.error_message}"
|
||||
|
||||
return compact_reduction_display(self.old_tokens, self.new_tokens)
|
||||
return compact_reduction_display(
|
||||
self.old_tokens,
|
||||
self.new_tokens,
|
||||
old_session_id=self.old_session_id,
|
||||
new_session_id=self.new_session_id,
|
||||
)
|
||||
|
||||
def set_complete(
|
||||
self, old_tokens: int | None = None, new_tokens: int | None = None
|
||||
self,
|
||||
old_tokens: int | None = None,
|
||||
new_tokens: int | None = None,
|
||||
*,
|
||||
old_session_id: str | None = None,
|
||||
new_session_id: str | None = None,
|
||||
) -> None:
|
||||
self.old_tokens = old_tokens
|
||||
self.new_tokens = new_tokens
|
||||
self.old_session_id = old_session_id
|
||||
self.new_session_id = new_session_id
|
||||
self.stop_spinning(success=True)
|
||||
self.post_message(self.Completed(self))
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ from vibe.core.telemetry.types import (
|
|||
TelemetryCallType,
|
||||
TelemetryRequestMetadata,
|
||||
)
|
||||
from vibe.core.teleport.errors import ServiceTeleportError
|
||||
from vibe.core.teleport.telemetry import TeleportTelemetryTracker
|
||||
from vibe.core.teleport.types import TeleportCompleteEvent
|
||||
from vibe.core.tools.base import (
|
||||
BaseTool,
|
||||
InvokeContext,
|
||||
|
|
@ -174,9 +177,18 @@ def _is_context_too_long_error(e: Exception) -> bool:
|
|||
|
||||
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))
|
||||
# Walks ``__cause__`` so an ``ActivityError`` whose cause is a non-retryable
|
||||
# ``ApplicationError`` is detected too — that's what callers driving the
|
||||
# agent loop from a Temporal activity will see when a sub-activity has
|
||||
# already failed terminally.
|
||||
seen: set[int] = set()
|
||||
current: BaseException | None = e
|
||||
while current is not None and id(current) not in seen:
|
||||
if getattr(current, "non_retryable", False):
|
||||
return True
|
||||
seen.add(id(current))
|
||||
current = current.__cause__
|
||||
return False
|
||||
|
||||
|
||||
def requires_init(fn: Callable[..., Any]) -> Callable[..., Any]:
|
||||
|
|
@ -504,6 +516,9 @@ class AgentLoop:
|
|||
def emit_ready_telemetry(self, init_duration_ms: int) -> None:
|
||||
self.telemetry_client.send_ready(init_duration_ms=init_duration_ms)
|
||||
|
||||
def emit_session_closed_telemetry(self) -> None:
|
||||
self.telemetry_client.send_session_closed()
|
||||
|
||||
def _create_connector_registry(self) -> ConnectorRegistry | None:
|
||||
if not connectors_enabled():
|
||||
return None
|
||||
|
|
@ -592,16 +607,25 @@ class AgentLoop:
|
|||
async def teleport_to_vibe_code(
|
||||
self, prompt: str | None
|
||||
) -> AsyncGenerator[TeleportYieldEvent, TeleportPushResponseEvent | None]:
|
||||
from vibe.core.teleport.errors import ServiceTeleportError
|
||||
from vibe.core.teleport.nuage import TeleportSession
|
||||
|
||||
session_messages = [
|
||||
msg.model_dump(exclude_none=True) for msg in self.messages[1:]
|
||||
]
|
||||
telemetry_tracker = TeleportTelemetryTracker(
|
||||
telemetry_client=self.telemetry_client,
|
||||
nb_session_messages=len(session_messages),
|
||||
stage="no_history"
|
||||
if prompt is None and not session_messages
|
||||
else "git_check",
|
||||
)
|
||||
session = TeleportSession(
|
||||
metadata={
|
||||
"agent": self.agent_profile.name,
|
||||
"model": self.config.active_model,
|
||||
"stats": self.stats.model_dump(),
|
||||
},
|
||||
messages=[msg.model_dump(exclude_none=True) for msg in self.messages[1:]],
|
||||
messages=session_messages,
|
||||
)
|
||||
try:
|
||||
async with self.teleport_service:
|
||||
|
|
@ -610,12 +634,23 @@ class AgentLoop:
|
|||
while True:
|
||||
try:
|
||||
event = await gen.asend(response)
|
||||
telemetry_tracker.record_event(event)
|
||||
if isinstance(event, TeleportCompleteEvent):
|
||||
telemetry_tracker.send_success()
|
||||
response = yield event
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
except ServiceTeleportError as e:
|
||||
telemetry_tracker.record_service_error(e)
|
||||
raise TeleportError(str(e)) from e
|
||||
except (asyncio.CancelledError, GeneratorExit):
|
||||
telemetry_tracker.record_cancelled()
|
||||
raise
|
||||
except Exception as e:
|
||||
telemetry_tracker.record_unexpected_error(e)
|
||||
raise
|
||||
finally:
|
||||
telemetry_tracker.send_failure_if_needed()
|
||||
self._teleport_service = None
|
||||
|
||||
def _setup_middleware(self) -> None:
|
||||
|
|
@ -709,6 +744,8 @@ class AgentLoop:
|
|||
old_context_tokens=old_tokens,
|
||||
new_context_tokens=new_tokens,
|
||||
summary_length=len(summary),
|
||||
old_session_id=old_session_id,
|
||||
new_session_id=self.session_id,
|
||||
)
|
||||
|
||||
case MiddlewareAction.CONTINUE:
|
||||
|
|
@ -1395,8 +1432,9 @@ class AgentLoop:
|
|||
responded_ids: set[str] = set()
|
||||
j = i + 1
|
||||
while j < len(self.messages) and self.messages[j].role == "tool":
|
||||
if self.messages[j].tool_call_id:
|
||||
responded_ids.add(self.messages[j].tool_call_id)
|
||||
tool_call_id = self.messages[j].tool_call_id
|
||||
if tool_call_id is not None:
|
||||
responded_ids.add(tool_call_id)
|
||||
j += 1
|
||||
|
||||
if len(responded_ids) < expected_responses:
|
||||
|
|
@ -1431,6 +1469,7 @@ class AgentLoop:
|
|||
|
||||
def _reset_session(self, keep_parent: bool = True) -> None:
|
||||
old_session_id = self.session_id
|
||||
self.emit_session_closed_telemetry()
|
||||
suffix = extract_suffix(self.session_id)
|
||||
self.session_id = generate_session_id(suffix=suffix)
|
||||
parent_session_id = old_session_id if keep_parent else None
|
||||
|
|
|
|||
|
|
@ -40,18 +40,22 @@ class AgentManager:
|
|||
" ".join(str(p) for p in self._search_paths),
|
||||
)
|
||||
|
||||
profile = self._available.get(initial_agent)
|
||||
if (
|
||||
not allow_subagent
|
||||
and profile is not None
|
||||
and profile.agent_type != AgentType.AGENT
|
||||
):
|
||||
available = self.available_agents
|
||||
profile = available.get(initial_agent)
|
||||
if profile is None:
|
||||
if initial_agent in self._available:
|
||||
raise ValueError(
|
||||
f"Agent '{initial_agent}' is not available. "
|
||||
f"It may be disabled, not installed, or excluded by your config."
|
||||
)
|
||||
raise ValueError(f"Agent '{initial_agent}' not found.")
|
||||
if not allow_subagent and profile.agent_type != AgentType.AGENT:
|
||||
raise ValueError(
|
||||
f"Agent '{initial_agent}' is a {profile.agent_type} and cannot be used"
|
||||
f" as the primary agent. Only agents of type 'agent' can be selected"
|
||||
f" with --agent."
|
||||
)
|
||||
self.active_profile = profile or self._available[BuiltinAgentName.DEFAULT]
|
||||
self.active_profile = profile
|
||||
self._cached_config: VibeConfig | None = None
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -64,13 +64,16 @@ class CommandCompleter(Completer):
|
|||
descriptions[alias] = description
|
||||
return list(descriptions.keys()), descriptions
|
||||
|
||||
def _head_word(self, text: str, cursor_pos: int) -> str:
|
||||
head = text.split(" ", 1)[0]
|
||||
return head[1 : min(cursor_pos, len(head))].lower()
|
||||
|
||||
def get_completions(self, text: str, cursor_pos: int) -> list[str]:
|
||||
if not text.startswith("/"):
|
||||
return []
|
||||
|
||||
aliases, _ = self._build_lookup()
|
||||
word = text[1:cursor_pos].lower()
|
||||
search_str = "/" + word
|
||||
search_str = "/" + self._head_word(text, cursor_pos)
|
||||
return [alias for alias in aliases if alias.lower().startswith(search_str)]
|
||||
|
||||
def get_completion_items(self, text: str, cursor_pos: int) -> list[tuple[str, str]]:
|
||||
|
|
@ -78,8 +81,7 @@ class CommandCompleter(Completer):
|
|||
return []
|
||||
|
||||
aliases, descriptions = self._build_lookup()
|
||||
word = text[1:cursor_pos].lower()
|
||||
search_str = "/" + word
|
||||
search_str = "/" + self._head_word(text, cursor_pos)
|
||||
items = [
|
||||
(alias, descriptions.get(alias, ""))
|
||||
for alias in aliases
|
||||
|
|
@ -90,9 +92,11 @@ class CommandCompleter(Completer):
|
|||
def get_replacement_range(
|
||||
self, text: str, cursor_pos: int
|
||||
) -> tuple[int, int] | None:
|
||||
if text.startswith("/"):
|
||||
return (0, cursor_pos)
|
||||
if not text.startswith("/"):
|
||||
return None
|
||||
first_space = text.find(" ")
|
||||
end = cursor_pos if first_space == -1 else min(cursor_pos, first_space)
|
||||
return (0, end)
|
||||
|
||||
|
||||
class PathCompleter(Completer):
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from pydantic_settings import (
|
|||
)
|
||||
import tomli_w
|
||||
|
||||
from vibe.core.agents.models import BuiltinAgentName
|
||||
from vibe.core.config.harness_files import get_harness_files_manager
|
||||
from vibe.core.logger import logger
|
||||
from vibe.core.paths import GLOBAL_ENV_FILE, SESSION_LOG_DIR
|
||||
|
|
@ -610,6 +611,15 @@ class VibeConfig(BaseSettings):
|
|||
"A list of opt-in builtin agent names that have been explicitly installed."
|
||||
),
|
||||
)
|
||||
default_agent: str = Field(
|
||||
default=BuiltinAgentName.DEFAULT,
|
||||
description=(
|
||||
"Agent profile to use when no --agent flag is passed in interactive "
|
||||
"mode. Builtin: default, plan, accept-edits, auto-approve. "
|
||||
"Ignored in programmatic mode (-p/--prompt), which falls back to "
|
||||
"auto-approve when --agent is not provided."
|
||||
),
|
||||
)
|
||||
skill_paths: list[Path] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ def apply_logging_config(target_logger: logging.Logger) -> None:
|
|||
|
||||
max_bytes = int(os.environ.get("LOG_MAX_BYTES", 10 * 1024 * 1024))
|
||||
|
||||
# DEBUG_MODE is the debugpy switch (see vibe/acp/entrypoint.py);
|
||||
# it also forces DEBUG-level logging here.
|
||||
if os.environ.get("DEBUG_MODE") == "true":
|
||||
log_level_str = "DEBUG"
|
||||
else:
|
||||
|
|
|
|||
250
vibe/core/loop.py
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum, auto
|
||||
import math
|
||||
import re
|
||||
import secrets
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from vibe.core.logger import logger
|
||||
from vibe.core.types import ScheduledLoop
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vibe.core.session.session_logger import SessionLogger
|
||||
|
||||
__all__ = [
|
||||
"MAX_LOOPS_PER_SESSION",
|
||||
"MIN_INTERVAL_SECONDS",
|
||||
"USAGE_HINT",
|
||||
"IntervalUnit",
|
||||
"LoopCommandResult",
|
||||
"LoopError",
|
||||
"LoopErrorResult",
|
||||
"LoopListResult",
|
||||
"LoopManager",
|
||||
"LoopOkResult",
|
||||
"format_duration",
|
||||
"parse_interval",
|
||||
]
|
||||
|
||||
|
||||
MIN_INTERVAL_SECONDS = 30
|
||||
MAX_LOOPS_PER_SESSION = 50
|
||||
USAGE_HINT = """\
|
||||
Usage:
|
||||
/loop <interval> <prompt>
|
||||
/loop list
|
||||
/loop cancel <id|all>
|
||||
"""
|
||||
|
||||
_INTERVAL_RE = re.compile(r"^(\d+)([smhd])$")
|
||||
_CANCEL_VERBS = frozenset({"cancel", "rm", "stop", "delete"})
|
||||
_LIST_VERBS = frozenset({"list", "ls"})
|
||||
|
||||
|
||||
class LoopError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class IntervalUnit(StrEnum):
|
||||
SECOND = auto()
|
||||
MINUTE = auto()
|
||||
HOUR = auto()
|
||||
DAY = auto()
|
||||
|
||||
@property
|
||||
def seconds(self) -> int:
|
||||
match self:
|
||||
case IntervalUnit.SECOND:
|
||||
return 1
|
||||
case IntervalUnit.MINUTE:
|
||||
return 60
|
||||
case IntervalUnit.HOUR:
|
||||
return 3600
|
||||
case IntervalUnit.DAY:
|
||||
return 86400
|
||||
|
||||
@property
|
||||
def suffix(self) -> str:
|
||||
match self:
|
||||
case IntervalUnit.SECOND:
|
||||
return "s"
|
||||
case IntervalUnit.MINUTE:
|
||||
return "m"
|
||||
case IntervalUnit.HOUR:
|
||||
return "h"
|
||||
case IntervalUnit.DAY:
|
||||
return "d"
|
||||
|
||||
@classmethod
|
||||
def from_suffix(cls, ch: str) -> IntervalUnit:
|
||||
match ch:
|
||||
case "s":
|
||||
return cls.SECOND
|
||||
case "m":
|
||||
return cls.MINUTE
|
||||
case "h":
|
||||
return cls.HOUR
|
||||
case "d":
|
||||
return cls.DAY
|
||||
case _:
|
||||
raise LoopError(f"Unknown interval unit `{ch}`.")
|
||||
|
||||
|
||||
def parse_interval(text: str) -> int:
|
||||
if not text:
|
||||
raise LoopError("Missing interval.")
|
||||
match = _INTERVAL_RE.match(text.strip().lower())
|
||||
if match is None:
|
||||
raise LoopError(
|
||||
f"Invalid interval `{text}`. "
|
||||
"Expected: <number><unit> (e.g., 30s, 5m, 2h, 1d)."
|
||||
)
|
||||
value = int(match.group(1))
|
||||
seconds = value * IntervalUnit.from_suffix(match.group(2)).seconds
|
||||
if seconds < MIN_INTERVAL_SECONDS:
|
||||
raise LoopError(f"Interval must be at least {MIN_INTERVAL_SECONDS}s.")
|
||||
return seconds
|
||||
|
||||
|
||||
def format_duration(seconds: int, short: bool = False) -> str:
|
||||
parts = []
|
||||
for unit in (
|
||||
IntervalUnit.DAY,
|
||||
IntervalUnit.HOUR,
|
||||
IntervalUnit.MINUTE,
|
||||
IntervalUnit.SECOND,
|
||||
):
|
||||
value = seconds // unit.seconds
|
||||
if value > 0:
|
||||
parts.append(f"{value}{unit.suffix}")
|
||||
seconds %= unit.seconds
|
||||
if not parts:
|
||||
parts = ["0s"]
|
||||
if short:
|
||||
return parts[0]
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
class _LoopResultBase(BaseModel):
|
||||
model_config = ConfigDict(frozen=True, extra="forbid")
|
||||
|
||||
|
||||
class LoopListResult(_LoopResultBase):
|
||||
loops: list[ScheduledLoop]
|
||||
|
||||
|
||||
class LoopOkResult(_LoopResultBase):
|
||||
message: str
|
||||
|
||||
|
||||
class LoopErrorResult(_LoopResultBase):
|
||||
message: str
|
||||
|
||||
|
||||
LoopCommandResult = LoopListResult | LoopOkResult | LoopErrorResult
|
||||
|
||||
|
||||
class LoopManager:
|
||||
def __init__(self, session_logger: SessionLogger) -> None:
|
||||
self._session_logger = session_logger
|
||||
self._loops: list[ScheduledLoop] = []
|
||||
|
||||
@property
|
||||
def loops(self) -> list[ScheduledLoop]:
|
||||
return list(self._loops)
|
||||
|
||||
def restore(self, loops: list[ScheduledLoop]) -> None:
|
||||
self._loops = list(loops)
|
||||
|
||||
def next_due_in(self, now: float | None = None) -> float:
|
||||
if not self._loops:
|
||||
return math.inf
|
||||
ts = now if now is not None else time.time()
|
||||
return max(0.0, min(loop.next_fire_at for loop in self._loops) - ts)
|
||||
|
||||
async def pop_due(self, now: float | None = None) -> ScheduledLoop | None:
|
||||
if not self._loops:
|
||||
return None
|
||||
ts = now if now is not None else time.time()
|
||||
due_loops = [loop for loop in self._loops if loop.next_fire_at <= ts]
|
||||
if not due_loops:
|
||||
return None
|
||||
due = min(due_loops, key=lambda loop: loop.next_fire_at)
|
||||
due.next_fire_at = ts + due.interval_seconds
|
||||
await self._persist()
|
||||
return due
|
||||
|
||||
async def handle_command(self, args: str) -> LoopCommandResult:
|
||||
text = args.strip()
|
||||
if not text:
|
||||
return self._list_result()
|
||||
verb, _, rest = text.partition(" ")
|
||||
verb_lower = verb.lower()
|
||||
if verb_lower in _LIST_VERBS:
|
||||
return self._list_result()
|
||||
if verb_lower in _CANCEL_VERBS:
|
||||
return await self._cancel(rest.strip())
|
||||
return await self._add(verb, rest)
|
||||
|
||||
def _list_result(self) -> LoopListResult:
|
||||
return LoopListResult(loops=list(self._loops))
|
||||
|
||||
async def _add(
|
||||
self, interval_text: str, prompt: str
|
||||
) -> LoopOkResult | LoopErrorResult:
|
||||
try:
|
||||
seconds = parse_interval(interval_text)
|
||||
except LoopError as e:
|
||||
return LoopErrorResult(message=str(e))
|
||||
prompt = prompt.strip()
|
||||
if not prompt:
|
||||
return LoopErrorResult(message="Missing prompt.")
|
||||
if prompt.startswith("/"):
|
||||
return LoopErrorResult(message="Prompt cannot start with '/'.")
|
||||
if len(self._loops) >= MAX_LOOPS_PER_SESSION:
|
||||
return LoopErrorResult(
|
||||
message=f"Loop limit reached ({MAX_LOOPS_PER_SESSION} per session)."
|
||||
)
|
||||
now = time.time()
|
||||
loop = ScheduledLoop(
|
||||
id=secrets.token_hex(4),
|
||||
interval_seconds=seconds,
|
||||
prompt=prompt,
|
||||
next_fire_at=now + seconds,
|
||||
created_at=now,
|
||||
)
|
||||
self._loops.append(loop)
|
||||
await self._persist()
|
||||
return LoopOkResult(
|
||||
message=(
|
||||
f"Scheduled loop `{loop.id}` every {format_duration(seconds)}: {prompt}"
|
||||
)
|
||||
)
|
||||
|
||||
async def _cancel(self, target: str) -> LoopOkResult | LoopErrorResult:
|
||||
if not target:
|
||||
return LoopErrorResult(message="Missing loop id.")
|
||||
if target.lower() == "all":
|
||||
count = len(self._loops)
|
||||
self._loops.clear()
|
||||
await self._persist()
|
||||
return LoopOkResult(message=f"Cancelled {count} scheduled loop(s).")
|
||||
match = next((loop for loop in self._loops if loop.id == target), None)
|
||||
if match is None:
|
||||
return LoopErrorResult(message=f"No scheduled loop with id `{target}`.")
|
||||
self._loops.remove(match)
|
||||
await self._persist()
|
||||
return LoopOkResult(message=f"Cancelled loop `{match.id}`: {match.prompt}")
|
||||
|
||||
async def _persist(self) -> None:
|
||||
metadata = self._session_logger.session_metadata
|
||||
if metadata is not None:
|
||||
metadata.loops = [*self._loops]
|
||||
try:
|
||||
await self._session_logger.persist_loops()
|
||||
except Exception as e:
|
||||
logger.error("Failed to persist scheduled loops", exc_info=e)
|
||||
|
|
@ -91,6 +91,7 @@ def run_programmatic( # noqa: PLR0913, PLR0917
|
|||
|
||||
return formatter.finalize()
|
||||
finally:
|
||||
agent_loop.emit_session_closed_telemetry()
|
||||
await agent_loop.telemetry_client.aclose()
|
||||
|
||||
return asyncio.run(_async_run())
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ Never claim completion without verification — a passing test, correct read-bac
|
|||
|
||||
Hard Rules:
|
||||
|
||||
Never Commit
|
||||
Do not run `git commit`, `git push`, or `git add` unless the user explicitly asks you to. Saving files is sufficient — the user will review changes and commit themselves.
|
||||
Never Commit Proactively
|
||||
Do not proactively run `git add`, `git commit`, or `git push`. Saving files is the default — the user usually reviews and commits themselves. If the user explicitly asks you to stage, commit, or push, do it.
|
||||
|
||||
Respect User Constraints
|
||||
"No writes", "just analyze", "plan only", "don't touch X" - these are hard constraints. Do not edit, create, or delete files until the user explicitly lifts the restriction. Violation of explicit user instructions is the worst failure mode.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from pathlib import Path
|
|||
import tempfile
|
||||
|
||||
from vibe.core.logger import logger
|
||||
from vibe.core.session.session_id import shorten_session_id
|
||||
|
||||
_active_scratchpads: dict[str, Path] = {}
|
||||
|
||||
|
|
@ -17,7 +18,11 @@ def init_scratchpad(session_id: str) -> Path | None:
|
|||
return _active_scratchpads[session_id]
|
||||
|
||||
try:
|
||||
dir_path = Path(tempfile.mkdtemp(prefix=f"vibe-scratchpad-{session_id[:8]}-"))
|
||||
dir_path = Path(
|
||||
tempfile.mkdtemp(
|
||||
prefix=f"vibe-scratchpad-{shorten_session_id(session_id)}-"
|
||||
)
|
||||
)
|
||||
_active_scratchpads[session_id] = dir_path
|
||||
logger.debug("Scratchpad initialized at %s", dir_path)
|
||||
return dir_path
|
||||
|
|
|
|||
|
|
@ -8,17 +8,14 @@ from vibe.core.config import VibeConfig
|
|||
from vibe.core.logger import logger
|
||||
from vibe.core.nuage.client import WorkflowsClient
|
||||
from vibe.core.nuage.workflow import WorkflowExecutionStatus
|
||||
from vibe.core.session.session_id import shorten_session_id
|
||||
from vibe.core.session.session_loader import SessionLoader
|
||||
|
||||
ResumeSessionSource = Literal["local", "remote"]
|
||||
|
||||
SHORT_SESSION_ID_LEN = 8
|
||||
|
||||
|
||||
def short_session_id(session_id: str, source: ResumeSessionSource = "local") -> str:
|
||||
if source == "remote":
|
||||
return session_id[-SHORT_SESSION_ID_LEN:]
|
||||
return session_id[:SHORT_SESSION_ID_LEN]
|
||||
return shorten_session_id(session_id, from_end=source == "remote")
|
||||
|
||||
|
||||
_ACTIVE_STATUSES = [
|
||||
|
|
|
|||
|
|
@ -22,3 +22,13 @@ def generate_session_id(*, suffix: str | None = None) -> str:
|
|||
def extract_suffix(session_id: str) -> str:
|
||||
"""Extract the stable suffix (last segment after the final hyphen)."""
|
||||
return session_id.rsplit("-", 1)[-1]
|
||||
|
||||
|
||||
_SHORT_LEN = 8
|
||||
|
||||
|
||||
def shorten_session_id(session_id: str, *, from_end: bool = False) -> str:
|
||||
"""Return a short human-readable slice of a session ID (8 chars)."""
|
||||
if from_end:
|
||||
return session_id[-_SHORT_LEN:]
|
||||
return session_id[:_SHORT_LEN]
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import json
|
|||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
|
||||
from vibe.core.session.session_id import shorten_session_id
|
||||
from vibe.core.types import LLMMessage, SessionMetadata
|
||||
from vibe.core.utils.io import read_safe
|
||||
|
||||
|
|
@ -129,7 +130,7 @@ class SessionLoader:
|
|||
if not save_dir.exists():
|
||||
return []
|
||||
|
||||
short_id = session_id[:8]
|
||||
short_id = shorten_session_id(session_id)
|
||||
return list(save_dir.glob(f"{config.session_prefix}_*_{short_id}"))
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any, Literal
|
|||
|
||||
from anyio import NamedTemporaryFile, Path as AsyncPath
|
||||
|
||||
from vibe.core.session.session_id import shorten_session_id
|
||||
from vibe.core.session.session_loader import (
|
||||
MESSAGES_FILENAME,
|
||||
METADATA_FILENAME,
|
||||
|
|
@ -63,9 +64,20 @@ class SessionLogger:
|
|||
)
|
||||
|
||||
timestamp = utc_now().strftime("%Y%m%d_%H%M%S")
|
||||
folder_name = f"{self.session_prefix}_{timestamp}_{self.session_id[:8]}"
|
||||
folder_name = (
|
||||
f"{self.session_prefix}_{timestamp}_{shorten_session_id(self.session_id)}"
|
||||
)
|
||||
return self.save_dir / folder_name
|
||||
|
||||
def _get_session_info(self) -> tuple[Path, SessionMetadata] | None:
|
||||
if (
|
||||
not self.enabled
|
||||
or self.session_dir is None
|
||||
or self.session_metadata is None
|
||||
):
|
||||
return None
|
||||
return (self.session_dir, self.session_metadata)
|
||||
|
||||
@property
|
||||
def metadata_filepath(self) -> Path:
|
||||
if self.session_dir is None:
|
||||
|
|
@ -244,34 +256,34 @@ class SessionLogger:
|
|||
tool_manager: ToolManager,
|
||||
agent_profile: AgentProfile,
|
||||
) -> None:
|
||||
if not self.enabled or self.session_dir is None:
|
||||
return
|
||||
|
||||
if self.session_metadata is None:
|
||||
session_info = self._get_session_info()
|
||||
if session_info is None:
|
||||
return
|
||||
session_dir, session_metadata = session_info
|
||||
metadata_path = session_dir / METADATA_FILENAME
|
||||
|
||||
if not any(msg.role != Role.system for msg in messages):
|
||||
return
|
||||
|
||||
# If the session directory does not exist, create it
|
||||
try:
|
||||
self.session_dir.mkdir(parents=True, exist_ok=True)
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to create session directory at {self.session_dir}: {type(e).__name__}: {e}"
|
||||
f"Failed to create session directory at {session_dir}: {type(e).__name__}: {e}"
|
||||
) from e
|
||||
|
||||
# Read old metadata and get total_messages
|
||||
try:
|
||||
if self.metadata_filepath.exists():
|
||||
raw = (await read_safe_async(self.metadata_filepath)).text
|
||||
if metadata_path.exists():
|
||||
raw = (await read_safe_async(metadata_path)).text
|
||||
old_metadata = json.loads(raw)
|
||||
old_total_messages = old_metadata["total_messages"]
|
||||
else:
|
||||
old_total_messages = 0
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to read session metadata at {self.metadata_filepath}: {e}"
|
||||
f"Failed to read session metadata at {metadata_path}: {e}"
|
||||
) from e
|
||||
|
||||
try:
|
||||
|
|
@ -283,7 +295,7 @@ class SessionLogger:
|
|||
return
|
||||
|
||||
messages_data = [m.model_dump(exclude_none=True) for m in new_messages]
|
||||
await SessionLogger.persist_messages(messages_data, self.session_dir)
|
||||
await SessionLogger.persist_messages(messages_data, session_dir)
|
||||
|
||||
# If message update succeeded, write metadata
|
||||
tools_available = [
|
||||
|
|
@ -307,7 +319,7 @@ class SessionLogger:
|
|||
total_messages = len(non_system_messages)
|
||||
|
||||
metadata_dump = {
|
||||
**self.session_metadata.model_dump(),
|
||||
**session_metadata.model_dump(),
|
||||
"end_time": utc_now().isoformat(),
|
||||
"stats": stats.model_dump(),
|
||||
"title": title,
|
||||
|
|
@ -321,14 +333,32 @@ class SessionLogger:
|
|||
"system_prompt": system_prompt,
|
||||
}
|
||||
|
||||
await SessionLogger.persist_metadata(metadata_dump, self.session_dir)
|
||||
await SessionLogger.persist_metadata(metadata_dump, session_dir)
|
||||
except Exception as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to save session to {self.session_dir}: {e}"
|
||||
) from e
|
||||
raise RuntimeError(f"Failed to save session to {session_dir}: {e}") from e
|
||||
finally:
|
||||
self.maybe_cleanup_tmp_files()
|
||||
|
||||
async def persist_loops(self) -> None:
|
||||
session_info = self._get_session_info()
|
||||
if session_info is None:
|
||||
return
|
||||
session_dir, session_metadata = session_info
|
||||
metadata_path = session_dir / METADATA_FILENAME
|
||||
if not metadata_path.exists():
|
||||
return
|
||||
try:
|
||||
raw = (await read_safe_async(metadata_path)).text
|
||||
metadata = json.loads(raw)
|
||||
except (OSError, json.JSONDecodeError) as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to read session metadata at {metadata_path}: {e}"
|
||||
) from e
|
||||
metadata["loops"] = [
|
||||
loop.model_dump(mode="json") for loop in session_metadata.loops
|
||||
]
|
||||
await SessionLogger.persist_metadata(metadata, session_dir)
|
||||
|
||||
def reset_session(
|
||||
self, session_id: str, *, parent_session_id: str | None = None
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -194,6 +194,15 @@ disabled_agents = ["auto-approve"]
|
|||
|
||||
# Opt-in builtin agents (only affects agents with install_required=True, e.g. lean)
|
||||
installed_agents = ["lean"]
|
||||
|
||||
# Agent profile to use when --agent is not passed in interactive mode
|
||||
# (default: "default"). Valid values: "default", "plan", "accept-edits",
|
||||
# "auto-approve", "lean" (only when listed in installed_agents), or any
|
||||
# custom agent name from ~/.vibe/agents/ or .vibe/agents/. Subagents
|
||||
# (e.g. "explore") are rejected. Ignored in programmatic mode
|
||||
# (-p/--prompt), which falls back to "auto-approve" when --agent is not
|
||||
# provided.
|
||||
default_agent = "plan"
|
||||
```
|
||||
|
||||
### MCP Servers
|
||||
|
|
@ -321,7 +330,7 @@ Tool, skill, and agent names support three matching modes:
|
|||
```
|
||||
vibe [PROMPT] # Start interactive session with optional prompt
|
||||
vibe -p TEXT / --prompt TEXT # Programmatic mode (auto-approve, one-shot, exit)
|
||||
vibe --agent NAME # Select agent profile
|
||||
vibe --agent NAME # Select agent profile (falls back to `default_agent` config)
|
||||
vibe --workdir DIR # Change working directory
|
||||
vibe --trust # Trust cwd for this invocation only (not persisted)
|
||||
vibe -c / --continue # Continue most recent session
|
||||
|
|
@ -374,6 +383,15 @@ Custom agents are TOML files in `~/.vibe/agents/NAME.toml`.
|
|||
- `/mcp` - Display available MCP servers (pass a server name to list its tools)
|
||||
- `/resume` (or `/continue`) - Browse and resume past sessions
|
||||
- `/rewind` - Rewind to a previous message
|
||||
- `/loop <interval> <prompt>` - Schedule a recurring prompt (e.g. `/loop 30s ping`).
|
||||
Intervals: `Ns/Nm/Nh/Nd`, minimum 30s, max 50 loops/session.
|
||||
- `/loop` (or `/loop list` / `/loop ls`) - List current scheduled loops.
|
||||
- `/loop cancel <id|all>` (aliases `rm`, `stop`, `delete`) - Cancel a loop.
|
||||
- Loops fire only when the agent is idle and the input bar is focused. At
|
||||
most one loop fires per poll. Overdue loops fire once on the next poll
|
||||
(no catch-up); `next_fire_at` advances to `now + interval`.
|
||||
- Loops are persisted in the session metadata (`loops` field of `meta.json`)
|
||||
and restored on `--resume`/`--continue`.
|
||||
- `/terminal-setup` - Configure Shift+Enter for newlines
|
||||
- `/proxy-setup` - Configure proxy and SSL certificate settings
|
||||
- `/leanstall` - Install the Lean 4 agent (leanstral)
|
||||
|
|
@ -415,6 +433,13 @@ Detailed instructions for the model...
|
|||
- `MISTRAL_API_KEY` - API key for Mistral provider
|
||||
- `VIBE_ACTIVE_MODEL` - Override active model
|
||||
- `VIBE_*` - Any config field can be overridden with the `VIBE_` prefix
|
||||
- `LOG_LEVEL` - Logging level for `$VIBE_HOME/logs/vibe.log`. One of `DEBUG`,
|
||||
`INFO`, `WARNING` (default), `ERROR`, `CRITICAL`. Invalid values fall back
|
||||
to `WARNING`.
|
||||
- `LOG_MAX_BYTES` - Max size in bytes of `vibe.log` before rotation
|
||||
(default: `10485760`, i.e. 10 MiB).
|
||||
- `DEBUG_MODE` - When `true`, forces `DEBUG`-level logging. Under `vibe-acp`
|
||||
it also attaches `debugpy` on `localhost:5678`.
|
||||
|
||||
## API Keys (.env file)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ from vibe.core.telemetry.types import (
|
|||
AgentEntrypoint,
|
||||
EntrypointMetadata,
|
||||
TelemetryCallType,
|
||||
TeleportCompletedPayload,
|
||||
TeleportFailedPayload,
|
||||
TeleportFailureStage,
|
||||
)
|
||||
from vibe.core.utils import get_server_url_from_api_base, get_user_agent
|
||||
|
||||
|
|
@ -63,7 +66,7 @@ class TelemetryClient:
|
|||
def _get_mistral_provider_and_api_key(self) -> tuple[ProviderConfig, str] | None:
|
||||
try:
|
||||
provider = self._config_getter().get_mistral_provider()
|
||||
except ValueError:
|
||||
except Exception:
|
||||
return None
|
||||
if provider is None:
|
||||
return None
|
||||
|
|
@ -77,7 +80,7 @@ class TelemetryClient:
|
|||
"""Check if telemetry is enabled in the current config."""
|
||||
try:
|
||||
return self._config_getter().enable_telemetry
|
||||
except ValueError:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def is_active(self) -> bool:
|
||||
|
|
@ -270,6 +273,9 @@ class TelemetryClient:
|
|||
}
|
||||
self.send_telemetry_event("vibe.new_session", payload)
|
||||
|
||||
def send_session_closed(self) -> None:
|
||||
self.send_telemetry_event("vibe.session_closed", {})
|
||||
|
||||
def send_onboarding_api_key_added(self) -> None:
|
||||
self.send_telemetry_event(
|
||||
"vibe.onboarding_api_key_added", {"version": __version__}
|
||||
|
|
@ -322,3 +328,35 @@ class TelemetryClient:
|
|||
{"rating": rating, "version": __version__, "model": model},
|
||||
correlation_id=self.last_correlation_id,
|
||||
)
|
||||
|
||||
def send_teleport_completed(
|
||||
self,
|
||||
*,
|
||||
push_required: bool,
|
||||
github_auth_required: bool,
|
||||
nb_session_messages: int,
|
||||
) -> None:
|
||||
payload: TeleportCompletedPayload = {
|
||||
"push_required": push_required,
|
||||
"github_auth_required": github_auth_required,
|
||||
"nb_session_messages": nb_session_messages,
|
||||
}
|
||||
self.send_telemetry_event("vibe.teleport_completed", dict(payload))
|
||||
|
||||
def send_teleport_failed(
|
||||
self,
|
||||
*,
|
||||
stage: TeleportFailureStage,
|
||||
error_class: str,
|
||||
push_required: bool,
|
||||
github_auth_required: bool,
|
||||
nb_session_messages: int,
|
||||
) -> None:
|
||||
payload: TeleportFailedPayload = {
|
||||
"stage": stage,
|
||||
"error_class": error_class,
|
||||
"push_required": push_required,
|
||||
"github_auth_required": github_auth_required,
|
||||
"nb_session_messages": nb_session_messages,
|
||||
}
|
||||
self.send_telemetry_event("vibe.teleport_failed", dict(payload))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
|
@ -36,3 +36,26 @@ class TelemetryRequestMetadata(TelemetryBaseMetadata):
|
|||
call_type: TelemetryCallType
|
||||
call_source: str = "vibe_code"
|
||||
message_id: str | None = None
|
||||
|
||||
|
||||
TeleportFailureStage = Literal[
|
||||
"no_history",
|
||||
"remote_session",
|
||||
"git_check",
|
||||
"push",
|
||||
"workflow_start",
|
||||
"github_auth",
|
||||
"fetch_url",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
|
||||
class TeleportCompletedPayload(TypedDict):
|
||||
push_required: bool
|
||||
github_auth_required: bool
|
||||
nb_session_messages: int
|
||||
|
||||
|
||||
class TeleportFailedPayload(TeleportCompletedPayload):
|
||||
stage: TeleportFailureStage
|
||||
error_class: str
|
||||
|
|
|
|||
94
vibe/core/teleport/telemetry.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from vibe.core.telemetry.send import TelemetryClient
|
||||
from vibe.core.telemetry.types import TeleportFailureStage
|
||||
from vibe.core.teleport.errors import ServiceTeleportError
|
||||
from vibe.core.teleport.types import (
|
||||
TeleportAuthRequiredEvent,
|
||||
TeleportCheckingGitEvent,
|
||||
TeleportCompleteEvent,
|
||||
TeleportFetchingUrlEvent,
|
||||
TeleportPushingEvent,
|
||||
TeleportPushRequiredEvent,
|
||||
TeleportStartingWorkflowEvent,
|
||||
TeleportWaitingForGitHubEvent,
|
||||
TeleportYieldEvent,
|
||||
)
|
||||
|
||||
|
||||
def send_teleport_early_failure_telemetry(
|
||||
telemetry_client: TelemetryClient,
|
||||
*,
|
||||
stage: TeleportFailureStage,
|
||||
error_class: str,
|
||||
nb_session_messages: int,
|
||||
) -> None:
|
||||
telemetry_client.send_teleport_failed(
|
||||
stage=stage,
|
||||
error_class=error_class,
|
||||
push_required=False,
|
||||
github_auth_required=False,
|
||||
nb_session_messages=nb_session_messages,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TeleportTelemetryTracker:
|
||||
telemetry_client: TelemetryClient
|
||||
nb_session_messages: int
|
||||
stage: TeleportFailureStage
|
||||
push_required: bool = False
|
||||
github_auth_required: bool = False
|
||||
success: bool = False
|
||||
error_class: str | None = None
|
||||
|
||||
def record_event(self, event: TeleportYieldEvent) -> None:
|
||||
match event:
|
||||
case TeleportCheckingGitEvent():
|
||||
self.stage = "git_check"
|
||||
case TeleportPushRequiredEvent():
|
||||
self.push_required = True
|
||||
self.stage = "cancelled"
|
||||
case TeleportPushingEvent():
|
||||
self.stage = "push"
|
||||
case TeleportStartingWorkflowEvent():
|
||||
self.stage = "workflow_start"
|
||||
case TeleportWaitingForGitHubEvent():
|
||||
self.stage = "github_auth"
|
||||
case TeleportAuthRequiredEvent():
|
||||
self.github_auth_required = True
|
||||
self.stage = "github_auth"
|
||||
case TeleportFetchingUrlEvent():
|
||||
self.stage = "fetch_url"
|
||||
case TeleportCompleteEvent():
|
||||
self.success = True
|
||||
|
||||
def record_service_error(self, error: ServiceTeleportError) -> None:
|
||||
self.error_class = type(error).__name__
|
||||
|
||||
def record_cancelled(self) -> None:
|
||||
self.stage = "cancelled"
|
||||
self.error_class = "CancelledError"
|
||||
|
||||
def record_unexpected_error(self, error: Exception) -> None:
|
||||
self.error_class = type(error).__name__
|
||||
|
||||
def send_success(self) -> None:
|
||||
self.telemetry_client.send_teleport_completed(
|
||||
push_required=self.push_required,
|
||||
github_auth_required=self.github_auth_required,
|
||||
nb_session_messages=self.nb_session_messages,
|
||||
)
|
||||
|
||||
def send_failure_if_needed(self) -> None:
|
||||
if self.success or self.error_class is None:
|
||||
return
|
||||
self.telemetry_client.send_teleport_failed(
|
||||
stage=self.stage,
|
||||
error_class=self.error_class,
|
||||
push_required=self.push_required,
|
||||
github_auth_required=self.github_auth_required,
|
||||
nb_session_messages=self.nb_session_messages,
|
||||
)
|
||||
|
|
@ -141,7 +141,7 @@ class ReadFile(
|
|||
try:
|
||||
raw_lines: list[bytes] = []
|
||||
bytes_read = 0
|
||||
was_truncated = False
|
||||
was_truncated = True
|
||||
|
||||
async with await anyio.Path(file_path).open("rb") as f:
|
||||
line_index = 0
|
||||
|
|
@ -155,12 +155,13 @@ class ReadFile(
|
|||
|
||||
line_bytes = len(raw_line)
|
||||
if bytes_read + line_bytes > self.config.max_read_bytes:
|
||||
was_truncated = True
|
||||
break
|
||||
|
||||
raw_lines.append(raw_line)
|
||||
bytes_read += line_bytes
|
||||
line_index += 1
|
||||
else:
|
||||
was_truncated = False
|
||||
except OSError as exc:
|
||||
raise ToolError(f"Error reading {file_path}: {exc}") from exc
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ VIBE_AGENT_NAME = "mistral-vibe"
|
|||
|
||||
|
||||
def setup_tracing(config: VibeConfig) -> None:
|
||||
if not config.enable_otel:
|
||||
if not config.enable_telemetry or not config.enable_otel:
|
||||
return
|
||||
|
||||
exporter_cfg = config.otel_span_exporter_config
|
||||
|
|
|
|||
|
|
@ -26,6 +26,16 @@ from pydantic import (
|
|||
)
|
||||
|
||||
|
||||
class ScheduledLoop(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
id: str
|
||||
interval_seconds: int
|
||||
prompt: str
|
||||
next_fire_at: float
|
||||
created_at: float
|
||||
|
||||
|
||||
class Backend(StrEnum):
|
||||
MISTRAL = auto()
|
||||
GENERIC = auto()
|
||||
|
|
@ -142,6 +152,7 @@ class SessionMetadata(BaseModel):
|
|||
git_branch: str | None
|
||||
environment: dict[str, str | None]
|
||||
username: str
|
||||
loops: list[ScheduledLoop] = Field(default_factory=list)
|
||||
title: str | None = None
|
||||
title_source: Literal["auto", "manual"] = "auto"
|
||||
|
||||
|
|
@ -418,6 +429,8 @@ class CompactEndEvent(BaseEvent):
|
|||
old_context_tokens: int
|
||||
new_context_tokens: int
|
||||
summary_length: int
|
||||
old_session_id: str | None = None
|
||||
new_session_id: str | None = None
|
||||
# WORKAROUND: Using tool_call to communicate compact events to the client.
|
||||
# This should be revisited when the ACP protocol defines how compact events
|
||||
# should be represented.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,31 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.core.session.session_id import shorten_session_id
|
||||
|
||||
def compact_reduction_display(old_tokens: int | None, new_tokens: int | None) -> str:
|
||||
if old_tokens is None or new_tokens is None:
|
||||
return "Compaction complete"
|
||||
|
||||
def compact_reduction_display(
|
||||
old_tokens: int | None,
|
||||
new_tokens: int | None,
|
||||
*,
|
||||
old_session_id: str | None = None,
|
||||
new_session_id: str | None = None,
|
||||
) -> str:
|
||||
|
||||
message = "Compaction complete"
|
||||
if old_tokens is not None and new_tokens is not None:
|
||||
reduction = old_tokens - new_tokens
|
||||
reduction_pct = (reduction / old_tokens * 100) if old_tokens > 0 else 0
|
||||
return (
|
||||
f"Compaction complete: {old_tokens:,} → "
|
||||
message = (
|
||||
f"{message}: {old_tokens:,} → "
|
||||
f"{new_tokens:,} tokens ({-reduction_pct:+#0.2g}%)"
|
||||
)
|
||||
|
||||
if old_session_id is not None and new_session_id is not None:
|
||||
short_old = shorten_session_id(old_session_id)
|
||||
short_new = shorten_session_id(new_session_id)
|
||||
message = (
|
||||
f"{message}\n"
|
||||
f"session: {short_old} (before compaction) → {short_new} (after compaction)"
|
||||
)
|
||||
|
||||
return message
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# What's new in v2.9.4
|
||||
# What's new in v2.9.5
|
||||
|
||||
- **`/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.
|
||||
- **`/loop` command**: Run a prompt or slash command on a recurring interval.
|
||||
- **`default_agent` config**: Set which agent profile starts each session.
|
||||
- **Parallel-safe history**: Multiple `vibe` instances no longer clobber each other's history file.
|
||||
|
|
|
|||