v2.17.1 (#823)
Co-authored-by: Jean Burellier <sheplu@users.noreply.github.com> Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com> Co-authored-by: renovate-mistral[bot] <253709520+renovate-mistral[bot]@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
6bedf271ce
commit
725d3a56ce
35 changed files with 1330 additions and 288 deletions
|
|
@ -165,6 +165,25 @@ class TestHandleHelp:
|
|||
for cmd in main_commands:
|
||||
assert f"/{cmd}" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lists_registered_commands_alphabetically(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_id = await _new_session_and_clear(acp_agent_loop)
|
||||
await _prompt(acp_agent_loop, session_id, "/help")
|
||||
|
||||
content = _get_message_texts(acp_agent_loop)[0]
|
||||
commands_section = content.split("### Available Commands\n\n", maxsplit=1)[
|
||||
1
|
||||
].split("\n\n", maxsplit=1)[0]
|
||||
command_names = [
|
||||
line.split("`", maxsplit=2)[1].removeprefix("/")
|
||||
for line in commands_section.splitlines()
|
||||
if line.startswith("- ")
|
||||
]
|
||||
|
||||
assert command_names == sorted(command_names)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_includes_user_invocable_skills(
|
||||
self, acp_agent_loop_with_skills: VibeAcpAgentLoop, skills_dir: Path
|
||||
|
|
@ -267,6 +286,28 @@ class TestHandleTeleport:
|
|||
]
|
||||
assert _get_tool_updates(acp_agent_loop) == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_replies_with_error_when_model_not_mistral(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
) -> None:
|
||||
session_id = await _new_session_and_clear(acp_agent_loop)
|
||||
|
||||
with patch(
|
||||
"vibe.core.config._settings.VibeConfig.is_active_model_mistral",
|
||||
return_value=False,
|
||||
):
|
||||
response = await _prompt(acp_agent_loop, session_id, "/teleport")
|
||||
|
||||
assert response.stop_reason == "end_turn"
|
||||
assert response.field_meta == {
|
||||
"tool_name": "teleport",
|
||||
"teleport": {"status": "unavailable"},
|
||||
}
|
||||
texts = _get_message_texts(acp_agent_loop)
|
||||
assert len(texts) == 1
|
||||
assert "active Mistral model" in texts[0]
|
||||
assert _get_tool_updates(acp_agent_loop) == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_sends_tool_updates_and_structured_url(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop
|
||||
|
|
@ -542,6 +583,25 @@ class TestCommandFallthrough:
|
|||
|
||||
|
||||
class TestAvailableCommandsWithSkills:
|
||||
@pytest.mark.asyncio
|
||||
async def test_available_commands_are_alphabetical(
|
||||
self, acp_agent_loop_with_skills: VibeAcpAgentLoop, skills_dir: Path
|
||||
) -> None:
|
||||
create_skill(skills_dir, "alpha-skill", "First skill")
|
||||
|
||||
await acp_agent_loop_with_skills.new_session(
|
||||
cwd=str(Path.cwd()), mcp_servers=[]
|
||||
)
|
||||
await _wait_for_available_commands(acp_agent_loop_with_skills)
|
||||
|
||||
updates = _get_client(acp_agent_loop_with_skills)._session_updates
|
||||
available = [
|
||||
u for u in updates if isinstance(u.update, AvailableCommandsUpdate)
|
||||
]
|
||||
cmd_names = [c.name for c in available[0].update.available_commands]
|
||||
|
||||
assert cmd_names == sorted(cmd_names)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skills_appear_in_available_commands(
|
||||
self, acp_agent_loop_with_skills: VibeAcpAgentLoop, skills_dir: Path
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.17.0"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.17.1"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
@ -172,7 +172,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.17.0"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.17.1"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ class TestLoadSession:
|
|||
"cwd": str(tmp_working_directory.resolve()),
|
||||
"repoRoot": None,
|
||||
"ignoredFiles": ["AGENTS.md"],
|
||||
"availableDecisions": ["trust_cwd", "trust_session", "decline"],
|
||||
"availableDecisions": ["trust_cwd", "decline"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ class TestACPNewSession:
|
|||
"cwd": str(tmp_working_directory.resolve()),
|
||||
"repoRoot": None,
|
||||
"ignoredFiles": ["AGENTS.md"],
|
||||
"availableDecisions": ["trust_cwd", "trust_session", "decline"],
|
||||
"availableDecisions": ["trust_cwd", "decline"],
|
||||
},
|
||||
}
|
||||
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
|
||||
|
|
@ -230,12 +230,7 @@ class TestACPNewSession:
|
|||
"cwd": str(cwd.resolve()),
|
||||
"repoRoot": str(repo.resolve()),
|
||||
"ignoredFiles": ["AGENTS.md"],
|
||||
"availableDecisions": [
|
||||
"trust_repo",
|
||||
"trust_cwd",
|
||||
"trust_session",
|
||||
"decline",
|
||||
],
|
||||
"availableDecisions": ["trust_repo", "trust_cwd", "decline"],
|
||||
},
|
||||
}
|
||||
assert trusted_folders_manager.is_trusted(repo) is None
|
||||
|
|
@ -264,7 +259,7 @@ class TestACPNewSession:
|
|||
"cwd": str(tmp_working_directory.resolve()),
|
||||
"repoRoot": None,
|
||||
"ignoredFiles": ["AGENTS.md"],
|
||||
"availableDecisions": ["trust_cwd", "trust_session", "decline"],
|
||||
"availableDecisions": ["trust_cwd", "decline"],
|
||||
},
|
||||
}
|
||||
assert trusted_folders_manager.is_trusted(tmp_working_directory) is False
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
|
@ -20,6 +21,14 @@ async def _system_prompt(acp_agent_loop: VibeAcpAgentLoop, session_id: str) -> s
|
|||
return session.agent_loop.messages[0].content or ""
|
||||
|
||||
|
||||
async def _wait_for_background_tasks(
|
||||
acp_agent_loop: VibeAcpAgentLoop, session_id: str
|
||||
) -> None:
|
||||
session = acp_agent_loop.sessions[session_id]
|
||||
while session._tasks:
|
||||
await asyncio.gather(*list(session._tasks))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acp_agent_loop(
|
||||
backend: FakeBackend, monkeypatch: pytest.MonkeyPatch
|
||||
|
|
@ -66,12 +75,12 @@ class TestWorkspaceTrustExtMethods:
|
|||
"cwd": str(tmp_working_directory.resolve()),
|
||||
"repoRoot": None,
|
||||
"ignoredFiles": ["AGENTS.md"],
|
||||
"availableDecisions": ["trust_cwd", "trust_session", "decline"],
|
||||
"availableDecisions": ["trust_cwd", "decline"],
|
||||
},
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_trust_decision_trusts_session_and_reloads_session(
|
||||
async def test_workspace_trust_decision_trusts_cwd_and_reloads_session(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop, tmp_working_directory: Path
|
||||
) -> None:
|
||||
(tmp_working_directory / "AGENTS.md").write_text(
|
||||
|
|
@ -90,19 +99,74 @@ class TestWorkspaceTrustExtMethods:
|
|||
"trust/decision",
|
||||
{
|
||||
"cwd": str(tmp_working_directory),
|
||||
"decision": "trust_session",
|
||||
"decision": "trust_cwd",
|
||||
"session_id": session_response.session_id,
|
||||
},
|
||||
)
|
||||
|
||||
assert response == {"trust_status": "session", "details": None}
|
||||
assert response == {"trust_status": "trusted", "details": None}
|
||||
normalized = str(tmp_working_directory.resolve())
|
||||
assert normalized in trusted_folders_manager._session_trusted
|
||||
assert normalized not in trusted_folders_manager._trusted
|
||||
assert normalized not in trusted_folders_manager._session_trusted
|
||||
assert normalized in trusted_folders_manager._trusted
|
||||
await _wait_for_background_tasks(acp_agent_loop, session_response.session_id)
|
||||
assert "Reloaded session instructions" in await _system_prompt(
|
||||
acp_agent_loop, session_response.session_id
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_trust_decision_returns_before_reload_completes(
|
||||
self,
|
||||
acp_agent_loop: VibeAcpAgentLoop,
|
||||
tmp_working_directory: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
(tmp_working_directory / "AGENTS.md").write_text(
|
||||
"Slow reload instructions", encoding="utf-8"
|
||||
)
|
||||
|
||||
session_response = await acp_agent_loop.new_session(
|
||||
cwd=str(tmp_working_directory), mcp_servers=[]
|
||||
)
|
||||
assert session_response.session_id is not None
|
||||
|
||||
reload_started = asyncio.Event()
|
||||
release_reload = asyncio.Event()
|
||||
|
||||
async def slow_reload_session_config(session) -> None:
|
||||
reload_started.set()
|
||||
await release_reload.wait()
|
||||
|
||||
monkeypatch.setattr(
|
||||
acp_agent_loop, "_reload_session_config", slow_reload_session_config
|
||||
)
|
||||
|
||||
decision_task = asyncio.create_task(
|
||||
acp_agent_loop.ext_method(
|
||||
"trust/decision",
|
||||
{
|
||||
"cwd": str(tmp_working_directory),
|
||||
"decision": "trust_cwd",
|
||||
"session_id": session_response.session_id,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(reload_started.wait(), timeout=1)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
assert decision_task.done()
|
||||
assert decision_task.result() == {
|
||||
"trust_status": "trusted",
|
||||
"details": None,
|
||||
}
|
||||
finally:
|
||||
release_reload.set()
|
||||
await decision_task
|
||||
await _wait_for_background_tasks(
|
||||
acp_agent_loop, session_response.session_id
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workspace_trust_decision_rejects_unavailable_decision(
|
||||
self, acp_agent_loop: VibeAcpAgentLoop, tmp_working_directory: Path
|
||||
|
|
@ -117,6 +181,12 @@ class TestWorkspaceTrustExtMethods:
|
|||
{"cwd": str(tmp_working_directory), "decision": "trust_repo"},
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidRequestError):
|
||||
await acp_agent_loop.ext_method(
|
||||
"trust/decision",
|
||||
{"cwd": str(tmp_working_directory), "decision": "trust_session"},
|
||||
)
|
||||
|
||||
assert trusted_folders_manager.is_trusted(tmp_working_directory) is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -132,7 +202,7 @@ class TestWorkspaceTrustExtMethods:
|
|||
"trust/decision",
|
||||
{
|
||||
"cwd": str(tmp_working_directory),
|
||||
"decision": "trust_session",
|
||||
"decision": "trust_cwd",
|
||||
"session_id": "missing-session",
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway
|
|||
from vibe.cli.plan_offer.decide_plan_offer import (
|
||||
PlanInfo,
|
||||
WhoAmIPlanType,
|
||||
check_teleport_eligibility,
|
||||
decide_plan_offer,
|
||||
plan_offer_cta,
|
||||
plan_title,
|
||||
|
|
@ -250,6 +251,62 @@ def test_plan_offer_cta_uses_configured_vibe_url() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_check_teleport_eligibility_returns_none_for_eligible_key() -> None:
|
||||
plan_info = PlanInfo(
|
||||
plan_type=WhoAmIPlanType.CHAT,
|
||||
plan_name="INDIVIDUAL",
|
||||
prompt_switching_to_pro_plan=False,
|
||||
)
|
||||
|
||||
assert check_teleport_eligibility(plan_info) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"plan_info",
|
||||
[
|
||||
PlanInfo(
|
||||
plan_type=WhoAmIPlanType.CHAT,
|
||||
plan_name="INDIVIDUAL",
|
||||
prompt_switching_to_pro_plan=True,
|
||||
),
|
||||
PlanInfo(
|
||||
plan_type=WhoAmIPlanType.API,
|
||||
plan_name="FREE",
|
||||
prompt_switching_to_pro_plan=False,
|
||||
),
|
||||
PlanInfo(
|
||||
plan_type=WhoAmIPlanType.CHAT,
|
||||
plan_name="FREE",
|
||||
prompt_switching_to_pro_plan=False,
|
||||
),
|
||||
None,
|
||||
],
|
||||
ids=["pro-plan-wrong-key", "api-free", "chat-free", "unresolved"],
|
||||
)
|
||||
def test_check_teleport_eligibility_points_ineligible_keys_to_api_key_url(
|
||||
plan_info: PlanInfo | None,
|
||||
) -> None:
|
||||
message = check_teleport_eligibility(plan_info)
|
||||
|
||||
assert message is not None
|
||||
assert "https://chat.mistral.ai/code/extensions?focus=key" in message
|
||||
|
||||
|
||||
def test_check_teleport_eligibility_uses_configured_vibe_url() -> None:
|
||||
plan_info = PlanInfo(
|
||||
plan_type=WhoAmIPlanType.CHAT,
|
||||
plan_name="INDIVIDUAL",
|
||||
prompt_switching_to_pro_plan=True,
|
||||
)
|
||||
|
||||
message = check_teleport_eligibility(
|
||||
plan_info, vibe_base_url="https://vibe.example.com/"
|
||||
)
|
||||
|
||||
assert message is not None
|
||||
assert "https://vibe.example.com/code/extensions?focus=key" in message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("response", "expected"),
|
||||
[
|
||||
|
|
|
|||
|
|
@ -1,20 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.cli.commands import Command, CommandAvailabilityContext, CommandRegistry
|
||||
from vibe.cli.plan_offer.decide_plan_offer import PlanInfo
|
||||
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIPlanType
|
||||
|
||||
|
||||
def _eligible_teleport_context() -> CommandAvailabilityContext:
|
||||
return CommandAvailabilityContext(
|
||||
vibe_code_enabled=True,
|
||||
is_active_model_mistral=True,
|
||||
plan_info=PlanInfo(
|
||||
plan_type=WhoAmIPlanType.CHAT,
|
||||
plan_name="INDIVIDUAL",
|
||||
prompt_switching_to_pro_plan=False,
|
||||
),
|
||||
)
|
||||
from vibe.cli.commands import Command, CommandRegistry
|
||||
|
||||
|
||||
class TestCommandRegistry:
|
||||
|
|
@ -79,7 +65,7 @@ class TestCommandRegistry:
|
|||
assert registry.parse_command("/teleport") is None
|
||||
|
||||
def test_teleport_command_registration_uses_resolved_context(self) -> None:
|
||||
registry = CommandRegistry(availability_context=_eligible_teleport_context())
|
||||
registry = CommandRegistry(vibe_code_enabled=True)
|
||||
assert registry.get_command_name("/teleport") == "teleport"
|
||||
assert registry.has_command("teleport")
|
||||
|
||||
|
|
@ -87,12 +73,23 @@ class TestCommandRegistry:
|
|||
registry = CommandRegistry()
|
||||
assert "/teleport" not in registry.get_help_text()
|
||||
|
||||
eligible_registry = CommandRegistry(
|
||||
availability_context=_eligible_teleport_context()
|
||||
)
|
||||
eligible_registry = CommandRegistry(vibe_code_enabled=True)
|
||||
assert eligible_registry.get("teleport") is not None
|
||||
assert "/teleport" in eligible_registry.get_help_text()
|
||||
|
||||
def test_help_text_lists_commands_alphabetically(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
commands_section = registry.get_help_text().split(
|
||||
"### Commands\n\n", maxsplit=1
|
||||
)[1]
|
||||
command_names = [
|
||||
line.split("`", maxsplit=2)[1].removeprefix("/")
|
||||
for line in commands_section.splitlines()
|
||||
if line.startswith("- ")
|
||||
]
|
||||
|
||||
assert command_names == sorted(command_names)
|
||||
|
||||
def test_resume_command_registration(self) -> None:
|
||||
registry = CommandRegistry()
|
||||
assert registry.get_command_name("/resume") == "resume"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from tests.conftest import build_test_vibe_app, build_test_vibe_config
|
|||
from tests.constants import OPENAI_BASE_URL
|
||||
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.messages import ErrorMessage
|
||||
from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig
|
||||
from vibe.core.types import Backend
|
||||
|
||||
|
|
@ -48,6 +49,10 @@ def _teleport_failed_events(
|
|||
]
|
||||
|
||||
|
||||
def _error_messages(app) -> list[str]:
|
||||
return [error._error for error in app.query(ErrorMessage)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_command_visible_for_paid_chat_users() -> None:
|
||||
app = build_test_vibe_app(
|
||||
|
|
@ -102,66 +107,101 @@ async def test_teleport_command_without_history_sends_early_failure_telemetry(
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_command_hidden_when_current_key_is_not_eligible() -> None:
|
||||
async def test_teleport_command_visible_but_errors_when_key_not_eligible(
|
||||
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=True),
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause(0.2)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: app.commands.get_command_name("/teleport") == "teleport",
|
||||
)
|
||||
|
||||
assert app.commands.get_command_name("/teleport") is None
|
||||
assert "/teleport" not in app.commands.get_help_text()
|
||||
assert "/teleport" in app.commands.get_help_text()
|
||||
input_widget = app.query_one(ChatInputContainer).input_widget
|
||||
assert input_widget is not None
|
||||
assert "&" not in input_widget.mode_characters
|
||||
assert "&" in input_widget.mode_characters
|
||||
|
||||
await app.on_chat_input_container_submitted(
|
||||
ChatInputContainer.Submitted("/teleport")
|
||||
)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: any("Vibe Pro API key" in error for error in _error_messages(app)),
|
||||
)
|
||||
|
||||
assert _teleport_failed_events(telemetry_events) == [
|
||||
{
|
||||
"event_name": "vibe.teleport_failed",
|
||||
"properties": {
|
||||
"stage": "ineligible",
|
||||
"error_class": "TeleportIneligibleError",
|
||||
"push_required": False,
|
||||
"nb_session_messages": 0,
|
||||
"session_id": app.agent_loop.session_id,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hidden_teleport_command_falls_through_as_user_text() -> None:
|
||||
async def test_teleport_command_errors_instead_of_user_text_when_not_eligible() -> None:
|
||||
app = build_test_vibe_app(
|
||||
config=_vibe_code_enabled_config(),
|
||||
plan_offer_gateway=_chat_plan_gateway(prompt_switching_to_pro_plan=True),
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause(0.2)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: app.commands.get_command_name("/teleport") == "teleport",
|
||||
)
|
||||
|
||||
app._handle_teleport_command = AsyncMock()
|
||||
app._handle_user_message = AsyncMock()
|
||||
|
||||
await app.on_chat_input_container_submitted(
|
||||
ChatInputContainer.Submitted("/teleport")
|
||||
)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: any("Vibe Pro API key" in error for error in _error_messages(app)),
|
||||
)
|
||||
|
||||
app._handle_teleport_command.assert_not_awaited()
|
||||
app._handle_user_message.assert_awaited_once_with("/teleport")
|
||||
app._handle_user_message.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_hidden_ampersand_teleport_shortcut_falls_through_as_user_text() -> None:
|
||||
async def test_ampersand_teleport_shortcut_errors_when_not_eligible() -> None:
|
||||
app = build_test_vibe_app(
|
||||
config=_vibe_code_enabled_config(),
|
||||
plan_offer_gateway=_chat_plan_gateway(prompt_switching_to_pro_plan=True),
|
||||
)
|
||||
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause(0.2)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: app.commands.get_command_name("/teleport") == "teleport",
|
||||
)
|
||||
|
||||
app._handle_teleport_command = AsyncMock()
|
||||
app._handle_user_message = AsyncMock()
|
||||
|
||||
await app.on_chat_input_container_submitted(
|
||||
ChatInputContainer.Submitted("&continue")
|
||||
)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: any("Vibe Pro API key" in error for error in _error_messages(app)),
|
||||
)
|
||||
|
||||
app._handle_teleport_command.assert_not_awaited()
|
||||
app._handle_user_message.assert_awaited_once_with("&continue")
|
||||
app._handle_user_message.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_teleport_command_hides_after_switching_to_non_mistral_model(
|
||||
async def test_teleport_command_errors_after_switching_to_non_mistral_model(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "mock-openai-key")
|
||||
|
|
@ -223,11 +263,18 @@ async def test_teleport_command_hides_after_switching_to_non_mistral_model(
|
|||
):
|
||||
await app._reload_config()
|
||||
|
||||
await _wait_until(
|
||||
pilot.pause, lambda: app.commands.get_command_name("/teleport") is None
|
||||
)
|
||||
|
||||
assert app.commands.get_command_name("/teleport") is None
|
||||
await _wait_until(pilot.pause, lambda: not app.config.is_active_model_mistral())
|
||||
assert app.commands.get_command_name("/teleport") == "teleport"
|
||||
input_widget = app.query_one(ChatInputContainer).input_widget
|
||||
assert input_widget is not None
|
||||
assert "&" not in input_widget.mode_characters
|
||||
assert "&" in input_widget.mode_characters
|
||||
|
||||
await app.on_chat_input_container_submitted(
|
||||
ChatInputContainer.Submitted("/teleport")
|
||||
)
|
||||
await _wait_until(
|
||||
pilot.pause,
|
||||
lambda: any(
|
||||
"active Mistral model" in error for error in _error_messages(app)
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from textual.content import Content
|
||||
from textual.highlight import HighlightTheme
|
||||
from textual.widgets import Static
|
||||
from textual.widget import Widget
|
||||
|
||||
from vibe.cli.textual_ui.widgets.diff_rendering import (
|
||||
_build_diff_line,
|
||||
|
|
@ -35,7 +35,9 @@ def _render_with_colors(*args, **kwargs):
|
|||
return widgets, diff_border_colors(widgets)
|
||||
|
||||
|
||||
def _plain(widget: Static) -> str:
|
||||
def _plain(widget: Widget) -> str:
|
||||
if plain := getattr(widget, "plain", None):
|
||||
return plain
|
||||
visual = widget.render()
|
||||
return visual.plain if isinstance(visual, Content) else str(visual)
|
||||
|
||||
|
|
|
|||
195
tests/skills/registry/test_client.py
Normal file
195
tests/skills/registry/test_client.py
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
from vibe.core.skills.registry._client import RegistrySkillsClient, RegistrySkillsError
|
||||
|
||||
_URL = "https://api.mistral.ai/v1/skills"
|
||||
|
||||
|
||||
def _page(skill_id: str, *, next_token: str = "") -> dict[str, object]:
|
||||
return {
|
||||
"data": [
|
||||
{"skillId": skill_id, "skill": {"skillName": skill_id, "skillBody": "b"}}
|
||||
],
|
||||
"nextPageToken": next_token,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_list_catalog_paginates() -> None:
|
||||
route = respx.get(_URL)
|
||||
route.side_effect = [
|
||||
httpx.Response(200, json=_page("a", next_token="p2")),
|
||||
httpx.Response(200, json=_page("b")),
|
||||
]
|
||||
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
items = await client.list_catalog(page_size=50)
|
||||
|
||||
assert [item.skill_id for item in items] == ["a", "b"]
|
||||
assert route.calls[0].request.url.params["pageSize"] == "50"
|
||||
assert "pageToken" not in route.calls[0].request.url.params
|
||||
assert route.calls[1].request.url.params["pageToken"] == "p2"
|
||||
assert route.calls[0].request.headers["Authorization"] == "Bearer key"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_list_catalog_single_page() -> None:
|
||||
respx.get(_URL).mock(return_value=httpx.Response(200, json=_page("only")))
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
items = await client.list_catalog(page_size=100)
|
||||
assert [item.skill_id for item in items] == ["only"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_unauthorized_raises() -> None:
|
||||
respx.get(_URL).mock(return_value=httpx.Response(401, json={"message": "no"}))
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
with pytest.raises(RegistrySkillsError, match="unauthorized"):
|
||||
await client.list_catalog(page_size=10)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_server_error_raises() -> None:
|
||||
respx.get(_URL).mock(return_value=httpx.Response(503))
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
with pytest.raises(RegistrySkillsError, match="unexpected status"):
|
||||
await client.list_catalog(page_size=10)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_non_json_raises() -> None:
|
||||
respx.get(_URL).mock(return_value=httpx.Response(200, text="not json"))
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
with pytest.raises(RegistrySkillsError, match="valid JSON"):
|
||||
await client.list_catalog(page_size=10)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_network_error_raises() -> None:
|
||||
respx.get(_URL).mock(side_effect=httpx.ConnectError("boom"))
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
with pytest.raises(RegistrySkillsError, match="request failed"):
|
||||
await client.list_catalog(page_size=10)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_list_catalog_sends_fields_mask() -> None:
|
||||
route = respx.get(_URL).mock(return_value=httpx.Response(200, json=_page("a")))
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
await client.list_catalog(page_size=10)
|
||||
params = route.calls[0].request.url.params
|
||||
assert params["pageSize"] == "10"
|
||||
assert "skillBody" not in params["fields"]
|
||||
assert "skillName" in params["fields"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_list_versions_parses_aliases_and_sorts_desc() -> None:
|
||||
respx.get(f"{_URL}/sid/versions").mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"items": [
|
||||
{"version": 1, "versionAttributes": {"aliases": ["old"]}},
|
||||
{
|
||||
"version": 3,
|
||||
"versionAttributes": {"aliases": ["stable", "main"]},
|
||||
},
|
||||
{"version": 2},
|
||||
]
|
||||
},
|
||||
)
|
||||
)
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
versions = await client.list_versions("sid")
|
||||
|
||||
assert [v.version for v in versions] == [3, 2, 1]
|
||||
assert versions[0].aliases == ["stable", "main"]
|
||||
assert versions[1].aliases == []
|
||||
assert versions[2].aliases == ["old"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_list_versions_invalid_payload_raises() -> None:
|
||||
respx.get(f"{_URL}/sid/versions").mock(
|
||||
return_value=httpx.Response(200, json={"items": [{"noVersion": 1}]})
|
||||
)
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
with pytest.raises(RegistrySkillsError, match="invalid versions response"):
|
||||
await client.list_versions("sid")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_get_skill_by_id() -> None:
|
||||
respx.get(f"{_URL}/sid").mock(
|
||||
return_value=httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"skillId": "sid",
|
||||
"skill": {"skillName": "n", "skillBody": "b"},
|
||||
"version": 3,
|
||||
},
|
||||
)
|
||||
)
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
item = await client.get_skill("sid")
|
||||
assert item.skill_id == "sid"
|
||||
assert item.version == 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_get_skill_not_found_raises() -> None:
|
||||
respx.get(f"{_URL}/missing").mock(return_value=httpx.Response(404))
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
with pytest.raises(RegistrySkillsError, match="not found"):
|
||||
await client.get_skill("missing")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_get_skill_invalid_payload_raises() -> None:
|
||||
# A 200 whose body isn't a valid skill object must surface as a registry
|
||||
# error, not a bare pydantic ValidationError.
|
||||
respx.get(f"{_URL}/sid").mock(
|
||||
return_value=httpx.Response(200, json="not-an-object")
|
||||
)
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
with pytest.raises(RegistrySkillsError, match="invalid skill response"):
|
||||
await client.get_skill("sid")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_list_catalog_invalid_payload_raises() -> None:
|
||||
respx.get(_URL).mock(return_value=httpx.Response(200, json="not-an-object"))
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
with pytest.raises(RegistrySkillsError, match="invalid catalog response"):
|
||||
await client.list_catalog(page_size=10)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_list_catalog_raises_when_page_cap_exceeded() -> None:
|
||||
# Every page reports more pages -> the cap is hit with data remaining, which
|
||||
# must raise rather than silently return a truncated catalog.
|
||||
respx.get(_URL).mock(
|
||||
return_value=httpx.Response(200, json=_page("a", next_token="more"))
|
||||
)
|
||||
async with RegistrySkillsClient("https://api.mistral.ai/v1", "key") as client:
|
||||
with pytest.raises(RegistrySkillsError, match="maximum number of pages"):
|
||||
await client.list_catalog(page_size=10)
|
||||
169
tests/skills/registry/test_models.py
Normal file
169
tests/skills/registry/test_models.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.skills.registry.models import (
|
||||
ListSkillsResponse,
|
||||
RegistryAssetContent,
|
||||
RegistrySkillItem,
|
||||
sanitize_skill_name,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw", "expected"),
|
||||
[
|
||||
("my_skill", "my-skill"),
|
||||
("My Skill", "my-skill"),
|
||||
(" weird__name--ok ", "weird-name-ok"),
|
||||
("grill-me", "grill-me"),
|
||||
("___", None),
|
||||
("", None),
|
||||
],
|
||||
)
|
||||
def test_sanitize_skill_name(raw: str, expected: str | None) -> None:
|
||||
assert sanitize_skill_name(raw) == expected
|
||||
|
||||
|
||||
def test_asset_to_bytes_text() -> None:
|
||||
asset = RegistryAssetContent(text_content="hello")
|
||||
assert asset.to_bytes() == b"hello"
|
||||
|
||||
|
||||
def test_asset_to_bytes_raw_base64() -> None:
|
||||
encoded = base64.b64encode(b"\x00\x01binary").decode("ascii")
|
||||
asset = RegistryAssetContent(raw_content=encoded, is_executable=True)
|
||||
assert asset.to_bytes() == b"\x00\x01binary"
|
||||
assert asset.is_executable is True
|
||||
|
||||
|
||||
def test_asset_to_bytes_invalid_base64_returns_none() -> None:
|
||||
assert RegistryAssetContent(raw_content="not!base64!").to_bytes() is None
|
||||
|
||||
|
||||
def test_asset_to_bytes_empty_returns_none() -> None:
|
||||
assert RegistryAssetContent().to_bytes() is None
|
||||
|
||||
|
||||
def test_item_parses_camel_case() -> None:
|
||||
item = RegistrySkillItem.model_validate({
|
||||
"skillId": "abc",
|
||||
"skill": {
|
||||
"skillName": "Free Form",
|
||||
"skillDescription": "does things",
|
||||
"skillBody": "# body",
|
||||
"skillAssets": {"ref.txt": {"textContent": "x", "isExecutable": False}},
|
||||
},
|
||||
"metadata": {"name": "my_skill", "latestVersion": 3},
|
||||
"version": 3,
|
||||
})
|
||||
assert item.skill_id == "abc"
|
||||
assert item.resolved_name == "my-skill"
|
||||
assert item.resolved_description == "does things"
|
||||
assert item.skill.skill_assets["ref.txt"].text_content == "x"
|
||||
assert item.version == 3
|
||||
|
||||
|
||||
def test_item_parses_snake_case_fallback() -> None:
|
||||
item = RegistrySkillItem.model_validate({
|
||||
"skill_id": "abc",
|
||||
"skill": {"skill_name": "n", "skill_body": "b"},
|
||||
"metadata": {"name": "snake_name"},
|
||||
})
|
||||
assert item.resolved_name == "snake-name"
|
||||
|
||||
|
||||
def test_item_name_falls_back_to_skill_name() -> None:
|
||||
item = RegistrySkillItem.model_validate({
|
||||
"skill": {"skillName": "Fallback Name", "skillBody": "b"}
|
||||
})
|
||||
assert item.resolved_name == "fallback-name"
|
||||
|
||||
|
||||
def test_item_name_falls_back_to_title() -> None:
|
||||
item = RegistrySkillItem.model_validate({
|
||||
"skillId": "abc",
|
||||
"skill": {"skillBody": "b"},
|
||||
"attributes": {"title": "My Cool Skill"},
|
||||
})
|
||||
assert item.resolved_name == "my-cool-skill"
|
||||
|
||||
|
||||
def test_item_name_falls_back_to_skill_id() -> None:
|
||||
item = RegistrySkillItem.model_validate({
|
||||
"skillId": "019de91a-84f5-76af-84a2-5f4389f372c7",
|
||||
"skill": {"skillBody": "b"},
|
||||
})
|
||||
assert item.resolved_name == "skill-019de91a84f576af84a25f4389f372c7"
|
||||
|
||||
|
||||
def test_item_name_none_without_any_identifier() -> None:
|
||||
item = RegistrySkillItem.model_validate({"skill": {"skillBody": "b"}})
|
||||
assert item.resolved_name is None
|
||||
|
||||
|
||||
def test_item_description_falls_back_to_attributes() -> None:
|
||||
item = RegistrySkillItem.model_validate({
|
||||
"skill": {"skillName": "n", "skillBody": "b"},
|
||||
"attributes": {"title": "T", "description": "attr desc"},
|
||||
})
|
||||
assert item.resolved_description == "attr desc"
|
||||
|
||||
|
||||
def test_list_response_parses_pagination() -> None:
|
||||
response = ListSkillsResponse.model_validate({
|
||||
"data": [{"skillId": "1", "skill": {"skillName": "a", "skillBody": "b"}}],
|
||||
"nextPageToken": "next",
|
||||
})
|
||||
assert len(response.data) == 1
|
||||
assert response.next_page_token == "next"
|
||||
|
||||
|
||||
def test_list_response_defaults_empty() -> None:
|
||||
response = ListSkillsResponse.model_validate({})
|
||||
assert response.data == []
|
||||
assert response.next_page_token == ""
|
||||
|
||||
|
||||
# Resolved/sanitized names must always satisfy SkillMetadata.name's pattern.
|
||||
_NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
||||
|
||||
|
||||
def test_sanitize_skill_name_caps_at_64_chars() -> None:
|
||||
name = sanitize_skill_name("x" * 200)
|
||||
assert name is not None
|
||||
assert len(name) <= 64
|
||||
assert _NAME_RE.match(name)
|
||||
|
||||
|
||||
def test_resolved_name_id_fallback_is_lowercase_and_valid() -> None:
|
||||
# No usable name anywhere -> falls back to the skill id, which may be
|
||||
# uppercase; it must still be a valid, lowercase skill name.
|
||||
item = RegistrySkillItem.model_validate({
|
||||
"skillId": "AB12CD34-EF56-7890-ABCD-EF1234567890"
|
||||
})
|
||||
assert item.resolved_name == "skill-ab12cd34ef567890abcdef1234567890"
|
||||
assert _NAME_RE.match(item.resolved_name or "")
|
||||
|
||||
|
||||
def test_resolved_name_id_fallback_uses_full_id_to_avoid_collisions() -> None:
|
||||
# Ids sharing a prefix (common for time-ordered UUIDv7) must not collapse
|
||||
# to the same fallback name.
|
||||
a = RegistrySkillItem.model_validate({
|
||||
"skillId": "ab12cd34-0000-0000-0000-000000000001"
|
||||
})
|
||||
b = RegistrySkillItem.model_validate({
|
||||
"skillId": "ab12cd34-0000-0000-0000-000000000002"
|
||||
})
|
||||
assert a.resolved_name != b.resolved_name
|
||||
|
||||
|
||||
def test_resolved_name_id_fallback_handles_degenerate_id() -> None:
|
||||
# A hyphen-only id must collapse to a valid name, not a trailing-hyphen
|
||||
# "skill-" that would fail validation.
|
||||
item = RegistrySkillItem.model_validate({"skillId": "----"})
|
||||
assert item.resolved_name == "skill"
|
||||
assert _NAME_RE.match(item.resolved_name or "")
|
||||
|
|
@ -212,31 +212,31 @@
|
|||
</text><text class="terminal-r1" 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-r1" x="24.4" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">⎢</text><text class="terminal-r3" x="48.8" y="337.2" textLength="97.6" clip-path="url(#terminal-line-13)">Commands</text><text class="terminal-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
|
||||
</text><text class="terminal-r1" x="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-r1" 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-r2" x="73.2" y="386" textLength="61" clip-path="url(#terminal-line-15)">/help</text><text class="terminal-r1" x="134.2" y="386" textLength="231.8" clip-path="url(#terminal-line-15)">: Show help message</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||
</text><text class="terminal-r1" x="24.4" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">⎢</text><text class="terminal-r1" x="48.8" y="410.4" textLength="24.4" clip-path="url(#terminal-line-16)">• </text><text class="terminal-r2" x="73.2" y="410.4" textLength="85.4" clip-path="url(#terminal-line-16)">/config</text><text class="terminal-r1" x="158.6" y="410.4" textLength="268.4" clip-path="url(#terminal-line-16)">: Edit config settings</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="434.8" textLength="73.2" clip-path="url(#terminal-line-17)">/model</text><text class="terminal-r1" x="146.4" y="434.8" textLength="256.2" clip-path="url(#terminal-line-17)">: Select active model</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="459.2" textLength="109.8" clip-path="url(#terminal-line-18)">/thinking</text><text class="terminal-r1" x="183" y="459.2" textLength="280.6" clip-path="url(#terminal-line-18)">: Select thinking level</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="483.6" textLength="85.4" clip-path="url(#terminal-line-19)">/reload</text><text class="terminal-r1" x="158.6" y="483.6" textLength="780.8" clip-path="url(#terminal-line-19)">: Reload configuration, agent instructions, and skills from disk</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="508" textLength="73.2" clip-path="url(#terminal-line-20)">/clear</text><text class="terminal-r1" x="146.4" y="508" textLength="341.6" clip-path="url(#terminal-line-20)">: Clear conversation history</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r1" x="24.4" y="532.4" textLength="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-r2" x="73.2" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/copy</text><text class="terminal-r1" x="134.2" y="532.4" textLength="561.2" clip-path="url(#terminal-line-21)">: Copy the last agent message to the clipboard</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r1" x="24.4" y="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-r2" x="73.2" y="556.8" textLength="48.8" clip-path="url(#terminal-line-22)">/log</text><text class="terminal-r1" x="122" y="556.8" textLength="524.6" clip-path="url(#terminal-line-22)">: Show path to current interaction log file</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="581.2" textLength="73.2" clip-path="url(#terminal-line-23)">/debug</text><text class="terminal-r1" x="146.4" y="581.2" textLength="268.4" clip-path="url(#terminal-line-23)">: Toggle debug console</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="605.6" textLength="97.6" clip-path="url(#terminal-line-24)">/compact</text><text class="terminal-r1" x="170.8" y="605.6" textLength="1171.2" clip-path="url(#terminal-line-24)">: Compact conversation history by summarizing. Optionally pass instructions to guide the summary</text><text class="terminal-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
|
||||
</text><text class="terminal-r1" x="24.4" y="630" textLength="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-r2" x="73.2" y="630" textLength="61" clip-path="url(#terminal-line-25)">/exit</text><text class="terminal-r1" x="134.2" y="630" textLength="24.4" clip-path="url(#terminal-line-25)">, </text><text class="terminal-r2" x="158.6" y="630" textLength="24.4" clip-path="url(#terminal-line-25)">:q</text><text class="terminal-r1" x="183" y="630" textLength="24.4" clip-path="url(#terminal-line-25)">, </text><text class="terminal-r2" x="207.4" y="630" textLength="61" clip-path="url(#terminal-line-25)">:quit</text><text class="terminal-r1" x="268.4" y="630" textLength="24.4" clip-path="url(#terminal-line-25)">, </text><text class="terminal-r2" x="292.8" y="630" textLength="48.8" clip-path="url(#terminal-line-25)">exit</text><text class="terminal-r1" x="341.6" y="630" textLength="24.4" clip-path="url(#terminal-line-25)">, </text><text class="terminal-r2" x="366" y="630" textLength="48.8" clip-path="url(#terminal-line-25)">quit</text><text class="terminal-r1" x="414.8" y="630" textLength="268.4" clip-path="url(#terminal-line-25)">: Exit the application</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r1" x="24.4" y="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-r2" x="73.2" y="654.4" textLength="85.4" clip-path="url(#terminal-line-26)">/status</text><text class="terminal-r1" x="158.6" y="654.4" textLength="317.2" clip-path="url(#terminal-line-26)">: Display agent statistics</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r1" x="24.4" y="678.8" textLength="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-r2" x="73.2" y="678.8" textLength="109.8" clip-path="url(#terminal-line-27)">/teleport</text><text class="terminal-r1" x="183" y="678.8" textLength="427" clip-path="url(#terminal-line-27)">: Teleport session to Vibe Code Web</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r1" x="24.4" y="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-r2" x="73.2" y="703.2" textLength="146.4" clip-path="url(#terminal-line-28)">/proxy-setup</text><text class="terminal-r1" x="219.6" y="703.2" textLength="561.2" clip-path="url(#terminal-line-28)">: Configure proxy and SSL certificate settings</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="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-r2" x="73.2" y="727.6" textLength="109.8" clip-path="url(#terminal-line-29)">/continue</text><text class="terminal-r1" x="183" y="727.6" textLength="24.4" clip-path="url(#terminal-line-29)">, </text><text class="terminal-r2" x="207.4" y="727.6" textLength="85.4" clip-path="url(#terminal-line-29)">/resume</text><text class="terminal-r1" x="292.8" y="727.6" textLength="512.4" clip-path="url(#terminal-line-29)">: Browse, resume, or delete saved sessions</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="752" textLength="85.4" clip-path="url(#terminal-line-30)">/rename</text><text class="terminal-r1" x="158.6" y="752" textLength="341.6" clip-path="url(#terminal-line-30)">: Rename the current session</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r1" 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-r2" x="73.2" y="776.4" textLength="134.2" clip-path="url(#terminal-line-31)">/connectors</text><text class="terminal-r1" x="207.4" y="776.4" textLength="24.4" clip-path="url(#terminal-line-31)">, </text><text class="terminal-r2" x="231.8" y="776.4" textLength="48.8" clip-path="url(#terminal-line-31)">/mcp</text><text class="terminal-r1" x="280.6" y="776.4" textLength="1061.4" clip-path="url(#terminal-line-31)">: Display available MCP servers and connectors. Pass a name to list tools; subcommands:</text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r1" x="24.4" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">⎢</text><text class="terminal-r1" x="73.2" y="800.8" textLength="280.6" clip-path="url(#terminal-line-32)">status, login , logout </text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="825.2" textLength="73.2" clip-path="url(#terminal-line-33)">/voice</text><text class="terminal-r1" x="146.4" y="825.2" textLength="317.2" clip-path="url(#terminal-line-33)">: Configure voice settings</text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="849.6" textLength="122" clip-path="url(#terminal-line-34)">/leanstall</text><text class="terminal-r1" x="195.2" y="849.6" textLength="463.6" clip-path="url(#terminal-line-34)">: Install the Lean 4 agent (leanstral)</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="874" textLength="146.4" clip-path="url(#terminal-line-35)">/unleanstall</text><text class="terminal-r1" x="219.6" y="874" textLength="341.6" clip-path="url(#terminal-line-35)">: Uninstall the Lean 4 agent</text><text class="terminal-r1" x="1464" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">
|
||||
</text><text class="terminal-r1" 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-r2" x="73.2" y="898.4" textLength="85.4" clip-path="url(#terminal-line-36)">/rewind</text><text class="terminal-r1" x="158.6" y="898.4" textLength="366" clip-path="url(#terminal-line-36)">: Rewind to a previous message</text><text class="terminal-r1" x="1464" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="922.8" textLength="61" clip-path="url(#terminal-line-37)">/loop</text><text class="terminal-r1" x="134.2" y="922.8" textLength="427" clip-path="url(#terminal-line-37)">: Schedule a recurring prompt. Use </text><text class="terminal-r2" x="561.2" y="922.8" textLength="305" clip-path="url(#terminal-line-37)">/loop <interval> <prompt></text><text class="terminal-r1" x="866.2" y="922.8" textLength="24.4" clip-path="url(#terminal-line-37)">, </text><text class="terminal-r2" x="890.6" y="922.8" textLength="122" clip-path="url(#terminal-line-37)">/loop list</text><text class="terminal-r1" x="1012.6" y="922.8" textLength="61" clip-path="url(#terminal-line-37)">, or </text><text class="terminal-r2" x="1073.6" y="922.8" textLength="256.2" clip-path="url(#terminal-line-37)">/loop cancel <id|all></text><text class="terminal-r1" x="1464" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="947.2" textLength="183" clip-path="url(#terminal-line-38)">/data-retention</text><text class="terminal-r1" x="256.2" y="947.2" textLength="402.6" clip-path="url(#terminal-line-38)">: Show data retention information</text><text class="terminal-r1" x="1464" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">
|
||||
</text><text class="terminal-r1" 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-r2" x="73.2" y="971.6" textLength="73.2" clip-path="url(#terminal-line-39)">/theme</text><text class="terminal-r1" x="146.4" y="971.6" textLength="170.8" clip-path="url(#terminal-line-39)">: Select theme</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="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-r2" x="73.2" y="386" textLength="73.2" clip-path="url(#terminal-line-15)">/clear</text><text class="terminal-r1" x="146.4" y="386" textLength="341.6" clip-path="url(#terminal-line-15)">: Clear conversation history</text><text class="terminal-r1" x="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
|
||||
</text><text class="terminal-r1" x="24.4" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">⎢</text><text class="terminal-r1" x="48.8" y="410.4" textLength="24.4" clip-path="url(#terminal-line-16)">• </text><text class="terminal-r2" x="73.2" y="410.4" textLength="97.6" clip-path="url(#terminal-line-16)">/compact</text><text class="terminal-r1" x="170.8" y="410.4" textLength="1171.2" clip-path="url(#terminal-line-16)">: Compact conversation history by summarizing. Optionally pass instructions to guide the summary</text><text class="terminal-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="434.8" textLength="85.4" clip-path="url(#terminal-line-17)">/config</text><text class="terminal-r1" x="158.6" y="434.8" textLength="268.4" clip-path="url(#terminal-line-17)">: Edit config settings</text><text class="terminal-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="459.2" textLength="61" clip-path="url(#terminal-line-18)">/copy</text><text class="terminal-r1" x="134.2" y="459.2" textLength="561.2" clip-path="url(#terminal-line-18)">: Copy the last agent message to the clipboard</text><text class="terminal-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="483.6" textLength="183" clip-path="url(#terminal-line-19)">/data-retention</text><text class="terminal-r1" x="256.2" y="483.6" textLength="402.6" clip-path="url(#terminal-line-19)">: Show data retention information</text><text class="terminal-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="508" textLength="73.2" clip-path="url(#terminal-line-20)">/debug</text><text class="terminal-r1" x="146.4" y="508" textLength="268.4" clip-path="url(#terminal-line-20)">: Toggle debug console</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
|
||||
</text><text class="terminal-r1" x="24.4" y="532.4" textLength="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-r2" x="73.2" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">/exit</text><text class="terminal-r1" x="134.2" y="532.4" textLength="24.4" clip-path="url(#terminal-line-21)">, </text><text class="terminal-r2" x="158.6" y="532.4" textLength="24.4" clip-path="url(#terminal-line-21)">:q</text><text class="terminal-r1" x="183" y="532.4" textLength="24.4" clip-path="url(#terminal-line-21)">, </text><text class="terminal-r2" x="207.4" y="532.4" textLength="61" clip-path="url(#terminal-line-21)">:quit</text><text class="terminal-r1" x="268.4" y="532.4" textLength="24.4" clip-path="url(#terminal-line-21)">, </text><text class="terminal-r2" x="292.8" y="532.4" textLength="48.8" clip-path="url(#terminal-line-21)">exit</text><text class="terminal-r1" x="341.6" y="532.4" textLength="24.4" clip-path="url(#terminal-line-21)">, </text><text class="terminal-r2" x="366" y="532.4" textLength="48.8" clip-path="url(#terminal-line-21)">quit</text><text class="terminal-r1" x="414.8" y="532.4" textLength="268.4" clip-path="url(#terminal-line-21)">: Exit the application</text><text class="terminal-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
|
||||
</text><text class="terminal-r1" x="24.4" y="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-r2" x="73.2" y="556.8" textLength="61" clip-path="url(#terminal-line-22)">/help</text><text class="terminal-r1" x="134.2" y="556.8" textLength="231.8" clip-path="url(#terminal-line-22)">: Show help message</text><text class="terminal-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="581.2" textLength="122" clip-path="url(#terminal-line-23)">/leanstall</text><text class="terminal-r1" x="195.2" y="581.2" textLength="463.6" clip-path="url(#terminal-line-23)">: Install the Lean 4 agent (leanstral)</text><text class="terminal-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
|
||||
</text><text class="terminal-r1" x="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-r2" 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-r1" 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-r2" x="73.2" y="630" textLength="61" clip-path="url(#terminal-line-25)">/loop</text><text class="terminal-r1" x="134.2" y="630" textLength="427" clip-path="url(#terminal-line-25)">: Schedule a recurring prompt. Use </text><text class="terminal-r2" x="561.2" y="630" textLength="305" clip-path="url(#terminal-line-25)">/loop <interval> <prompt></text><text class="terminal-r1" x="866.2" y="630" textLength="24.4" clip-path="url(#terminal-line-25)">, </text><text class="terminal-r2" x="890.6" y="630" textLength="122" clip-path="url(#terminal-line-25)">/loop list</text><text class="terminal-r1" x="1012.6" y="630" textLength="61" clip-path="url(#terminal-line-25)">, or </text><text class="terminal-r2" x="1073.6" y="630" textLength="256.2" clip-path="url(#terminal-line-25)">/loop cancel <id|all></text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
|
||||
</text><text class="terminal-r1" x="24.4" y="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-r2" x="73.2" y="654.4" textLength="48.8" clip-path="url(#terminal-line-26)">/mcp</text><text class="terminal-r1" x="122" y="654.4" textLength="24.4" clip-path="url(#terminal-line-26)">, </text><text class="terminal-r2" x="146.4" y="654.4" textLength="134.2" clip-path="url(#terminal-line-26)">/connectors</text><text class="terminal-r1" x="280.6" y="654.4" textLength="1061.4" clip-path="url(#terminal-line-26)">: Display available MCP servers and connectors. Pass a name to list tools; subcommands:</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
|
||||
</text><text class="terminal-r1" x="24.4" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">⎢</text><text class="terminal-r1" x="73.2" y="678.8" textLength="280.6" clip-path="url(#terminal-line-27)">status, login , logout </text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
|
||||
</text><text class="terminal-r1" x="24.4" y="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-r2" x="73.2" y="703.2" textLength="73.2" clip-path="url(#terminal-line-28)">/model</text><text class="terminal-r1" x="146.4" y="703.2" textLength="256.2" clip-path="url(#terminal-line-28)">: Select active model</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="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-r2" x="73.2" y="727.6" textLength="146.4" clip-path="url(#terminal-line-29)">/proxy-setup</text><text class="terminal-r1" x="219.6" y="727.6" textLength="561.2" clip-path="url(#terminal-line-29)">: Configure proxy and SSL certificate settings</text><text class="terminal-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="752" textLength="85.4" clip-path="url(#terminal-line-30)">/reload</text><text class="terminal-r1" x="158.6" y="752" textLength="780.8" clip-path="url(#terminal-line-30)">: Reload configuration, agent instructions, and skills from disk</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
|
||||
</text><text class="terminal-r1" 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-r2" x="73.2" y="776.4" textLength="85.4" clip-path="url(#terminal-line-31)">/rename</text><text class="terminal-r1" x="158.6" y="776.4" textLength="341.6" clip-path="url(#terminal-line-31)">: Rename the current session</text><text class="terminal-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="800.8" textLength="85.4" clip-path="url(#terminal-line-32)">/resume</text><text class="terminal-r1" x="158.6" y="800.8" textLength="24.4" clip-path="url(#terminal-line-32)">, </text><text class="terminal-r2" x="183" y="800.8" textLength="109.8" clip-path="url(#terminal-line-32)">/continue</text><text class="terminal-r1" x="292.8" y="800.8" textLength="512.4" clip-path="url(#terminal-line-32)">: Browse, resume, or delete saved sessions</text><text class="terminal-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="825.2" textLength="85.4" clip-path="url(#terminal-line-33)">/rewind</text><text class="terminal-r1" x="158.6" y="825.2" textLength="366" clip-path="url(#terminal-line-33)">: Rewind to a previous message</text><text class="terminal-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="849.6" textLength="85.4" clip-path="url(#terminal-line-34)">/status</text><text class="terminal-r1" x="158.6" y="849.6" textLength="317.2" clip-path="url(#terminal-line-34)">: Display agent statistics</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="874" textLength="109.8" clip-path="url(#terminal-line-35)">/teleport</text><text class="terminal-r1" x="183" y="874" textLength="427" clip-path="url(#terminal-line-35)">: Teleport session to Vibe Code Web</text><text class="terminal-r1" x="1464" y="874" textLength="12.2" clip-path="url(#terminal-line-35)">
|
||||
</text><text class="terminal-r1" 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-r2" x="73.2" y="898.4" textLength="73.2" clip-path="url(#terminal-line-36)">/theme</text><text class="terminal-r1" x="146.4" y="898.4" textLength="170.8" clip-path="url(#terminal-line-36)">: Select theme</text><text class="terminal-r1" x="1464" y="898.4" textLength="12.2" clip-path="url(#terminal-line-36)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="922.8" textLength="109.8" clip-path="url(#terminal-line-37)">/thinking</text><text class="terminal-r1" x="183" y="922.8" textLength="280.6" clip-path="url(#terminal-line-37)">: Select thinking level</text><text class="terminal-r1" x="1464" y="922.8" textLength="12.2" clip-path="url(#terminal-line-37)">
|
||||
</text><text class="terminal-r1" x="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-r2" x="73.2" y="947.2" textLength="146.4" clip-path="url(#terminal-line-38)">/unleanstall</text><text class="terminal-r1" x="219.6" y="947.2" textLength="341.6" clip-path="url(#terminal-line-38)">: Uninstall the Lean 4 agent</text><text class="terminal-r1" x="1464" y="947.2" textLength="12.2" clip-path="url(#terminal-line-38)">
|
||||
</text><text class="terminal-r1" 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-r2" x="73.2" y="971.6" textLength="73.2" clip-path="url(#terminal-line-39)">/voice</text><text class="terminal-r1" x="146.4" y="971.6" textLength="317.2" clip-path="url(#terminal-line-39)">: Configure voice settings</text><text class="terminal-r1" x="1464" y="971.6" textLength="12.2" clip-path="url(#terminal-line-39)">
|
||||
</text><text class="terminal-r1" x="1464" y="996" textLength="12.2" clip-path="url(#terminal-line-40)">
|
||||
</text><text class="terminal-r1" x="1464" y="1020.4" textLength="12.2" clip-path="url(#terminal-line-41)">
|
||||
</text><text class="terminal-r1" 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)">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
|
@ -149,19 +149,6 @@ class TeleportCommandHelpProApp(TeleportCommandHelpSnapshotApp):
|
|||
)
|
||||
|
||||
|
||||
class TeleportCommandHelpFreeApp(TeleportCommandHelpSnapshotApp):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
FakeWhoAmIGateway(
|
||||
WhoAmIResponse(
|
||||
plan_type=WhoAmIPlanType.API,
|
||||
plan_name="FREE",
|
||||
prompt_switching_to_pro_plan=False,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_teleport_status_checking_git(snap_compare: SnapCompare) -> None:
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await pilot.pause(0.2)
|
||||
|
|
@ -302,16 +289,3 @@ def test_snapshot_teleport_command_visible_for_pro_account(
|
|||
terminal_size=(120, 48),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
||||
|
||||
def test_snapshot_teleport_command_hidden_for_non_pro_account(
|
||||
snap_compare: SnapCompare,
|
||||
) -> None:
|
||||
async def run_before(pilot: Pilot) -> None:
|
||||
await pilot.pause(0.2)
|
||||
|
||||
assert snap_compare(
|
||||
"test_ui_snapshot_teleport.py:TeleportCommandHelpFreeApp",
|
||||
terminal_size=(120, 48),
|
||||
run_before=run_before,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue