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
|
|
@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.17.1] - 2026-06-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Commands in `/help` are now listed alphabetically in both the CLI and ACP
|
||||
- `/teleport` is now always available and shows an explicit error when its prerequisites aren't met, instead of being hidden
|
||||
|
||||
|
||||
## [2.17.0] - 2026-06-19
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
id = "mistral-vibe"
|
||||
name = "Mistral Vibe"
|
||||
description = "Mistral's open-source coding assistant"
|
||||
version = "2.17.0"
|
||||
version = "2.17.1"
|
||||
schema_version = 1
|
||||
authors = ["Mistral AI"]
|
||||
repository = "https://github.com/mistralai/mistral-vibe"
|
||||
|
|
@ -11,21 +11,21 @@ name = "Mistral Vibe"
|
|||
icon = "./icons/mistral_vibe.svg"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.darwin-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.17.0/vibe-acp-darwin-aarch64-2.17.0.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.17.1/vibe-acp-darwin-aarch64-2.17.1.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.darwin-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.17.0/vibe-acp-darwin-x86_64-2.17.0.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.17.1/vibe-acp-darwin-x86_64-2.17.1.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.linux-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.17.0/vibe-acp-linux-aarch64-2.17.0.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.17.1/vibe-acp-linux-aarch64-2.17.1.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.linux-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.17.0/vibe-acp-linux-x86_64-2.17.0.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.17.1/vibe-acp-linux-x86_64-2.17.1.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.windows-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.17.0/vibe-acp-windows-x86_64-2.17.0.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.17.1/vibe-acp-windows-x86_64-2.17.1.zip"
|
||||
cmd = "./vibe-acp.exe"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "mistral-vibe"
|
||||
version = "2.17.0"
|
||||
version = "2.17.1"
|
||||
description = "Minimal CLI coding agent by Mistral"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
|
@ -50,7 +50,7 @@ dependencies = [
|
|||
"httpx==0.28.1",
|
||||
"httpx-sse==0.4.3",
|
||||
"humanize==4.15.0",
|
||||
"idna==3.13",
|
||||
"idna==3.18",
|
||||
"importlib-metadata==8.7.1",
|
||||
"jaraco-classes==3.4.0",
|
||||
"jaraco-context==6.1.2",
|
||||
|
|
@ -65,10 +65,10 @@ dependencies = [
|
|||
"linkify-it-py==2.1.0",
|
||||
"markdown-it-py==4.0.0",
|
||||
"markdownify==1.2.2",
|
||||
"mcp==1.27.2",
|
||||
"mcp==1.28.0",
|
||||
"mdit-py-plugins==0.5.0",
|
||||
"mdurl==0.1.2",
|
||||
"mistralai==2.4.9",
|
||||
"mistralai==2.4.11",
|
||||
"more-itertools==11.0.2",
|
||||
"opentelemetry-api==1.39.1",
|
||||
"opentelemetry-exporter-otlp-proto-common==1.39.1",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
38
uv.lock
generated
38
uv.lock
generated
|
|
@ -1,6 +1,12 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||
"python_full_version >= '3.14' and sys_platform != 'win32'",
|
||||
"python_full_version < '3.14' and sys_platform == 'win32'",
|
||||
"python_full_version < '3.14' and sys_platform != 'win32'",
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
|
||||
|
|
@ -504,11 +510,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.13"
|
||||
version = "3.18"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -677,7 +683,7 @@ name = "macholib"
|
|||
version = "1.16.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "altgraph" },
|
||||
{ name = "altgraph", marker = "sys_platform != 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" }
|
||||
wheels = [
|
||||
|
|
@ -779,7 +785,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.27.2"
|
||||
version = "1.28.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
|
@ -797,9 +803,9 @@ dependencies = [
|
|||
{ name = "typing-inspection" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/27/3c/347cf965d313f5d41764e7d46bea6ffe7d9ef13b983cc429b0340962a082/mcp-1.27.2.tar.gz", hash = "sha256:8e02db104096d1c25b28e64bde29a5c32b31bc241710213e12fd4d84985bdfef", size = 621116, upload-time = "2026-05-29T17:16:04.039Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c1/ee/94c6c50ffc5b5cf4737052275d11b57367f32d1a8516e31dcd60591b3916/mcp-1.28.0.tar.gz", hash = "sha256:559d3f9943674cafbe5744c5d3794f3237e8b47f9bbc58e20c0fad680d8487c2", size = 636040, upload-time = "2026-06-16T21:37:17.996Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/11/252c6f971dc4f16af1d98a1c469d8ba523aab00d1bb76b4d3bc1ff32eacc/mcp-1.27.2-py3-none-any.whl", hash = "sha256:d6ff5160c6ca65d93013626efb3fc249de683c30b2d8570755ceddd490344de5", size = 220498, upload-time = "2026-05-29T17:16:02.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/e1/4c1dc1fbb688641a712d34650c3d58bbbdcb314ddb75bc5817bbf33515a4/mcp-1.28.0-py3-none-any.whl", hash = "sha256:9c1e7cf3a9125557e418ecd4fed8e9adddce81b0dfdae4d6601d700f5beb71a4", size = 221959, upload-time = "2026-06-16T21:37:16.579Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -825,7 +831,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "mistral-vibe"
|
||||
version = "2.17.0"
|
||||
version = "2.17.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "agent-client-protocol" },
|
||||
|
|
@ -980,7 +986,7 @@ requires-dist = [
|
|||
{ name = "httpx", specifier = "==0.28.1" },
|
||||
{ name = "httpx-sse", specifier = "==0.4.3" },
|
||||
{ name = "humanize", specifier = "==4.15.0" },
|
||||
{ name = "idna", specifier = "==3.13" },
|
||||
{ name = "idna", specifier = "==3.18" },
|
||||
{ name = "importlib-metadata", specifier = "==8.7.1" },
|
||||
{ name = "jaraco-classes", specifier = "==3.4.0" },
|
||||
{ name = "jaraco-context", specifier = "==6.1.2" },
|
||||
|
|
@ -995,10 +1001,10 @@ requires-dist = [
|
|||
{ name = "linkify-it-py", specifier = "==2.1.0" },
|
||||
{ name = "markdown-it-py", specifier = "==4.0.0" },
|
||||
{ name = "markdownify", specifier = "==1.2.2" },
|
||||
{ name = "mcp", specifier = "==1.27.2" },
|
||||
{ name = "mcp", specifier = "==1.28.0" },
|
||||
{ name = "mdit-py-plugins", specifier = "==0.5.0" },
|
||||
{ name = "mdurl", specifier = "==0.1.2" },
|
||||
{ name = "mistralai", specifier = "==2.4.9" },
|
||||
{ name = "mistralai", specifier = "==2.4.11" },
|
||||
{ name = "more-itertools", specifier = "==11.0.2" },
|
||||
{ name = "opentelemetry-api", specifier = "==1.39.1" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-common", specifier = "==1.39.1" },
|
||||
|
|
@ -1086,7 +1092,7 @@ dev = [
|
|||
|
||||
[[package]]
|
||||
name = "mistralai"
|
||||
version = "2.4.9"
|
||||
version = "2.4.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "eval-type-backport" },
|
||||
|
|
@ -1098,9 +1104,9 @@ dependencies = [
|
|||
{ name = "python-dateutil" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/f1/f22fa0fac767279df95e0796f37520eae0678a75b62d7bca3e85c46b684b/mistralai-2.4.9.tar.gz", hash = "sha256:9a6465b8bddf825d76cf80ab54d55bb8089fdf5aafc0a7943ae74c825df97939", size = 471000, upload-time = "2026-06-03T13:04:22.565Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/de/afe647785ba603d6473dd407edfda917c31e182522381930b43bb6aa25ef/mistralai-2.4.11.tar.gz", hash = "sha256:bc2bb5cae59ce4b07928f5c824eeb5c9a33f6a2a12049725be14619969036f82", size = 488149, upload-time = "2026-06-16T15:41:18.87Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/bc/977b65cddce185dce0414d14c783040a9f9d647bbfe9b0f5c74ff9262816/mistralai-2.4.9-py3-none-any.whl", hash = "sha256:8e2f8a58697455f2113b4f6d0d1bd28d66d3eb51f94ff24b9d22a21dea531520", size = 1121336, upload-time = "2026-06-03T13:04:20.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/6d/0529d895ca381cfa636857259c1fec91d6d7c985b77bf72b9cf5a626d431/mistralai-2.4.11-py3-none-any.whl", hash = "sha256:479f8007ef522e674fbf617e65f8dcdf5d702fc1468c470ae40b1ee0aa0f8136", size = 1170945, upload-time = "2026-06-16T15:41:17.11Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1969,8 +1975,8 @@ name = "secretstorage"
|
|||
version = "3.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "jeepney" },
|
||||
{ name = "cryptography", marker = "sys_platform != 'win32'" },
|
||||
{ name = "jeepney", marker = "sys_platform != 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
|
||||
wheels = [
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ from __future__ import annotations
|
|||
from pathlib import Path
|
||||
|
||||
VIBE_ROOT = Path(__file__).parent
|
||||
__version__ = "2.17.0"
|
||||
__version__ = "2.17.1"
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ from pydantic import AliasChoices, BaseModel, ConfigDict, Field, ValidationError
|
|||
|
||||
from vibe import VIBE_ROOT, __version__
|
||||
from vibe.acp.acp_logger import acp_message_observer
|
||||
from vibe.acp.commands import AcpCommandAvailabilityContext, AcpCommandRegistry
|
||||
from vibe.acp.commands import AcpCommandRegistry
|
||||
from vibe.acp.exceptions import (
|
||||
CompactionError,
|
||||
ConfigurationError,
|
||||
|
|
@ -224,7 +224,6 @@ WORKSPACE_TRUST_META_KEY = "workspace_trust"
|
|||
TRUST_GRANT_DECISIONS = {
|
||||
WorkspaceTrustDecision.TRUST_REPO,
|
||||
WorkspaceTrustDecision.TRUST_CWD,
|
||||
WorkspaceTrustDecision.TRUST_SESSION,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -711,10 +710,8 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
self, session_id: str, agent_loop: AgentLoop
|
||||
) -> AcpSessionLoop:
|
||||
command_registry = AcpCommandRegistry(
|
||||
availability_context=AcpCommandAvailabilityContext(
|
||||
vibe_code_enabled=agent_loop.base_config.vibe_code_enabled
|
||||
)
|
||||
)
|
||||
session = AcpSessionLoop(
|
||||
id=session_id, agent_loop=agent_loop, command_registry=command_registry
|
||||
)
|
||||
|
|
@ -824,7 +821,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
repoRoot=str(prompt.repo_root.resolve()) if prompt.repo_root else None,
|
||||
ignoredFiles=self._workspace_trust_ignored_files(prompt),
|
||||
availableDecisions=available_workspace_trust_decisions(
|
||||
prompt, include_session=True
|
||||
prompt, include_session=False
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -873,7 +870,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
raise InvalidRequestError("No workspace trust decision is available.")
|
||||
|
||||
available_decisions = available_workspace_trust_decisions(
|
||||
prompt, include_session=True
|
||||
prompt, include_session=False
|
||||
)
|
||||
if decision not in available_decisions:
|
||||
raise InvalidRequestError(f"Unsupported trust decision: {decision}")
|
||||
|
|
@ -914,9 +911,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
raise InvalidRequestError(str(exc)) from exc
|
||||
|
||||
if session is not None and request.decision in TRUST_GRANT_DECISIONS:
|
||||
os.chdir(cwd)
|
||||
await self._reload_session_config(session)
|
||||
await session.command_registry.notify_changed()
|
||||
session.spawn(self._reload_trusted_workspace_session(session, cwd))
|
||||
|
||||
return self._workspace_trust_response(cwd).model_dump(
|
||||
mode="json", by_alias=True
|
||||
|
|
@ -1201,6 +1196,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
)
|
||||
)
|
||||
|
||||
commands.sort(key=lambda command: command.name)
|
||||
await self.client.session_update(
|
||||
session_id=session.id, update=update_available_commands(commands)
|
||||
)
|
||||
|
|
@ -1277,10 +1273,7 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
return True
|
||||
|
||||
async def _reload_config(self, session: AcpSessionLoop) -> None:
|
||||
new_config = VibeConfig.load(tool_paths=session.agent_loop.config.tool_paths)
|
||||
self._apply_client_project_name(new_config)
|
||||
_merge_non_interactive_disabled_tools(new_config)
|
||||
await session.agent_loop.reload_with_initial_messages(base_config=new_config)
|
||||
await self._reload_session_config(session)
|
||||
|
||||
async def _apply_model_change(self, session: AcpSessionLoop, model_id: str) -> bool:
|
||||
model_aliases = [model.alias for model in session.agent_loop.config.models]
|
||||
|
|
@ -2037,20 +2030,25 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
self, session: AcpSessionLoop, text_prompt: str, message_id: str
|
||||
) -> PromptResponse:
|
||||
lines = ["### Available Commands", ""]
|
||||
for cmd in session.command_registry.commands.values():
|
||||
for cmd in sorted(
|
||||
session.command_registry.commands.values(), key=lambda command: command.name
|
||||
):
|
||||
hint = f" `<{cmd.input_hint}>`" if cmd.input_hint else ""
|
||||
lines.append(f"- `/{cmd.name}`{hint}: {cmd.description}")
|
||||
|
||||
builtin_names = set(session.command_registry.commands)
|
||||
invocable = {
|
||||
n: s
|
||||
for n, s in session.agent_loop.skill_manager.available_skills.items()
|
||||
if s.user_invocable and n not in builtin_names
|
||||
}
|
||||
invocable = sorted(
|
||||
(
|
||||
skill
|
||||
for skill in session.agent_loop.skill_manager.available_skills.values()
|
||||
if skill.user_invocable and skill.name not in builtin_names
|
||||
),
|
||||
key=lambda skill: skill.name,
|
||||
)
|
||||
if invocable:
|
||||
lines.extend(["", "### Available Skills", ""])
|
||||
for name, info in invocable.items():
|
||||
lines.append(f"- `/{name}`: {info.description}")
|
||||
for skill in invocable:
|
||||
lines.append(f"- `/{skill.name}`: {skill.description}")
|
||||
|
||||
return await self._command_reply(session, "\n".join(lines), message_id)
|
||||
|
||||
|
|
@ -2186,6 +2184,23 @@ class VibeAcpAgentLoop(AcpAgent):
|
|||
_merge_non_interactive_disabled_tools(new_config)
|
||||
await session.agent_loop.reload_with_initial_messages(base_config=new_config)
|
||||
|
||||
async def _reload_trusted_workspace_session(
|
||||
self, session: AcpSessionLoop, cwd: Path
|
||||
) -> None:
|
||||
try:
|
||||
os.chdir(cwd)
|
||||
await self._reload_session_config(session)
|
||||
await session.command_registry.notify_changed()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Failed to reload trusted workspace session config: session_id=%s cwd=%s",
|
||||
session.id,
|
||||
cwd,
|
||||
exc_info=exc,
|
||||
)
|
||||
|
||||
async def _handle_reload(
|
||||
self, session: AcpSessionLoop, text_prompt: str, message_id: str
|
||||
) -> PromptResponse:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from vibe.acp.commands.registry import AcpCommandAvailabilityContext, AcpCommandRegistry
|
||||
from vibe.acp.commands.registry import AcpCommandRegistry
|
||||
|
||||
__all__ = ["AcpCommandAvailabilityContext", "AcpCommandRegistry"]
|
||||
__all__ = ["AcpCommandRegistry"]
|
||||
|
|
|
|||
|
|
@ -6,17 +6,7 @@ from dataclasses import dataclass, field
|
|||
type OnCommandsChanged = Callable[[], Awaitable[None]]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AcpCommandAvailabilityContext:
|
||||
"""Context used to decide whether a command should be advertised."""
|
||||
|
||||
vibe_code_enabled: bool = False
|
||||
|
||||
def is_teleport_available(self) -> bool:
|
||||
return self.vibe_code_enabled
|
||||
|
||||
|
||||
type CommandAvailability = Callable[[AcpCommandAvailabilityContext], bool]
|
||||
type CommandAvailability = Callable[[bool], bool]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -34,9 +24,7 @@ class AcpCommand:
|
|||
class AcpCommandRegistry:
|
||||
"""Registry of ACP commands. Notifies listeners when commands change."""
|
||||
|
||||
availability_context: AcpCommandAvailabilityContext = field(
|
||||
default_factory=AcpCommandAvailabilityContext
|
||||
)
|
||||
vibe_code_enabled: bool = False
|
||||
_commands: dict[str, AcpCommand] = field(default_factory=dict)
|
||||
_on_changed: OnCommandsChanged | None = None
|
||||
|
||||
|
|
@ -51,7 +39,7 @@ class AcpCommandRegistry:
|
|||
def _is_available(self, command: AcpCommand) -> bool:
|
||||
if command.is_available is None:
|
||||
return True
|
||||
return command.is_available(self.availability_context)
|
||||
return command.is_available(self.vibe_code_enabled)
|
||||
|
||||
def set_on_changed(self, callback: OnCommandsChanged) -> None:
|
||||
self._on_changed = callback
|
||||
|
|
@ -101,7 +89,7 @@ def _build_commands() -> dict[str, AcpCommand]:
|
|||
name="teleport",
|
||||
description="Teleport session to Vibe Code Web",
|
||||
handler="_handle_teleport",
|
||||
is_available=AcpCommandAvailabilityContext.is_teleport_available,
|
||||
is_available=lambda vibe_code_enabled: vibe_code_enabled,
|
||||
),
|
||||
"proxy-setup": AcpCommand(
|
||||
name="proxy-setup",
|
||||
|
|
|
|||
|
|
@ -194,6 +194,22 @@ async def handle_teleport_command(
|
|||
field_meta=_teleport_field_meta("unavailable"),
|
||||
)
|
||||
|
||||
if not session.agent_loop.config.is_active_model_mistral():
|
||||
send_teleport_early_failure_telemetry(
|
||||
session.agent_loop.telemetry_client,
|
||||
stage="ineligible",
|
||||
error_class="TeleportIneligibleError",
|
||||
nb_session_messages=len(session.agent_loop.messages[1:]),
|
||||
)
|
||||
return await _teleport_command_reply(
|
||||
client,
|
||||
session,
|
||||
"Teleport requires an active Mistral model. Switch to a Mistral "
|
||||
"model, then try again.",
|
||||
message_id,
|
||||
field_meta=_teleport_field_meta("unavailable"),
|
||||
)
|
||||
|
||||
last_user_message = next(
|
||||
(
|
||||
msg
|
||||
|
|
|
|||
|
|
@ -4,27 +4,10 @@ from collections.abc import Callable
|
|||
from dataclasses import dataclass
|
||||
import sys
|
||||
|
||||
from vibe.cli.plan_offer.decide_plan_offer import PlanInfo
|
||||
|
||||
ALT_KEY = "⌥" if sys.platform == "darwin" else "Alt"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommandAvailabilityContext:
|
||||
vibe_code_enabled: bool = False
|
||||
is_active_model_mistral: bool = False
|
||||
plan_info: PlanInfo | None = None
|
||||
|
||||
def is_teleport_available(self) -> bool:
|
||||
return (
|
||||
self.vibe_code_enabled
|
||||
and self.is_active_model_mistral
|
||||
and self.plan_info is not None
|
||||
and self.plan_info.is_teleport_eligible()
|
||||
)
|
||||
|
||||
|
||||
CommandAvailability = Callable[[CommandAvailabilityContext], bool]
|
||||
CommandAvailability = Callable[[bool], bool]
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -40,14 +23,13 @@ class CommandRegistry:
|
|||
def __init__(
|
||||
self,
|
||||
excluded_commands: list[str] | None = None,
|
||||
availability_context: CommandAvailabilityContext | None = None,
|
||||
vibe_code_enabled: bool = False,
|
||||
) -> None:
|
||||
if excluded_commands is None:
|
||||
excluded_commands = []
|
||||
self._disabled_commands = set(excluded_commands)
|
||||
self._availability_context = CommandAvailabilityContext()
|
||||
self._commands: dict[str, Command] = {}
|
||||
self.refresh(availability_context)
|
||||
self.refresh(vibe_code_enabled)
|
||||
|
||||
def _build_commands(self) -> dict[str, Command]:
|
||||
return {
|
||||
|
|
@ -116,7 +98,7 @@ class CommandRegistry:
|
|||
aliases=frozenset(["/teleport"]),
|
||||
description="Teleport session to Vibe Code Web",
|
||||
handler="_teleport_command",
|
||||
is_available=CommandAvailabilityContext.is_teleport_available,
|
||||
is_available=lambda vibe_code_enabled: vibe_code_enabled,
|
||||
),
|
||||
"proxy-setup": Command(
|
||||
aliases=frozenset(["/proxy-setup"]),
|
||||
|
|
@ -186,12 +168,8 @@ class CommandRegistry:
|
|||
def commands(self) -> dict[str, Command]:
|
||||
return self._commands
|
||||
|
||||
def refresh(
|
||||
self, availability_context: CommandAvailabilityContext | None = None
|
||||
) -> None:
|
||||
self._availability_context = (
|
||||
availability_context or CommandAvailabilityContext()
|
||||
)
|
||||
def refresh(self, vibe_code_enabled: bool = False) -> None:
|
||||
self._vibe_code_enabled = vibe_code_enabled
|
||||
self._commands = {
|
||||
name: command
|
||||
for name, command in self._build_commands().items()
|
||||
|
|
@ -202,7 +180,7 @@ class CommandRegistry:
|
|||
def _is_command_available(self, command: Command) -> bool:
|
||||
if command.is_available is None:
|
||||
return True
|
||||
return command.is_available(self._availability_context)
|
||||
return command.is_available(self._vibe_code_enabled)
|
||||
|
||||
def _alias_map(self) -> dict[str, str]:
|
||||
return {
|
||||
|
|
@ -261,7 +239,13 @@ class CommandRegistry:
|
|||
"",
|
||||
]
|
||||
|
||||
for cmd in self.commands.values():
|
||||
aliases = ", ".join(f"`{alias}`" for alias in sorted(cmd.aliases))
|
||||
for name, cmd in sorted(self.commands.items()):
|
||||
canonical_alias = f"/{name}"
|
||||
aliases = ", ".join(
|
||||
f"`{alias}`"
|
||||
for alias in sorted(
|
||||
cmd.aliases, key=lambda alias: (alias != canonical_alias, alias)
|
||||
)
|
||||
)
|
||||
lines.append(f"- {aliases}: {cmd.description}")
|
||||
return "\n".join(lines)
|
||||
|
|
|
|||
|
|
@ -113,20 +113,41 @@ def resolve_api_key_for_plan(provider: ProviderConfig) -> str | None:
|
|||
return resolve_api_key(api_env_key)
|
||||
|
||||
|
||||
def vibe_api_key_url(vibe_base_url: str = DEFAULT_VIBE_BASE_URL) -> str:
|
||||
return f"{vibe_base_url.rstrip('/')}/code/extensions?focus=key"
|
||||
|
||||
|
||||
def plan_offer_cta(
|
||||
payload: PlanInfo | None, *, vibe_base_url: str = DEFAULT_VIBE_BASE_URL
|
||||
) -> str | None:
|
||||
if not payload:
|
||||
return
|
||||
vibe_api_key_url = f"{vibe_base_url.rstrip('/')}/code/extensions?focus=key"
|
||||
key_url = vibe_api_key_url(vibe_base_url)
|
||||
if payload.prompt_switching_to_pro_plan:
|
||||
return f"### Switch to your [Vibe Pro API key]({vibe_api_key_url})"
|
||||
return f"### Switch to your [Vibe Pro API key]({key_url})"
|
||||
if (
|
||||
payload.plan_type in {WhoAmIPlanType.API, WhoAmIPlanType.UNAUTHORIZED}
|
||||
or payload.is_free_chat_plan()
|
||||
or payload.is_free_mistral_code_plan()
|
||||
):
|
||||
return f"### Unlock more with Vibe - [Upgrade to Vibe Pro]({vibe_api_key_url})"
|
||||
return f"### Unlock more with Vibe - [Upgrade to Vibe Pro]({key_url})"
|
||||
|
||||
|
||||
def check_teleport_eligibility(
|
||||
payload: PlanInfo | None, *, vibe_base_url: str = DEFAULT_VIBE_BASE_URL
|
||||
) -> str | None:
|
||||
if payload is not None and payload.is_teleport_eligible():
|
||||
return None
|
||||
key_url = vibe_api_key_url(vibe_base_url)
|
||||
if payload is not None and payload.prompt_switching_to_pro_plan:
|
||||
return (
|
||||
"Teleport requires a Vibe Pro API key, but the current key is on a "
|
||||
f"different plan. Switch to your Vibe Pro API key: {key_url}"
|
||||
)
|
||||
return (
|
||||
"Teleport requires a Vibe Pro subscription. Your current API key isn't "
|
||||
f"eligible. Upgrade to Vibe Pro: {key_url}"
|
||||
)
|
||||
|
||||
|
||||
def plan_title(payload: PlanInfo | None) -> str | None: # noqa: PLR0911
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ from textual.widgets import Static
|
|||
|
||||
from vibe import __version__ as CORE_VERSION
|
||||
from vibe.cli.clipboard import copy_selection_to_clipboard, copy_text_to_clipboard
|
||||
from vibe.cli.commands import CommandAvailabilityContext, CommandRegistry
|
||||
from vibe.cli.commands import CommandRegistry
|
||||
from vibe.cli.narrator_manager import (
|
||||
NarratorManager,
|
||||
NarratorManagerPort,
|
||||
|
|
@ -40,6 +40,7 @@ from vibe.cli.narrator_manager import (
|
|||
from vibe.cli.plan_offer.adapters.http_whoami_gateway import HttpWhoAmIGateway
|
||||
from vibe.cli.plan_offer.decide_plan_offer import (
|
||||
PlanInfo,
|
||||
check_teleport_eligibility,
|
||||
decide_plan_offer,
|
||||
plan_offer_cta,
|
||||
plan_title,
|
||||
|
|
@ -184,6 +185,7 @@ from vibe.core.session.saved_sessions import (
|
|||
from vibe.core.session.session_loader import SessionLoader
|
||||
from vibe.core.session.title_format import format_session_title
|
||||
from vibe.core.skills.manager import SkillManager
|
||||
from vibe.core.telemetry.types import TeleportFailureStage
|
||||
from vibe.core.teleport.telemetry import send_teleport_early_failure_telemetry
|
||||
from vibe.core.teleport.types import (
|
||||
TeleportCheckingGitEvent,
|
||||
|
|
@ -588,20 +590,13 @@ class VibeApp(App): # noqa: PLR0904
|
|||
def _connectors_enabled(self) -> bool:
|
||||
return self.agent_loop.connector_registry is not None
|
||||
|
||||
def _get_command_availability_context(self) -> CommandAvailabilityContext:
|
||||
return CommandAvailabilityContext(
|
||||
vibe_code_enabled=self.agent_loop.base_config.vibe_code_enabled,
|
||||
is_active_model_mistral=self.config.is_active_model_mistral(),
|
||||
plan_info=self._plan_info,
|
||||
)
|
||||
|
||||
def _build_command_registry(self) -> CommandRegistry:
|
||||
return CommandRegistry(
|
||||
availability_context=self._get_command_availability_context()
|
||||
vibe_code_enabled=self.agent_loop.base_config.vibe_code_enabled
|
||||
)
|
||||
|
||||
def _refresh_command_registry(self) -> None:
|
||||
self.commands.refresh(self._get_command_availability_context())
|
||||
self.commands.refresh(self.agent_loop.base_config.vibe_code_enabled)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with ChatScroll(id="chat"):
|
||||
|
|
@ -1881,29 +1876,54 @@ class VibeApp(App): # noqa: PLR0904
|
|||
async def _teleport_command(self, **kwargs: Any) -> None:
|
||||
await self._handle_teleport_command(show_message=False)
|
||||
|
||||
def _teleport_unavailable_reason(self) -> str | None:
|
||||
if not self.config.is_active_model_mistral():
|
||||
return (
|
||||
"Teleport requires an active Mistral model. Use /model to switch to "
|
||||
"a Mistral model, then try again."
|
||||
)
|
||||
return check_teleport_eligibility(
|
||||
self._plan_info, vibe_base_url=self.config.vibe_base_url
|
||||
)
|
||||
|
||||
async def _fail_teleport_early(
|
||||
self, *, stage: TeleportFailureStage, error_class: str, message: str
|
||||
) -> None:
|
||||
send_teleport_early_failure_telemetry(
|
||||
self.agent_loop.telemetry_client,
|
||||
stage=stage,
|
||||
error_class=error_class,
|
||||
nb_session_messages=len(self.agent_loop.messages[1:]),
|
||||
)
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(message, collapsed=self._tools_collapsed)
|
||||
)
|
||||
|
||||
async def _handle_teleport_command(
|
||||
self, value: str | None = None, show_message: bool = True
|
||||
) -> None:
|
||||
has_history = any(msg.role != Role.system for msg in self.agent_loop.messages)
|
||||
if not value:
|
||||
if show_message:
|
||||
await self._mount_and_scroll(SlashCommandMessage("teleport"))
|
||||
if not has_history:
|
||||
send_teleport_early_failure_telemetry(
|
||||
self.agent_loop.telemetry_client,
|
||||
stage="no_history",
|
||||
error_class="TeleportNoHistoryError",
|
||||
nb_session_messages=len(self.agent_loop.messages[1:]),
|
||||
)
|
||||
await self._mount_and_scroll(
|
||||
ErrorMessage(
|
||||
"No conversation history to teleport.",
|
||||
collapsed=self._tools_collapsed,
|
||||
TeleportUserMessage(value) if value else SlashCommandMessage("teleport")
|
||||
)
|
||||
|
||||
if reason := self._teleport_unavailable_reason():
|
||||
await self._fail_teleport_early(
|
||||
stage="ineligible",
|
||||
error_class="TeleportIneligibleError",
|
||||
message=reason,
|
||||
)
|
||||
return
|
||||
elif show_message:
|
||||
await self._mount_and_scroll(TeleportUserMessage(value))
|
||||
|
||||
if not value and not has_history:
|
||||
await self._fail_teleport_early(
|
||||
stage="no_history",
|
||||
error_class="TeleportNoHistoryError",
|
||||
message="No conversation history to teleport.",
|
||||
)
|
||||
return
|
||||
|
||||
self.run_worker(self._teleport(value), exclusive=False)
|
||||
|
||||
async def _teleport(self, prompt: str | None = None) -> None:
|
||||
|
|
|
|||
|
|
@ -842,9 +842,24 @@ StatusMessage {
|
|||
color: $warning;
|
||||
}
|
||||
|
||||
.diff-removed {
|
||||
.diff-removed,
|
||||
.diff-added,
|
||||
.diff-context {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
.diff-gutter {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.diff-body {
|
||||
width: 1fr;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.diff-removed {
|
||||
&:dark {
|
||||
background: $error 10%;
|
||||
}
|
||||
|
|
@ -859,8 +874,6 @@ StatusMessage {
|
|||
}
|
||||
|
||||
.diff-added {
|
||||
height: auto;
|
||||
|
||||
&:dark {
|
||||
background: $success 10%;
|
||||
}
|
||||
|
|
@ -883,10 +896,6 @@ StatusMessage {
|
|||
}
|
||||
}
|
||||
|
||||
.diff-context {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.todo-empty,
|
||||
.todo-cancelled {
|
||||
height: auto;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import difflib
|
|||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Horizontal
|
||||
from textual.content import Content
|
||||
from textual.highlight import (
|
||||
ANSIDarkHighlightTheme,
|
||||
|
|
@ -12,9 +14,13 @@ from textual.highlight import (
|
|||
HighlightTheme,
|
||||
highlight as highlight_code,
|
||||
)
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import (
|
||||
NoMarkupStatic,
|
||||
NonSelectableStatic,
|
||||
)
|
||||
from vibe.core.utils.io import read_safe
|
||||
from vibe.core.utils.text import snippet_start_lines
|
||||
|
||||
|
|
@ -60,6 +66,38 @@ def _highlight_line(code: str, language: str, theme: type[HighlightTheme]) -> Co
|
|||
return lines[0] if lines else Content(code)
|
||||
|
||||
|
||||
def _gutter_styles(prefix_char: str, *, ansi: bool) -> tuple[str, str]:
|
||||
if prefix_char == "-":
|
||||
if ansi:
|
||||
return f"bold {_REMOVED_STYLE}", f"bold {_REMOVED_STYLE}"
|
||||
return _REMOVED_STYLE, _DIM_MUTED_STYLE
|
||||
if prefix_char == "+":
|
||||
sign_style = _ADDED_STYLE
|
||||
return sign_style, _ADDED_STYLE if ansi else _DIM_MUTED_STYLE
|
||||
return _MUTED_STYLE, _DIM_MUTED_STYLE
|
||||
|
||||
|
||||
def _build_diff_gutter(prefix_char: str, lineno: int | None, *, ansi: bool) -> Content:
|
||||
sign_style, lineno_style = _gutter_styles(prefix_char, ansi=ansi)
|
||||
lineno_str = f"{lineno:>4} " if lineno is not None else ""
|
||||
prefix = f"{prefix_char} "
|
||||
return Content.styled(lineno_str, lineno_style) + Content.styled(prefix, sign_style)
|
||||
|
||||
|
||||
def _build_diff_body(
|
||||
code: str,
|
||||
prefix_char: str,
|
||||
language: str,
|
||||
*,
|
||||
ansi: bool,
|
||||
theme: type[HighlightTheme],
|
||||
) -> Content:
|
||||
body = _highlight_line(code, language, theme)
|
||||
if prefix_char == "-" and ansi:
|
||||
body = body.stylize("dim")
|
||||
return body
|
||||
|
||||
|
||||
def _build_diff_line(
|
||||
code: str,
|
||||
prefix_char: str,
|
||||
|
|
@ -69,31 +107,23 @@ def _build_diff_line(
|
|||
ansi: bool,
|
||||
theme: type[HighlightTheme],
|
||||
) -> Content:
|
||||
# ANSI themes lack row backgrounds; the gutter carries the diff color instead.
|
||||
body = _highlight_line(code, language, theme)
|
||||
|
||||
if prefix_char == "-":
|
||||
if ansi:
|
||||
sign_style = lineno_style = f"bold {_REMOVED_STYLE}"
|
||||
body = body.stylize("dim")
|
||||
else:
|
||||
sign_style, lineno_style = _REMOVED_STYLE, _DIM_MUTED_STYLE
|
||||
elif prefix_char == "+":
|
||||
sign_style = _ADDED_STYLE
|
||||
lineno_style = _ADDED_STYLE if ansi else _DIM_MUTED_STYLE
|
||||
else:
|
||||
sign_style, lineno_style = _MUTED_STYLE, _DIM_MUTED_STYLE
|
||||
|
||||
lineno_str = f"{lineno:>4} " if lineno is not None else ""
|
||||
prefix = f"{prefix_char} "
|
||||
|
||||
return (
|
||||
Content.styled(lineno_str, lineno_style)
|
||||
+ Content.styled(prefix, sign_style)
|
||||
+ body
|
||||
return _build_diff_gutter(prefix_char, lineno, ansi=ansi) + _build_diff_body(
|
||||
code, prefix_char, language, ansi=ansi, theme=theme
|
||||
)
|
||||
|
||||
|
||||
class _DiffRow(Horizontal):
|
||||
def __init__(self, gutter: Content, body: Content, *, classes: str) -> None:
|
||||
self._gutter = gutter
|
||||
self._body = body
|
||||
self.plain = gutter.plain + body.plain
|
||||
super().__init__(classes=classes)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield NonSelectableStatic(self._gutter, classes="diff-gutter")
|
||||
yield Static(self._body, classes="diff-body")
|
||||
|
||||
|
||||
def render_edit_diff(
|
||||
old_string: str,
|
||||
new_string: str,
|
||||
|
|
@ -102,7 +132,7 @@ def render_edit_diff(
|
|||
*,
|
||||
ansi: bool,
|
||||
dark: bool,
|
||||
) -> list[Static]:
|
||||
) -> list[Widget]:
|
||||
theme = _pick_theme(ansi=ansi, dark=dark)
|
||||
diff_lines = list(
|
||||
difflib.unified_diff(
|
||||
|
|
@ -119,7 +149,7 @@ def render_edit_diff(
|
|||
|
||||
# replace_all repeats the same change at each match; render one block per
|
||||
# occurrence, anchored at its own line number, with a gap in between.
|
||||
widgets: list[Static] = []
|
||||
widgets: list[Widget] = []
|
||||
for index, start_line in enumerate(start_lines):
|
||||
if index > 0:
|
||||
widgets.append(NoMarkupStatic("⋯", classes="diff-gap"))
|
||||
|
|
@ -136,10 +166,10 @@ def _render_occurrence(
|
|||
*,
|
||||
ansi: bool,
|
||||
theme: type[HighlightTheme],
|
||||
) -> list[Static]:
|
||||
) -> list[Widget]:
|
||||
offset = (start_line - 1) if start_line else 0
|
||||
old_lineno = new_lineno = 0 # overwritten by the first @@ header
|
||||
widgets: list[Static] = []
|
||||
widgets: list[Widget] = []
|
||||
first_hunk = True
|
||||
|
||||
for line in diff_lines:
|
||||
|
|
@ -167,20 +197,18 @@ def _render_occurrence(
|
|||
old_lineno += 1
|
||||
new_lineno += 1
|
||||
|
||||
content = _build_diff_line(
|
||||
code,
|
||||
prefix_char,
|
||||
lineno if start_line else None,
|
||||
language,
|
||||
ansi=ansi,
|
||||
theme=theme,
|
||||
lineno_val = lineno if start_line else None
|
||||
gutter = _build_diff_gutter(prefix_char, lineno_val, ansi=ansi)
|
||||
body = _build_diff_body(code, prefix_char, language, ansi=ansi, theme=theme)
|
||||
|
||||
widgets.append(
|
||||
_DiffRow(gutter, body, classes=_DIFF_CSS_CLASS_BY_PREFIX[prefix_char])
|
||||
)
|
||||
widgets.append(Static(content, classes=_DIFF_CSS_CLASS_BY_PREFIX[prefix_char]))
|
||||
|
||||
return widgets
|
||||
|
||||
|
||||
def diff_border_colors(rows: Iterable[Static]) -> dict[int, str]:
|
||||
def diff_border_colors(rows: Iterable[Widget]) -> dict[int, str]:
|
||||
return {
|
||||
i: color
|
||||
for i, row in enumerate(rows)
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ class EditResultWidget(ToolResultWidget[EditResult]):
|
|||
if not self.result:
|
||||
yield from self._footer()
|
||||
return
|
||||
rows: list[Static] = [
|
||||
rows: list[Widget] = [
|
||||
NoMarkupStatic(f"⚠ {w}", classes="tool-result-warning")
|
||||
for w in self.warnings
|
||||
]
|
||||
|
|
|
|||
|
|
@ -785,6 +785,15 @@ class VibeConfig(BaseSettings):
|
|||
" is set. Supports glob patterns and regex with 're:' prefix."
|
||||
),
|
||||
)
|
||||
experimental_enable_registry_skills: bool = Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Experimental: pull workspace skills from the Mistral AI Registry"
|
||||
" (api.mistral.ai) and make them available alongside local skills."
|
||||
" Requires a Mistral provider and API key. Local and builtin skills take"
|
||||
" precedence on name collision."
|
||||
),
|
||||
)
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="VIBE_", case_sensitive=False, extra="ignore"
|
||||
|
|
|
|||
|
|
@ -177,6 +177,15 @@ class VibeConfigSchema(ConfigSchema):
|
|||
" is set. Supports glob patterns and regex with 're:' prefix."
|
||||
),
|
||||
)
|
||||
experimental_enable_registry_skills: Annotated[bool, WithReplaceMerge()] = Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Experimental: pull workspace skills from the Mistral AI Registry"
|
||||
" (api.mistral.ai) and make them available alongside local skills."
|
||||
" Requires a Mistral provider and API key. Local and builtin skills take"
|
||||
" precedence on name collision."
|
||||
),
|
||||
)
|
||||
|
||||
# Internal
|
||||
vibe_code_enabled: Annotated[bool, WithReplaceMerge()] = True
|
||||
|
|
|
|||
|
|
@ -1,11 +1,39 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum, auto
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class SkillSource(StrEnum):
|
||||
BUILTIN = auto()
|
||||
LOCAL = auto()
|
||||
REGISTRY = auto()
|
||||
|
||||
|
||||
class SkillScope(StrEnum):
|
||||
BUILTIN = auto()
|
||||
GLOBAL = auto()
|
||||
PROJECT = auto()
|
||||
|
||||
|
||||
# The registry's reserved alias that always resolves to the newest version,
|
||||
# server-side (see ai-registry versioning.RESERVED_ALIAS). A pin set to this
|
||||
# alias auto-resolves to latest and never reports an "update available".
|
||||
REGISTRY_LATEST_ALIAS = "latest"
|
||||
|
||||
|
||||
class RegistryRef(BaseModel):
|
||||
skill_id: str
|
||||
# The concrete, materialized version currently on disk (for display/loading).
|
||||
version: int
|
||||
# The pinned alias (e.g. "latest") when this is an alias pin; None when the
|
||||
# pin is frozen to a specific version number.
|
||||
alias: str | None = None
|
||||
|
||||
|
||||
class SkillMetadata(BaseModel):
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
|
@ -72,6 +100,9 @@ class SkillInfo(BaseModel):
|
|||
user_invocable: bool = True
|
||||
skill_path: Path | None = None
|
||||
prompt: str
|
||||
source: SkillSource = SkillSource.LOCAL
|
||||
scope: SkillScope = SkillScope.GLOBAL
|
||||
registry: RegistryRef | None = None
|
||||
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
|
||||
|
|
@ -83,7 +114,14 @@ class SkillInfo(BaseModel):
|
|||
|
||||
@classmethod
|
||||
def from_metadata(
|
||||
cls, meta: SkillMetadata, skill_path: Path, prompt: str
|
||||
cls,
|
||||
meta: SkillMetadata,
|
||||
skill_path: Path,
|
||||
prompt: str,
|
||||
*,
|
||||
source: SkillSource = SkillSource.LOCAL,
|
||||
scope: SkillScope = SkillScope.GLOBAL,
|
||||
registry: RegistryRef | None = None,
|
||||
) -> SkillInfo:
|
||||
return cls(
|
||||
name=meta.name,
|
||||
|
|
@ -95,6 +133,9 @@ class SkillInfo(BaseModel):
|
|||
user_invocable=meta.user_invocable,
|
||||
skill_path=skill_path.resolve(),
|
||||
prompt=prompt,
|
||||
source=source,
|
||||
scope=scope,
|
||||
registry=registry,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
0
vibe/core/skills/registry/__init__.py
Normal file
0
vibe/core/skills/registry/__init__.py
Normal file
135
vibe/core/skills/registry/_client.py
Normal file
135
vibe/core/skills/registry/_client.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from vibe.core.skills.registry.models import (
|
||||
ListSkillsResponse,
|
||||
ListVersionsResponse,
|
||||
RegistrySkillItem,
|
||||
SkillVersionInfo,
|
||||
)
|
||||
from vibe.core.utils.http import build_ssl_context
|
||||
|
||||
_MAX_PAGES = 50
|
||||
|
||||
|
||||
class RegistrySkillsError(Exception):
|
||||
def __init__(self, reason: str) -> None:
|
||||
super().__init__(reason)
|
||||
self.reason = reason
|
||||
|
||||
|
||||
def _parse[M: BaseModel](model: type[M], payload: Any, what: str) -> M:
|
||||
"""Validate a registry payload, normalizing failures to RegistrySkillsError.
|
||||
|
||||
Keeps every response-parsing path consistent: a malformed 200 surfaces as a
|
||||
registry error, not a bare ValidationError that callers wouldn't catch.
|
||||
"""
|
||||
try:
|
||||
return model.model_validate(payload)
|
||||
except ValidationError as exc:
|
||||
raise RegistrySkillsError(f"invalid {what} response") from exc
|
||||
|
||||
|
||||
class RegistrySkillsClient:
|
||||
_CATALOG_FIELDS = (
|
||||
"skillId,skill.skillName,skill.skillDescription,attributes,metadata,version"
|
||||
)
|
||||
|
||||
def __init__(self, base_url: str, api_key: str, *, timeout: float = 30.0) -> None:
|
||||
self._skills_url = f"{base_url.rstrip('/')}/skills"
|
||||
self._api_key = api_key
|
||||
self._timeout = timeout
|
||||
self._client: httpx.AsyncClient | None = None
|
||||
|
||||
async def __aenter__(self) -> RegistrySkillsClient:
|
||||
self._client = httpx.AsyncClient(
|
||||
timeout=self._timeout,
|
||||
headers={
|
||||
"Authorization": f"Bearer {self._api_key}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
verify=build_ssl_context(),
|
||||
)
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
if self._client is not None:
|
||||
await self._client.aclose()
|
||||
self._client = None
|
||||
|
||||
async def list_catalog(self, *, page_size: int) -> list[RegistrySkillItem]:
|
||||
return await self._list(page_size=page_size, fields=self._CATALOG_FIELDS)
|
||||
|
||||
async def list_versions(self, skill_id: str) -> list[SkillVersionInfo]:
|
||||
payload = await self._get_json(f"{self._skills_url}/{skill_id}/versions", {})
|
||||
parsed = _parse(ListVersionsResponse, payload, "versions")
|
||||
infos = [row.to_info() for row in parsed.items]
|
||||
infos.sort(key=lambda v: v.version, reverse=True)
|
||||
return infos
|
||||
|
||||
async def get_skill(
|
||||
self, skill_id: str, *, version: int | None = None, alias: str | None = None
|
||||
) -> RegistrySkillItem:
|
||||
# version_selector is a oneof: version XOR alias (omit both = latest).
|
||||
params: dict[str, Any] = {}
|
||||
if version is not None:
|
||||
params["version"] = version
|
||||
elif alias is not None:
|
||||
params["alias"] = alias
|
||||
url = f"{self._skills_url}/{skill_id}"
|
||||
payload = await self._get_json(url, params)
|
||||
return _parse(RegistrySkillItem, payload, "skill")
|
||||
|
||||
async def _list(
|
||||
self, *, page_size: int, fields: str | None
|
||||
) -> list[RegistrySkillItem]:
|
||||
items: list[RegistrySkillItem] = []
|
||||
page_token = ""
|
||||
for _ in range(_MAX_PAGES):
|
||||
params: dict[str, Any] = {"pageSize": page_size}
|
||||
if fields:
|
||||
params["fields"] = fields
|
||||
if page_token:
|
||||
params["pageToken"] = page_token
|
||||
page = _parse(
|
||||
ListSkillsResponse,
|
||||
await self._get_json(self._skills_url, params),
|
||||
"catalog",
|
||||
)
|
||||
items.extend(page.data)
|
||||
if not page.next_page_token:
|
||||
return items
|
||||
page_token = page.next_page_token
|
||||
# Cap reached with pages still remaining: fail loudly rather than return
|
||||
# a silently-truncated catalog that a caller would treat as complete.
|
||||
raise RegistrySkillsError("catalog exceeds the maximum number of pages")
|
||||
|
||||
async def _get_json(self, url: str, params: dict[str, Any]) -> Any:
|
||||
if self._client is None:
|
||||
raise RegistrySkillsError("client used outside of an async context")
|
||||
try:
|
||||
response = await self._client.get(url, params=params)
|
||||
except httpx.RequestError as exc:
|
||||
raise RegistrySkillsError(f"request failed: {exc}") from exc
|
||||
|
||||
if response.status_code in {httpx.codes.UNAUTHORIZED, httpx.codes.FORBIDDEN}:
|
||||
raise RegistrySkillsError(f"unauthorized ({response.status_code})")
|
||||
if response.status_code == httpx.codes.NOT_FOUND:
|
||||
raise RegistrySkillsError("not found (404)")
|
||||
if not response.is_success:
|
||||
raise RegistrySkillsError(f"unexpected status {response.status_code}")
|
||||
|
||||
try:
|
||||
return response.json()
|
||||
except ValueError as exc:
|
||||
raise RegistrySkillsError("response was not valid JSON") from exc
|
||||
187
vibe/core/skills/registry/models.py
Normal file
187
vibe/core/skills/registry/models.py
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import re
|
||||
|
||||
from pydantic import AliasChoices, BaseModel, ConfigDict, Field
|
||||
|
||||
_NON_NAME_CHARS = re.compile(r"[^a-z0-9]+")
|
||||
# Mirror SkillMetadata.name's max_length so a sanitized name always validates.
|
||||
_MAX_NAME_LEN = 64
|
||||
|
||||
|
||||
def sanitize_skill_name(raw: str) -> str | None:
|
||||
collapsed = _NON_NAME_CHARS.sub("-", raw.strip().lower()).strip("-")
|
||||
collapsed = collapsed[:_MAX_NAME_LEN].rstrip("-")
|
||||
return collapsed or None
|
||||
|
||||
|
||||
class RegistryAssetContent(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||
|
||||
text_content: str | None = Field(
|
||||
default=None, validation_alias=AliasChoices("textContent", "text_content")
|
||||
)
|
||||
raw_content: str | None = Field(
|
||||
default=None, validation_alias=AliasChoices("rawContent", "raw_content")
|
||||
)
|
||||
is_executable: bool = Field(
|
||||
default=False, validation_alias=AliasChoices("isExecutable", "is_executable")
|
||||
)
|
||||
|
||||
def to_bytes(self) -> bytes | None:
|
||||
if self.text_content is not None:
|
||||
return self.text_content.encode("utf-8")
|
||||
if self.raw_content is not None:
|
||||
try:
|
||||
return base64.b64decode(self.raw_content, validate=True)
|
||||
except (binascii.Error, ValueError):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
class RegistrySkillPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||
|
||||
skill_name: str = Field(
|
||||
default="", validation_alias=AliasChoices("skillName", "skill_name")
|
||||
)
|
||||
skill_description: str = Field(
|
||||
default="",
|
||||
validation_alias=AliasChoices("skillDescription", "skill_description"),
|
||||
)
|
||||
skill_body: str = Field(
|
||||
default="", validation_alias=AliasChoices("skillBody", "skill_body")
|
||||
)
|
||||
skill_assets: dict[str, RegistryAssetContent] = Field(
|
||||
default_factory=dict,
|
||||
validation_alias=AliasChoices("skillAssets", "skill_assets"),
|
||||
)
|
||||
|
||||
|
||||
class RegistryAttributes(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||
|
||||
title: str = ""
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class RegistryMetadata(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||
|
||||
name: str = ""
|
||||
latest_version: int = Field(
|
||||
default=0, validation_alias=AliasChoices("latestVersion", "latest_version")
|
||||
)
|
||||
sharing_scope: str = Field(
|
||||
default="", validation_alias=AliasChoices("sharingScope", "sharing_scope")
|
||||
)
|
||||
created_at: str = Field(
|
||||
default="", validation_alias=AliasChoices("createdAt", "created_at")
|
||||
)
|
||||
last_modified_at: str = Field(
|
||||
default="", validation_alias=AliasChoices("lastModifiedAt", "last_modified_at")
|
||||
)
|
||||
created_by: str = Field(
|
||||
default="", validation_alias=AliasChoices("createdBy", "created_by")
|
||||
)
|
||||
|
||||
|
||||
class RegistryVersionMetadata(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||
|
||||
created_at: str = Field(
|
||||
default="", validation_alias=AliasChoices("createdAt", "created_at")
|
||||
)
|
||||
|
||||
|
||||
class RegistryVersionAttributes(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||
|
||||
notes: str = ""
|
||||
aliases: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class RegistrySkillItem(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||
|
||||
skill_id: str = Field(
|
||||
default="", validation_alias=AliasChoices("skillId", "skill_id")
|
||||
)
|
||||
skill: RegistrySkillPayload = Field(default_factory=RegistrySkillPayload)
|
||||
attributes: RegistryAttributes = Field(default_factory=RegistryAttributes)
|
||||
metadata: RegistryMetadata = Field(default_factory=RegistryMetadata)
|
||||
version: int = 0
|
||||
version_metadata: RegistryVersionMetadata = Field(
|
||||
default_factory=RegistryVersionMetadata,
|
||||
validation_alias=AliasChoices("versionMetadata", "version_metadata"),
|
||||
)
|
||||
version_attributes: RegistryVersionAttributes = Field(
|
||||
default_factory=RegistryVersionAttributes,
|
||||
validation_alias=AliasChoices("versionAttributes", "version_attributes"),
|
||||
)
|
||||
|
||||
@property
|
||||
def resolved_name(self) -> str | None:
|
||||
for candidate in (
|
||||
self.metadata.name,
|
||||
self.skill.skill_name,
|
||||
self.attributes.title,
|
||||
):
|
||||
if (name := sanitize_skill_name(candidate)) is not None:
|
||||
return name
|
||||
if self.skill_id:
|
||||
# Run the id-based fallback through the same sanitizer so it obeys
|
||||
# the charset / length / hyphen rules SkillMetadata.name enforces.
|
||||
# The full id (not a prefix) keeps unnamed skills collision-free.
|
||||
return sanitize_skill_name(f"skill-{self.skill_id.replace('-', '')}")
|
||||
return None
|
||||
|
||||
@property
|
||||
def resolved_description(self) -> str:
|
||||
if self.skill.skill_description.strip():
|
||||
return self.skill.skill_description.strip()
|
||||
if self.attributes.description and self.attributes.description.strip():
|
||||
return self.attributes.description.strip()
|
||||
return ""
|
||||
|
||||
|
||||
class ListSkillsResponse(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||
|
||||
data: list[RegistrySkillItem] = Field(default_factory=list)
|
||||
next_page_token: str = Field(
|
||||
default="", validation_alias=AliasChoices("nextPageToken", "next_page_token")
|
||||
)
|
||||
|
||||
|
||||
class SkillVersionInfo(BaseModel):
|
||||
"""One row from the versions endpoint: a concrete version and its author
|
||||
aliases (e.g. 'main', 'stable'). The reserved 'latest' alias is dynamic and
|
||||
not part of this list.
|
||||
"""
|
||||
|
||||
version: int
|
||||
aliases: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class RegistryVersionRow(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||
|
||||
version: int
|
||||
version_attributes: RegistryVersionAttributes = Field(
|
||||
default_factory=RegistryVersionAttributes,
|
||||
validation_alias=AliasChoices("versionAttributes", "version_attributes"),
|
||||
)
|
||||
|
||||
def to_info(self) -> SkillVersionInfo:
|
||||
return SkillVersionInfo(
|
||||
version=self.version, aliases=list(self.version_attributes.aliases)
|
||||
)
|
||||
|
||||
|
||||
class ListVersionsResponse(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
||||
|
||||
items: list[RegistryVersionRow] = Field(default_factory=list)
|
||||
|
|
@ -60,7 +60,7 @@ class TelemetryRequestMetadata(TelemetryBaseMetadata):
|
|||
|
||||
|
||||
TeleportFailureStage = Literal[
|
||||
"no_history", "git_check", "push", "workflow_start", "cancelled"
|
||||
"no_history", "ineligible", "git_check", "push", "workflow_start", "cancelled"
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue