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)
|
# Profiler HTML/TXT reports (generated by vibe.cli.profiler)
|
||||||
*-profile.html
|
*-profile.html
|
||||||
*-profile.txt
|
*-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
|
## Logging & errors
|
||||||
|
|
||||||
- Use `from vibe.core.logger import logger` — stdlib `logging` with `StructuredLogFormatter`, not `structlog`.
|
- 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}")`.
|
- 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.
|
- 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/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.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
|
## [2.9.4] - 2026-05-05
|
||||||
|
|
||||||
### Added
|
### 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_LEVEL` | Logging level (`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`) | `WARNING` |
|
||||||
| `LOG_MAX_BYTES` | Max log file size in bytes before rotation | `10485760` (10 MB) |
|
| `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:
|
Example:
|
||||||
|
|
||||||
|
|
|
||||||
16
README.md
|
|
@ -123,6 +123,22 @@ Use the `--agent` flag to select a different agent:
|
||||||
vibe --agent plan
|
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
|
### 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.
|
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"
|
id = "mistral-vibe"
|
||||||
name = "Mistral Vibe"
|
name = "Mistral Vibe"
|
||||||
description = "Mistral's open-source coding assistant"
|
description = "Mistral's open-source coding assistant"
|
||||||
version = "2.9.4"
|
version = "2.9.5"
|
||||||
schema_version = 1
|
schema_version = 1
|
||||||
authors = ["Mistral AI"]
|
authors = ["Mistral AI"]
|
||||||
repository = "https://github.com/mistralai/mistral-vibe"
|
repository = "https://github.com/mistralai/mistral-vibe"
|
||||||
|
|
@ -11,21 +11,21 @@ name = "Mistral Vibe"
|
||||||
icon = "./icons/mistral_vibe.svg"
|
icon = "./icons/mistral_vibe.svg"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.darwin-aarch64]
|
[agent_servers.mistral-vibe.targets.darwin-aarch64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.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"
|
cmd = "./vibe-acp"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.darwin-x86_64]
|
[agent_servers.mistral-vibe.targets.darwin-x86_64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.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"
|
cmd = "./vibe-acp"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.linux-aarch64]
|
[agent_servers.mistral-vibe.targets.linux-aarch64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.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"
|
cmd = "./vibe-acp"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.linux-x86_64]
|
[agent_servers.mistral-vibe.targets.linux-x86_64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.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"
|
cmd = "./vibe-acp"
|
||||||
|
|
||||||
[agent_servers.mistral-vibe.targets.windows-x86_64]
|
[agent_servers.mistral-vibe.targets.windows-x86_64]
|
||||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.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"
|
cmd = "./vibe-acp.exe"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "mistral-vibe"
|
name = "mistral-vibe"
|
||||||
version = "2.9.4"
|
version = "2.9.5"
|
||||||
description = "Minimal CLI coding agent by Mistral"
|
description = "Minimal CLI coding agent by Mistral"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,11 @@ def _build_env(vibe_home_dir: Path, *, include_api_key: bool) -> dict[str, str]:
|
||||||
env["PYTHONUNBUFFERED"] = "1"
|
env["PYTHONUNBUFFERED"] = "1"
|
||||||
env["VIBE_HOME"] = str(vibe_home_dir)
|
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:
|
if include_api_key:
|
||||||
env["MISTRAL_API_KEY"] = "mock"
|
env["MISTRAL_API_KEY"] = "mock"
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
||||||
class TestCloseSession:
|
class TestCloseSession:
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_close_session_removes_session_and_closes_resources(
|
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:
|
) -> None:
|
||||||
session_response = await acp_agent_loop.new_session(cwd=".", mcp_servers=[])
|
session_response = await acp_agent_loop.new_session(cwd=".", mcp_servers=[])
|
||||||
session = acp_agent_loop.sessions[session_response.session_id]
|
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
|
assert session_response.session_id not in acp_agent_loop.sessions
|
||||||
backend_close.assert_awaited_once()
|
backend_close.assert_awaited_once()
|
||||||
telemetry_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
|
@pytest.mark.asyncio
|
||||||
async def test_close_session_cancels_active_prompt(
|
async def test_close_session_cancels_active_prompt(
|
||||||
|
|
@ -69,6 +80,23 @@ class TestCloseSession:
|
||||||
|
|
||||||
assert bg_task.cancelled()
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_close_session_rejects_new_background_tasks(
|
async def test_close_session_rejects_new_background_tasks(
|
||||||
self, acp_agent_loop: VibeAcpAgentLoop
|
self, acp_agent_loop: VibeAcpAgentLoop
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from tests.stubs.fake_backend import FakeBackend
|
||||||
from tests.stubs.fake_client import FakeClient
|
from tests.stubs.fake_client import FakeClient
|
||||||
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
||||||
from vibe.core.agent_loop import AgentLoop
|
from vibe.core.agent_loop import AgentLoop
|
||||||
|
from vibe.core.session.session_id import shorten_session_id
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -79,3 +80,10 @@ class TestCompactEventHandling:
|
||||||
assert compact_end.status == "completed"
|
assert compact_end.status == "completed"
|
||||||
|
|
||||||
assert compact_start.tool_call_id == compact_end.tool_call_id
|
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(
|
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 == []
|
assert response.auth_methods == []
|
||||||
|
|
@ -62,7 +62,7 @@ class TestACPInitialize:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
assert response.agent_info == Implementation(
|
assert response.agent_info == Implementation(
|
||||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.4"
|
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.9.5"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.auth_methods is not None
|
assert response.auth_methods is not None
|
||||||
|
|
|
||||||
|
|
@ -92,20 +92,26 @@ class TestMultiSessionCore:
|
||||||
)
|
)
|
||||||
assert user_message1 is not None
|
assert user_message1 is not None
|
||||||
assert user_message1.content == "Prompt for session 1"
|
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(
|
user_message2 = next(
|
||||||
(msg for msg in session2.agent_loop.messages if msg.role == Role.user), None
|
(msg for msg in session2.agent_loop.messages if msg.role == Role.user), None
|
||||||
)
|
)
|
||||||
assert user_message2 is not None
|
assert user_message2 is not None
|
||||||
assert user_message2.content == "Prompt for session 2"
|
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(
|
assistant_message2 = next(
|
||||||
(msg for msg in session2.agent_loop.messages if msg.role == Role.assistant),
|
(msg for msg in session2.agent_loop.messages if msg.role == Role.assistant),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
assert assistant_message1 is not None
|
||||||
assert assistant_message2 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"
|
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:
|
def test_callable_entries_reflects_enabled_disabled_skills() -> None:
|
||||||
"""Test that skill enable/disable changes are reflected in completions.
|
"""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
|
@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,
|
vibe_app: VibeApp,
|
||||||
) -> None:
|
) -> None:
|
||||||
async with vibe_app.run_test() as pilot:
|
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")
|
await pilot.press("tab")
|
||||||
|
|
||||||
assert chat_input.value == "/config"
|
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:
|
def ensure_selected_command(popup: CompletionPopup, expected_alias: str) -> None:
|
||||||
|
|
|
||||||
|
|
@ -133,3 +133,13 @@ class TestCommandRegistry:
|
||||||
assert result is not None
|
assert result is not None
|
||||||
_, cmd, _ = result
|
_, cmd, _ = result
|
||||||
assert cmd.handler == "_show_data_retention"
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import pytest
|
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.plan_offer.ports.whoami_gateway import WhoAmIPlanType, WhoAmIResponse
|
||||||
from vibe.cli.textual_ui.widgets.chat_input import ChatInputContainer
|
from vibe.cli.textual_ui.widgets.chat_input import ChatInputContainer
|
||||||
from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig
|
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:
|
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")
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_teleport_command_visible_for_paid_chat_users() -> None:
|
async def test_teleport_command_visible_for_paid_chat_users() -> None:
|
||||||
app = build_test_vibe_app(
|
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
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_teleport_command_hidden_when_current_key_is_not_eligible() -> None:
|
async def test_teleport_command_hidden_when_current_key_is_not_eligible() -> None:
|
||||||
app = build_test_vibe_app(
|
app = build_test_vibe_app(
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ def get_base_config() -> dict[str, Any]:
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"enable_auto_update": False,
|
"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,
|
correlation_id: str | None = 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:
|
if correlation_id is not None:
|
||||||
event["correlation_id"] = correlation_id
|
event["correlation_id"] = correlation_id
|
||||||
events.append(event)
|
events.append(event)
|
||||||
|
|
|
||||||
|
|
@ -104,3 +104,19 @@ class TestAgentManager:
|
||||||
)
|
)
|
||||||
manager = AgentManager(lambda: config, initial_agent="plan")
|
manager = AgentManager(lambda: config, initial_agent="plan")
|
||||||
assert manager.active_profile.name == "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"] == "my_skill"
|
||||||
assert telemetry_events[1]["properties"]["command_type"] == "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(
|
def test_send_new_session_payload(
|
||||||
self, telemetry_events: list[dict[str, Any]]
|
self, telemetry_events: list[dict[str, Any]]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -393,6 +435,55 @@ class TestTelemetryClient:
|
||||||
assert properties["terminal_emulator"] == "vscode"
|
assert properties["terminal_emulator"] == "vscode"
|
||||||
assert "version" in properties
|
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:
|
def test_build_base_metadata_includes_entrypoint_and_session(self) -> None:
|
||||||
metadata = build_base_metadata(
|
metadata = build_base_metadata(
|
||||||
entrypoint_metadata=EntrypointMetadata(
|
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
|
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
|
import vibe.core.utils.io as io_utils
|
||||||
from vibe.core.utils.io import decode_safe, read_safe, read_safe_async
|
from vibe.core.utils.io import 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
|
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:
|
class TestReadSafe:
|
||||||
def test_reads_utf8(self, tmp_path: Path) -> None:
|
def test_reads_utf8(self, tmp_path: Path) -> None:
|
||||||
f = tmp_path / "hello.txt"
|
f = tmp_path / "hello.txt"
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,62 @@ class TestReadFileExecution:
|
||||||
|
|
||||||
assert result.content == "line 50\nline 51\n"
|
assert result.content == "line 50\nline 51\n"
|
||||||
assert result.lines_read == 2
|
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 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:
|
class TestGetResultExtra:
|
||||||
|
|
|
||||||
|
|
@ -5,33 +5,41 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from vibe.core.session.resume_sessions import (
|
from vibe.core.session.resume_sessions import (
|
||||||
SHORT_SESSION_ID_LEN,
|
|
||||||
list_remote_resume_sessions,
|
list_remote_resume_sessions,
|
||||||
short_session_id,
|
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:
|
class TestShortSessionId:
|
||||||
def test_local_shortens_to_first_chars(self) -> None:
|
def test_local_delegates_to_shorten(self) -> None:
|
||||||
sid = "abcdef1234567890"
|
sid = "abcdef1234567890"
|
||||||
result = short_session_id(sid)
|
assert short_session_id(sid) == shorten_session_id(sid)
|
||||||
assert result == sid[:SHORT_SESSION_ID_LEN]
|
|
||||||
assert len(result) == SHORT_SESSION_ID_LEN
|
|
||||||
|
|
||||||
def test_local_is_default(self) -> None:
|
def test_local_is_default(self) -> None:
|
||||||
sid = "abcdef1234567890"
|
sid = "abcdef1234567890"
|
||||||
assert short_session_id(sid) == short_session_id(sid, source="local")
|
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"
|
sid = "abcdef1234567890"
|
||||||
result = short_session_id(sid, source="remote")
|
assert short_session_id(sid, source="remote") == shorten_session_id(
|
||||||
assert result == sid[-SHORT_SESSION_ID_LEN:]
|
sid, from_end=True
|
||||||
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"
|
|
||||||
|
|
||||||
def test_empty_string(self) -> None:
|
def test_empty_string(self) -> None:
|
||||||
assert short_session_id("") == ""
|
assert short_session_id("") == ""
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import pytest
|
||||||
from tests.conftest import build_test_vibe_config
|
from tests.conftest import build_test_vibe_config
|
||||||
from vibe.core.agents.models import AgentProfile, AgentSafety
|
from vibe.core.agents.models import AgentProfile, AgentSafety
|
||||||
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
||||||
|
from vibe.core.loop import ScheduledLoop
|
||||||
from vibe.core.session.session_logger import SessionLogger
|
from vibe.core.session.session_logger import SessionLogger
|
||||||
from vibe.core.tools.manager import ToolManager
|
from vibe.core.tools.manager import ToolManager
|
||||||
from vibe.core.types import AgentStats, LLMMessage, Role, SessionMetadata
|
from vibe.core.types import AgentStats, LLMMessage, Role, SessionMetadata
|
||||||
|
|
@ -868,3 +869,136 @@ class TestSessionLoggerCleanupTmpFiles:
|
||||||
logger.maybe_cleanup_tmp_files()
|
logger.maybe_cleanup_tmp_files()
|
||||||
|
|
||||||
assert cleanup_spy.call_count == 2
|
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="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="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-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="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="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)">
|
</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="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="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-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="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="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)">
|
</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-r1 { fill: #c5c8c6 }
|
||||||
.terminal-r2 { fill: #68a0b3 }
|
.terminal-r2 { fill: #9a9b99 }
|
||||||
.terminal-r3 { fill: #4b4e55 }
|
.terminal-r3 { fill: #608ab1;font-weight: bold }
|
||||||
.terminal-r4 { fill: #9a9b99 }
|
.terminal-r4 { fill: #98a84b;font-weight: bold }
|
||||||
.terminal-r5 { fill: #608ab1;font-weight: bold }
|
.terminal-r5 { fill: #4b4e55 }
|
||||||
.terminal-r6 { fill: #292929 }
|
.terminal-r6 { fill: #292929 }
|
||||||
.terminal-r7 { fill: #98a84b;font-weight: bold }
|
.terminal-r7 { fill: #ff8205;font-weight: bold }
|
||||||
.terminal-r8 { fill: #ff8205;font-weight: bold }
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<defs>
|
<defs>
|
||||||
|
|
@ -197,56 +196,56 @@
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||||
<rect fill="#4b4e55" x="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">
|
<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 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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-r4" x="24.4" y="971.6" textLength="12.2" clip-path="url(#terminal-line-39)">⎣</text><text class="terminal-r1" x="48.8" y="971.6" textLength="24.4" clip-path="url(#terminal-line-39)">• </text><text class="terminal-r7" x="73.2" y="971.6" textLength="183" clip-path="url(#terminal-line-39)">/data-retention</text><text class="terminal-r1" x="256.2" y="971.6" textLength="402.6" clip-path="url(#terminal-line-39)">: Show data retention information</text><text class="terminal-r1" x="1464" y="971.6" textLength="12.2" clip-path="url(#terminal-line-39)">
|
</text><text class="terminal-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="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-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-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-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-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-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-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-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-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-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-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-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="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>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
|
@ -196,47 +196,47 @@
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
|
||||||
<rect fill="#4b4e55" x="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">
|
<g class="terminal-matrix">
|
||||||
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
|
<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-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="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="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="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)">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="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)">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="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+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="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+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="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="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="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="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="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="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="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-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
|
</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-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="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="1464" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
|
</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="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="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="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="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-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
</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-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="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="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
</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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="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="85.4" clip-path="url(#terminal-line-38)">/rewind</text><text class="terminal-r1" x="158.6" y="947.2" textLength="366" clip-path="url(#terminal-line-38)">: Rewind to a previous message</text><text class="terminal-r1" x="1464" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">
|
</text><text class="terminal-r2" x="24.4" y="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-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="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-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()
|
time.tzset()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _pin_snapshot_colors(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.delenv("NO_COLOR", raising=False)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _pin_banner_version(monkeypatch: pytest.MonkeyPatch) -> None:
|
def _pin_banner_version(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr(
|
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.base_snapshot_test_app import BaseSnapshotTestApp
|
||||||
from tests.snapshots.snap_compare import SnapCompare
|
from tests.snapshots.snap_compare import SnapCompare
|
||||||
from tests.stubs.fake_backend import FakeBackend
|
from tests.stubs.fake_backend import FakeBackend
|
||||||
|
from vibe.core.telemetry.send import TelemetryClient
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@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",
|
"vibe.cli.textual_ui.widgets.feedback_bar_manager.MIN_USER_MESSAGES_FOR_FEEDBACK",
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
|
monkeypatch.setattr(TelemetryClient, "is_active", lambda self: True)
|
||||||
|
|
||||||
|
|
||||||
class FeedbackBarSnapshotApp(BaseSnapshotTestApp):
|
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
|
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]]:
|
def _snapshot_events(events: list) -> list[tuple[str, str]]:
|
||||||
return [
|
return [
|
||||||
(type(e).__name__, e.content)
|
(type(e).__name__, e.content)
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,15 @@ def test_run_programmatic_preload_streaming_is_batched(
|
||||||
new_session = [
|
new_session = [
|
||||||
e for e in telemetry_events if e.get("event_name") == "vibe.new_session"
|
e for e in telemetry_events if e.get("event_name") == "vibe.new_session"
|
||||||
]
|
]
|
||||||
|
|
||||||
assert len(new_session) == 0
|
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 (
|
assert (
|
||||||
spy.emitted[0][1] == "You are Vibe, a super useful programming assistant."
|
spy.emitted[0][1] == "You are Vibe, a super useful programming assistant."
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from vibe.cli.history_manager import HistoryManager
|
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
|
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:
|
def test_history_manager_allows_navigation_round_trip(tmp_path: Path) -> None:
|
||||||
history_file = tmp_path / "history.jsonl"
|
history_file = tmp_path / "history.jsonl"
|
||||||
manager = HistoryManager(history_file)
|
manager = HistoryManager(history_file)
|
||||||
|
|
|
||||||
|
|
@ -48,19 +48,28 @@ def _otel_provider(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
|
||||||
class TestSetupTracing:
|
class TestSetupTracing:
|
||||||
def test_noop_when_disabled(self) -> None:
|
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:
|
with patch("vibe.core.tracing.trace.set_tracer_provider") as mock_set:
|
||||||
setup_tracing(config)
|
setup_tracing(config)
|
||||||
mock_set.assert_not_called()
|
mock_set.assert_not_called()
|
||||||
|
|
||||||
def test_noop_when_exporter_config_is_none(self) -> None:
|
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:
|
with patch("vibe.core.tracing.trace.set_tracer_provider") as mock_set:
|
||||||
setup_tracing(config)
|
setup_tracing(config)
|
||||||
mock_set.assert_not_called()
|
mock_set.assert_not_called()
|
||||||
|
|
||||||
def test_configures_provider_from_exporter_config(self) -> None:
|
def test_configures_provider_from_exporter_config(self) -> None:
|
||||||
config = MagicMock(
|
config = MagicMock(
|
||||||
|
enable_telemetry=True,
|
||||||
enable_otel=True,
|
enable_otel=True,
|
||||||
otel_span_exporter_config=OtelSpanExporterConfig(
|
otel_span_exporter_config=OtelSpanExporterConfig(
|
||||||
endpoint="https://customer.mistral.ai/telemetry/v1/traces",
|
endpoint="https://customer.mistral.ai/telemetry/v1/traces",
|
||||||
|
|
@ -85,6 +94,7 @@ class TestSetupTracing:
|
||||||
|
|
||||||
def test_custom_endpoint_has_no_auth_headers(self) -> None:
|
def test_custom_endpoint_has_no_auth_headers(self) -> None:
|
||||||
config = MagicMock(
|
config = MagicMock(
|
||||||
|
enable_telemetry=True,
|
||||||
enable_otel=True,
|
enable_otel=True,
|
||||||
otel_span_exporter_config=OtelSpanExporterConfig(
|
otel_span_exporter_config=OtelSpanExporterConfig(
|
||||||
endpoint="https://my-collector:4318/v1/traces"
|
endpoint="https://my-collector:4318/v1/traces"
|
||||||
|
|
|
||||||
4
uv.lock
generated
|
|
@ -3,7 +3,7 @@ revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
exclude-newer = "2026-04-28T08:28:58.401523Z"
|
exclude-newer = "2026-04-29T14:28:22.645142Z"
|
||||||
exclude-newer-span = "P7D"
|
exclude-newer-span = "P7D"
|
||||||
|
|
||||||
[options.exclude-newer-package]
|
[options.exclude-newer-package]
|
||||||
|
|
@ -820,7 +820,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mistral-vibe"
|
name = "mistral-vibe"
|
||||||
version = "2.9.4"
|
version = "2.9.5"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "agent-client-protocol" },
|
{ name = "agent-client-protocol" },
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@ from __future__ import annotations
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
VIBE_ROOT = Path(__file__).parent
|
VIBE_ROOT = Path(__file__).parent
|
||||||
__version__ = "2.9.4"
|
__version__ = "2.9.5"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import inspect
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, cast, override
|
from typing import Any, cast, override
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
@ -994,11 +995,22 @@ class VibeAcpAgentLoop(AcpAgent):
|
||||||
session = self._get_session(session_id)
|
session = self._get_session(session_id)
|
||||||
self.sessions.pop(session_id, None)
|
self.sessions.pop(session_id, None)
|
||||||
|
|
||||||
|
session.agent_loop.emit_session_closed_telemetry()
|
||||||
await session.close()
|
await session.close()
|
||||||
await self._close_agent_loop(session.agent_loop)
|
await self._close_agent_loop(session.agent_loop)
|
||||||
|
|
||||||
return CloseSessionResponse()
|
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:
|
async def _close_agent_loop(self, agent_loop: AgentLoop) -> None:
|
||||||
deferred_init_thread = agent_loop._deferred_init_thread
|
deferred_init_thread = agent_loop._deferred_init_thread
|
||||||
if deferred_init_thread is not None and deferred_init_thread.is_alive():
|
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())
|
tool_call_id = str(uuid4())
|
||||||
old_tokens = session.agent_loop.stats.context_tokens
|
old_tokens = session.agent_loop.stats.context_tokens
|
||||||
|
old_session_id = session.agent_loop.session_id
|
||||||
parts = text_prompt.strip().split(None, 1)
|
parts = text_prompt.strip().split(None, 1)
|
||||||
cmd_args = parts[1] if len(parts) > 1 else ""
|
cmd_args = parts[1] if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
|
@ -1268,6 +1281,8 @@ class VibeAcpAgentLoop(AcpAgent):
|
||||||
old_context_tokens=old_tokens or 0,
|
old_context_tokens=old_tokens or 0,
|
||||||
new_context_tokens=new_tokens or 0,
|
new_context_tokens=new_tokens or 0,
|
||||||
summary_length=0,
|
summary_length=0,
|
||||||
|
old_session_id=old_session_id,
|
||||||
|
new_session_id=session.agent_loop.session_id,
|
||||||
tool_call_id=tool_call_id,
|
tool_call_id=tool_call_id,
|
||||||
)
|
)
|
||||||
await self.client.session_update(
|
await self.client.session_update(
|
||||||
|
|
@ -1419,19 +1434,48 @@ class VibeAcpAgentLoop(AcpAgent):
|
||||||
return await self._command_reply(session, DATA_RETENTION_MESSAGE, message_id)
|
return await self._command_reply(session, DATA_RETENTION_MESSAGE, message_id)
|
||||||
|
|
||||||
|
|
||||||
|
SESSION_CLOSED_FLUSH_TIMEOUT_SECONDS = 1.0
|
||||||
|
|
||||||
|
|
||||||
def run_acp_server() -> None:
|
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:
|
try:
|
||||||
asyncio.run(
|
asyncio.run(
|
||||||
run_agent(
|
run_agent(
|
||||||
agent=VibeAcpAgentLoop(),
|
agent=agent,
|
||||||
use_unstable_protocol=True,
|
use_unstable_protocol=True,
|
||||||
observers=[acp_message_observer],
|
observers=[acp_message_observer],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except KeyboardInterrupt:
|
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
|
# This is expected when the server is terminated
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Log any unexpected errors
|
# Log any unexpected errors
|
||||||
print(f"ACP Agent Server error: {e}", file=sys.stderr)
|
print(f"ACP Agent Server error: {e}", file=sys.stderr)
|
||||||
raise
|
raise
|
||||||
|
finally:
|
||||||
|
if install_sigterm_flush:
|
||||||
|
signal.signal(signal.SIGTERM, previous_sigterm_handler)
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ def bootstrap_config_files() -> None:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# When DEBUG_MODE=true, attaches debugpy on localhost:5678.
|
||||||
def handle_debug_mode() -> None:
|
def handle_debug_mode() -> None:
|
||||||
if os.environ.get("DEBUG_MODE") != "true":
|
if os.environ.get("DEBUG_MODE") != "true":
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,10 @@ def create_compact_end_session_update(event: CompactEndEvent) -> ToolCallProgres
|
||||||
type="text",
|
type="text",
|
||||||
text=(
|
text=(
|
||||||
compact_reduction_display(
|
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
|
return
|
||||||
|
|
||||||
suggestions = self._completer.get_completion_items(text, cursor_index)
|
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._suggestions = suggestions
|
||||||
self._selected_index = 0
|
self._selected_index = 0
|
||||||
self._view.render_completion_suggestions(
|
self._view.render_completion_suggestions(
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,10 @@ def _build_cli_entrypoint_metadata() -> EntrypointMetadata:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_initial_agent_name(args: argparse.Namespace) -> str:
|
def get_initial_agent_name(args: argparse.Namespace, config: VibeConfig) -> str:
|
||||||
if args.prompt is not None and args.agent == BuiltinAgentName.DEFAULT:
|
if args.prompt is not None and args.agent is None:
|
||||||
return BuiltinAgentName.AUTO_APPROVE
|
return BuiltinAgentName.AUTO_APPROVE
|
||||||
return args.agent
|
return args.agent or config.default_agent
|
||||||
|
|
||||||
|
|
||||||
def get_prompt_from_stdin() -> str | None:
|
def get_prompt_from_stdin() -> str | None:
|
||||||
|
|
@ -196,9 +196,9 @@ def run_cli(args: argparse.Namespace) -> None:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
initial_agent_name = get_initial_agent_name(args)
|
|
||||||
is_interactive = args.prompt is None
|
is_interactive = args.prompt is None
|
||||||
config = load_config_or_exit(interactive=is_interactive)
|
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)
|
hook_config_result = load_hooks_from_fs(config)
|
||||||
setup_tracing(config)
|
setup_tracing(config)
|
||||||
|
|
||||||
|
|
@ -243,10 +243,11 @@ def run_cli(args: argparse.Namespace) -> None:
|
||||||
except TeleportError as e:
|
except TeleportError as e:
|
||||||
print(f"Teleport error: {e}", file=sys.stderr)
|
print(f"Teleport error: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except RuntimeError as e:
|
except (RuntimeError, ValueError) as e:
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
agent_loop = AgentLoop(
|
agent_loop = AgentLoop(
|
||||||
config,
|
config,
|
||||||
agent_name=initial_agent_name,
|
agent_name=initial_agent_name,
|
||||||
|
|
@ -255,6 +256,9 @@ def run_cli(args: argparse.Namespace) -> None:
|
||||||
defer_heavy_init=True,
|
defer_heavy_init=True,
|
||||||
hook_config_result=hook_config_result,
|
hook_config_result=hook_config_result,
|
||||||
)
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
rprint(f"[red]Error:[/] {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if loaded_session:
|
if loaded_session:
|
||||||
_resume_previous_session(agent_loop, *loaded_session)
|
_resume_previous_session(agent_loop, *loaded_session)
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,14 @@ class CommandRegistry:
|
||||||
description="Rewind to a previous message",
|
description="Rewind to a previous message",
|
||||||
handler="_start_rewind_mode",
|
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(
|
"data-retention": Command(
|
||||||
aliases=frozenset(["/data-retention"]),
|
aliases=frozenset(["/data-retention"]),
|
||||||
description="Show data retention information",
|
description="Show data retention information",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import sys
|
||||||
from rich import print as rprint
|
from rich import print as rprint
|
||||||
|
|
||||||
from vibe import __version__
|
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.config.harness_files import init_harness_files_manager
|
||||||
from vibe.core.trusted_folders import find_trustable_files, trusted_folders_manager
|
from vibe.core.trusted_folders import find_trustable_files, trusted_folders_manager
|
||||||
from vibe.setup.trusted_folders.trust_folder_dialog import (
|
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:
|
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(
|
parser.add_argument(
|
||||||
"-v", "--version", action="version", version=f"%(prog)s {__version__}"
|
"-v", "--version", action="version", version=f"%(prog)s {__version__}"
|
||||||
)
|
)
|
||||||
|
|
@ -72,9 +82,12 @@ def parse_arguments() -> argparse.Namespace:
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--agent",
|
"--agent",
|
||||||
metavar="NAME",
|
metavar="NAME",
|
||||||
default=BuiltinAgentName.DEFAULT,
|
default=None,
|
||||||
help="Agent to use (builtin: default, plan, accept-edits, auto-approve, "
|
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("--setup", action="store_true", help="Setup API key and exit")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from vibe.core.utils.io import read_safe
|
from vibe.core.utils.io import read_safe
|
||||||
|
|
||||||
|
|
@ -22,12 +24,10 @@ class HistoryManager:
|
||||||
try:
|
try:
|
||||||
text = read_safe(self.history_file).text
|
text = read_safe(self.history_file).text
|
||||||
except OSError:
|
except OSError:
|
||||||
self._entries = []
|
|
||||||
return
|
return
|
||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
for raw_line in text.splitlines():
|
for raw_line in text.splitlines():
|
||||||
raw_line = raw_line.rstrip("\n\r")
|
|
||||||
if not raw_line:
|
if not raw_line:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
|
|
@ -36,13 +36,24 @@ class HistoryManager:
|
||||||
entry = raw_line
|
entry = raw_line
|
||||||
entries.append(entry if isinstance(entry, str) else str(entry))
|
entries.append(entry if isinstance(entry, str) else str(entry))
|
||||||
self._entries = entries[-self.max_entries :]
|
self._entries = entries[-self.max_entries :]
|
||||||
|
self.reset_navigation()
|
||||||
|
|
||||||
def _save_history(self) -> None:
|
def _save_history(self) -> None:
|
||||||
try:
|
try:
|
||||||
self.history_file.parent.mkdir(parents=True, exist_ok=True)
|
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:
|
for entry in self._entries:
|
||||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
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:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -51,6 +62,8 @@ class HistoryManager:
|
||||||
if not text:
|
if not text:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._load_history()
|
||||||
|
|
||||||
if self._entries and self._entries[-1] == text:
|
if self._entries and self._entries[-1] == text:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -60,7 +73,6 @@ class HistoryManager:
|
||||||
self._entries = self._entries[-self.max_entries :]
|
self._entries = self._entries[-self.max_entries :]
|
||||||
|
|
||||||
self._save_history()
|
self._save_history()
|
||||||
self.reset_navigation()
|
|
||||||
|
|
||||||
def get_previous(self, current_input: str) -> str | None:
|
def get_previous(self, current_input: str) -> str | None:
|
||||||
if not self._entries:
|
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.quit_manager import QuitManager
|
||||||
from vibe.cli.textual_ui.remote import RemoteSessionManager, is_progress_event
|
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.session_exit import print_session_resume_message
|
||||||
from vibe.cli.textual_ui.widgets.approval_app import ApprovalApp
|
from vibe.cli.textual_ui.widgets.approval_app import ApprovalApp
|
||||||
from vibe.cli.textual_ui.widgets.banner.banner import Banner
|
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.saved_sessions import update_saved_session_title_at_path
|
||||||
from vibe.core.session.session_loader import SessionLoader
|
from vibe.core.session.session_loader import SessionLoader
|
||||||
from vibe.core.skills.manager import SkillManager
|
from vibe.core.skills.manager import SkillManager
|
||||||
|
from vibe.core.teleport.telemetry import send_teleport_early_failure_telemetry
|
||||||
from vibe.core.teleport.types import (
|
from vibe.core.teleport.types import (
|
||||||
TeleportAuthCompleteEvent,
|
TeleportAuthCompleteEvent,
|
||||||
TeleportAuthRequiredEvent,
|
TeleportAuthRequiredEvent,
|
||||||
|
|
@ -339,7 +341,6 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.scroll_sensitivity_y = 1.0
|
|
||||||
self.agent_loop = agent_loop
|
self.agent_loop = agent_loop
|
||||||
self._plan_info: PlanInfo | None = None
|
self._plan_info: PlanInfo | None = None
|
||||||
self._voice_manager: VoiceManagerPort = (
|
self._voice_manager: VoiceManagerPort = (
|
||||||
|
|
@ -397,7 +398,17 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
self._rewind_mode = False
|
self._rewind_mode = False
|
||||||
self._rewind_highlighted_widget: UserMessage | None = None
|
self._rewind_highlighted_widget: UserMessage | None = None
|
||||||
self._fatal_init_error = False
|
self._fatal_init_error = False
|
||||||
|
self._force_quit_task: asyncio.Task[None] | None = None
|
||||||
self.commands = self._build_command_registry()
|
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:
|
def _configure_startup_options(self, startup: StartupOptions | None) -> None:
|
||||||
opts = startup or StartupOptions()
|
opts = startup or StartupOptions()
|
||||||
|
|
@ -504,6 +515,8 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
await self._resolve_plan()
|
await self._resolve_plan()
|
||||||
await self._show_dangerous_directory_warning()
|
await self._show_dangerous_directory_warning()
|
||||||
await self._resume_history_from_messages()
|
await self._resume_history_from_messages()
|
||||||
|
self._loop_runner.restore_from_session()
|
||||||
|
self._loop_runner.start()
|
||||||
await self._check_and_show_whats_new()
|
await self._check_and_show_whats_new()
|
||||||
self._schedule_update_notification()
|
self._schedule_update_notification()
|
||||||
if not self._is_resuming_session:
|
if not self._is_resuming_session:
|
||||||
|
|
@ -1407,6 +1420,12 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
if show_message:
|
if show_message:
|
||||||
await self._mount_and_scroll(UserMessage("/teleport"))
|
await self._mount_and_scroll(UserMessage("/teleport"))
|
||||||
if not has_history:
|
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(
|
await self._mount_and_scroll(
|
||||||
ErrorMessage(
|
ErrorMessage(
|
||||||
"No conversation history to teleport.",
|
"No conversation history to teleport.",
|
||||||
|
|
@ -1429,6 +1448,12 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
await self._mount_and_scroll(teleport_msg)
|
await self._mount_and_scroll(teleport_msg)
|
||||||
|
|
||||||
if self._remote_manager.is_active:
|
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 loading.remove()
|
||||||
await self._mount_and_scroll(
|
await self._mount_and_scroll(
|
||||||
ErrorMessage(
|
ErrorMessage(
|
||||||
|
|
@ -1818,6 +1843,8 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
f"Session `{short_session_id(session.session_id)}` not found."
|
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)
|
loaded_messages, metadata = SessionLoader.load_session(session_path)
|
||||||
if self._chat_input_container:
|
if self._chat_input_container:
|
||||||
self._chat_input_container.set_custom_border(None)
|
self._chat_input_container.set_custom_border(None)
|
||||||
|
|
@ -1846,6 +1873,7 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
if self.event_handler:
|
if self.event_handler:
|
||||||
self.event_handler.is_remote = False
|
self.event_handler.is_remote = False
|
||||||
await self._resume_history_from_messages()
|
await self._resume_history_from_messages()
|
||||||
|
self._loop_runner.restore_from_session()
|
||||||
await self._mount_and_scroll(
|
await self._mount_and_scroll(
|
||||||
UserCommandMessage(
|
UserCommandMessage(
|
||||||
f"Resumed session `{short_session_id(session.session_id)}`"
|
f"Resumed session `{short_session_id(session.session_id)}`"
|
||||||
|
|
@ -1856,6 +1884,8 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
await self._remote_manager.attach(
|
await self._remote_manager.attach(
|
||||||
session_id=session.session_id, config=self.config
|
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()
|
self._refresh_profile_widgets()
|
||||||
if self._chat_input_container:
|
if self._chat_input_container:
|
||||||
self._chat_input_container.set_custom_border(None)
|
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:
|
async def _compact_history(self, cmd_args: str = "", **kwargs: Any) -> None:
|
||||||
if self._agent_running:
|
if self._agent_running:
|
||||||
await self._mount_and_scroll(
|
await self._mount_and_scroll(
|
||||||
|
|
@ -2041,22 +2075,32 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
return
|
return
|
||||||
|
|
||||||
old_tokens = self.agent_loop.stats.context_tokens
|
old_tokens = self.agent_loop.stats.context_tokens
|
||||||
|
old_session_id = self.agent_loop.session_id
|
||||||
compact_msg = CompactMessage()
|
compact_msg = CompactMessage()
|
||||||
self.event_handler.current_compact = compact_msg
|
self.event_handler.current_compact = compact_msg
|
||||||
await self._mount_and_scroll(compact_msg)
|
await self._mount_and_scroll(compact_msg)
|
||||||
|
|
||||||
self._agent_task = asyncio.create_task(
|
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(
|
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:
|
) -> None:
|
||||||
self._agent_running = True
|
self._agent_running = True
|
||||||
try:
|
try:
|
||||||
await self.agent_loop.compact(extra_instructions=extra_instructions)
|
await self.agent_loop.compact(extra_instructions=extra_instructions)
|
||||||
new_tokens = self.agent_loop.stats.context_tokens
|
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:
|
except asyncio.CancelledError:
|
||||||
compact_msg.set_error("Compaction interrupted")
|
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)
|
return short_session_id(self.agent_loop.session_logger.session_id)
|
||||||
|
|
||||||
async def _exit_app(self, **kwargs: Any) -> None:
|
async def _exit_app(self, **kwargs: Any) -> None:
|
||||||
|
self._emit_session_closed_for_active_session()
|
||||||
|
await self._loop_runner.stop()
|
||||||
self._log_reader.shutdown()
|
self._log_reader.shutdown()
|
||||||
await self._narrator_manager.close()
|
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())
|
self.exit(result=self._get_session_resume_info())
|
||||||
|
|
||||||
def _make_default_voice_manager(self) -> VoiceManager:
|
def _make_default_voice_manager(self) -> VoiceManager:
|
||||||
|
|
@ -2746,7 +2797,16 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
return
|
return
|
||||||
self._quit_manager.request_confirmation("Ctrl+D")
|
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:
|
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():
|
if self._agent_task and not self._agent_task.done():
|
||||||
self._agent_task.cancel()
|
self._agent_task.cancel()
|
||||||
if self._bash_task and not self._bash_task.done():
|
if self._bash_task and not self._bash_task.done():
|
||||||
|
|
@ -2755,6 +2815,13 @@ class VibeApp(App): # noqa: PLR0904
|
||||||
|
|
||||||
self._log_reader.shutdown()
|
self._log_reader.shutdown()
|
||||||
self._narrator_manager.cancel()
|
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())
|
self.exit(result=self._get_session_resume_info())
|
||||||
|
|
||||||
def action_scroll_chat_up(self) -> None:
|
def action_scroll_chat_up(self) -> None:
|
||||||
|
|
|
||||||
|
|
@ -980,7 +980,7 @@ NarratorStatus {
|
||||||
height: auto;
|
height: auto;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0 0 0 1;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#banner-container {
|
#banner-container {
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,10 @@ class EventHandler:
|
||||||
async def _handle_compact_end(self, event: CompactEndEvent) -> None:
|
async def _handle_compact_end(self, event: CompactEndEvent) -> None:
|
||||||
if self.current_compact:
|
if self.current_compact:
|
||||||
self.current_compact.set_complete(
|
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
|
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.add_class("compact-message")
|
||||||
self.old_tokens: int | None = None
|
self.old_tokens: int | None = None
|
||||||
self.new_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
|
self.error_message: str | None = None
|
||||||
|
|
||||||
def get_content(self) -> str:
|
def get_content(self) -> str:
|
||||||
|
|
@ -26,13 +28,25 @@ class CompactMessage(StatusMessage):
|
||||||
if self.error_message:
|
if self.error_message:
|
||||||
return f"Error: {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(
|
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:
|
) -> None:
|
||||||
self.old_tokens = old_tokens
|
self.old_tokens = old_tokens
|
||||||
self.new_tokens = new_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.stop_spinning(success=True)
|
||||||
self.post_message(self.Completed(self))
|
self.post_message(self.Completed(self))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,9 @@ from vibe.core.telemetry.types import (
|
||||||
TelemetryCallType,
|
TelemetryCallType,
|
||||||
TelemetryRequestMetadata,
|
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 (
|
from vibe.core.tools.base import (
|
||||||
BaseTool,
|
BaseTool,
|
||||||
InvokeContext,
|
InvokeContext,
|
||||||
|
|
@ -174,9 +177,18 @@ def _is_context_too_long_error(e: Exception) -> bool:
|
||||||
|
|
||||||
def _is_non_retryable_error(e: BaseException) -> bool:
|
def _is_non_retryable_error(e: BaseException) -> bool:
|
||||||
# Detect Temporal-style ``non_retryable`` flag without importing temporalio.
|
# Detect Temporal-style ``non_retryable`` flag without importing temporalio.
|
||||||
# Wrapping such an exception in a plain RuntimeError strips the flag, so
|
# Walks ``__cause__`` so an ``ActivityError`` whose cause is a non-retryable
|
||||||
# Temporal's activity retry policy will retry the call until exhaustion.
|
# ``ApplicationError`` is detected too — that's what callers driving the
|
||||||
return bool(getattr(e, "non_retryable", False))
|
# 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]:
|
def requires_init(fn: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
|
|
@ -504,6 +516,9 @@ class AgentLoop:
|
||||||
def emit_ready_telemetry(self, init_duration_ms: int) -> None:
|
def emit_ready_telemetry(self, init_duration_ms: int) -> None:
|
||||||
self.telemetry_client.send_ready(init_duration_ms=init_duration_ms)
|
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:
|
def _create_connector_registry(self) -> ConnectorRegistry | None:
|
||||||
if not connectors_enabled():
|
if not connectors_enabled():
|
||||||
return None
|
return None
|
||||||
|
|
@ -592,16 +607,25 @@ class AgentLoop:
|
||||||
async def teleport_to_vibe_code(
|
async def teleport_to_vibe_code(
|
||||||
self, prompt: str | None
|
self, prompt: str | None
|
||||||
) -> AsyncGenerator[TeleportYieldEvent, TeleportPushResponseEvent | None]:
|
) -> AsyncGenerator[TeleportYieldEvent, TeleportPushResponseEvent | None]:
|
||||||
from vibe.core.teleport.errors import ServiceTeleportError
|
|
||||||
from vibe.core.teleport.nuage import TeleportSession
|
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(
|
session = TeleportSession(
|
||||||
metadata={
|
metadata={
|
||||||
"agent": self.agent_profile.name,
|
"agent": self.agent_profile.name,
|
||||||
"model": self.config.active_model,
|
"model": self.config.active_model,
|
||||||
"stats": self.stats.model_dump(),
|
"stats": self.stats.model_dump(),
|
||||||
},
|
},
|
||||||
messages=[msg.model_dump(exclude_none=True) for msg in self.messages[1:]],
|
messages=session_messages,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
async with self.teleport_service:
|
async with self.teleport_service:
|
||||||
|
|
@ -610,12 +634,23 @@ class AgentLoop:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
event = await gen.asend(response)
|
event = await gen.asend(response)
|
||||||
|
telemetry_tracker.record_event(event)
|
||||||
|
if isinstance(event, TeleportCompleteEvent):
|
||||||
|
telemetry_tracker.send_success()
|
||||||
response = yield event
|
response = yield event
|
||||||
except StopAsyncIteration:
|
except StopAsyncIteration:
|
||||||
break
|
break
|
||||||
except ServiceTeleportError as e:
|
except ServiceTeleportError as e:
|
||||||
|
telemetry_tracker.record_service_error(e)
|
||||||
raise TeleportError(str(e)) from 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:
|
finally:
|
||||||
|
telemetry_tracker.send_failure_if_needed()
|
||||||
self._teleport_service = None
|
self._teleport_service = None
|
||||||
|
|
||||||
def _setup_middleware(self) -> None:
|
def _setup_middleware(self) -> None:
|
||||||
|
|
@ -709,6 +744,8 @@ class AgentLoop:
|
||||||
old_context_tokens=old_tokens,
|
old_context_tokens=old_tokens,
|
||||||
new_context_tokens=new_tokens,
|
new_context_tokens=new_tokens,
|
||||||
summary_length=len(summary),
|
summary_length=len(summary),
|
||||||
|
old_session_id=old_session_id,
|
||||||
|
new_session_id=self.session_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
case MiddlewareAction.CONTINUE:
|
case MiddlewareAction.CONTINUE:
|
||||||
|
|
@ -1395,8 +1432,9 @@ class AgentLoop:
|
||||||
responded_ids: set[str] = set()
|
responded_ids: set[str] = set()
|
||||||
j = i + 1
|
j = i + 1
|
||||||
while j < len(self.messages) and self.messages[j].role == "tool":
|
while j < len(self.messages) and self.messages[j].role == "tool":
|
||||||
if self.messages[j].tool_call_id:
|
tool_call_id = self.messages[j].tool_call_id
|
||||||
responded_ids.add(self.messages[j].tool_call_id)
|
if tool_call_id is not None:
|
||||||
|
responded_ids.add(tool_call_id)
|
||||||
j += 1
|
j += 1
|
||||||
|
|
||||||
if len(responded_ids) < expected_responses:
|
if len(responded_ids) < expected_responses:
|
||||||
|
|
@ -1431,6 +1469,7 @@ class AgentLoop:
|
||||||
|
|
||||||
def _reset_session(self, keep_parent: bool = True) -> None:
|
def _reset_session(self, keep_parent: bool = True) -> None:
|
||||||
old_session_id = self.session_id
|
old_session_id = self.session_id
|
||||||
|
self.emit_session_closed_telemetry()
|
||||||
suffix = extract_suffix(self.session_id)
|
suffix = extract_suffix(self.session_id)
|
||||||
self.session_id = generate_session_id(suffix=suffix)
|
self.session_id = generate_session_id(suffix=suffix)
|
||||||
parent_session_id = old_session_id if keep_parent else None
|
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),
|
" ".join(str(p) for p in self._search_paths),
|
||||||
)
|
)
|
||||||
|
|
||||||
profile = self._available.get(initial_agent)
|
available = self.available_agents
|
||||||
if (
|
profile = available.get(initial_agent)
|
||||||
not allow_subagent
|
if profile is None:
|
||||||
and profile is not None
|
if initial_agent in self._available:
|
||||||
and profile.agent_type != AgentType.AGENT
|
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(
|
raise ValueError(
|
||||||
f"Agent '{initial_agent}' is a {profile.agent_type} and cannot be used"
|
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" as the primary agent. Only agents of type 'agent' can be selected"
|
||||||
f" with --agent."
|
f" with --agent."
|
||||||
)
|
)
|
||||||
self.active_profile = profile or self._available[BuiltinAgentName.DEFAULT]
|
self.active_profile = profile
|
||||||
self._cached_config: VibeConfig | None = None
|
self._cached_config: VibeConfig | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -64,13 +64,16 @@ class CommandCompleter(Completer):
|
||||||
descriptions[alias] = description
|
descriptions[alias] = description
|
||||||
return list(descriptions.keys()), descriptions
|
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]:
|
def get_completions(self, text: str, cursor_pos: int) -> list[str]:
|
||||||
if not text.startswith("/"):
|
if not text.startswith("/"):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
aliases, _ = self._build_lookup()
|
aliases, _ = self._build_lookup()
|
||||||
word = text[1:cursor_pos].lower()
|
search_str = "/" + self._head_word(text, cursor_pos)
|
||||||
search_str = "/" + word
|
|
||||||
return [alias for alias in aliases if alias.lower().startswith(search_str)]
|
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]]:
|
def get_completion_items(self, text: str, cursor_pos: int) -> list[tuple[str, str]]:
|
||||||
|
|
@ -78,8 +81,7 @@ class CommandCompleter(Completer):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
aliases, descriptions = self._build_lookup()
|
aliases, descriptions = self._build_lookup()
|
||||||
word = text[1:cursor_pos].lower()
|
search_str = "/" + self._head_word(text, cursor_pos)
|
||||||
search_str = "/" + word
|
|
||||||
items = [
|
items = [
|
||||||
(alias, descriptions.get(alias, ""))
|
(alias, descriptions.get(alias, ""))
|
||||||
for alias in aliases
|
for alias in aliases
|
||||||
|
|
@ -90,9 +92,11 @@ class CommandCompleter(Completer):
|
||||||
def get_replacement_range(
|
def get_replacement_range(
|
||||||
self, text: str, cursor_pos: int
|
self, text: str, cursor_pos: int
|
||||||
) -> tuple[int, int] | None:
|
) -> tuple[int, int] | None:
|
||||||
if text.startswith("/"):
|
if not text.startswith("/"):
|
||||||
return (0, cursor_pos)
|
|
||||||
return None
|
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):
|
class PathCompleter(Completer):
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from pydantic_settings import (
|
||||||
)
|
)
|
||||||
import tomli_w
|
import tomli_w
|
||||||
|
|
||||||
|
from vibe.core.agents.models import BuiltinAgentName
|
||||||
from vibe.core.config.harness_files import get_harness_files_manager
|
from vibe.core.config.harness_files import get_harness_files_manager
|
||||||
from vibe.core.logger import logger
|
from vibe.core.logger import logger
|
||||||
from vibe.core.paths import GLOBAL_ENV_FILE, SESSION_LOG_DIR
|
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."
|
"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(
|
skill_paths: list[Path] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description=(
|
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))
|
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":
|
if os.environ.get("DEBUG_MODE") == "true":
|
||||||
log_level_str = "DEBUG"
|
log_level_str = "DEBUG"
|
||||||
else:
|
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()
|
return formatter.finalize()
|
||||||
finally:
|
finally:
|
||||||
|
agent_loop.emit_session_closed_telemetry()
|
||||||
await agent_loop.telemetry_client.aclose()
|
await agent_loop.telemetry_client.aclose()
|
||||||
|
|
||||||
return asyncio.run(_async_run())
|
return asyncio.run(_async_run())
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ Never claim completion without verification — a passing test, correct read-bac
|
||||||
|
|
||||||
Hard Rules:
|
Hard Rules:
|
||||||
|
|
||||||
Never Commit
|
Never Commit Proactively
|
||||||
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.
|
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
|
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.
|
"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
|
import tempfile
|
||||||
|
|
||||||
from vibe.core.logger import logger
|
from vibe.core.logger import logger
|
||||||
|
from vibe.core.session.session_id import shorten_session_id
|
||||||
|
|
||||||
_active_scratchpads: dict[str, Path] = {}
|
_active_scratchpads: dict[str, Path] = {}
|
||||||
|
|
||||||
|
|
@ -17,7 +18,11 @@ def init_scratchpad(session_id: str) -> Path | None:
|
||||||
return _active_scratchpads[session_id]
|
return _active_scratchpads[session_id]
|
||||||
|
|
||||||
try:
|
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
|
_active_scratchpads[session_id] = dir_path
|
||||||
logger.debug("Scratchpad initialized at %s", dir_path)
|
logger.debug("Scratchpad initialized at %s", dir_path)
|
||||||
return dir_path
|
return dir_path
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,14 @@ from vibe.core.config import VibeConfig
|
||||||
from vibe.core.logger import logger
|
from vibe.core.logger import logger
|
||||||
from vibe.core.nuage.client import WorkflowsClient
|
from vibe.core.nuage.client import WorkflowsClient
|
||||||
from vibe.core.nuage.workflow import WorkflowExecutionStatus
|
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
|
from vibe.core.session.session_loader import SessionLoader
|
||||||
|
|
||||||
ResumeSessionSource = Literal["local", "remote"]
|
ResumeSessionSource = Literal["local", "remote"]
|
||||||
|
|
||||||
SHORT_SESSION_ID_LEN = 8
|
|
||||||
|
|
||||||
|
|
||||||
def short_session_id(session_id: str, source: ResumeSessionSource = "local") -> str:
|
def short_session_id(session_id: str, source: ResumeSessionSource = "local") -> str:
|
||||||
if source == "remote":
|
return shorten_session_id(session_id, from_end=source == "remote")
|
||||||
return session_id[-SHORT_SESSION_ID_LEN:]
|
|
||||||
return session_id[:SHORT_SESSION_ID_LEN]
|
|
||||||
|
|
||||||
|
|
||||||
_ACTIVE_STATUSES = [
|
_ACTIVE_STATUSES = [
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,13 @@ def generate_session_id(*, suffix: str | None = None) -> str:
|
||||||
def extract_suffix(session_id: str) -> str:
|
def extract_suffix(session_id: str) -> str:
|
||||||
"""Extract the stable suffix (last segment after the final hyphen)."""
|
"""Extract the stable suffix (last segment after the final hyphen)."""
|
||||||
return session_id.rsplit("-", 1)[-1]
|
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 pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, TypedDict
|
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.types import LLMMessage, SessionMetadata
|
||||||
from vibe.core.utils.io import read_safe
|
from vibe.core.utils.io import read_safe
|
||||||
|
|
||||||
|
|
@ -129,7 +130,7 @@ class SessionLoader:
|
||||||
if not save_dir.exists():
|
if not save_dir.exists():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
short_id = session_id[:8]
|
short_id = shorten_session_id(session_id)
|
||||||
return list(save_dir.glob(f"{config.session_prefix}_*_{short_id}"))
|
return list(save_dir.glob(f"{config.session_prefix}_*_{short_id}"))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any, Literal
|
||||||
|
|
||||||
from anyio import NamedTemporaryFile, Path as AsyncPath
|
from anyio import NamedTemporaryFile, Path as AsyncPath
|
||||||
|
|
||||||
|
from vibe.core.session.session_id import shorten_session_id
|
||||||
from vibe.core.session.session_loader import (
|
from vibe.core.session.session_loader import (
|
||||||
MESSAGES_FILENAME,
|
MESSAGES_FILENAME,
|
||||||
METADATA_FILENAME,
|
METADATA_FILENAME,
|
||||||
|
|
@ -63,9 +64,20 @@ class SessionLogger:
|
||||||
)
|
)
|
||||||
|
|
||||||
timestamp = utc_now().strftime("%Y%m%d_%H%M%S")
|
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
|
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
|
@property
|
||||||
def metadata_filepath(self) -> Path:
|
def metadata_filepath(self) -> Path:
|
||||||
if self.session_dir is None:
|
if self.session_dir is None:
|
||||||
|
|
@ -244,34 +256,34 @@ class SessionLogger:
|
||||||
tool_manager: ToolManager,
|
tool_manager: ToolManager,
|
||||||
agent_profile: AgentProfile,
|
agent_profile: AgentProfile,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not self.enabled or self.session_dir is None:
|
session_info = self._get_session_info()
|
||||||
return
|
if session_info is None:
|
||||||
|
|
||||||
if self.session_metadata is None:
|
|
||||||
return
|
return
|
||||||
|
session_dir, session_metadata = session_info
|
||||||
|
metadata_path = session_dir / METADATA_FILENAME
|
||||||
|
|
||||||
if not any(msg.role != Role.system for msg in messages):
|
if not any(msg.role != Role.system for msg in messages):
|
||||||
return
|
return
|
||||||
|
|
||||||
# If the session directory does not exist, create it
|
# If the session directory does not exist, create it
|
||||||
try:
|
try:
|
||||||
self.session_dir.mkdir(parents=True, exist_ok=True)
|
session_dir.mkdir(parents=True, exist_ok=True)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise RuntimeError(
|
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
|
) from e
|
||||||
|
|
||||||
# Read old metadata and get total_messages
|
# Read old metadata and get total_messages
|
||||||
try:
|
try:
|
||||||
if self.metadata_filepath.exists():
|
if metadata_path.exists():
|
||||||
raw = (await read_safe_async(self.metadata_filepath)).text
|
raw = (await read_safe_async(metadata_path)).text
|
||||||
old_metadata = json.loads(raw)
|
old_metadata = json.loads(raw)
|
||||||
old_total_messages = old_metadata["total_messages"]
|
old_total_messages = old_metadata["total_messages"]
|
||||||
else:
|
else:
|
||||||
old_total_messages = 0
|
old_total_messages = 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(
|
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
|
) from e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -283,7 +295,7 @@ class SessionLogger:
|
||||||
return
|
return
|
||||||
|
|
||||||
messages_data = [m.model_dump(exclude_none=True) for m in new_messages]
|
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
|
# If message update succeeded, write metadata
|
||||||
tools_available = [
|
tools_available = [
|
||||||
|
|
@ -307,7 +319,7 @@ class SessionLogger:
|
||||||
total_messages = len(non_system_messages)
|
total_messages = len(non_system_messages)
|
||||||
|
|
||||||
metadata_dump = {
|
metadata_dump = {
|
||||||
**self.session_metadata.model_dump(),
|
**session_metadata.model_dump(),
|
||||||
"end_time": utc_now().isoformat(),
|
"end_time": utc_now().isoformat(),
|
||||||
"stats": stats.model_dump(),
|
"stats": stats.model_dump(),
|
||||||
"title": title,
|
"title": title,
|
||||||
|
|
@ -321,14 +333,32 @@ class SessionLogger:
|
||||||
"system_prompt": system_prompt,
|
"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:
|
except Exception as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(f"Failed to save session to {session_dir}: {e}") from e
|
||||||
f"Failed to save session to {self.session_dir}: {e}"
|
|
||||||
) from e
|
|
||||||
finally:
|
finally:
|
||||||
self.maybe_cleanup_tmp_files()
|
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(
|
def reset_session(
|
||||||
self, session_id: str, *, parent_session_id: str | None = None
|
self, session_id: str, *, parent_session_id: str | None = 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)
|
# Opt-in builtin agents (only affects agents with install_required=True, e.g. lean)
|
||||||
installed_agents = ["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
|
### MCP Servers
|
||||||
|
|
@ -321,7 +330,7 @@ Tool, skill, and agent names support three matching modes:
|
||||||
```
|
```
|
||||||
vibe [PROMPT] # Start interactive session with optional prompt
|
vibe [PROMPT] # Start interactive session with optional prompt
|
||||||
vibe -p TEXT / --prompt TEXT # Programmatic mode (auto-approve, one-shot, exit)
|
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 --workdir DIR # Change working directory
|
||||||
vibe --trust # Trust cwd for this invocation only (not persisted)
|
vibe --trust # Trust cwd for this invocation only (not persisted)
|
||||||
vibe -c / --continue # Continue most recent session
|
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)
|
- `/mcp` - Display available MCP servers (pass a server name to list its tools)
|
||||||
- `/resume` (or `/continue`) - Browse and resume past sessions
|
- `/resume` (or `/continue`) - Browse and resume past sessions
|
||||||
- `/rewind` - Rewind to a previous message
|
- `/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
|
- `/terminal-setup` - Configure Shift+Enter for newlines
|
||||||
- `/proxy-setup` - Configure proxy and SSL certificate settings
|
- `/proxy-setup` - Configure proxy and SSL certificate settings
|
||||||
- `/leanstall` - Install the Lean 4 agent (leanstral)
|
- `/leanstall` - Install the Lean 4 agent (leanstral)
|
||||||
|
|
@ -415,6 +433,13 @@ Detailed instructions for the model...
|
||||||
- `MISTRAL_API_KEY` - API key for Mistral provider
|
- `MISTRAL_API_KEY` - API key for Mistral provider
|
||||||
- `VIBE_ACTIVE_MODEL` - Override active model
|
- `VIBE_ACTIVE_MODEL` - Override active model
|
||||||
- `VIBE_*` - Any config field can be overridden with the `VIBE_` prefix
|
- `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)
|
## API Keys (.env file)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ from vibe.core.telemetry.types import (
|
||||||
AgentEntrypoint,
|
AgentEntrypoint,
|
||||||
EntrypointMetadata,
|
EntrypointMetadata,
|
||||||
TelemetryCallType,
|
TelemetryCallType,
|
||||||
|
TeleportCompletedPayload,
|
||||||
|
TeleportFailedPayload,
|
||||||
|
TeleportFailureStage,
|
||||||
)
|
)
|
||||||
from vibe.core.utils import get_server_url_from_api_base, get_user_agent
|
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:
|
def _get_mistral_provider_and_api_key(self) -> tuple[ProviderConfig, str] | None:
|
||||||
try:
|
try:
|
||||||
provider = self._config_getter().get_mistral_provider()
|
provider = self._config_getter().get_mistral_provider()
|
||||||
except ValueError:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
if provider is None:
|
if provider is None:
|
||||||
return None
|
return None
|
||||||
|
|
@ -77,7 +80,7 @@ class TelemetryClient:
|
||||||
"""Check if telemetry is enabled in the current config."""
|
"""Check if telemetry is enabled in the current config."""
|
||||||
try:
|
try:
|
||||||
return self._config_getter().enable_telemetry
|
return self._config_getter().enable_telemetry
|
||||||
except ValueError:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_active(self) -> bool:
|
def is_active(self) -> bool:
|
||||||
|
|
@ -270,6 +273,9 @@ class TelemetryClient:
|
||||||
}
|
}
|
||||||
self.send_telemetry_event("vibe.new_session", payload)
|
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:
|
def send_onboarding_api_key_added(self) -> None:
|
||||||
self.send_telemetry_event(
|
self.send_telemetry_event(
|
||||||
"vibe.onboarding_api_key_added", {"version": __version__}
|
"vibe.onboarding_api_key_added", {"version": __version__}
|
||||||
|
|
@ -322,3 +328,35 @@ class TelemetryClient:
|
||||||
{"rating": rating, "version": __version__, "model": model},
|
{"rating": rating, "version": __version__, "model": model},
|
||||||
correlation_id=self.last_correlation_id,
|
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 __future__ import annotations
|
||||||
|
|
||||||
from typing import Literal
|
from typing import Literal, TypedDict
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
@ -36,3 +36,26 @@ class TelemetryRequestMetadata(TelemetryBaseMetadata):
|
||||||
call_type: TelemetryCallType
|
call_type: TelemetryCallType
|
||||||
call_source: str = "vibe_code"
|
call_source: str = "vibe_code"
|
||||||
message_id: str | None = None
|
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:
|
try:
|
||||||
raw_lines: list[bytes] = []
|
raw_lines: list[bytes] = []
|
||||||
bytes_read = 0
|
bytes_read = 0
|
||||||
was_truncated = False
|
was_truncated = True
|
||||||
|
|
||||||
async with await anyio.Path(file_path).open("rb") as f:
|
async with await anyio.Path(file_path).open("rb") as f:
|
||||||
line_index = 0
|
line_index = 0
|
||||||
|
|
@ -155,12 +155,13 @@ class ReadFile(
|
||||||
|
|
||||||
line_bytes = len(raw_line)
|
line_bytes = len(raw_line)
|
||||||
if bytes_read + line_bytes > self.config.max_read_bytes:
|
if bytes_read + line_bytes > self.config.max_read_bytes:
|
||||||
was_truncated = True
|
|
||||||
break
|
break
|
||||||
|
|
||||||
raw_lines.append(raw_line)
|
raw_lines.append(raw_line)
|
||||||
bytes_read += line_bytes
|
bytes_read += line_bytes
|
||||||
line_index += 1
|
line_index += 1
|
||||||
|
else:
|
||||||
|
was_truncated = False
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
raise ToolError(f"Error reading {file_path}: {exc}") from 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:
|
def setup_tracing(config: VibeConfig) -> None:
|
||||||
if not config.enable_otel:
|
if not config.enable_telemetry or not config.enable_otel:
|
||||||
return
|
return
|
||||||
|
|
||||||
exporter_cfg = config.otel_span_exporter_config
|
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):
|
class Backend(StrEnum):
|
||||||
MISTRAL = auto()
|
MISTRAL = auto()
|
||||||
GENERIC = auto()
|
GENERIC = auto()
|
||||||
|
|
@ -142,6 +152,7 @@ class SessionMetadata(BaseModel):
|
||||||
git_branch: str | None
|
git_branch: str | None
|
||||||
environment: dict[str, str | None]
|
environment: dict[str, str | None]
|
||||||
username: str
|
username: str
|
||||||
|
loops: list[ScheduledLoop] = Field(default_factory=list)
|
||||||
title: str | None = None
|
title: str | None = None
|
||||||
title_source: Literal["auto", "manual"] = "auto"
|
title_source: Literal["auto", "manual"] = "auto"
|
||||||
|
|
||||||
|
|
@ -418,6 +429,8 @@ class CompactEndEvent(BaseEvent):
|
||||||
old_context_tokens: int
|
old_context_tokens: int
|
||||||
new_context_tokens: int
|
new_context_tokens: int
|
||||||
summary_length: 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.
|
# WORKAROUND: Using tool_call to communicate compact events to the client.
|
||||||
# This should be revisited when the ACP protocol defines how compact events
|
# This should be revisited when the ACP protocol defines how compact events
|
||||||
# should be represented.
|
# should be represented.
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,31 @@
|
||||||
from __future__ import annotations
|
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 = old_tokens - new_tokens
|
||||||
reduction_pct = (reduction / old_tokens * 100) if old_tokens > 0 else 0
|
reduction_pct = (reduction / old_tokens * 100) if old_tokens > 0 else 0
|
||||||
return (
|
message = (
|
||||||
f"Compaction complete: {old_tokens:,} → "
|
f"{message}: {old_tokens:,} → "
|
||||||
f"{new_tokens:,} tokens ({-reduction_pct:+#0.2g}%)"
|
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.
|
- **`/loop` command**: Run a prompt or slash command on a recurring interval.
|
||||||
- **Persistent "always allow"**: Tool permissions granted with "always allow" now stick across sessions.
|
- **`default_agent` config**: Set which agent profile starts each session.
|
||||||
- **Faster bash bang commands**: `!command` runs via async subprocess.
|
- **Parallel-safe history**: Multiple `vibe` instances no longer clobber each other's history file.
|
||||||
|
|
|
||||||