Co-authored-by: Hdandria <henri.dandria@mistral.ai>
Co-authored-by: Liam Lyons <65613603+lyons-liam@users.noreply.github.com>
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: allansimon-mistral <allan.simon@ext.mistral.ai>
Co-authored-by: angelapopopo <angele.lenglemetz@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Clément Drouin 2026-06-15 17:36:21 +02:00 committed by GitHub
parent cafb6d4147
commit c2cb612ac1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 2704 additions and 197 deletions

View file

@ -2,7 +2,7 @@ from __future__ import annotations
from pathlib import Path
from typing import cast
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from acp.schema import TextContentBlock, ToolCallProgress, ToolCallStart
import pytest
@ -11,8 +11,10 @@ from tests.conftest import build_test_vibe_config, make_test_models
from tests.stubs.fake_backend import FakeBackend
from tests.stubs.fake_client import FakeClient
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.core.agent_loop import AgentLoop
from vibe.acp.exceptions import CompactionError
from vibe.core.agent_loop import AgentLoop, CompactionFailedError
from vibe.core.session.session_id import shorten_session_id
from vibe.core.types import LLMMessage, Role
@pytest.fixture
@ -87,3 +89,31 @@ class TestCompactEventHandling:
assert (
shorten_session_id(session.agent_loop.session_id) in compact_end_text.text
)
@pytest.mark.asyncio
async def test_slash_compact_maps_compaction_failure(
self, acp_agent_loop: VibeAcpAgentLoop
) -> None:
"""A /compact failure surfaces as a mapped CompactionError, not a raw
core exception same as the auto-compaction path.
"""
session_response = await acp_agent_loop.new_session(
cwd=str(Path.cwd()), mcp_servers=[]
)
session = acp_agent_loop.sessions[session_response.session_id]
# Need >1 message so /compact does not early-return.
session.agent_loop.messages.append(LLMMessage(role=Role.user, content="hello"))
with patch.object(
session.agent_loop,
"compact",
AsyncMock(side_effect=CompactionFailedError("tool_call")),
):
with pytest.raises(CompactionError) as exc_info:
await acp_agent_loop.prompt(
prompt=[TextContentBlock(type="text", text="/compact")],
session_id=session_response.session_id,
)
assert exc_info.value.code == -31006
assert exc_info.value.data == {"reason": "tool_call"}

View file

@ -1,5 +1,9 @@
from __future__ import annotations
from types import SimpleNamespace
from typing import Any, cast
from unittest.mock import AsyncMock
from acp import PROTOCOL_VERSION
from acp.schema import (
AgentCapabilities,
@ -13,6 +17,7 @@ from acp.schema import (
)
import pytest
from tests.conftest import build_test_vibe_config
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
from vibe.core.config import ProviderConfig
from vibe.core.types import Backend
@ -67,7 +72,7 @@ class TestACPInitialize:
),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.15.0"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.16.0"
)
assert response.auth_methods is not None
@ -77,6 +82,72 @@ class TestACPInitialize:
assert auth_method.name == BROWSER_AUTH_NAME
assert auth_method.description == BROWSER_AUTH_DESCRIPTION
@pytest.mark.asyncio
async def test_load_config_uses_client_info_title_for_vibe_code_project_name(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
config = build_test_vibe_config()
monkeypatch.setattr(
"vibe.acp.acp_agent_loop.VibeConfig.load", lambda *args, **kwargs: config
)
acp_agent_loop = build_acp_agent_loop()
await acp_agent_loop.initialize(
protocol_version=PROTOCOL_VERSION,
client_info=Implementation(name="zed", title="Zed", version="0.999.0"),
)
assert acp_agent_loop._load_config().vibe_code_project_name == "Zed"
@pytest.mark.asyncio
async def test_load_config_preserves_explicit_vibe_code_project_name(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
config = build_test_vibe_config(vibe_code_project_name="Configured Project")
monkeypatch.setattr(
"vibe.acp.acp_agent_loop.VibeConfig.load", lambda *args, **kwargs: config
)
acp_agent_loop = build_acp_agent_loop()
await acp_agent_loop.initialize(
protocol_version=PROTOCOL_VERSION,
client_info=Implementation(name="zed", title="Zed", version="0.999.0"),
)
assert (
acp_agent_loop._load_config().vibe_code_project_name == "Configured Project"
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"reload_method_name", ["_reload_config", "_reload_session_config"]
)
async def test_reload_config_uses_client_info_title_for_vibe_code_project_name(
self, monkeypatch: pytest.MonkeyPatch, reload_method_name: str
) -> None:
config = build_test_vibe_config()
monkeypatch.setattr(
"vibe.acp.acp_agent_loop.VibeConfig.load", lambda *args, **kwargs: config
)
acp_agent_loop = build_acp_agent_loop()
agent_loop = SimpleNamespace(
config=SimpleNamespace(tool_paths=[]),
reload_with_initial_messages=AsyncMock(),
)
session = cast(Any, SimpleNamespace(agent_loop=agent_loop))
await acp_agent_loop.initialize(
protocol_version=PROTOCOL_VERSION,
client_info=Implementation(name="zed", title="Zed", version="0.999.0"),
)
await getattr(acp_agent_loop, reload_method_name)(session)
agent_loop.reload_with_initial_messages.assert_awaited_once()
reloaded_config = agent_loop.reload_with_initial_messages.await_args.kwargs[
"base_config"
]
assert reloaded_config.vibe_code_project_name == "Zed"
@pytest.mark.asyncio
async def test_initialize_with_terminal_auth(
self, unauthenticated_env: None
@ -101,7 +172,7 @@ class TestACPInitialize:
),
)
assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.15.0"
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.16.0"
)
assert response.auth_methods is not None

View file

@ -221,6 +221,56 @@ def test_enter_on_slash_command_with_args_submits_with_head_only_replacement() -
assert view.replacements == [Replacement(0, 8, "/compact")]
def test_on_text_change_matches_substring_not_just_prefix() -> None:
controller, view = make_controller()
controller.on_text_changed("/path", cursor_index=5)
suggestions, _ = view.suggestion_events[-1]
assert any(s.alias == "/logpath" for s in suggestions)
def test_on_text_change_matches_middle_segment_of_hyphenated_command() -> None:
commands = [
("/foo-bar-skill", "A skill"),
("/baz-bar-other", "Another skill"),
("/unrelated", "No match"),
]
completer = CommandCompleter(lambda: commands)
view = StubView()
controller = SlashCommandController(completer, view)
controller.on_text_changed("/bar", cursor_index=4)
suggestions, _ = view.suggestion_events[-1]
aliases = [s.alias for s in suggestions]
assert "/foo-bar-skill" in aliases
assert "/baz-bar-other" in aliases
assert "/unrelated" not in aliases
def test_on_text_change_fuzzy_matches_scattered_characters() -> None:
controller, view = make_controller()
controller.on_text_changed("/sm", cursor_index=3)
suggestions, _ = view.suggestion_events[-1]
assert any(s.alias == "/summarize" for s in suggestions)
def test_on_text_change_fuzzy_ranks_prefix_matches_higher() -> None:
commands = [("/zoo-config", "Zoo config"), ("/config", "Main config")]
completer = CommandCompleter(lambda: commands)
view = StubView()
controller = SlashCommandController(completer, view)
controller.on_text_changed("/config", cursor_index=7)
suggestions, _ = view.suggestion_events[-1]
aliases = [s.alias for s in suggestions]
assert aliases.index("/config") < aliases.index("/zoo-config")
def test_callable_entries_reflects_enabled_disabled_skills() -> None:
"""Test that skill enable/disable changes are reflected in completions.

View file

@ -205,6 +205,59 @@ class TestBackend:
tool_call.index == expected_result["tool_calls"][i]["index"]
)
@pytest.mark.asyncio
async def test_backend_complete_streaming_keeps_unicode_line_breaks(self):
content = "first\u2028second\u0085third"
chunk = json.dumps(
{
"id": "fake_id_1234",
"object": "chat.completion.chunk",
"created": 1234567890,
"model": "model_name",
"choices": [
{
"index": 0,
"delta": {"role": "assistant", "content": content},
"finish_reason": None,
}
],
},
ensure_ascii=False,
).encode()
with respx.mock(base_url="https://api.fireworks.ai") as mock_api:
mock_api.post("/v1/chat/completions").mock(
return_value=httpx.Response(
status_code=200,
stream=httpx.ByteStream(
stream=b"data: " + chunk + b"\n\ndata: [DONE]\n\n"
),
headers={"Content-Type": "text/event-stream"},
)
)
provider = ProviderConfig(
name="provider_name",
api_base="https://api.fireworks.ai/v1",
api_key_env_var="API_KEY",
)
backend = GenericBackend(provider=provider)
model = ModelConfig(
name="model_name", provider="provider_name", alias="model_alias"
)
results: list[LLMChunk] = []
async for result in backend.complete_streaming(
model=model,
messages=[LLMMessage(role=Role.user, content="hi")],
temperature=0.2,
tools=None,
max_tokens=None,
tool_choice=None,
extra_headers=None,
):
results.append(result)
assert [result.message.content for result in results] == [content]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"base_url,backend_class,response",
@ -592,6 +645,52 @@ class TestMistralBackendReasoningEffort:
assert call_kwargs["reasoning_effort"] == expected_effort
assert call_kwargs["temperature"] == expected_temperature
@pytest.mark.asyncio
async def test_complete_omits_reasoning_content_when_thinking_off(
self, backend: MistralBackend
) -> None:
model = ModelConfig(
name="devstral-small-latest",
provider="mistral",
alias="devstral-small",
thinking="off",
)
messages = [
LLMMessage(role=Role.user, content="Hi"),
LLMMessage(
role=Role.assistant,
content="Answer",
reasoning_content="Hidden reasoning",
),
]
with patch.object(backend, "_get_client") as mock_get_client:
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "ok"
mock_response.choices[0].message.tool_calls = None
mock_response.usage.prompt_tokens = 10
mock_response.usage.completion_tokens = 5
mock_client.chat.complete_async = AsyncMock(return_value=mock_response)
mock_get_client.return_value = mock_client
await backend.complete(
model=model,
messages=messages,
temperature=0.2,
tools=None,
max_tokens=None,
tool_choice=None,
extra_headers=None,
)
call_kwargs = mock_client.chat.complete_async.call_args.kwargs
sent_messages = call_kwargs["messages"]
assert len(sent_messages) == 2
assert isinstance(sent_messages[1], AssistantMessage)
assert sent_messages[1].content == "Answer"
class TestBuildHttpErrorBodyReading:
_MESSAGES: ClassVar[list[LLMMessage]] = [LLMMessage(role=Role.user, content="hi")]

View file

@ -195,8 +195,20 @@ def test_resolve_api_key_for_plan_with_missing_env_var() -> None:
),
"### Unlock more with Vibe - [Upgrade to Vibe Pro](https://chat.mistral.ai/code/extensions?focus=key)",
),
(
PlanInfo(
plan_type=WhoAmIPlanType.CHAT,
plan_name="FREE",
prompt_switching_to_pro_plan=False,
),
"### Unlock more with Vibe - [Upgrade to Vibe Pro](https://chat.mistral.ai/code/extensions?focus=key)",
),
],
ids=[
"switch-to-vibe-pro-key",
"upgrade-api-to-vibe-pro",
"upgrade-free-vibe-to-pro",
],
ids=["switch-to-vibe-pro-key", "upgrade-to-vibe-pro"],
)
def test_plan_offer_cta_routes_users_to_vibe_api_key_extensions(
plan_info: PlanInfo, expected_cta: str
@ -236,6 +248,22 @@ def test_plan_offer_cta_uses_configured_vibe_url() -> None:
),
False,
),
(
WhoAmIResponse(
plan_type=WhoAmIPlanType.CHAT,
plan_name="FREE",
prompt_switching_to_pro_plan=False,
),
False,
),
(
WhoAmIResponse(
plan_type=WhoAmIPlanType.CHAT,
plan_name="UNKNOWN",
prompt_switching_to_pro_plan=False,
),
False,
),
(
WhoAmIResponse(
plan_type=WhoAmIPlanType.API,
@ -256,6 +284,8 @@ def test_plan_offer_cta_uses_configured_vibe_url() -> None:
ids=[
"chat-plan-is-eligible",
"chat-plan-requiring-key-switch-is-ineligible",
"free-vibe-plan-is-ineligible",
"unknown-chat-plan-is-ineligible",
"api-plan-is-ineligible",
"mistral-code-enterprise-is-ineligible",
],
@ -270,14 +300,26 @@ def test_teleport_eligibility_depends_on_chat_plan_and_current_key(
("payload", "expected_title"),
[
(PlanInfo(plan_type=WhoAmIPlanType.API, plan_name="FREE"), "Free"),
(PlanInfo(plan_type=WhoAmIPlanType.CHAT, plan_name="FREE"), "Free"),
(
PlanInfo(plan_type=WhoAmIPlanType.CHAT, plan_name="INDIVIDUAL"),
"[Subscription] Pro",
),
(PlanInfo(plan_type=WhoAmIPlanType.CHAT, plan_name="UNKNOWN"), None),
(
PlanInfo(plan_type=WhoAmIPlanType.API, plan_name="Scale plan"),
"[API] Scale plan",
),
],
ids=["free-api-plan", "paid-api-plan"],
ids=[
"free-api-plan",
"free-vibe-plan",
"chat-pro-plan",
"unknown-chat-plan",
"paid-api-plan",
],
)
def test_plan_title_uses_current_api_plan_labels(
def test_plan_title_uses_current_plan_labels(
payload: PlanInfo, expected_title: str
) -> None:
assert plan_title(payload) == expected_title

View file

@ -37,7 +37,7 @@ async def test_mouse_up_respects_autocopy_config_enabled() -> None:
with patch("vibe.cli.textual_ui.app.copy_selection_to_clipboard") as mock_copy:
async with app.run_test() as pilot:
await pilot.click()
mock_copy.assert_called_once_with(app, show_toast=True)
mock_copy.assert_called_once_with(app, show_toast=False)
@pytest.mark.asyncio

View file

@ -0,0 +1,74 @@
from __future__ import annotations
import os
from unittest.mock import patch
import pytest
from vibe.cli.terminal_detect import Terminal, detect_terminal
def _detect_with(env: dict[str, str]) -> Terminal:
with patch.dict(os.environ, env, clear=True):
return detect_terminal()
def test_detects_vscode() -> None:
assert _detect_with({"TERM_PROGRAM": "vscode"}) is Terminal.VSCODE
def test_detects_vscode_insiders() -> None:
assert (
_detect_with({"TERM_PROGRAM": "vscode", "TERM_PROGRAM_VERSION": "1.2-insider"})
is Terminal.VSCODE_INSIDERS
)
def test_detects_cursor_from_vscode_environment() -> None:
assert (
_detect_with({
"TERM_PROGRAM": "vscode",
"VSCODE_IPC_HOOK_CLI": "/Applications/Cursor.app/hook",
})
is Terminal.CURSOR
)
@pytest.mark.parametrize(
("term_program", "terminal"),
[
("iterm.app", Terminal.ITERM2),
("wezterm", Terminal.WEZTERM),
("ghostty", Terminal.GHOSTTY),
("alacritty", Terminal.ALACRITTY),
("kitty", Terminal.KITTY),
("hyper", Terminal.HYPER),
],
)
def test_detects_term_program_mapping(term_program: str, terminal: Terminal) -> None:
assert _detect_with({"TERM_PROGRAM": term_program}) is terminal
@pytest.mark.parametrize(
("env_var", "terminal"),
[
("WEZTERM_PANE", Terminal.WEZTERM),
("GHOSTTY_RESOURCES_DIR", Terminal.GHOSTTY),
("KITTY_WINDOW_ID", Terminal.KITTY),
("ALACRITTY_SOCKET", Terminal.ALACRITTY),
("ALACRITTY_LOG", Terminal.ALACRITTY),
("WT_SESSION", Terminal.WINDOWS_TERMINAL),
],
)
def test_detects_environment_marker_fallback(env_var: str, terminal: Terminal) -> None:
assert _detect_with({env_var: "1"}) is terminal
def test_detects_jetbrains_environment_fallback() -> None:
assert (
_detect_with({"TERMINAL_EMULATOR": "JetBrains-JediTerm"}) is Terminal.JETBRAINS
)
def test_returns_unknown_without_markers() -> None:
assert _detect_with({}) is Terminal.UNKNOWN

View file

@ -0,0 +1,181 @@
from __future__ import annotations
from textual.content import Content
from textual.highlight import HighlightTheme
from textual.widgets import Static
from vibe.cli.textual_ui.widgets.diff_rendering import (
_build_diff_line,
diff_border_colors,
language_for_path,
render_edit_diff,
)
def _build(
code: str, prefix: str, lineno: int | None, language: str, *, ansi: bool
) -> Content:
return _build_diff_line(
code, prefix, lineno, language, ansi=ansi, theme=HighlightTheme
)
def _render(*args, **kwargs):
kwargs.setdefault("dark", True)
return render_edit_diff(*args, **kwargs)
def _render_with_colors(*args, **kwargs):
widgets = _render(*args, **kwargs)
return widgets, diff_border_colors(widgets)
def _plain(widget: Static) -> str:
visual = widget.render()
return visual.plain if isinstance(visual, Content) else str(visual)
def _styles_at(content: Content, index: int) -> list[str]:
return [str(s.style) for s in content.spans if s.start <= index < s.end]
class TestLanguageForPath:
def test_extension(self) -> None:
assert language_for_path("/src/main.py") == "py"
def test_no_extension_falls_back_to_text(self) -> None:
assert language_for_path("/src/Makefile") == "text"
class TestBuildDiffLine:
def test_line_number_in_content(self) -> None:
content = _build("x = 1", "+", 42, "py", ansi=False)
assert "42" in content.plain
def test_no_line_number(self) -> None:
content = _build("x = 1", "+", None, "py", ansi=False)
assert content.plain.startswith("+ ")
def test_sign_is_colored_in_both_modes(self) -> None:
for ansi in (False, True):
content = _build("x = 1", "-", 10, "py", ansi=ansi)
# " 10 - x = 1": the gutter is 5 chars, so "-" sits at index 5.
assert any("$text-error" in s for s in _styles_at(content, 5))
def test_line_number_dimmed_uncolored_in_non_ansi(self) -> None:
content = _build("x = 1", "-", 10, "py", ansi=False)
styles = _styles_at(content, 0)
assert any("dim" in s and "$text-muted" in s for s in styles)
assert all("$text-error" not in s for s in styles)
def test_added_line_number_colored_undimmed_in_ansi(self) -> None:
content = _build("x = 1", "+", 10, "py", ansi=True)
styles = _styles_at(content, 0)
assert "$text-success" in styles
assert all("dim" not in s for s in styles)
def test_removed_line_number_and_sign_bright_in_ansi(self) -> None:
content = _build("x = 1", "-", 10, "py", ansi=True)
lineno_styles = _styles_at(content, 0)
sign_styles = _styles_at(content, 5)
assert any("bold" in s and "$text-error" in s for s in lineno_styles)
assert any("bold" in s and "$text-error" in s for s in sign_styles)
assert all("dim" not in s for s in lineno_styles)
assert all("dim" not in s for s in sign_styles)
def test_line_number_dimmed_for_unchanged_rows_in_ansi(self) -> None:
content = _build("x = 1", " ", 10, "py", ansi=True)
styles = _styles_at(content, 0)
assert any("dim" in s and "$text-muted" in s for s in styles)
def test_removed_body_dimmed_in_ansi(self) -> None:
content = _build("foo", "-", 10, "py", ansi=True)
# body starts after " 10 - " (7 chars)
assert any("dim" in s for s in _styles_at(content, 7))
def test_removed_body_not_dimmed_in_non_ansi(self) -> None:
content = _build("foo", "-", 10, "py", ansi=False)
assert all("dim" not in s for s in _styles_at(content, 7))
class TestRenderEditDiff:
def test_simple_replacement(self) -> None:
widgets = _render("x = 100", "x = 200", "py", 1, ansi=False)
classes = [w.classes for w in widgets]
assert any("diff-removed" in c for c in classes)
assert any("diff-added" in c for c in classes)
def test_no_hunk_header_rendered(self) -> None:
widgets = _render("a\nb\nc\nd\ne\nf", "a\nb\nX\nd\ne\nf", "py", 1, ansi=False)
for w in widgets:
assert not _plain(w).startswith("@@")
def test_gap_separator_between_hunks(self) -> None:
search = "A\nB\nC\nD\nE\nF\nG\nH"
replace = "Z\nB\nC\nD\nE\nF\nG\nY"
widgets = _render(search, replace, "py", 1, ansi=False)
assert any("diff-gap" in w.classes for w in widgets)
def test_no_leading_gap_for_single_hunk(self) -> None:
widgets = _render("x = 100", "x = 200", "py", 1, ansi=False)
assert all("diff-gap" not in w.classes for w in widgets)
def test_pure_insertion(self) -> None:
widgets = _render("x = 1", "x = 1\ny = 2", "py", 1, ansi=False)
assert any("diff-added" in w.classes for w in widgets)
def test_pure_deletion(self) -> None:
widgets = _render("x = 1\ny = 2", "x = 1", "py", 1, ansi=False)
assert any("diff-removed" in w.classes for w in widgets)
def test_line_numbers_use_start_line_offset(self) -> None:
widgets = _render("x = 100", "x = 200", "py", 42, ansi=False)
assert any("42" in _plain(w) for w in widgets)
def test_multi_hunk_line_numbers(self) -> None:
search = "A\nB\nC\nD\nE\nF\nG\nH"
replace = "Z\nB\nC\nD\nE\nF\nG\nY"
widgets = _render(search, replace, "py", 10, ansi=False)
joined = "\n".join(_plain(w) for w in widgets)
assert "10" in joined
assert "17" in joined
def test_no_line_numbers_without_start_line(self) -> None:
widgets = _render("x = 100", "x = 200", "py", None, ansi=False)
for w in widgets:
plain = _plain(w)
if plain.startswith(("- ", "+ ")):
assert not plain[0].isdigit()
def test_blank_lines_preserved(self) -> None:
search = "a\n\nb\nc\nd"
replace = "a\n\nb\nc\nZ"
widgets = _render(search, replace, "py", 1, ansi=False)
removed = [w for w in widgets if "diff-removed" in w.classes]
added = [w for w in widgets if "diff-added" in w.classes]
assert any(_plain(w).rstrip().endswith("d") for w in removed)
assert any(_plain(w).rstrip().endswith("Z") for w in added)
class TestBorderColors:
def test_keys_index_into_widgets(self) -> None:
widgets, colors = _render_with_colors("x = 100", "x = 200", "py", 1, ansi=False)
assert colors and all(0 <= k < len(widgets) for k in colors)
def test_added_lines_get_bright_success(self) -> None:
widgets, colors = _render_with_colors("x = 100", "x = 200", "py", 1, ansi=False)
added_keys = [i for i, w in enumerate(widgets) if "diff-added" in w.classes]
assert added_keys and all(colors[i] == "not dim $success" for i in added_keys)
def test_removed_lines_get_bright_error(self) -> None:
widgets, colors = _render_with_colors("x = 100", "x = 200", "py", 1, ansi=False)
removed_keys = [i for i, w in enumerate(widgets) if "diff-removed" in w.classes]
assert removed_keys and all(colors[i] == "not dim $error" for i in removed_keys)
def test_context_and_gap_not_in_dict(self) -> None:
search = "A\nB\nC\nD\nE\nF\nG\nH"
replace = "Z\nB\nC\nD\nE\nF\nG\nY"
widgets, colors = _render_with_colors(search, replace, "py", 1, ansi=False)
for i, w in enumerate(widgets):
if "diff-context" in w.classes or "diff-gap" in w.classes:
assert i not in colors

View file

@ -12,13 +12,16 @@ from vibe.core.experiments.session import (
hydrate_experiments_from_session,
initialize_experiments,
)
from vibe.core.telemetry.types import TerminalEmulator
class _StubClient(RemoteEvalClient):
def __init__(self, response: EvalResponse | None) -> None:
self._response = response
self.attributes: ExperimentAttributes | None = None
async def evaluate(self, attributes: ExperimentAttributes) -> EvalResponse | None:
self.attributes = attributes
return self._response
async def aclose(self) -> None:
@ -168,6 +171,36 @@ async def test_initialize_returns_true_and_persists_when_remote_eval_succeeds(
persist.assert_awaited_once()
@pytest.mark.asyncio
async def test_initialize_uses_provided_terminal_emulator(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"vibe.core.experiments.session.get_mistral_provider_and_api_key",
lambda _config: (MagicMock(), "fake-key"),
)
persist = AsyncMock()
session_logger = MagicMock()
session_logger.persist_experiments = persist
response = EvalResponse.model_validate({
"features": {"vibe_cli_system_prompt": {"defaultValue": "cli"}}
})
client = _StubClient(response)
manager = ExperimentManager(client=client)
result = await initialize_experiments(
config=_make_config(),
manager=manager,
session_logger=session_logger,
entrypoint_metadata=None,
terminal_emulator=TerminalEmulator.VSCODE,
)
assert result is True
assert client.attributes is not None
assert client.attributes.terminal_emulator is TerminalEmulator.VSCODE
@pytest.mark.asyncio
async def test_hydrate_returns_false_when_telemetry_disabled() -> None:
session_logger = MagicMock()

View file

@ -67,7 +67,7 @@ class TestIterSSEEvents:
payload = _valid_event_payload()
lines = [f"data: {json.dumps(payload)}"]
response = AsyncMock()
response.aiter_lines = _async_line_iter(lines)
response.aiter_bytes = _async_byte_iter(lines)
events = [e async for e in client._iter_sse_events(response)]
assert len(events) == 1
@ -79,7 +79,7 @@ class TestIterSSEEvents:
payload = _valid_event_payload()
lines = ["", ": this is a comment", f"data: {json.dumps(payload)}", ""]
response = AsyncMock()
response.aiter_lines = _async_line_iter(lines)
response.aiter_bytes = _async_byte_iter(lines)
events = [e async for e in client._iter_sse_events(response)]
assert len(events) == 1
@ -90,7 +90,7 @@ class TestIterSSEEvents:
payload = {"error": "server broke"}
lines = ["event: error", f"data: {json.dumps(payload)}"]
response = AsyncMock()
response.aiter_lines = _async_line_iter(lines)
response.aiter_bytes = _async_byte_iter(lines)
with pytest.raises(WorkflowsException) as exc_info:
_ = [e async for e in client._iter_sse_events(response)]
@ -106,7 +106,7 @@ class TestIterSSEEvents:
f"data: {json.dumps(payload)}",
]
response = AsyncMock()
response.aiter_lines = _async_line_iter(lines)
response.aiter_bytes = _async_byte_iter(lines)
with patch.object(
client, "_parse_sse_data", wraps=client._parse_sse_data
@ -122,7 +122,7 @@ class TestIterSSEEvents:
payload = _valid_event_payload()
lines = ["id: 123", "retry: 5000", f"data: {json.dumps(payload)}"]
response = AsyncMock()
response.aiter_lines = _async_line_iter(lines)
response.aiter_bytes = _async_byte_iter(lines)
events = [e async for e in client._iter_sse_events(response)]
assert len(events) == 1
@ -133,7 +133,7 @@ class TestIterSSEEvents:
payload = _valid_event_payload()
lines = ["data: {not valid json}", f"data: {json.dumps(payload)}"]
response = AsyncMock()
response.aiter_lines = _async_line_iter(lines)
response.aiter_bytes = _async_byte_iter(lines)
with patch("vibe.core.nuage.client.logger") as mock_logger:
events = [e async for e in client._iter_sse_events(response)]
@ -159,7 +159,7 @@ class TestStreamEvents:
mock_response = AsyncMock()
mock_response.raise_for_status = lambda: None
mock_response.aiter_lines = _async_line_iter(lines)
mock_response.aiter_bytes = _async_byte_iter(lines)
_setup_mock_client(client, mock_response)
params = StreamEventsQueryParams(workflow_exec_id="wf-1", start_seq=0)
@ -173,7 +173,7 @@ class TestStreamEvents:
mock_response = AsyncMock()
mock_response.raise_for_status = lambda: None
mock_response.aiter_lines = _async_line_iter([
mock_response.aiter_bytes = _async_byte_iter([
"event: error",
'data: {"error": "stream error"}',
])
@ -240,9 +240,9 @@ class TestGetWorkflowRuns:
assert call_params["user_id"] == "user-123"
def _async_line_iter(lines: list[str]):
def _async_byte_iter(lines: list[str]):
async def _iter():
for line in lines:
yield line
yield f"{line}\n".encode()
return _iter

View file

@ -17,7 +17,10 @@ def _make_payload_summary() -> PayloadSummary:
def _make_error(
*, status: int | None, headers: dict[str, str] | None = None
*,
status: int | None,
headers: dict[str, str] | None = None,
body_text: str = "body",
) -> BackendError:
return BackendError(
provider="test-provider",
@ -25,7 +28,7 @@ def _make_error(
status=status,
reason="some reason",
headers=headers or {},
body_text="body",
body_text=body_text,
parsed_error=None,
model="test-model",
payload_summary=_make_payload_summary(),
@ -72,3 +75,49 @@ class TestBackendErrorFmt:
msg = str(err)
assert str(code) in msg
assert "LLM backend error" in msg
class TestBackendErrorIsContextTooLong:
@pytest.mark.parametrize(
("status", "body_text"),
[
(400, "context too long"),
(400, "prompt is too long"),
# orchestral_runtime wraps context errors as 422
(422, '{"error":{"type":"model_context_exceeded"}}'),
(422, '{"error":{"type":"prompt_too_long"}}'),
],
)
def test_true(self, status: int, body_text: str) -> None:
err = _make_error(status=status, body_text=body_text)
assert err.is_context_too_long
def test_false_on_unrelated_status(self) -> None:
err = _make_error(status=500, body_text="context too long")
assert not err.is_context_too_long
def test_false_on_max_tokens(self) -> None:
# max-tokens truncation must not be misread as context-too-long
err = _make_error(status=422, body_text="max_tokens_exceeded")
assert not err.is_context_too_long
class TestBackendErrorIsResponseTooLong:
@pytest.mark.parametrize(
"body_text",
[
'{"error":{"type":"max_tokens_exceeded"}}',
"Generation truncated: finish_reason=length",
],
)
def test_true_on_422_with_substring(self, body_text: str) -> None:
err = _make_error(status=422, body_text=body_text)
assert err.is_response_too_long
def test_false_when_status_not_422(self) -> None:
err = _make_error(status=400, body_text="max_tokens_exceeded")
assert not err.is_response_too_long
def test_false_when_substring_missing(self) -> None:
err = _make_error(status=422, body_text="some unrelated error")
assert not err.is_response_too_long

75
tests/core/test_sse.py Normal file
View file

@ -0,0 +1,75 @@
from __future__ import annotations
from collections.abc import AsyncIterator
import httpx
import pytest
from vibe.core.utils.sse import iter_sse_lines
class _ChunkedStream(httpx.AsyncByteStream):
def __init__(self, chunks: list[bytes]) -> None:
self._chunks = chunks
async def __aiter__(self) -> AsyncIterator[bytes]:
for chunk in self._chunks:
yield chunk
async def _collect(chunks: list[bytes]) -> list[str]:
response = httpx.Response(
status_code=200,
stream=_ChunkedStream(chunks),
request=httpx.Request("POST", "https://example.com"),
)
return [line async for line in iter_sse_lines(response)]
class TestIterSseLines:
@pytest.mark.asyncio
async def test_splits_on_lf_and_crlf(self) -> None:
lines = await _collect([b"data: a\r\ndata: b\n\ndata: c\n"])
assert lines == ["data: a", "data: b", "", "data: c"]
@pytest.mark.asyncio
@pytest.mark.parametrize("separator", ["\u2028", "\u2029", "\x85", "\x0b", "\x0c"])
async def test_keeps_unicode_line_breaks_within_line(self, separator: str) -> None:
payload = f'data: {{"arguments": "{separator}value"}}\n'
lines = await _collect([payload.encode()])
assert lines == [payload.removesuffix("\n")]
@pytest.mark.asyncio
async def test_buffers_partial_lines_across_chunks(self) -> None:
lines = await _collect([b"data: one", b"two\ndata: three", b"four\n"])
assert lines == ["data: onetwo", "data: threefour"]
@pytest.mark.asyncio
async def test_multibyte_char_split_across_chunks(self) -> None:
encoded = "data: café\n".encode()
lines = await _collect([encoded[:9], encoded[9:]])
assert lines == ["data: café"]
@pytest.mark.asyncio
async def test_yields_trailing_line_without_newline(self) -> None:
lines = await _collect([b"data: a\ndata: b"])
assert lines == ["data: a", "data: b"]
@pytest.mark.asyncio
async def test_crlf_split_across_chunks(self) -> None:
lines = await _collect([b"data: a\r", b"\ndata: b\n"])
assert lines == ["data: a", "data: b"]
@pytest.mark.asyncio
async def test_splits_on_lone_cr(self) -> None:
lines = await _collect([b"data: a\rdata: b\r"])
assert lines == ["data: a", "data: b"]
@pytest.mark.asyncio
async def test_empty_stream(self) -> None:
assert await _collect([]) == []
@pytest.mark.asyncio
async def test_replaces_undecodable_bytes(self) -> None:
lines = await _collect([b"data: a\xff b\n"])
assert lines == ["data: a<> b"]

View file

@ -21,6 +21,7 @@ from vibe.core.telemetry.types import (
AttachmentKind,
EntrypointMetadata,
TelemetryRequestMetadata,
TerminalEmulator,
)
from vibe.core.tools.base import BaseTool, ToolPermission
from vibe.core.types import Backend
@ -557,7 +558,7 @@ class TestTelemetryClient:
entrypoint="cli",
client_name="vscode",
client_version="1.96.0",
terminal_emulator="vscode",
terminal_emulator=TerminalEmulator.VSCODE,
)
assert len(telemetry_events) == 1

View file

@ -13,11 +13,13 @@ import httpx
import pytest
import zstandard
from tests.conftest import build_test_vibe_config
from vibe.core.teleport.errors import (
ServiceTeleportError,
ServiceTeleportNotSupportedError,
)
from vibe.core.teleport.git import GitRepoInfo
from vibe.core.teleport.nuage import DEFAULT_NUAGE_PROJECT_NAME
from vibe.core.teleport.teleport import TeleportService
from vibe.core.teleport.types import (
TeleportCheckingGitEvent,
@ -157,6 +159,49 @@ class TestTeleportServiceIsSupported:
class TestTeleportServiceExecute:
def test_build_nuage_request_uses_configured_project_name(
self, tmp_path: Path
) -> None:
service = _make_service(
tmp_path,
vibe_config=build_test_vibe_config(vibe_code_project_name=" Zed "),
)
request = service._build_nuage_request(
prompt="test prompt",
git_info=GitRepoInfo(
remote_url="https://github.com/owner/repo",
owner="owner",
repo="repo",
branch="main",
commit="abc123",
diff="",
),
)
assert request.project_name == "Zed"
def test_build_nuage_request_uses_default_project_name_for_blank_config(
self, tmp_path: Path
) -> None:
service = _make_service(
tmp_path, vibe_config=build_test_vibe_config(vibe_code_project_name=" ")
)
request = service._build_nuage_request(
prompt="test prompt",
git_info=GitRepoInfo(
remote_url="https://github.com/owner/repo",
owner="owner",
repo="repo",
branch="main",
commit="abc123",
diff="",
),
)
assert request.project_name == DEFAULT_NUAGE_PROJECT_NAME
@pytest.mark.asyncio
async def test_execute_happy_path(self, tmp_path: Path) -> None:
seen_body: dict[str, object] | None = None

29
tests/core/test_text.py Normal file
View file

@ -0,0 +1,29 @@
from __future__ import annotations
from vibe.core.utils.text import snippet_start_line
class TestSnippetStartLine:
def test_finds_line_number(self) -> None:
assert snippet_start_line("a\nb\nc\nd\n", "c") == 3
def test_first_line(self) -> None:
assert snippet_start_line("hello\nworld", "hello") == 1
def test_multiline_snippet(self) -> None:
assert snippet_start_line("a\nb\nc", "\nb\n") == 2
def test_first_occurrence_when_repeated(self) -> None:
assert snippet_start_line("x\nx\nx", "x") == 1
def test_leading_newline_anchors_first_content_line(self) -> None:
assert snippet_start_line("bar\nx\nbar", "\nbar") == 3
def test_returns_none_when_exact_snippet_absent(self) -> None:
assert snippet_start_line("a\nb\nfoo", "foo\n") is None
def test_not_found(self) -> None:
assert snippet_start_line("hello\nworld", "missing") is None
def test_blank_snippet(self) -> None:
assert snippet_start_line("hello", "\n") is None

View file

@ -228,3 +228,46 @@ def test_get_result_display() -> None:
assert isinstance(display, ToolResultDisplay)
assert display.success is True
assert "foo.py" in display.message
def test_ui_start_line_not_part_of_model_contract() -> None:
result = EditResult(file="/x", message="m", old_string="a", new_string="b")
result._ui_start_line = 42
assert result.ui_start_line == 42
assert "ui_start_line" not in result.model_dump()
assert "_ui_start_line" not in result.model_dump()
assert "ui_start_line" not in result.model_dump_json()
assert "ui_start_line" not in EditResult.model_fields
assert "ui_start_line" not in EditResult.model_json_schema().get("properties", {})
assert "ui_start_line" not in dict(result)
@pytest.mark.asyncio
async def test_ui_start_line_computed_at_edit_site(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "alpha\nbeta\ngamma\ndelta\n")
edit = _make_edit()
result = await collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="gamma", new_string="GAMMA"))
)
assert result.ui_start_line == 3
@pytest.mark.asyncio
async def test_ui_start_line_set_for_pure_deletion(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
_write(tmp_path, "f.txt", "keep1\nkeep2\nremove\nkeep3\n")
edit = _make_edit()
result = await collect_result(
edit.run(EditArgs(file_path="f.txt", old_string="remove\n", new_string=""))
)
assert result.ui_start_line == 3

View file

@ -0,0 +1,200 @@
<svg class="rich-terminal" viewBox="0 0 1482 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #868887 }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1463.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1464" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1464" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">ClipboardNoticeSnapshotApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="1464" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1464" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1464" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</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="1464" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</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="1464" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</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="1464" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</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="1464" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="134.2" clip-path="url(#terminal-line-20)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r1" x="1464" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="0" y="532.4" textLength="134.2" clip-path="url(#terminal-line-21)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</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="0" y="556.8" textLength="134.2" clip-path="url(#terminal-line-22)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</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="1464" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r2" x="0" y="605.6" textLength="146.4" clip-path="url(#terminal-line-24)">Mistral&#160;Vibe</text><text class="terminal-r1" x="146.4" y="605.6" textLength="122" clip-path="url(#terminal-line-24)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="268.4" y="605.6" textLength="244" clip-path="url(#terminal-line-24)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="605.6" textLength="256.2" clip-path="url(#terminal-line-24)">&#160;·&#160;[Subscription]&#160;Pro</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="0" y="630" textLength="414.8" clip-path="url(#terminal-line-25)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1464" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="0" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">Type&#160;</text><text class="terminal-r3" x="61" y="654.4" textLength="61" clip-path="url(#terminal-line-26)">/help</text><text class="terminal-r1" x="122" y="654.4" textLength="256.2" clip-path="url(#terminal-line-26)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r4" x="1110.2" y="727.6" textLength="353.8" clip-path="url(#terminal-line-29)">Selection&#160;copied&#160;to&#160;clipboard</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="0" y="752" textLength="1464" clip-path="url(#terminal-line-30)">──────────────────────────────────────────────────────────────────────────────────────────────────────────────&#160;default&#160;</text><text class="terminal-r1" x="1464" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r2" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">&gt;</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="1464" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</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="0" y="849.6" textLength="1464" clip-path="url(#terminal-line-34)">────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="terminal-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r4" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r4" x="1256.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -0,0 +1,183 @@
<svg class="rich-terminal" viewBox="0 0 1238 782.0" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #d0b344;font-weight: bold }
.terminal-r5 { fill: #868887 }
.terminal-r6 { fill: #cc555a;font-weight: bold }
.terminal-r7 { fill: #8d7b39 }
.terminal-r8 { fill: #98a84b }
.terminal-r9 { fill: #d0b344 }
.terminal-r10 { fill: #98a84b;font-weight: bold }
.terminal-r11 { fill: #cc555a }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1219.0" height="731.0" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1220" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="780" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">EditApprovalAnsiApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<g class="terminal-matrix">
<text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="0" y="44.4" textLength="134.2" clip-path="url(#terminal-line-1)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="0" y="68.8" textLength="134.2" clip-path="url(#terminal-line-2)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="0" y="93.2" textLength="134.2" clip-path="url(#terminal-line-3)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r2" x="0" y="142" textLength="146.4" clip-path="url(#terminal-line-5)">Mistral&#160;Vibe</text><text class="terminal-r1" x="146.4" y="142" textLength="122" clip-path="url(#terminal-line-5)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="268.4" y="142" textLength="244" clip-path="url(#terminal-line-5)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="142" textLength="256.2" clip-path="url(#terminal-line-5)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="0" y="166.4" textLength="414.8" clip-path="url(#terminal-line-6)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="0" y="190.8" textLength="61" clip-path="url(#terminal-line-7)">Type&#160;</text><text class="terminal-r3" x="61" y="190.8" textLength="61" clip-path="url(#terminal-line-7)">/help</text><text class="terminal-r1" x="122" y="190.8" textLength="256.2" clip-path="url(#terminal-line-7)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="0" y="288.4" textLength="1220" clip-path="url(#terminal-line-11)">┌──────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="0" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r4" x="24.4" y="312.8" textLength="341.6" clip-path="url(#terminal-line-12)">Permission&#160;for&#160;the&#160;edit&#160;tool</text><text class="terminal-r1" x="1207.8" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)"></text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="0" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r5" x="24.4" y="337.2" textLength="244" clip-path="url(#terminal-line-13)">File:&#160;src/example.py</text><text class="terminal-r1" x="1207.8" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)"></text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="0" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="1207.8" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)"></text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="0" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r6" x="24.4" y="386" textLength="61" clip-path="url(#terminal-line-15)">&#160;&#160;&#160;4&#160;</text><text class="terminal-r6" x="85.4" y="386" textLength="24.4" clip-path="url(#terminal-line-15)">-&#160;</text><text class="terminal-r5" x="109.8" y="386" textLength="109.8" clip-path="url(#terminal-line-15)">MAX_USERS</text><text class="terminal-r5" x="231.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">=</text><text class="terminal-r7" x="256.2" y="386" textLength="36.6" clip-path="url(#terminal-line-15)">100</text><text class="terminal-r1" x="1207.8" y="386" textLength="12.2" clip-path="url(#terminal-line-15)"></text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="0" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r8" x="24.4" y="410.4" textLength="61" clip-path="url(#terminal-line-16)">&#160;&#160;&#160;4&#160;</text><text class="terminal-r8" x="85.4" y="410.4" textLength="24.4" clip-path="url(#terminal-line-16)">+&#160;</text><text class="terminal-r1" x="109.8" y="410.4" textLength="109.8" clip-path="url(#terminal-line-16)">MAX_USERS</text><text class="terminal-r1" x="231.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">=</text><text class="terminal-r9" x="256.2" y="410.4" textLength="36.6" clip-path="url(#terminal-line-16)">200</text><text class="terminal-r1" x="1207.8" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)"></text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r5" x="24.4" y="434.8" textLength="61" clip-path="url(#terminal-line-17)">&#160;&#160;&#160;5&#160;</text><text class="terminal-r1" x="109.8" y="434.8" textLength="85.4" clip-path="url(#terminal-line-17)">TIMEOUT</text><text class="terminal-r1" x="207.4" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">=</text><text class="terminal-r9" x="231.8" y="434.8" textLength="24.4" clip-path="url(#terminal-line-17)">30</text><text class="terminal-r1" x="1207.8" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)"></text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="1207.8" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)"></text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r10" x="24.4" y="483.6" textLength="183" clip-path="url(#terminal-line-19)">&#160;1.&#160;Allow&#160;once</text><text class="terminal-r1" x="1207.8" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)"></text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="0" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="1207.8" y="508" textLength="12.2" clip-path="url(#terminal-line-20)"></text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r1" x="0" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="24.4" y="532.4" textLength="488" clip-path="url(#terminal-line-21)">&#160;&#160;2.&#160;Allow&#160;for&#160;remainder&#160;of&#160;this&#160;session</text><text class="terminal-r1" x="1207.8" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)"></text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="0" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="1207.8" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)"></text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="0" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="24.4" y="581.2" textLength="207.4" clip-path="url(#terminal-line-23)">&#160;&#160;3.&#160;Always&#160;allow</text><text class="terminal-r1" x="1207.8" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)"></text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r1" x="0" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="1207.8" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)"></text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="0" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r11" x="24.4" y="630" textLength="109.8" clip-path="url(#terminal-line-25)">&#160;&#160;4.&#160;Deny</text><text class="terminal-r1" x="1207.8" y="630" textLength="12.2" clip-path="url(#terminal-line-25)"></text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="0" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1207.8" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)"></text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="0" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r5" x="24.4" y="678.8" textLength="451.4" clip-path="url(#terminal-line-27)">↑↓&#160;navigate&#160;&#160;Enter&#160;select&#160;&#160;ESC&#160;reject</text><text class="terminal-r1" x="1207.8" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)"></text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r1" x="0" y="703.2" textLength="1220" clip-path="url(#terminal-line-28)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r5" x="0" y="727.6" textLength="158.6" clip-path="url(#terminal-line-29)">/test/workdir</text><text class="terminal-r5" x="1012.6" y="727.6" textLength="207.4" clip-path="url(#terminal-line-29)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,203 @@
<svg class="rich-terminal" viewBox="0 0 1238 928.4" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-r1 { fill: #c5c8c6 }
.terminal-r2 { fill: #ff8205;font-weight: bold }
.terminal-r3 { fill: #68a0b3 }
.terminal-r4 { fill: #868887 }
.terminal-r5 { fill: #7b7e82;font-weight: bold }
.terminal-r6 { fill: #4b4e55;font-weight: bold }
.terminal-r7 { fill: #68a0b3;font-weight: bold }
</style>
<defs>
<clipPath id="terminal-clip-terminal">
<rect x="0" y="0" width="1219.0" height="877.4" />
</clipPath>
<clipPath id="terminal-line-0">
<rect x="0" y="1.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-1">
<rect x="0" y="25.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-2">
<rect x="0" y="50.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-3">
<rect x="0" y="74.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-4">
<rect x="0" y="99.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-5">
<rect x="0" y="123.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-6">
<rect x="0" y="147.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-7">
<rect x="0" y="172.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-8">
<rect x="0" y="196.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-9">
<rect x="0" y="221.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-10">
<rect x="0" y="245.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-11">
<rect x="0" y="269.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-12">
<rect x="0" y="294.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-13">
<rect x="0" y="318.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-14">
<rect x="0" y="343.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-15">
<rect x="0" y="367.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-16">
<rect x="0" y="391.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-17">
<rect x="0" y="416.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-18">
<rect x="0" y="440.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-19">
<rect x="0" y="465.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-20">
<rect x="0" y="489.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-21">
<rect x="0" y="513.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-22">
<rect x="0" y="538.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-23">
<rect x="0" y="562.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-24">
<rect x="0" y="587.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-25">
<rect x="0" y="611.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-26">
<rect x="0" y="635.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-27">
<rect x="0" y="660.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-28">
<rect x="0" y="684.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-29">
<rect x="0" y="709.1" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-30">
<rect x="0" y="733.5" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-31">
<rect x="0" y="757.9" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-32">
<rect x="0" y="782.3" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-33">
<rect x="0" y="806.7" width="1220" height="24.65"/>
</clipPath>
<clipPath id="terminal-line-34">
<rect x="0" y="831.1" width="1220" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="926.4" rx="8"/><text class="terminal-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">SessionPickerTestApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-clip-terminal)">
<rect fill="#c5c8c6" x="36.6" y="733.5" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#c5c8c6" x="158.6" y="733.5" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#c5c8c6" x="183" y="733.5" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#c5c8c6" x="256.2" y="733.5" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#c5c8c6" x="280.6" y="733.5" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#c5c8c6" x="402.6" y="733.5" width="292.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#c5c8c6" x="695.4" y="733.5" width="488" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-matrix">
<text class="terminal-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-line-0)">
</text><text class="terminal-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-line-1)">
</text><text class="terminal-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-line-2)">
</text><text class="terminal-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-line-3)">
</text><text class="terminal-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-line-4)">
</text><text class="terminal-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-line-5)">
</text><text class="terminal-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-line-6)">
</text><text class="terminal-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-line-7)">
</text><text class="terminal-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-line-8)">
</text><text class="terminal-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-line-9)">
</text><text class="terminal-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-line-10)">
</text><text class="terminal-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-line-11)">
</text><text class="terminal-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-line-12)">
</text><text class="terminal-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-line-13)">
</text><text class="terminal-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-line-14)">
</text><text class="terminal-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-line-15)">
</text><text class="terminal-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-line-16)">
</text><text class="terminal-r1" x="0" y="434.8" textLength="134.2" clip-path="url(#terminal-line-17)">&#160;&#160;⡠⣒⠄&#160;&#160;⡔⢄⠔⡄</text><text class="terminal-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-line-17)">
</text><text class="terminal-r1" x="0" y="459.2" textLength="134.2" clip-path="url(#terminal-line-18)">&#160;⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣</text><text class="terminal-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-line-18)">
</text><text class="terminal-r1" x="0" y="483.6" textLength="134.2" clip-path="url(#terminal-line-19)">&#160;&#160;⠉⠒⠣⠤⠵⠤⠬⠮⠆</text><text class="terminal-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-line-19)">
</text><text class="terminal-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-line-20)">
</text><text class="terminal-r2" x="0" y="532.4" textLength="146.4" clip-path="url(#terminal-line-21)">Mistral&#160;Vibe</text><text class="terminal-r1" x="146.4" y="532.4" textLength="122" clip-path="url(#terminal-line-21)">&#160;v0.0.0&#160;·&#160;</text><text class="terminal-r3" x="268.4" y="532.4" textLength="244" clip-path="url(#terminal-line-21)">devstral-latest[off]</text><text class="terminal-r1" x="512.4" y="532.4" textLength="256.2" clip-path="url(#terminal-line-21)">&#160;·&#160;[Subscription]&#160;Pro</text><text class="terminal-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-line-21)">
</text><text class="terminal-r1" x="0" y="556.8" textLength="414.8" clip-path="url(#terminal-line-22)">1&#160;model&#160;·&#160;0&#160;MCP&#160;servers&#160;·&#160;0&#160;skills</text><text class="terminal-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-line-22)">
</text><text class="terminal-r1" x="0" y="581.2" textLength="61" clip-path="url(#terminal-line-23)">Type&#160;</text><text class="terminal-r3" x="61" y="581.2" textLength="61" clip-path="url(#terminal-line-23)">/help</text><text class="terminal-r1" x="122" y="581.2" textLength="256.2" clip-path="url(#terminal-line-23)">&#160;for&#160;more&#160;information</text><text class="terminal-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-line-23)">
</text><text class="terminal-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-line-24)">
</text><text class="terminal-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-line-25)">
</text><text class="terminal-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-line-26)">
</text><text class="terminal-r1" x="0" y="678.8" textLength="1220" clip-path="url(#terminal-line-27)">┌──────────────────────────────────────────────────────────────────────────────────────────────────┐</text><text class="terminal-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-line-27)">
</text><text class="terminal-r1" x="0" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r3" x="24.4" y="703.2" textLength="73.2" clip-path="url(#terminal-line-28)">local&#160;</text><text class="terminal-r4" x="97.6" y="703.2" textLength="158.6" clip-path="url(#terminal-line-28)">/test/workdir</text><text class="terminal-r4" x="256.2" y="703.2" textLength="61" clip-path="url(#terminal-line-28)">&#160;&#160;·&#160;&#160;</text><text class="terminal-r3" x="317.2" y="703.2" textLength="85.4" clip-path="url(#terminal-line-28)">remote&#160;</text><text class="terminal-r4" x="402.6" y="703.2" textLength="134.2" clip-path="url(#terminal-line-28)">all&#160;folders</text><text class="terminal-r1" x="1207.8" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)"></text><text class="terminal-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-line-28)">
</text><text class="terminal-r1" x="0" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1207.8" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)"></text><text class="terminal-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-line-29)">
</text><text class="terminal-r1" x="0" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r5" x="36.6" y="752" textLength="122" clip-path="url(#terminal-line-30)">unknown&#160;&#160;&#160;</text><text class="terminal-r7" x="183" y="752" textLength="73.2" clip-path="url(#terminal-line-30)">local&#160;</text><text class="terminal-r5" x="280.6" y="752" textLength="122" clip-path="url(#terminal-line-30)">local-se&#160;&#160;</text><text class="terminal-r6" x="402.6" y="752" textLength="292.8" clip-path="url(#terminal-line-30)">Refactor&#160;the&#160;auth&#160;module</text><text class="terminal-r1" x="1207.8" y="752" textLength="12.2" clip-path="url(#terminal-line-30)"></text><text class="terminal-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-line-30)">
</text><text class="terminal-r1" x="0" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r4" x="36.6" y="776.4" textLength="122" clip-path="url(#terminal-line-31)">unknown&#160;&#160;&#160;</text><text class="terminal-r3" x="183" y="776.4" textLength="73.2" clip-path="url(#terminal-line-31)">remote</text><text class="terminal-r4" x="280.6" y="776.4" textLength="122" clip-path="url(#terminal-line-31)">ion-0002&#160;&#160;</text><text class="terminal-r1" x="402.6" y="776.4" textLength="231.8" clip-path="url(#terminal-line-31)">Vibe&#160;Code&#160;(running)</text><text class="terminal-r1" x="1207.8" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)"></text><text class="terminal-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-line-31)">
</text><text class="terminal-r1" x="0" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1207.8" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)"></text><text class="terminal-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-line-32)">
</text><text class="terminal-r1" x="0" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r4" x="24.4" y="825.2" textLength="573.4" clip-path="url(#terminal-line-33)">↑↓&#160;Navigate&#160;&#160;Enter&#160;Select&#160;&#160;D&#160;Delete&#160;&#160;Esc&#160;Cancel</text><text class="terminal-r1" x="1207.8" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)"></text><text class="terminal-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-line-33)">
</text><text class="terminal-r1" x="0" y="849.6" textLength="1220" clip-path="url(#terminal-line-34)">└──────────────────────────────────────────────────────────────────────────────────────────────────┘</text><text class="terminal-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-line-34)">
</text><text class="terminal-r4" x="0" y="874" textLength="158.6" clip-path="url(#terminal-line-35)">/test/workdir</text><text class="terminal-r4" x="1012.6" y="874" textLength="207.4" clip-path="url(#terminal-line-35)">0%&#160;of&#160;200k&#160;tokens</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,25 @@
from __future__ import annotations
from textual.pilot import Pilot
from textual.widgets import Static
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp
from tests.snapshots.snap_compare import SnapCompare
class ClipboardNoticeSnapshotApp(BaseSnapshotTestApp):
pass
def test_snapshot_clipboard_notice_visible(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
notice = pilot.app.query_one("#clipboard-notice", Static)
notice.update("Selection copied to clipboard")
notice.display = True
await pilot.pause(0.1)
assert snap_compare(
"test_ui_snapshot_clipboard_notice.py:ClipboardNoticeSnapshotApp",
terminal_size=(120, 36),
run_before=run_before,
)

View file

@ -0,0 +1,60 @@
from __future__ import annotations
from pathlib import Path
from textual.pilot import Pilot
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp
from tests.snapshots.snap_compare import SnapCompare
from vibe.core.tools.builtins.edit import EditArgs
FILE_CONTENT = "\n".join([
"def greet(name):",
' return f"hello {name}"',
"",
"MAX_USERS = 100",
"TIMEOUT = 30",
])
class EditApprovalApp(BaseSnapshotTestApp):
_diff_theme: str = "tokyo-night"
async def on_ready(self) -> None:
await super().on_ready()
self.theme = self._diff_theme
path = Path("src/example.py")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(FILE_CONTENT)
args = EditArgs(
file_path="src/example.py",
old_string="MAX_USERS = 100\nTIMEOUT = 30",
new_string="MAX_USERS = 200\nTIMEOUT = 30",
)
await self._switch_to_approval_app("edit", args)
class EditApprovalAnsiApp(EditApprovalApp):
_diff_theme = "ansi-dark"
def test_snapshot_edit_approval_diff(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.3)
assert snap_compare(
"test_ui_snapshot_edit_diff.py:EditApprovalApp",
terminal_size=(100, 30),
run_before=run_before,
)
def test_snapshot_edit_approval_diff_ansi(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.3)
assert snap_compare(
"test_ui_snapshot_edit_diff.py:EditApprovalAnsiApp",
terminal_size=(100, 30),
run_before=run_before,
)

View file

@ -0,0 +1,51 @@
from __future__ import annotations
from textual.pilot import Pilot
from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp
from tests.snapshots.snap_compare import SnapCompare
from vibe.cli.textual_ui.widgets.session_picker import SessionPickerApp
from vibe.core.session.resume_sessions import ResumeSessionInfo
_SESSIONS = [
ResumeSessionInfo(
session_id="local-session-0001",
source="local",
cwd="/test/workdir",
title="Refactor the auth module",
end_time=None,
),
ResumeSessionInfo(
session_id="remote-session-0002",
source="remote",
cwd="",
title="Vibe Code",
end_time=None,
status="RUNNING",
),
]
_LATEST_MESSAGES = {
_SESSIONS[0].option_id: "Refactor the auth module",
_SESSIONS[1].option_id: "Vibe Code (running)",
}
class SessionPickerTestApp(BaseSnapshotTestApp):
async def on_mount(self) -> None:
await super().on_mount()
picker = SessionPickerApp(
sessions=_SESSIONS, latest_messages=_LATEST_MESSAGES, cwd="/test/workdir"
)
await self._switch_from_input(picker)
def test_snapshot_session_picker_header(snap_compare: SnapCompare) -> None:
async def run_before(pilot: Pilot) -> None:
await pilot.pause(0.2)
assert snap_compare(
"test_ui_snapshot_session_picker.py:SessionPickerTestApp",
terminal_size=(100, 36),
run_before=run_before,
)

View file

@ -14,13 +14,16 @@ from tests.conftest import (
)
from tests.mock.utils import mock_llm_chunk
from tests.stubs.fake_backend import FakeBackend
from vibe.core.agent_loop import CompactionFailedError
from vibe.core.config import ModelConfig
from vibe.core.types import (
AssistantEvent,
CompactEndEvent,
CompactStartEvent,
FunctionCall,
LLMMessage,
Role,
ToolCall,
UserMessageEvent,
)
@ -302,6 +305,66 @@ async def test_compact_without_extra_instructions_has_no_additional_section() ->
assert "## Additional Instructions" not in compaction_prompt
@pytest.mark.asyncio
async def test_compact_raises_on_tool_call_when_flag_enabled() -> None:
"""With the flag on, a compaction that returns a tool call raises."""
backend = FakeBackend([
[
mock_llm_chunk(
content="",
tool_calls=[
ToolCall(
id="t1",
index=0,
function=FunctionCall(name="bash", arguments="{}"),
)
],
)
]
])
cfg = build_test_vibe_config(
models=make_test_models(auto_compact_threshold=999),
raise_on_compaction_failure=True,
)
agent = build_test_agent_loop(config=cfg, backend=backend)
agent.messages.append(LLMMessage(role=Role.user, content="Hello"))
agent.stats.context_tokens = 100
with pytest.raises(CompactionFailedError) as exc_info:
await agent.compact()
assert exc_info.value.reason == "tool_call"
@pytest.mark.asyncio
async def test_compact_raises_on_empty_summary_when_flag_enabled() -> None:
"""With the flag on, a compaction with empty content raises."""
backend = FakeBackend([[mock_llm_chunk(content=" ")]])
cfg = build_test_vibe_config(
models=make_test_models(auto_compact_threshold=999),
raise_on_compaction_failure=True,
)
agent = build_test_agent_loop(config=cfg, backend=backend)
agent.messages.append(LLMMessage(role=Role.user, content="Hello"))
agent.stats.context_tokens = 100
with pytest.raises(CompactionFailedError) as exc_info:
await agent.compact()
assert exc_info.value.reason == "empty_summary"
@pytest.mark.asyncio
async def test_compact_falls_back_when_flag_disabled() -> None:
"""With the flag off (default), empty content uses the legacy fallback."""
backend = FakeBackend([[mock_llm_chunk(content="")]])
cfg = build_test_vibe_config(models=make_test_models(auto_compact_threshold=999))
agent = build_test_agent_loop(config=cfg, backend=backend)
agent.messages.append(LLMMessage(role=Role.user, content="Hello"))
agent.stats.context_tokens = 100
summary = await agent.compact()
assert summary == "(no summary available)"
@pytest.mark.asyncio
async def test_compact_message_shape_preserves_prior_user_messages() -> None:
from vibe.core.compaction import parse_previous_user_messages

View file

@ -16,6 +16,7 @@ from tests.stubs.fake_mcp_registry import FakeMCPRegistry
from vibe.core import agent_loop as agent_loop_module
from vibe.core.agent_loop import AgentLoop
from vibe.core.config import MCPStdio
from vibe.core.telemetry.types import TerminalEmulator
from vibe.core.tools.manager import ToolManager
from vibe.core.tools.mcp.tools import RemoteTool
@ -317,20 +318,37 @@ class TestStartInitializeExperiments:
@pytest.mark.asyncio
async def test_refreshes_system_prompt_when_experiments_update(self) -> None:
loop = build_test_agent_loop()
loop = build_test_agent_loop(terminal_emulator=TerminalEmulator.VSCODE)
refresh_mock = AsyncMock()
init_mock = AsyncMock(return_value=True)
with (
patch.object(
agent_loop_module,
"session_initialize_experiments",
new=AsyncMock(return_value=True),
agent_loop_module, "session_initialize_experiments", new=init_mock
),
patch.object(loop, "refresh_system_prompt", new=refresh_mock),
):
await loop.initialize_experiments()
refresh_mock.assert_awaited_once()
init_mock.assert_awaited_once()
init_args = init_mock.await_args
assert init_args is not None
assert init_args.kwargs["terminal_emulator"] is TerminalEmulator.VSCODE
def test_new_session_telemetry_uses_provided_terminal_emulator(self) -> None:
loop = build_test_agent_loop(terminal_emulator=TerminalEmulator.VSCODE)
send_new_session = MagicMock()
with patch.object(
loop.telemetry_client, "send_new_session", new=send_new_session
):
loop.emit_new_session_telemetry()
assert (
send_new_session.call_args.kwargs["terminal_emulator"]
is TerminalEmulator.VSCODE
)
@pytest.mark.asyncio
async def test_does_not_refresh_system_prompt_when_experiments_unchanged(

View file

@ -8,6 +8,7 @@ from tests.conftest import build_test_vibe_config
from tests.mock.utils import collect_result
from vibe.core.agents.manager import AgentManager
from vibe.core.agents.models import BUILTIN_AGENTS, AgentType
from vibe.core.telemetry.types import TerminalEmulator
from vibe.core.tools.base import BaseToolState, InvokeContext, ToolError, ToolPermission
from vibe.core.tools.builtins.task import Task, TaskArgs, TaskResult, TaskToolConfig
from vibe.core.tools.permissions import PermissionContext
@ -37,7 +38,11 @@ class TestTaskToolValidation:
include_project_context=False, include_prompt_detail=False
)
manager = AgentManager(lambda: config)
return InvokeContext(tool_call_id="test-call-id", agent_manager=manager)
return InvokeContext(
tool_call_id="test-call-id",
agent_manager=manager,
terminal_emulator=TerminalEmulator.VSCODE,
)
@pytest.mark.asyncio
async def test_rejects_primary_agent(
@ -132,7 +137,11 @@ class TestTaskToolExecution:
include_project_context=False, include_prompt_detail=False
)
manager = AgentManager(lambda: config)
return InvokeContext(tool_call_id="test-call-id", agent_manager=manager)
return InvokeContext(
tool_call_id="test-call-id",
agent_manager=manager,
terminal_emulator=TerminalEmulator.VSCODE,
)
@pytest.mark.asyncio
async def test_happy_path_returns_subagent_response(
@ -164,6 +173,10 @@ class TestTaskToolExecution:
assert result.response == "Hello from subagent! More content."
assert result.turns_used == 2 # 2 assistant messages in mock_messages
assert result.completed is True
assert (
mock_agent_loop_class.call_args.kwargs["terminal_emulator"]
is TerminalEmulator.VSCODE
)
@pytest.mark.asyncio
async def test_handles_stopped_by_middleware(