diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index dc72f8d..d49b361 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -18,6 +18,7 @@ body: - CLI - ACP - Both + - VS Code extension - Other validations: required: true @@ -45,8 +46,9 @@ body: attributes: label: Versions / environment description: > - Include Mistral Vibe version, uv or pip version and OS. - placeholder: "mistral-vibe 1.2.0, uv 0.9.0 on macOS 15.7" + Include the version for the affected component, uv or pip when + relevant and OS. + placeholder: "mistral-vibe 1.2.0 or Mistral Vibe for VS Code 1.1.0, uv 0.9.0 on macOS 15.7" validations: required: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c011522..913ac65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release to Pipy and Zed's store +name: Release to PyPI on: release: @@ -43,19 +43,3 @@ jobs: - name: Publish distribution to PyPI if: github.repository == 'mistralai/mistral-vibe' uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - - release-zed-extension: - name: Release Zed Extension - runs-on: ubuntu-latest - if: github.repository == 'mistralai/mistral-vibe' - permissions: - contents: read - environment: - name: zed - steps: - - uses: huacnlee/zed-extension-action@8cd592a0d24e1e41157740f1a529aeabddc88a1b # v2 - with: - extension-name: mistral-vibe - push-to: mistralai/zed-extensions - env: - COMMITTER_TOKEN: ${{ secrets.ZED_EXTENSION_GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 46f653c..bcbea3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.11.1] - 2026-05-27 + +### Added + +- Custom compaction prompts via the `compaction_prompt_id` config setting, resolved from `~/.vibe/prompts/` or `.vibe/prompts/` +- `--max-tokens` flag for programmatic mode (`-p`) +- ACP `_auth/status` and `_auth/signOut` extension methods so IDE extensions can show auth state and sign the user out +- Auth source assessment in `vibe.setup.auth` to classify the active credential source +- VS Code extension promo line in the CLI banner when launched from a VS Code, Cursor, or VS Code Insiders terminal +- `OverridesLayer` and `ProjectConfigLayer` for the layered configuration system + +### Changed + +- Programmatic mode (`-p`) no longer auto-approves tool calls by default +- Browser sign-in is enabled by default for Mistral providers; the `enable_experimental_browser_sign_in` flag is no longer required (stale entries are silently ignored) +- Compaction quality improved +- Refreshed user message styling and slash/teleport variants +- `/teleport` to Vibe Code Web now carries the working diff and last commit, matching the legacy teleport +- Reworded `/teleport` help and completion copy to say "Vibe Code Web" + +### Removed + +- Zed extension publishing job from the release workflow + + ## [2.11.0] - 2026-05-25 ### Added diff --git a/README.md b/README.md index 9d7e6c7..cda97da 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,7 @@ When using `--prompt`, you can specify additional options: - **`--max-turns N`**: Limit the maximum number of assistant turns. The session will stop after N turns. - **`--max-price DOLLARS`**: Set a maximum cost limit in dollars. The session will be interrupted if the cost exceeds this limit. +- **`--max-tokens N`**: Set a maximum cumulative LLM token budget for the session, counting both prompt and completion tokens. The session will be interrupted if usage exceeds this limit. - **`--enabled-tools TOOL`**: Enable specific tools. In programmatic mode, this disables all other tools. Can be specified multiple times. Supports exact names, glob patterns (e.g., `bash*`), or regex with `re:` prefix (e.g., `re:^serena_.*$`). - **`--output FORMAT`**: Set the output format. Options: - `text` (default): Human-readable text output @@ -276,7 +277,7 @@ When using `--prompt`, you can specify additional options: Example: ```bash -vibe --prompt "Analyze the codebase" --max-turns 5 --max-price 1.0 --output json +vibe --prompt "Analyze the codebase" --max-turns 5 --max-price 1.0 --max-tokens 50000 --output json ``` ## Voice Mode @@ -445,6 +446,21 @@ system_prompt_id = "my_custom_prompt" This will load the prompt from `~/.vibe/prompts/my_custom_prompt.md`. +Project-local prompts in `.vibe/prompts/` are also supported and override user-level prompts with the same name. This applies to all custom prompts (system and compaction). + +### Custom Compaction Prompts + +Compaction uses the built-in prompt at `prompts/compact.md` by default. You can replace it with a custom prompt from `~/.vibe/prompts/` (or `.vibe/prompts/`) using the same resolution rules as system prompts. + +To use a custom compaction prompt, set `compaction_prompt_id` in your configuration to match the filename (without the `.md` extension): + +```toml +# Use a custom compaction prompt +compaction_prompt_id = "my_compaction_prompt" +``` + +Any extra instructions passed to `/compact ...` are appended after the configured compaction prompt. + ### Custom Agent Configurations You can create custom agent configurations for specific use cases (e.g., red-teaming, specialized tasks) by adding agent-specific TOML files in the `~/.vibe/agents/` directory. @@ -650,7 +666,7 @@ This affects where Vibe looks for: - `config.toml` - Main configuration - `.env` - API keys - `agents/` - Custom agent configurations -- `prompts/` - Custom system prompts +- `prompts/` - Custom system and compaction prompts - `tools/` - Custom tools - `logs/` - Session logs diff --git a/distribution/zed/extension.toml b/distribution/zed/extension.toml index 33aab8f..09d8140 100644 --- a/distribution/zed/extension.toml +++ b/distribution/zed/extension.toml @@ -1,7 +1,7 @@ id = "mistral-vibe" name = "Mistral Vibe" description = "Mistral's open-source coding assistant" -version = "2.11.0" +version = "2.11.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.11.0/vibe-acp-darwin-aarch64-2.11.0.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.1/vibe-acp-darwin-aarch64-2.11.1.zip" cmd = "./vibe-acp" [agent_servers.mistral-vibe.targets.darwin-x86_64] -archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.0/vibe-acp-darwin-x86_64-2.11.0.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.1/vibe-acp-darwin-x86_64-2.11.1.zip" cmd = "./vibe-acp" [agent_servers.mistral-vibe.targets.linux-aarch64] -archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.0/vibe-acp-linux-aarch64-2.11.0.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.1/vibe-acp-linux-aarch64-2.11.1.zip" cmd = "./vibe-acp" [agent_servers.mistral-vibe.targets.linux-x86_64] -archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.0/vibe-acp-linux-x86_64-2.11.0.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.1/vibe-acp-linux-x86_64-2.11.1.zip" cmd = "./vibe-acp" [agent_servers.mistral-vibe.targets.windows-x86_64] -archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.0/vibe-acp-windows-x86_64-2.11.0.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.1/vibe-acp-windows-x86_64-2.11.1.zip" cmd = "./vibe-acp.exe" diff --git a/pyproject.toml b/pyproject.toml index d4f0760..60e7a9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mistral-vibe" -version = "2.11.0" +version = "2.11.1" description = "Minimal CLI coding agent by Mistral" readme = "README.md" requires-python = ">=3.12" diff --git a/tests/acp/test_acp_entrypoint_smoke.py b/tests/acp/test_acp_entrypoint_smoke.py index 1230ea6..b219101 100644 --- a/tests/acp/test_acp_entrypoint_smoke.py +++ b/tests/acp/test_acp_entrypoint_smoke.py @@ -101,22 +101,15 @@ async def _terminate_process(proc: asyncio.subprocess.Process) -> None: await proc.wait() -def _build_env( - vibe_home_dir: Path, *, include_api_key: bool, enable_browser_sign_in: bool = False -) -> dict[str, str]: +def _build_env(vibe_home_dir: Path, *, include_api_key: bool) -> dict[str, str]: env = os.environ.copy() env["PYTHONUNBUFFERED"] = "1" env["VIBE_HOME"] = str(vibe_home_dir) vibe_home_dir.mkdir(parents=True, exist_ok=True) - config_lines = ["enable_telemetry = false"] - if enable_browser_sign_in: - config_lines.append("enable_experimental_browser_sign_in = true") config_file = vibe_home_dir / "config.toml" if not config_file.exists(): - config_file.write_text("\n".join(config_lines) + "\n") - elif enable_browser_sign_in: - config_file.write_text(config_file.read_text() + config_lines[-1] + "\n") + config_file.write_text("enable_telemetry = false\n") if include_api_key: env["MISTRAL_API_KEY"] = "mock" @@ -144,15 +137,10 @@ async def _connect_and_initialize( *, vibe_home_dir: Path, include_api_key: bool, - enable_browser_sign_in: bool = False, terminal_auth: bool = False, delegated_browser_auth: bool = False, ) -> tuple[asyncio.subprocess.Process, Any, Any]: - env = _build_env( - vibe_home_dir, - include_api_key=include_api_key, - enable_browser_sign_in=enable_browser_sign_in, - ) + env = _build_env(vibe_home_dir, include_api_key=include_api_key) proc = await _spawn_vibe_acp(env) try: @@ -219,7 +207,7 @@ async def test_vibe_acp_bootstraps_default_files(vibe_home_dir: Path) -> None: @pytest.mark.asyncio async def test_vibe_acp_initialize_exposes_browser_auth(vibe_home_dir: Path) -> None: proc, initialize_response, _conn = await _connect_and_initialize( - vibe_home_dir=vibe_home_dir, include_api_key=True, enable_browser_sign_in=True + vibe_home_dir=vibe_home_dir, include_api_key=True ) try: @@ -238,10 +226,7 @@ async def test_vibe_acp_initialize_exposes_delegated_browser_auth_when_supported vibe_home_dir: Path, ) -> None: proc, initialize_response, _conn = await _connect_and_initialize( - vibe_home_dir=vibe_home_dir, - include_api_key=True, - enable_browser_sign_in=True, - delegated_browser_auth=True, + vibe_home_dir=vibe_home_dir, include_api_key=True, delegated_browser_auth=True ) try: @@ -266,10 +251,7 @@ async def test_vibe_acp_initialize_exposes_terminal_auth_when_supported( vibe_home_dir: Path, ) -> None: proc, initialize_response, _conn = await _connect_and_initialize( - vibe_home_dir=vibe_home_dir, - include_api_key=True, - enable_browser_sign_in=True, - terminal_auth=True, + vibe_home_dir=vibe_home_dir, include_api_key=True, terminal_auth=True ) try: diff --git a/tests/acp/test_auth_status.py b/tests/acp/test_auth_status.py new file mode 100644 index 0000000..b322a20 --- /dev/null +++ b/tests/acp/test_auth_status.py @@ -0,0 +1,270 @@ +from __future__ import annotations + +import os +from pathlib import Path + +from dotenv import dotenv_values +import pytest + +from vibe.acp.acp_agent_loop import VibeAcpAgentLoop +from vibe.acp.exceptions import InvalidRequestError +from vibe.core.config import ( + DEFAULT_MISTRAL_API_ENV_KEY, + ProviderConfig, + load_dotenv_values, +) +from vibe.core.types import Backend +from vibe.setup.auth import AuthStateKind +from vibe.setup.onboarding.context import OnboardingContext + + +def build_mistral_provider( + *, api_key_env_var: str = DEFAULT_MISTRAL_API_ENV_KEY +) -> ProviderConfig: + return ProviderConfig( + name="mistral", + api_base="https://api.mistral.ai/v1", + api_key_env_var=api_key_env_var, + browser_auth_base_url="https://console.mistral.ai", + browser_auth_api_base_url="https://console.mistral.ai/api", + backend=Backend.MISTRAL, + ) + + +def build_generic_provider( + *, name: str = "custom", api_key_env_var: str = "CUSTOM_API_KEY" +) -> ProviderConfig: + return ProviderConfig( + name=name, + api_base="https://custom.example/v1", + api_key_env_var=api_key_env_var, + backend=Backend.GENERIC, + ) + + +def build_acp_agent_loop( + provider: ProviderConfig, + *, + environ_before_dotenv_load: dict[str, str] | None = None, +) -> VibeAcpAgentLoop: + return VibeAcpAgentLoop( + onboarding_context_loader=lambda: OnboardingContext(provider=provider), + environ_before_dotenv_load=environ_before_dotenv_load, + ) + + +def write_env_file(config_dir: Path, content: str) -> None: + (config_dir / ".env").write_text(content, encoding="utf-8") + + +class TestACPAuthStatus: + @pytest.mark.asyncio + async def test_returns_signed_out_when_no_key_source( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv(DEFAULT_MISTRAL_API_ENV_KEY, raising=False) + acp_agent_loop = build_acp_agent_loop(build_mistral_provider()) + + response = await acp_agent_loop.ext_method("auth/status", {}) + + assert response == { + "authenticated": False, + "authState": AuthStateKind.SIGNED_OUT.value, + "signOutAvailable": False, + } + + @pytest.mark.asyncio + async def test_returns_sign_out_available_for_default_dotenv_key( + self, config_dir: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv(DEFAULT_MISTRAL_API_ENV_KEY, raising=False) + write_env_file(config_dir, f"{DEFAULT_MISTRAL_API_ENV_KEY}=file-key\n") + acp_agent_loop = build_acp_agent_loop(build_mistral_provider()) + + response = await acp_agent_loop.ext_method("auth/status", {}) + + assert response == { + "authenticated": True, + "authState": AuthStateKind.VIBE_HOME_ENV_FILE.value, + "signOutAvailable": True, + } + + @pytest.mark.asyncio + async def test_uses_startup_env_snapshot_for_dotenv_key( + self, config_dir: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv(DEFAULT_MISTRAL_API_ENV_KEY, raising=False) + write_env_file(config_dir, f"{DEFAULT_MISTRAL_API_ENV_KEY}=file-key\n") + environ_before_dotenv_load = os.environ.copy() + load_dotenv_values() + acp_agent_loop = build_acp_agent_loop( + build_mistral_provider(), + environ_before_dotenv_load=environ_before_dotenv_load, + ) + + await acp_agent_loop.ext_method("auth/status", {}) + response = await acp_agent_loop.ext_method("auth/status", {}) + + assert response == { + "authenticated": True, + "authState": AuthStateKind.VIBE_HOME_ENV_FILE.value, + "signOutAvailable": True, + } + + @pytest.mark.asyncio + async def test_returns_process_env_when_key_only_exists_before_dotenv( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv(DEFAULT_MISTRAL_API_ENV_KEY, "process-key") + acp_agent_loop = build_acp_agent_loop(build_mistral_provider()) + + response = await acp_agent_loop.ext_method("auth/status", {}) + + assert response == { + "authenticated": True, + "authState": AuthStateKind.PROCESS_ENV.value, + "signOutAvailable": False, + } + + @pytest.mark.asyncio + async def test_returns_combined_source_when_process_env_existed_before_dotenv( + self, config_dir: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv(DEFAULT_MISTRAL_API_ENV_KEY, "process-key") + write_env_file(config_dir, f"{DEFAULT_MISTRAL_API_ENV_KEY}=file-key\n") + acp_agent_loop = build_acp_agent_loop(build_mistral_provider()) + + response = await acp_agent_loop.ext_method("auth/status", {}) + + assert response == { + "authenticated": True, + "authState": AuthStateKind.VIBE_HOME_ENV_FILE_OVERRIDES_PROCESS_ENV.value, + "signOutAvailable": False, + } + + @pytest.mark.asyncio + async def test_returns_auth_not_required_for_provider_without_env_key( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv(DEFAULT_MISTRAL_API_ENV_KEY, raising=False) + acp_agent_loop = build_acp_agent_loop( + build_generic_provider(name="llamacpp", api_key_env_var="") + ) + + response = await acp_agent_loop.ext_method("auth/status", {}) + + assert response == { + "authenticated": True, + "authState": AuthStateKind.AUTH_NOT_REQUIRED.value, + "signOutAvailable": False, + } + + @pytest.mark.asyncio + async def test_returns_unsupported_provider_for_custom_key_setup( + self, config_dir: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv("CUSTOM_API_KEY", raising=False) + write_env_file(config_dir, "CUSTOM_API_KEY=file-key\n") + acp_agent_loop = build_acp_agent_loop(build_generic_provider()) + + response = await acp_agent_loop.ext_method("auth/status", {}) + + assert response == { + "authenticated": True, + "authState": AuthStateKind.UNSUPPORTED_PROVIDER.value, + "signOutAvailable": False, + } + + +class TestACPAuthSignOut: + @pytest.mark.asyncio + async def test_removes_default_dotenv_key( + self, config_dir: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv(DEFAULT_MISTRAL_API_ENV_KEY, raising=False) + write_env_file( + config_dir, f"{DEFAULT_MISTRAL_API_ENV_KEY}=file-key\nOTHER_KEY=other\n" + ) + acp_agent_loop = build_acp_agent_loop(build_mistral_provider()) + + response = await acp_agent_loop.ext_method("auth/signOut", {}) + + env_values = dotenv_values(config_dir / ".env") + assert response == {} + assert DEFAULT_MISTRAL_API_ENV_KEY not in env_values + assert env_values["OTHER_KEY"] == "other" + assert DEFAULT_MISTRAL_API_ENV_KEY not in os.environ + assert await acp_agent_loop.ext_method("auth/status", {}) == { + "authenticated": False, + "authState": AuthStateKind.SIGNED_OUT.value, + "signOutAvailable": False, + } + + @pytest.mark.asyncio + async def test_refuses_process_env_key( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv(DEFAULT_MISTRAL_API_ENV_KEY, "process-key") + acp_agent_loop = build_acp_agent_loop(build_mistral_provider()) + + with pytest.raises(InvalidRequestError, match=AuthStateKind.PROCESS_ENV.value): + await acp_agent_loop.ext_method("auth/signOut", {}) + + assert os.environ[DEFAULT_MISTRAL_API_ENV_KEY] == "process-key" + + @pytest.mark.asyncio + async def test_refuses_combined_dotenv_and_process_env_key( + self, config_dir: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv(DEFAULT_MISTRAL_API_ENV_KEY, "process-key") + write_env_file(config_dir, f"{DEFAULT_MISTRAL_API_ENV_KEY}=file-key\n") + acp_agent_loop = build_acp_agent_loop(build_mistral_provider()) + + with pytest.raises( + InvalidRequestError, + match=AuthStateKind.VIBE_HOME_ENV_FILE_OVERRIDES_PROCESS_ENV.value, + ): + await acp_agent_loop.ext_method("auth/signOut", {}) + + assert dotenv_values(config_dir / ".env")[DEFAULT_MISTRAL_API_ENV_KEY] == ( + "file-key" + ) + + @pytest.mark.asyncio + async def test_refuses_unsupported_provider_key( + self, config_dir: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv("CUSTOM_API_KEY", raising=False) + write_env_file(config_dir, "CUSTOM_API_KEY=file-key\n") + acp_agent_loop = build_acp_agent_loop(build_generic_provider()) + + with pytest.raises( + InvalidRequestError, match=AuthStateKind.UNSUPPORTED_PROVIDER.value + ): + await acp_agent_loop.ext_method("auth/signOut", {}) + + assert dotenv_values(config_dir / ".env")["CUSTOM_API_KEY"] == "file-key" + + @pytest.mark.asyncio + async def test_refuses_auth_not_required( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv(DEFAULT_MISTRAL_API_ENV_KEY, raising=False) + acp_agent_loop = build_acp_agent_loop( + build_generic_provider(name="llamacpp", api_key_env_var="") + ) + + with pytest.raises( + InvalidRequestError, match=AuthStateKind.AUTH_NOT_REQUIRED.value + ): + await acp_agent_loop.ext_method("auth/signOut", {}) + + @pytest.mark.asyncio + async def test_refuses_signed_out_state( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.delenv(DEFAULT_MISTRAL_API_ENV_KEY, raising=False) + acp_agent_loop = build_acp_agent_loop(build_mistral_provider()) + + with pytest.raises(InvalidRequestError, match=AuthStateKind.SIGNED_OUT.value): + await acp_agent_loop.ext_method("auth/signOut", {}) diff --git a/tests/acp/test_authenticate.py b/tests/acp/test_authenticate.py index dc7b9fa..b40139e 100644 --- a/tests/acp/test_authenticate.py +++ b/tests/acp/test_authenticate.py @@ -55,17 +55,11 @@ def build_unsupported_provider() -> ProviderConfig: class MutableOnboardingContextLoader: - def __init__( - self, provider: ProviderConfig, *, enable_browser_sign_in: bool = True - ) -> None: + def __init__(self, provider: ProviderConfig) -> None: self.provider = provider - self.enable_browser_sign_in = enable_browser_sign_in def __call__(self) -> OnboardingContext: - return OnboardingContext( - provider=self.provider, - enable_experimental_browser_sign_in=self.enable_browser_sign_in, - ) + return OnboardingContext(provider=self.provider) class FakeBrowserSignInService: @@ -122,16 +116,13 @@ class InMemoryApiKeyPersister: def build_acp_agent( *, provider: ProviderConfig | None = None, - enable_browser_sign_in: bool = True, browser_sign_in: FakeBrowserSignInService | None = None, api_key_persister: InMemoryApiKeyPersister | None = None, ) -> tuple[VibeAcpAgentLoop, MutableOnboardingContextLoader, InMemoryApiKeyPersister]: provider = provider or build_mistral_provider() browser_sign_in = browser_sign_in or FakeBrowserSignInService() api_key_persister = api_key_persister or InMemoryApiKeyPersister() - context_loader = MutableOnboardingContextLoader( - provider, enable_browser_sign_in=enable_browser_sign_in - ) + context_loader = MutableOnboardingContextLoader(provider) return ( VibeAcpAgentLoop( @@ -213,42 +204,6 @@ class TestACPAuthenticate: ): await acp_agent_loop.authenticate("browser-auth") - @pytest.mark.asyncio - async def test_authenticate_rejects_browser_sign_in_when_flag_disabled( - self, - ) -> None: - browser_sign_in = FakeBrowserSignInService(api_key="api-key") - acp_agent_loop, _, api_key_persister = build_acp_agent( - browser_sign_in=browser_sign_in, enable_browser_sign_in=False - ) - - with pytest.raises( - InvalidRequestError, - match="Browser sign-in is not available for the configured provider.", - ): - await acp_agent_loop.authenticate("browser-auth") - - assert api_key_persister.saved == [] - assert browser_sign_in.close_count == 0 - - @pytest.mark.asyncio - async def test_authenticate_rejects_delegated_browser_sign_in_when_flag_disabled( - self, - ) -> None: - browser_sign_in = FakeBrowserSignInService() - acp_agent_loop, _, api_key_persister = build_acp_agent( - browser_sign_in=browser_sign_in, enable_browser_sign_in=False - ) - - with pytest.raises( - InvalidRequestError, - match="Browser sign-in is not available for the configured provider.", - ): - await acp_agent_loop.authenticate("browser-auth-delegated") - - assert api_key_persister.saved == [] - assert browser_sign_in.close_count == 0 - @pytest.mark.asyncio async def test_authenticate_surfaces_start_failures(self) -> None: browser_sign_in = FakeBrowserSignInService( @@ -318,7 +273,7 @@ class TestACPAuthenticate: assert api_key_persister.saved == [(start_provider, "api-key")] @pytest.mark.asyncio - async def test_authenticate_delegated_completion_allows_flag_disabled_after_start( + async def test_authenticate_delegated_completion_uses_started_provider( self, ) -> None: provider = build_mistral_provider() @@ -330,7 +285,7 @@ class TestACPAuthenticate: attempt_id = require_auth_meta(start_response, "browser-auth-delegated")[ "attemptId" ] - context_loader.enable_browser_sign_in = False + context_loader.provider = build_unsupported_provider() await acp_agent_loop.authenticate( "browser-auth-delegated", action="complete", attemptId=attempt_id diff --git a/tests/acp/test_initialize.py b/tests/acp/test_initialize.py index 0409ffc..158a40f 100644 --- a/tests/acp/test_initialize.py +++ b/tests/acp/test_initialize.py @@ -35,13 +35,10 @@ def build_mistral_provider() -> ProviderConfig: ) -def build_acp_agent_loop( - *, provider: ProviderConfig | None = None, enable_browser_sign_in: bool = True -) -> VibeAcpAgentLoop: +def build_acp_agent_loop(*, provider: ProviderConfig | None = None) -> VibeAcpAgentLoop: return VibeAcpAgentLoop( onboarding_context_loader=lambda: OnboardingContext( - provider=provider or build_mistral_provider(), - enable_experimental_browser_sign_in=enable_browser_sign_in, + provider=provider or build_mistral_provider() ) ) @@ -65,7 +62,7 @@ class TestACPInitialize: ), ) assert response.agent_info == Implementation( - name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.11.0" + name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.11.1" ) assert response.auth_methods is not None @@ -97,7 +94,7 @@ class TestACPInitialize: ), ) assert response.agent_info == Implementation( - name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.11.0" + name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.11.1" ) assert response.auth_methods is not None @@ -143,16 +140,6 @@ class TestACPInitialize: assert delegated_browser_auth_method.name == BROWSER_AUTH_NAME assert delegated_browser_auth_method.description == BROWSER_AUTH_DESCRIPTION - @pytest.mark.asyncio - async def test_initialize_omits_browser_auth_when_experimental_flag_disabled( - self, - ) -> None: - acp_agent_loop = build_acp_agent_loop(enable_browser_sign_in=False) - - response = await acp_agent_loop.initialize(protocol_version=PROTOCOL_VERSION) - - assert response.auth_methods == [] - @pytest.mark.asyncio async def test_initialize_omits_browser_auth_when_provider_unsupported( self, diff --git a/tests/cli/test_initial_agent_name.py b/tests/cli/test_initial_agent_name.py index 037678d..6e16c3a 100644 --- a/tests/cli/test_initial_agent_name.py +++ b/tests/cli/test_initial_agent_name.py @@ -32,20 +32,18 @@ def test_defaults_to_default_when_unset_in_config_and_args() -> None: assert get_initial_agent_name(args, config) == BuiltinAgentName.DEFAULT -def test_programmatic_mode_promotes_default_to_auto_approve() -> None: +def test_programmatic_mode_falls_back_to_default_agent() -> None: config = VibeConfig.model_construct(default_agent=BuiltinAgentName.DEFAULT) args = _make_args(agent=None, prompt="hello") - assert get_initial_agent_name(args, config) == BuiltinAgentName.AUTO_APPROVE + assert get_initial_agent_name(args, config) == BuiltinAgentName.DEFAULT -def test_programmatic_mode_ignores_config_default_agent_when_args_agent_is_none() -> ( - None -): +def test_programmatic_mode_uses_config_default_agent_when_args_agent_is_none() -> None: config = VibeConfig.model_construct(default_agent=BuiltinAgentName.PLAN) args = _make_args(agent=None, prompt="hello") - assert get_initial_agent_name(args, config) == BuiltinAgentName.AUTO_APPROVE + assert get_initial_agent_name(args, config) == BuiltinAgentName.PLAN def test_programmatic_mode_keeps_explicit_agent_arg() -> None: diff --git a/tests/cli/test_programmatic_setup.py b/tests/cli/test_programmatic_setup.py index 0ee2504..dd670d9 100644 --- a/tests/cli/test_programmatic_setup.py +++ b/tests/cli/test_programmatic_setup.py @@ -5,6 +5,7 @@ from pathlib import Path import pytest +from tests.conftest import build_test_vibe_config from vibe.cli import cli as cli_mod, entrypoint as entrypoint_mod from vibe.core.config import MissingAPIKeyError from vibe.core.trusted_folders import trusted_folders_manager @@ -16,6 +17,7 @@ def _make_args(**overrides: object) -> argparse.Namespace: "prompt": "hello", "max_turns": None, "max_price": None, + "max_tokens": None, "enabled_tools": None, "output": "text", "agent": "default", @@ -193,3 +195,32 @@ def test_session_trust_does_not_write_to_disk( assert trusted_folders_manager.is_trusted(project) is True assert not trust_file.exists() + + +def test_run_cli_passes_max_tokens_to_run_programmatic( + monkeypatch: pytest.MonkeyPatch, +) -> None: + args = _make_args(max_tokens=123) + call: dict[str, object] = {} + config = build_test_vibe_config() + + monkeypatch.setattr(cli_mod, "bootstrap_config_files", lambda: None) + monkeypatch.setattr(cli_mod, "load_config_or_exit", lambda interactive: config) + monkeypatch.setattr(cli_mod, "load_hooks_from_fs", lambda _config: None) + monkeypatch.setattr(cli_mod, "setup_tracing", lambda _config: None) + monkeypatch.setattr(cli_mod, "load_session", lambda _args, _config: None) + monkeypatch.setattr(cli_mod, "get_prompt_from_stdin", lambda: None) + monkeypatch.setattr(cli_mod, "warn_if_workdir_trust_is_unset", lambda: None) + monkeypatch.setattr(cli_mod, "get_initial_agent_name", lambda _args, _config: "x") + + def fake_run_programmatic(**kwargs: object) -> str: + call.update(kwargs) + return "done" + + monkeypatch.setattr(cli_mod, "run_programmatic", fake_run_programmatic) + + with pytest.raises(SystemExit) as exc_info: + cli_mod.run_cli(args) + + assert exc_info.value.code == 0 + assert call["max_session_tokens"] == 123 diff --git a/tests/cli/test_ui_session_incremental_renderer.py b/tests/cli/test_ui_session_incremental_renderer.py index a7f6a8e..f9003e7 100644 --- a/tests/cli/test_ui_session_incremental_renderer.py +++ b/tests/cli/test_ui_session_incremental_renderer.py @@ -166,7 +166,15 @@ async def test_ui_session_incremental_loader_keeps_top_alignment_when_not_scroll app = VibeApp(agent_loop=agent_loop, plan_offer_gateway=_pro_plan_gateway()) - async with app.run_test(size=(120, 80)) as pilot: + # Each UserMessage renders as ~3 rows (top margin + content + separator); + # add chrome (input box, banner, status) so all messages fit without scrolling. + user_message_rows = 3 + chrome_rows = 40 + viewport_height = ( + HISTORY_RESUME_TAIL_MESSAGES + 1 + ) * user_message_rows + chrome_rows + + async with app.run_test(size=(120, viewport_height)) as pilot: await _wait_for_load_more(app, pilot.pause) chat = app.query_one("#chat", ChatScroll) assert chat.max_scroll_y == 0 diff --git a/tests/conftest.py b/tests/conftest.py index 2fd7530..3273ca8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ import tomli_w from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway from tests.stubs.fake_backend import FakeBackend +from tests.stubs.fake_mcp_registry import FakeMCPRegistry from tests.stubs.fake_voice_manager import FakeVoiceManager from tests.update_notifier.adapters.fake_update_cache_repository import ( FakeUpdateCacheRepository, @@ -26,6 +27,7 @@ from vibe.core.config import ( VibeConfig, ) from vibe.core.config.harness_files import ( + HarnessFilesManager, init_harness_files_manager, reset_harness_files_manager, ) @@ -201,6 +203,31 @@ def telemetry_events(monkeypatch: pytest.MonkeyPatch) -> list[dict[str, Any]]: return events +@pytest.fixture +def mock_prompts_dirs( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> tuple[Path, Path]: + project = tmp_path / "project" / ".vibe" / "prompts" + user = tmp_path / "home" / ".vibe" / "prompts" + project.mkdir(parents=True) + user.mkdir(parents=True) + + class _MockManager(HarnessFilesManager): + @property + def project_prompts_dirs(self) -> list[Path]: + return [project] + + @property + def user_prompts_dirs(self) -> list[Path]: + return [user] + + monkeypatch.setattr( + "vibe.core.prompts.get_harness_files_manager", + lambda: _MockManager(sources=("user",)), + ) + return project, user + + @pytest.fixture def vibe_app() -> VibeApp: return build_test_vibe_app() @@ -259,6 +286,7 @@ def build_test_agent_loop( agent_name=agent_name, backend=backend or FakeBackend(), enable_streaming=enable_streaming, + mcp_registry=kwargs.pop("mcp_registry", FakeMCPRegistry()), **kwargs, ) diff --git a/tests/core/test_compaction.py b/tests/core/test_compaction.py new file mode 100644 index 0000000..53cc283 --- /dev/null +++ b/tests/core/test_compaction.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from vibe.core.compaction import collect_prior_user_messages +from vibe.core.types import LLMMessage, Role + +_PREFIX = "Another language model started to solve this problem" + + +def _user(content: str, *, injected: bool = False) -> LLMMessage: + return LLMMessage(role=Role.user, content=content, injected=injected) + + +def test_empty_messages() -> None: + assert collect_prior_user_messages([], _PREFIX) == [] + + +def test_only_non_user_messages() -> None: + messages = [ + LLMMessage(role=Role.system, content="sys"), + LLMMessage(role=Role.assistant, content="hi"), + ] + assert collect_prior_user_messages(messages, _PREFIX) == [] + + +def test_single_user_message_preserved() -> None: + messages = [LLMMessage(role=Role.system, content="sys"), _user("first question")] + out = collect_prior_user_messages(messages, _PREFIX) + assert [m.content for m in out] == ["first question"] + + +def test_chronological_order_preserved() -> None: + messages = [_user("first"), _user("second"), _user("third")] + out = collect_prior_user_messages(messages, _PREFIX) + assert [m.content for m in out] == ["first", "second", "third"] + + +def test_injected_messages_filtered_out() -> None: + messages = [ + _user("real ask"), + _user("middleware reminder", injected=True), + _user("follow-up"), + ] + out = collect_prior_user_messages(messages, _PREFIX) + assert [m.content for m in out] == ["real ask", "follow-up"] + + +def test_empty_content_filtered_out() -> None: + messages = [_user(""), _user("real")] + out = collect_prior_user_messages(messages, _PREFIX) + assert [m.content for m in out] == ["real"] + + +def test_prior_summary_filtered_out() -> None: + # A user message starting with the summary prefix represents a previous + # compaction summary and must not be re-injected (would stack). + messages = [ + _user("original ask"), + _user(f"{_PREFIX}\nold summary content"), + _user("newer ask"), + ] + out = collect_prior_user_messages(messages, _PREFIX) + assert [m.content for m in out] == ["original ask", "newer ask"] + + +def test_budget_drops_oldest_first() -> None: + # max_tokens=2 → 8 char budget. Walks newest-first, so "old" gets dropped. + messages = [ + _user("old message that is long enough to matter"), + _user("abc"), # 1 token, fits + _user("def"), # 1 token, fits + ] + out = collect_prior_user_messages(messages, _PREFIX, max_tokens=2) + assert [m.content for m in out] == ["abc", "def"] + + +def test_spillover_message_middle_truncated() -> None: + # newest fits whole, middle one is partially trimmed, oldest dropped. + messages = [ + _user("OLDEST" + "x" * 10_000 + "OLDEST_END"), + _user("MIDDLE_HEAD" + "y" * 1_000 + "MIDDLE_TAIL"), + _user("recent"), # ~2 tokens + ] + out = collect_prior_user_messages(messages, _PREFIX, max_tokens=50) + assert len(out) == 2 # oldest dropped + assert out[-1].content == "recent" + middle = out[0].content + assert middle is not None + assert middle.startswith("MIDDLE_HEAD") + assert middle.endswith("MIDDLE_TAIL") + assert "[... truncated ...]" in middle + + +def test_fresh_message_ids() -> None: + # Returned messages must have new message_ids — they'll live in a fresh + # session and reusing the source ids would cause collisions. + original = _user("hello") + out = collect_prior_user_messages([original], _PREFIX) + assert len(out) == 1 + assert out[0].message_id != original.message_id + + +def test_only_assistant_and_system_around_users() -> None: + messages = [ + LLMMessage(role=Role.system, content="sys"), + _user("u1"), + LLMMessage(role=Role.assistant, content="a1"), + _user("u2"), + LLMMessage(role=Role.assistant, content="a2"), + ] + out = collect_prior_user_messages(messages, _PREFIX) + assert [m.content for m in out] == ["u1", "u2"] + assert all(m.role == Role.user for m in out) diff --git a/tests/core/test_config_layer.py b/tests/core/test_config_layer.py index ebdfc96..f74de98 100644 --- a/tests/core/test_config_layer.py +++ b/tests/core/test_config_layer.py @@ -10,6 +10,7 @@ from vibe.core.config.layer import ( ConfigLayer, LayerImplementationError, RawConfig, + TrustNotResolvedError, UntrustedLayerError, ) @@ -122,6 +123,7 @@ async def test_resolve_trust_no_callback_when_unchanged() -> None: @pytest.mark.asyncio async def test_grant_trust() -> None: layer = StubLayer(trusted=False) + await layer.resolve_trust() await layer.grant_trust() assert layer.is_trusted is True @@ -129,8 +131,10 @@ async def test_grant_trust() -> None: @pytest.mark.asyncio async def test_grant_trust_fires_callback() -> None: layer = ObservableStubLayer(trusted=False) + await layer.resolve_trust() + layer.trust_changes.clear() await layer.grant_trust() - assert layer.trust_changes == [(None, True)] + assert layer.trust_changes == [(False, True)] @pytest.mark.asyncio @@ -184,10 +188,17 @@ async def test_on_trust_changed_failure_preserves_state() -> None: layer = FailingLayer(trusted=False) assert layer.is_trusted is None + # Resolve trust without failing (so we can test grant_trust separately) + layer.should_fail = False + await layer.resolve_trust() + assert layer.is_trusted is False + + # Now make _on_trust_changed fail during grant_trust + layer.should_fail = True with pytest.raises(LayerImplementationError, match="_on_trust_changed") as exc_info: await layer.grant_trust() assert isinstance(exc_info.value.__cause__, RuntimeError) - assert layer.is_trusted is None + assert layer.is_trusted is False layer.should_fail = False await layer.grant_trust() @@ -251,6 +262,7 @@ async def test_load_untrusted_raises() -> None: @pytest.mark.asyncio async def test_load_after_grant_trust() -> None: layer = StubLayer(trusted=False) + await layer.resolve_trust() await layer.grant_trust() result = await layer.load() assert isinstance(result, RawConfig) @@ -392,6 +404,42 @@ async def test_apply_not_implemented() -> None: await layer.apply({"op": "set"}) +@pytest.mark.asyncio +async def test_grant_trust_raises_when_trust_not_resolved() -> None: + """grant_trust() must fail if trust has never been resolved.""" + layer = StubLayer(trusted=True, data={"k": "v"}) + + # Trust not yet resolved — grant_trust should raise + with pytest.raises(TrustNotResolvedError): + await layer.grant_trust() + assert layer.is_trusted is None + + # Resolve trust first, then grant_trust succeeds + await layer.resolve_trust() + await layer.grant_trust() + assert layer.is_trusted is True + + +@pytest.mark.asyncio +async def test_revoke_trust_raises_when_trust_not_resolved() -> None: + """revoke_trust() must fail if trust has never been resolved.""" + trust_store: dict[str, bool] = {"/tmp/proj": True} + layer = FakeLocalProjectLayer( + project_path="/tmp/proj", data={"key": "val"}, trust_store=trust_store + ) + + # Trust not yet resolved — revoke_trust should raise + with pytest.raises(TrustNotResolvedError): + await layer.revoke_trust() + assert layer.is_trusted is None + + # Resolve trust (storage says trusted), then revoke succeeds + await layer.resolve_trust() + assert layer.is_trusted is True + await layer.revoke_trust() + assert layer.is_trusted is False + + # Scenario: LocalUserConfigLayer diff --git a/tests/core/test_overrides_layer.py b/tests/core/test_overrides_layer.py new file mode 100644 index 0000000..7ed0b24 --- /dev/null +++ b/tests/core/test_overrides_layer.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import pytest + +from vibe.core.config.layers.overrides import OverridesLayer + + +@pytest.mark.asyncio +async def test_returns_provided_dict() -> None: + data = {"active_model": "custom", "api_timeout": 30.0} + layer = OverridesLayer(data=data) + result = await layer.load() + assert result.model_extra == data + + +@pytest.mark.asyncio +async def test_always_trusted() -> None: + layer = OverridesLayer(data={}) + assert await layer.resolve_trust() is True + + +@pytest.mark.asyncio +async def test_apply_raises_not_implemented() -> None: + layer = OverridesLayer(data={}) + with pytest.raises(NotImplementedError, match="M2"): + await layer.apply({"op": "set"}) + + +@pytest.mark.asyncio +async def test_default_name() -> None: + layer = OverridesLayer(data={}) + assert layer.name == "overrides" + + +@pytest.mark.asyncio +async def test_custom_name() -> None: + layer = OverridesLayer(data={}, name="cli-overrides") + assert layer.name == "cli-overrides" + + +@pytest.mark.asyncio +async def test_empty_dict() -> None: + layer = OverridesLayer(data={}) + result = await layer.load() + assert result.model_extra == {} + + +@pytest.mark.asyncio +async def test_nested_data_preserved() -> None: + data = {"models": {"active_model": "test"}, "tools": {"enabled_tools": ["a"]}} + layer = OverridesLayer(data=data) + result = await layer.load() + assert result.model_extra == data + + +@pytest.mark.asyncio +async def test_force_reload_returns_same_data() -> None: + data = {"key": "value"} + layer = OverridesLayer(data=data) + await layer.load() + result = await layer.load(force=True) + assert result.model_extra == data + + +@pytest.mark.asyncio +async def test_output_isolated_from_internal_data() -> None: + data = {"key": "value"} + layer = OverridesLayer(data=data) + result = await layer.load() + # Mutating the returned model_extra must not affect subsequent loads + assert result.model_extra is not None + result.model_extra["key"] = "mutated" + result2 = await layer.load(force=True) + assert result2.model_extra == {"key": "value"} + + +@pytest.mark.asyncio +async def test_live_reference_picks_up_caller_mutation() -> None: + data: dict[str, object] = {"key": "original"} + layer = OverridesLayer(data=data) + await layer.load() + data["key"] = "updated" + result = await layer.load(force=True) + assert result.model_extra == {"key": "updated"} diff --git a/tests/core/test_project_config_layer.py b/tests/core/test_project_config_layer.py new file mode 100644 index 0000000..c8b74f3 --- /dev/null +++ b/tests/core/test_project_config_layer.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +from pathlib import Path + +import pytest + +from vibe.core.config.layer import UntrustedLayerError +from vibe.core.config.layers.project import ProjectConfigLayer +from vibe.core.paths._vibe_home import GlobalPath +from vibe.core.trusted_folders import trusted_folders_manager + + +@pytest.mark.asyncio +async def test_reads_toml_when_trusted(tmp_working_directory: Path) -> None: + trusted_folders_manager.add_trusted(tmp_working_directory) + config_path = tmp_working_directory / ".vibe" / "config.toml" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text('active_model = "project-model"\n') + + layer = ProjectConfigLayer(path=tmp_working_directory) + data = await layer.load() + assert data.model_extra == {"active_model": "project-model"} + + +@pytest.mark.asyncio +async def test_untrusted_raises(tmp_working_directory: Path) -> None: + trusted_folders_manager.add_untrusted(tmp_working_directory) + config_path = tmp_working_directory / ".vibe" / "config.toml" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text('active_model = "project-model"\n') + + layer = ProjectConfigLayer(path=tmp_working_directory) + with pytest.raises(UntrustedLayerError): + await layer.load() + + +@pytest.mark.asyncio +async def test_unresolved_trust_defaults_to_untrusted( + tmp_working_directory: Path, +) -> None: + config_path = tmp_working_directory / ".vibe" / "config.toml" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text('key = "value"\n') + + layer = ProjectConfigLayer(path=tmp_working_directory) + with pytest.raises(UntrustedLayerError): + await layer.load() + + +@pytest.mark.asyncio +async def test_missing_file_returns_empty(tmp_working_directory: Path) -> None: + trusted_folders_manager.add_trusted(tmp_working_directory) + + layer = ProjectConfigLayer(path=tmp_working_directory) + data = await layer.load() + assert data.model_extra == {} + + +@pytest.mark.asyncio +async def test_default_name(tmp_working_directory: Path) -> None: + layer = ProjectConfigLayer(path=tmp_working_directory) + assert layer.name == "project-toml" + + +@pytest.mark.asyncio +async def test_apply_raises_not_implemented(tmp_working_directory: Path) -> None: + layer = ProjectConfigLayer(path=tmp_working_directory) + with pytest.raises(NotImplementedError, match="M2"): + await layer.apply({"op": "set"}) + + +@pytest.mark.asyncio +async def test_trust_uses_path_parent_for_resolution( + tmp_working_directory: Path, +) -> None: + project_dir = tmp_working_directory / "my-project" + project_dir.mkdir() + + config_path = project_dir / ".vibe" / "config.toml" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text('value = "trusted"\n') + trusted_folders_manager.add_trusted(project_dir / ".vibe") + + layer = ProjectConfigLayer(path=project_dir) + data = await layer.load() + assert data.model_extra == {"value": "trusted"} + + +@pytest.mark.asyncio +async def test_grant_trust_marks_folder_trusted(tmp_working_directory: Path) -> None: + config_path = tmp_working_directory / ".vibe" / "config.toml" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text('key = "value"\n') + layer = ProjectConfigLayer(path=tmp_working_directory) + trusted_folders_manager.add_untrusted(tmp_working_directory / ".vibe") + + await layer.resolve_trust() + await layer.grant_trust() + assert trusted_folders_manager.is_trusted(tmp_working_directory / ".vibe") is True + + +@pytest.mark.asyncio +async def test_revoke_trust_marks_folder_untrusted(tmp_working_directory: Path) -> None: + config_path = tmp_working_directory / ".vibe" / "config.toml" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text('key = "value"\n') + layer = ProjectConfigLayer(path=tmp_working_directory) + trusted_folders_manager.add_trusted(tmp_working_directory / ".vibe") + + await layer.resolve_trust() + await layer.revoke_trust() + assert trusted_folders_manager.is_trusted(tmp_working_directory / ".vibe") is False + + +@pytest.mark.asyncio +async def test_grant_trust_without_config_file_is_noop( + tmp_working_directory: Path, +) -> None: + layer = ProjectConfigLayer(path=tmp_working_directory) + trusted_folders_manager.add_untrusted(tmp_working_directory) + + await layer.grant_trust() + assert trusted_folders_manager.is_trusted(tmp_working_directory) is False + + +@pytest.mark.asyncio +async def test_revoke_trust_without_config_file_is_noop( + tmp_working_directory: Path, +) -> None: + layer = ProjectConfigLayer(path=tmp_working_directory) + trusted_folders_manager.add_trusted(tmp_working_directory) + + await layer.revoke_trust() + assert trusted_folders_manager.is_trusted(tmp_working_directory) is True + + +@pytest.mark.asyncio +async def test_trust_stored_at_vibe_subdir(tmp_working_directory: Path) -> None: + config_path = tmp_working_directory / ".vibe" / "config.toml" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text('key = "value"\n') + layer = ProjectConfigLayer(path=tmp_working_directory) + + await layer.resolve_trust() + await layer.grant_trust() + + assert ( + str((tmp_working_directory / ".vibe").resolve()) + in trusted_folders_manager._trusted + ) + assert str(tmp_working_directory.resolve()) not in trusted_folders_manager._trusted + + +@pytest.mark.asyncio +async def test_find_file_prefers_closest_config(tmp_working_directory: Path) -> None: + parent_config = tmp_working_directory / ".vibe" / "config.toml" + parent_config.parent.mkdir(parents=True, exist_ok=True) + parent_config.write_text('level = "parent"\n') + + child = tmp_working_directory / "child" + child.mkdir() + child_config = child / ".vibe" / "config.toml" + child_config.parent.mkdir(parents=True, exist_ok=True) + child_config.write_text('level = "child"\n') + + trusted_folders_manager.add_trusted(child) + layer = ProjectConfigLayer(path=child) + data = await layer.load() + assert data.model_extra == {"level": "child"} + + +@pytest.mark.asyncio +async def test_find_file_result_is_cached(tmp_working_directory: Path) -> None: + config_path = tmp_working_directory / ".vibe" / "config.toml" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text("x = 1\n") + trusted_folders_manager.add_trusted(tmp_working_directory) + + layer = ProjectConfigLayer(path=tmp_working_directory) + await layer.load() + + assert layer._is_set is True + cached_path = layer._config_file_path + await layer.resolve_trust() + assert layer._config_file_path is cached_path + + +@pytest.mark.asyncio +async def test_config_file_added_after_first_search_is_not_detected( + tmp_working_directory: Path, +) -> None: + layer = ProjectConfigLayer(path=tmp_working_directory) + await layer._find_config_file() + + assert layer._is_set is True + assert layer._config_file_path is None + + config_path = tmp_working_directory / ".vibe" / "config.toml" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text('active_model = "new-model"\n') + + await layer._find_config_file() + + assert layer._config_file_path is None + + +@pytest.mark.asyncio +async def test_finds_config_in_parent_directory(tmp_working_directory: Path) -> None: + trusted_folders_manager.add_trusted(tmp_working_directory) + config_path = tmp_working_directory / ".vibe" / "config.toml" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text('active_model = "parent-model"\n') + + subdir = tmp_working_directory / "sub" / "project" + subdir.mkdir(parents=True) + + layer = ProjectConfigLayer(path=subdir) + data = await layer.load() + assert data.model_extra == {"active_model": "parent-model"} + + +@pytest.mark.asyncio +async def test_trusted_ancestor_satisfies_trust_check( + tmp_working_directory: Path, +) -> None: + parent = tmp_working_directory / "workspace" + parent.mkdir() + child = parent / "project" + child.mkdir() + + trusted_folders_manager.add_trusted(parent) + + config_path = child / ".vibe" / "config.toml" + config_path.parent.mkdir(parents=True) + config_path.write_text('active_model = "child-model"\n') + + layer = ProjectConfigLayer(path=child) + data = await layer.load() + assert data.model_extra == {"active_model": "child-model"} + + +@pytest.mark.asyncio +async def test_grant_trust_then_load_reads_config_without_prior_resolve( + tmp_working_directory: Path, +) -> None: + config_path = tmp_working_directory / ".vibe" / "config.toml" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text('key = "value"\n') + + layer = ProjectConfigLayer(path=tmp_working_directory) + await layer.resolve_trust() + await layer.grant_trust() + data = await layer.load() + + assert data.model_extra == {"key": "value"} + + +@pytest.mark.asyncio +async def test_walk_stops_at_vibe_home_parent( + tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch +) -> None: + # Create an isolated tmp root and point VIBE_HOME into it + tmp_root = tmp_path_factory.mktemp("walk_stop") + fake_vibe_home = tmp_root / ".vibe" + fake_vibe_home.mkdir() + monkeypatch.setattr( + "vibe.core.config.layers.project.VIBE_HOME", GlobalPath(lambda: fake_vibe_home) + ) + + # Place a config at tmp_root/.vibe/config.toml — should NOT be picked up + home_config = fake_vibe_home / "config.toml" + home_config.write_text('active_model = "home-model"\n') + + # subdir lives inside tmp_root so the walk would reach the config without the stop guard + subdir = tmp_root / "vibe-test-project" + subdir.mkdir() + trusted_folders_manager.add_trusted(subdir) + + layer = ProjectConfigLayer(path=subdir) + data = await layer.load() + assert data.model_extra == {} diff --git a/tests/core/test_teleport_experimental_nuage.py b/tests/core/test_teleport_experimental_nuage.py index 3559ffb..cf04db4 100644 --- a/tests/core/test_teleport_experimental_nuage.py +++ b/tests/core/test_teleport_experimental_nuage.py @@ -9,6 +9,7 @@ from vibe.core.teleport.errors import ServiceTeleportError from vibe.core.teleport.experimental_nuage import ( ExperimentalNuageClient, ExperimentalNuageContext, + ExperimentalNuageDiff, ExperimentalNuageMessage, ExperimentalNuageRepository, ExperimentalNuageRequest, @@ -125,6 +126,63 @@ async def test_start_omits_empty_branch() -> None: } +@pytest.mark.asyncio +async def test_start_serializes_commit_sha_and_diff() -> None: + seen_body: dict[str, object] | None = None + + async def handler(request: httpx.Request) -> httpx.Response: + nonlocal seen_body + seen_body = json.loads(request.content) + return httpx.Response( + 200, + json={ + "sessionId": "s", + "webSessionId": "ws", + "projectId": "p", + "status": "running", + "url": "https://chat.example.com/code/p/ws", + }, + ) + + request = ExperimentalNuageRequest( + idempotency_key="idem-1", + message=ExperimentalNuageMessage( + parts=[ExperimentalNuageTextPart(text="prompt")] + ), + context=ExperimentalNuageContext( + repositories=[ + ExperimentalNuageRepository( + repo_url="https://github.com/owner/repo", + branch="main", + commit_sha="abc123", + diff=ExperimentalNuageDiff( + format="git-diff", + encoding="base64", + compression="zstd", + content="ZGlmZnM=", + ), + ) + ] + ), + ) + async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + nuage = ExperimentalNuageClient( + "https://chat.example.com", "api-key", client=client + ) + await nuage.start(request) + + assert seen_body is not None + repos = seen_body["context"]["repositories"] + assert len(repos) == 1 + assert repos[0]["commitSha"] == "abc123" + assert repos[0]["diff"] == { + "format": "git-diff", + "encoding": "base64", + "compression": "zstd", + "content": "ZGlmZnM=", + } + + @pytest.mark.asyncio async def test_start_raises_for_unsuccessful_response() -> None: async def handler(request: httpx.Request) -> httpx.Response: diff --git a/tests/core/test_teleport_service.py b/tests/core/test_teleport_service.py index 52752ea..b7be892 100644 --- a/tests/core/test_teleport_service.py +++ b/tests/core/test_teleport_service.py @@ -499,7 +499,7 @@ class TestTeleportServiceExecute: repo="repo", branch="main", commit="abc123", - diff="local diff is intentionally not sent", + diff="some local diff", ) ) service._git.is_commit_pushed = AsyncMock(return_value=True) @@ -523,11 +523,15 @@ class TestTeleportServiceExecute: "role": "user", "parts": [{"type": "text", "text": "test prompt"}], } - assert seen_body["context"] == { - "repositories": [ - {"repoUrl": "https://github.com/owner/repo", "branch": "main"} - ] - } + repos = seen_body["context"]["repositories"] + assert len(repos) == 1 + assert repos[0]["repoUrl"] == "https://github.com/owner/repo" + assert repos[0]["branch"] == "main" + assert repos[0]["commitSha"] == "abc123" + assert repos[0]["diff"]["format"] == "git-diff" + assert repos[0]["diff"]["encoding"] == "base64" + assert repos[0]["diff"]["compression"] == "zstd" + assert len(repos[0]["diff"]["content"]) > 0 assert "idempotencyKey" in seen_body @pytest.mark.asyncio diff --git a/tests/core/test_tokens.py b/tests/core/test_tokens.py new file mode 100644 index 0000000..cd639e2 --- /dev/null +++ b/tests/core/test_tokens.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from vibe.core.utils.tokens import approx_token_count, truncate_middle_to_tokens + +_MARKER = "\n\n[... truncated ...]\n\n" + + +def test_approx_token_count_empty() -> None: + assert approx_token_count("") == 0 + + +def test_approx_token_count_rounds_up() -> None: + # 1..4 chars all round to 1 token; 5 rolls over to 2. + assert approx_token_count("a") == 1 + assert approx_token_count("a" * 4) == 1 + assert approx_token_count("a" * 5) == 2 + + +def test_approx_token_count_budget_boundary() -> None: + # 80_000 chars == 20_000 tokens exactly; +1 char crosses the boundary. + assert approx_token_count("a" * 80_000) == 20_000 + assert approx_token_count("a" * 80_001) == 20_001 + + +def test_truncate_middle_returns_text_when_under_budget() -> None: + assert truncate_middle_to_tokens("hello", 10) == "hello" + + +def test_truncate_middle_empty_inputs() -> None: + assert truncate_middle_to_tokens("", 10) == "" + assert truncate_middle_to_tokens("anything", 0) == "" + assert truncate_middle_to_tokens("anything", -1) == "" + + +def test_truncate_middle_keeps_head_and_tail() -> None: + text = "HEAD" + ("x" * 1000) + "TAIL" + out = truncate_middle_to_tokens(text, max_tokens=20) # 80 char budget + assert _MARKER in out + assert out.startswith("HEAD") + assert out.endswith("TAIL") + assert len(out) <= 80 + + +def test_truncate_middle_tight_budget_falls_back_to_head_cut() -> None: + # Budget too tight to fit the marker (24 chars). Function head-truncates. + text = "abcdefghijklmnopqrstuvwxyz" + out = truncate_middle_to_tokens(text, max_tokens=2) # 8 char budget + assert _MARKER not in out + assert out == "abcdefgh" + assert len(out) == 8 diff --git a/tests/onboarding/test_ui_onboarding.py b/tests/onboarding/test_ui_onboarding.py index 9246f48..8333368 100644 --- a/tests/onboarding/test_ui_onboarding.py +++ b/tests/onboarding/test_ui_onboarding.py @@ -70,7 +70,6 @@ def _build_onboarding_config( api_key_env_var: str = "MISTRAL_API_KEY", browser_auth_base_url: str | None = None, browser_auth_api_base_url: str | None = None, - enable_experimental_browser_sign_in: bool = False, ) -> VibeConfig: provider = ProviderConfig( name=provider_name, @@ -85,11 +84,7 @@ def _build_onboarding_config( provider=model_provider or provider_name, alias="devstral-2", ) - return build_test_vibe_config( - providers=[provider], - models=[model], - enable_experimental_browser_sign_in=enable_experimental_browser_sign_in, - ) + return build_test_vibe_config(providers=[provider], models=[model]) def _build_browser_onboarding_app( @@ -101,7 +96,6 @@ def _build_browser_onboarding_app( config=_build_onboarding_config( browser_auth_base_url=CONSOLE_URL, browser_auth_api_base_url=BROWSER_AUTH_API_URL, - enable_experimental_browser_sign_in=True, ), browser_sign_in_service_factory=browser_sign_in_service_factory, browser_sign_in_success_delay=browser_sign_in_success_delay, @@ -246,9 +240,7 @@ async def _show_browser_sign_in(pilot: Pilot) -> None: async def test_ui_keeps_manual_flow_when_browser_sign_in_is_unsupported() -> None: app = OnboardingApp( config=_build_onboarding_config( - browser_auth_base_url="", - browser_auth_api_base_url="", - enable_experimental_browser_sign_in=True, + browser_auth_base_url="", browser_auth_api_base_url="" ) ) api_key_value = "sk-onboarding-test-key" @@ -268,7 +260,7 @@ async def test_ui_keeps_manual_flow_when_browser_sign_in_is_unsupported() -> Non @pytest.mark.asyncio -async def test_ui_hides_browser_sign_in_when_experimental_flag_is_disabled() -> None: +async def test_ui_supports_browser_sign_in_when_provider_supports_it() -> None: _, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory( outcomes=["completed"] ) @@ -280,28 +272,6 @@ async def test_ui_hides_browser_sign_in_when_experimental_flag_is_disabled() -> browser_sign_in_service_factory=browser_sign_in_service_factory, ) - assert app.supports_browser_sign_in is False - - async with app.run_test() as pilot: - await _pass_welcome_screen(pilot) - await _pass_theme_selection_screen(pilot) - await _wait_for(lambda: isinstance(pilot.app.screen, ApiKeyScreen), pilot) - - -@pytest.mark.asyncio -async def test_ui_supports_browser_sign_in_when_experimental_flag_is_enabled() -> None: - _, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory( - outcomes=["completed"] - ) - app = OnboardingApp( - config=_build_onboarding_config( - browser_auth_base_url=CONSOLE_URL, - browser_auth_api_base_url=BROWSER_AUTH_API_URL, - enable_experimental_browser_sign_in=True, - ), - browser_sign_in_service_factory=browser_sign_in_service_factory, - ) - assert app.supports_browser_sign_in is True async with app.run_test() as pilot: @@ -316,7 +286,6 @@ async def test_ui_offers_browser_sign_in_for_renamed_mistral_provider() -> None: backend=Backend.MISTRAL, browser_auth_base_url=CONSOLE_URL, browser_auth_api_base_url=BROWSER_AUTH_API_URL, - enable_experimental_browser_sign_in=True, ) ) @@ -476,9 +445,7 @@ async def test_ui_skips_success_delay_when_browser_api_key_cannot_be_persisted() backend=Backend.MISTRAL, ) app = OnboardingApp( - config=OnboardingContext( - provider=provider, enable_experimental_browser_sign_in=True - ), + config=OnboardingContext(provider=provider), browser_sign_in_service_factory=browser_sign_in_service_factory, browser_sign_in_success_delay=2.0, ) @@ -501,7 +468,6 @@ async def test_ui_browser_sign_in_falls_back_to_mistral_env_var_when_missing() - api_key_env_var="", browser_auth_base_url=CONSOLE_URL, browser_auth_api_base_url=BROWSER_AUTH_API_URL, - enable_experimental_browser_sign_in=True, ), browser_sign_in_service_factory=browser_sign_in_service_factory, ) @@ -754,15 +720,13 @@ async def test_ui_switches_to_manual_path_while_browser_sign_in_is_running() -> @pytest.mark.asyncio -async def test_ui_uses_default_mistral_browser_auth_urls_when_experiment_is_enabled( +async def test_ui_uses_default_mistral_browser_auth_urls( monkeypatch: pytest.MonkeyPatch, ) -> None: captured_base_urls: list[tuple[str, str]] = [] _patch_failing_browser_sign_in_service(monkeypatch, captured_base_urls) - app = OnboardingApp( - config=build_test_vibe_config(enable_experimental_browser_sign_in=True) - ) + app = OnboardingApp(config=build_test_vibe_config()) assert app.supports_browser_sign_in is True @@ -778,46 +742,6 @@ async def test_ui_uses_default_mistral_browser_auth_urls_when_experiment_is_enab ] -@pytest.mark.asyncio -async def test_ui_enables_browser_sign_in_from_env_var( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("VIBE_ENABLE_EXPERIMENTAL_BROWSER_SIGN_IN", "true") - captured_base_urls: list[tuple[str, str]] = [] - _patch_failing_browser_sign_in_service(monkeypatch, captured_base_urls) - - app = OnboardingApp() - - assert app.supports_browser_sign_in is True - - async with app.run_test() as pilot: - await _show_browser_sign_in(pilot) - await _wait_for(lambda: bool(captured_base_urls), pilot) - - assert captured_base_urls == [ - ( - DEFAULT_MISTRAL_BROWSER_AUTH_BASE_URL, - DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL, - ) - ] - - -@pytest.mark.asyncio -async def test_ui_keeps_browser_sign_in_disabled_from_false_env_var( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("VIBE_ENABLE_EXPERIMENTAL_BROWSER_SIGN_IN", "false") - - app = OnboardingApp() - - assert app.supports_browser_sign_in is False - - async with app.run_test() as pilot: - await _pass_welcome_screen(pilot) - await _pass_theme_selection_screen(pilot) - await _wait_for(lambda: isinstance(pilot.app.screen, ApiKeyScreen), pilot) - - @pytest.mark.asyncio async def test_ui_preserves_custom_browser_auth_urls_when_api_key_is_missing( monkeypatch: pytest.MonkeyPatch, tmp_path: Path @@ -828,7 +752,6 @@ async def test_ui_preserves_custom_browser_auth_urls_when_api_key_is_missing( config_file.write_text( "\n".join([ 'active_model = "devstral-2"', - "enable_experimental_browser_sign_in = true", "[[providers]]", 'name = "mistral"', 'api_base = "https://api.mistral.ai/v1"', @@ -871,7 +794,6 @@ async def test_ui_falls_back_to_default_onboarding_context_with_invalid_active_m config_file.write_text( "\n".join([ 'active_model = "does-not-exist"', - "enable_experimental_browser_sign_in = true", "", "[[providers]]", 'name = "mistral"', @@ -922,7 +844,7 @@ async def test_ui_can_pick_a_theme_and_saves_selection() -> None: assert app.theme == target_theme await pilot.press("enter") - await _wait_for(lambda: isinstance(app.screen, ApiKeyScreen), pilot) + await _wait_for(lambda: isinstance(app.screen, AuthMethodScreen), pilot) config_path = VIBE_HOME.path / "config.toml" assert config_path.is_file() diff --git a/tests/setup/auth/test_auth_state.py b/tests/setup/auth/test_auth_state.py new file mode 100644 index 0000000..5d28797 --- /dev/null +++ b/tests/setup/auth/test_auth_state.py @@ -0,0 +1,216 @@ +from __future__ import annotations + +from pathlib import Path + +from vibe.core.config import DEFAULT_MISTRAL_API_ENV_KEY, ProviderConfig +from vibe.core.types import Backend +from vibe.setup.auth import AuthState, AuthStateKind, assess_auth_state + + +def _mistral_provider( + *, api_key_env_var: str = DEFAULT_MISTRAL_API_ENV_KEY +) -> ProviderConfig: + return ProviderConfig( + name="mistral", + api_base="https://api.mistral.ai/v1", + api_key_env_var=api_key_env_var, + browser_auth_base_url="https://console.mistral.ai", + browser_auth_api_base_url="https://console.mistral.ai/api", + backend=Backend.MISTRAL, + ) + + +def _generic_provider( + *, name: str = "custom", api_key_env_var: str = "CUSTOM_API_KEY" +) -> ProviderConfig: + return ProviderConfig( + name=name, + api_base="https://custom.example/v1", + api_key_env_var=api_key_env_var, + backend=Backend.GENERIC, + ) + + +def _write_env_file(path: Path, content: str) -> None: + path.write_text(content, encoding="utf-8") + + +def test_assess_signed_out_when_provider_requires_key_without_any_source( + tmp_path: Path, +) -> None: + state = assess_auth_state( + _mistral_provider(), env_path=tmp_path / ".env", environ={} + ) + + assert state == AuthState( + kind=AuthStateKind.SIGNED_OUT, + can_use_active_provider=False, + sign_out_available=False, + env_key=DEFAULT_MISTRAL_API_ENV_KEY, + ) + + +def test_assess_auth_not_required_when_provider_has_no_api_key_env_var( + tmp_path: Path, +) -> None: + state = assess_auth_state( + _generic_provider(name="llamacpp", api_key_env_var=""), + env_path=tmp_path / ".env", + environ={}, + ) + + assert state == AuthState( + kind=AuthStateKind.AUTH_NOT_REQUIRED, + can_use_active_provider=True, + sign_out_available=False, + env_key=None, + ) + + +def test_assess_vibe_home_env_file_when_default_key_is_in_dotenv( + tmp_path: Path, +) -> None: + env_path = tmp_path / ".env" + _write_env_file(env_path, f"{DEFAULT_MISTRAL_API_ENV_KEY}=file-key\n") + + state = assess_auth_state(_mistral_provider(), env_path=env_path, environ={}) + + assert state == AuthState( + kind=AuthStateKind.VIBE_HOME_ENV_FILE, + can_use_active_provider=True, + sign_out_available=True, + env_key=DEFAULT_MISTRAL_API_ENV_KEY, + ) + + +def test_assess_process_env_when_default_key_is_only_in_process_env( + tmp_path: Path, +) -> None: + state = assess_auth_state( + _mistral_provider(), + env_path=tmp_path / ".env", + environ={DEFAULT_MISTRAL_API_ENV_KEY: "process-key"}, + ) + + assert state == AuthState( + kind=AuthStateKind.PROCESS_ENV, + can_use_active_provider=True, + sign_out_available=False, + env_key=DEFAULT_MISTRAL_API_ENV_KEY, + ) + + +def test_assess_vibe_home_env_file_overrides_process_env_when_both_sources_exist( + tmp_path: Path, +) -> None: + env_path = tmp_path / ".env" + _write_env_file(env_path, f"{DEFAULT_MISTRAL_API_ENV_KEY}=file-key\n") + + state = assess_auth_state( + _mistral_provider(), + env_path=env_path, + environ={DEFAULT_MISTRAL_API_ENV_KEY: "file-key"}, + process_env_had_value_before_dotenv_load=True, + ) + + assert state == AuthState( + kind=AuthStateKind.VIBE_HOME_ENV_FILE_OVERRIDES_PROCESS_ENV, + can_use_active_provider=True, + sign_out_available=False, + env_key=DEFAULT_MISTRAL_API_ENV_KEY, + ) + + +def test_assess_vibe_home_env_file_when_dotenv_did_not_override_process_env( + tmp_path: Path, +) -> None: + env_path = tmp_path / ".env" + _write_env_file(env_path, f"{DEFAULT_MISTRAL_API_ENV_KEY}=file-key\n") + + state = assess_auth_state( + _mistral_provider(), + env_path=env_path, + environ={DEFAULT_MISTRAL_API_ENV_KEY: "file-key"}, + process_env_had_value_before_dotenv_load=False, + ) + + assert state == AuthState( + kind=AuthStateKind.VIBE_HOME_ENV_FILE, + can_use_active_provider=True, + sign_out_available=True, + env_key=DEFAULT_MISTRAL_API_ENV_KEY, + ) + + +def test_assess_vibe_home_env_file_is_sign_out_eligible_even_without_browser_sign_in( + tmp_path: Path, +) -> None: + env_path = tmp_path / ".env" + _write_env_file(env_path, f"{DEFAULT_MISTRAL_API_ENV_KEY}=file-key\n") + provider = ProviderConfig( + name="mistral", + api_base="https://api.mistral.ai/v1", + api_key_env_var=DEFAULT_MISTRAL_API_ENV_KEY, + backend=Backend.GENERIC, + ) + assert not provider.supports_browser_sign_in + + state = assess_auth_state(provider, env_path=env_path, environ={}) + + assert state == AuthState( + kind=AuthStateKind.VIBE_HOME_ENV_FILE, + can_use_active_provider=True, + sign_out_available=True, + env_key=DEFAULT_MISTRAL_API_ENV_KEY, + ) + + +def test_assess_unsupported_provider_when_custom_env_key_is_set_in_dotenv( + tmp_path: Path, +) -> None: + env_path = tmp_path / ".env" + _write_env_file(env_path, "CUSTOM_API_KEY=file-key\n") + + state = assess_auth_state( + _mistral_provider(api_key_env_var="CUSTOM_API_KEY"), + env_path=env_path, + environ={}, + ) + + assert state == AuthState( + kind=AuthStateKind.UNSUPPORTED_PROVIDER, + can_use_active_provider=True, + sign_out_available=False, + env_key="CUSTOM_API_KEY", + ) + + +def test_assess_unsupported_provider_when_custom_provider_uses_process_env( + tmp_path: Path, +) -> None: + state = assess_auth_state( + _generic_provider(), + env_path=tmp_path / ".env", + environ={"CUSTOM_API_KEY": "process-key"}, + ) + + assert state == AuthState( + kind=AuthStateKind.UNSUPPORTED_PROVIDER, + can_use_active_provider=True, + sign_out_available=False, + env_key="CUSTOM_API_KEY", + ) + + +def test_assess_empty_dotenv_value_as_signed_out(tmp_path: Path) -> None: + env_path = tmp_path / ".env" + _write_env_file(env_path, f"{DEFAULT_MISTRAL_API_ENV_KEY}=\n") + + state = assess_auth_state(_mistral_provider(), env_path=env_path, environ={}) + + assert state == AuthState( + kind=AuthStateKind.SIGNED_OUT, + can_use_active_provider=False, + sign_out_available=False, + env_key=DEFAULT_MISTRAL_API_ENV_KEY, + ) diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_ask_user_question/test_snapshot_ask_user_question_collapsed.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_ask_user_question/test_snapshot_ask_user_question_collapsed.svg index cf8eb44..43e2f16 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_ask_user_question/test_snapshot_ask_user_question_collapsed.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_ask_user_question/test_snapshot_ask_user_question_collapsed.svg @@ -125,11 +125,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_ask_user_question/test_snapshot_ask_user_question_expanded.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_ask_user_question/test_snapshot_ask_user_question_expanded.svg index 3ac35fc..78cff99 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_ask_user_question/test_snapshot_ask_user_question_expanded.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_ask_user_question/test_snapshot_ask_user_question_expanded.svg @@ -165,11 +165,11 @@ (Other) VS Code -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_basic_conversation/test_snapshot_shows_basic_conversation.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_basic_conversation/test_snapshot_shows_basic_conversation.svg index 76ab5f1..f3f813b 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_basic_conversation/test_snapshot_shows_basic_conversation.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_basic_conversation/test_snapshot_shows_basic_conversation.svg @@ -35,9 +35,8 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #868887 } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } @@ -179,24 +178,24 @@ - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + - -Hello there, who are you? +Hello there, who are you? +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── I'm the Vibe agent and I'm ready to help. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir6% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir6% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_code_block_horizontal_scrolling/test_snapshot_allows_horizontal_scrolling_for_long_code_blocks.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_code_block_horizontal_scrolling/test_snapshot_allows_horizontal_scrolling_for_long_code_blocks.svg index f11becd..85ee6fa 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_code_block_horizontal_scrolling/test_snapshot_allows_horizontal_scrolling_for_long_code_blocks.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_code_block_horizontal_scrolling/test_snapshot_allows_horizontal_scrolling_for_long_code_blocks.svg @@ -196,11 +196,11 @@ The print statement includes a very long line of Lorem Ipsum text to demonstrate a lengthy output. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_escape_closes.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_escape_closes.svg index f805425..327f3b2 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_escape_closes.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_escape_closes.svg @@ -189,11 +189,11 @@ Configuration closed (no changes saved). -┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_data_retention/test_snapshot_data_retention.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_data_retention/test_snapshot_data_retention.svg index 059d47b..cd628aa 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_data_retention/test_snapshot_data_retention.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_data_retention/test_snapshot_data_retention.svg @@ -191,11 +191,11 @@ Manage your data settings here -┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_debug_console/test_snapshot_debug_console_close.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_debug_console/test_snapshot_debug_console_close.svg index dbecc6e..4d39d95 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_debug_console/test_snapshot_debug_console_close.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_debug_console/test_snapshot_debug_console_close.svg @@ -189,11 +189,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_debug_console/test_snapshot_debug_console_live_append.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_debug_console/test_snapshot_debug_console_live_append.svg index 01a2032..87f69e7 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_debug_console/test_snapshot_debug_console_live_append.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_debug_console/test_snapshot_debug_console_live_append.svg @@ -192,11 +192,11 @@ -┌──────────────────────────────────────────────────────────── default ─┐│ ->││ -││ -││ -└──────────────────────────────────────────────────────────────────────┘│ +────────────────────────────────────────────────────────────── default ─│ +>                                                                      │ +                                                                      │ +                                                                      │ +────────────────────────────────────────────────────────────────────────│ /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_debug_console/test_snapshot_debug_console_open.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_debug_console/test_snapshot_debug_console_open.svg index 0abf547..ec88dd7 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_debug_console/test_snapshot_debug_console_open.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_debug_console/test_snapshot_debug_console_open.svg @@ -191,11 +191,11 @@ -┌──────────────────────────────────────────────────────────── default ─┐│ ->││ -││ -││ -└──────────────────────────────────────────────────────────────────────┘│ +────────────────────────────────────────────────────────────── default ─│ +>                                                                      │ +                                                                      │ +                                                                      │ +────────────────────────────────────────────────────────────────────────│ /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_empty_assistant_before_reasoning/test_snapshot_empty_assistant_removed_when_reasoning_starts.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_empty_assistant_before_reasoning/test_snapshot_empty_assistant_removed_when_reasoning_starts.svg index 69206b4..a70ea5c 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_empty_assistant_before_reasoning/test_snapshot_empty_assistant_removed_when_reasoning_starts.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_empty_assistant_before_reasoning/test_snapshot_empty_assistant_removed_when_reasoning_starts.svg @@ -35,10 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #6b753d } -.terminal-r7 { fill: #868887 } @@ -178,26 +177,26 @@ - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + - -What is the answer? +What is the answer? +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -Thought +Thought The answer is 42. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_feedback_bar/test_snapshot_feedback_bar_visible.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_feedback_bar/test_snapshot_feedback_bar_visible.svg index 680777a..1b579b7 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_feedback_bar/test_snapshot_feedback_bar_visible.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_feedback_bar/test_snapshot_feedback_bar_visible.svg @@ -35,10 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #608ab1 } -.terminal-r7 { fill: #868887 } @@ -180,24 +179,24 @@ - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + - -Hello +Hello +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Sure, I can help with that. How is Vibe doing so far?  1: good  2: fine  3: bad -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir6% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir6% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_connector_auth_back_to_mcp.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_connector_auth_back_to_mcp.svg index cab9511..832cb25 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_connector_auth_back_to_mcp.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_connector_auth_back_to_mcp.svg @@ -35,13 +35,12 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } +.terminal-r4 { fill: #608ab1;font-weight: bold } .terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #7b7e82;font-weight: bold } -.terminal-r9 { fill: #98a84b;font-weight: bold } -.terminal-r10 { fill: #868887 } +.terminal-r6 { fill: #4b4e55;font-weight: bold } +.terminal-r7 { fill: #7b7e82;font-weight: bold } +.terminal-r8 { fill: #98a84b;font-weight: bold } +.terminal-r9 { fill: #868887 } @@ -180,27 +179,27 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 3 connectors · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 3 connectors · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers & Connectors +MCP Servers & Connectors Workspace Connectors -  alpha  [connector]  1 tool   connected -  beta   [connector]  no tools needs auth -  zeta   [connector]  no tools needs setup -↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close +  alpha  [connector]  1 tool   connected +  beta   [connector]  no tools needs auth +  zeta   [connector]  no tools needs setup +↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_connector_auth_opens_on_disconnected.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_connector_auth_opens_on_disconnected.svg index 13088ff..13ea4dc 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_connector_auth_opens_on_disconnected.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_connector_auth_opens_on_disconnected.svg @@ -35,11 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #868887 } +.terminal-r4 { fill: #608ab1;font-weight: bold } +.terminal-r5 { fill: #4b4e55;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -175,30 +173,30 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 3 connectors · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 3 connectors · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -Connector: beta +Connector: beta This connector requires authentication -  Press enter to open auth in your browser +  Press enter to open auth in your browser   Copy URL to clipboard   Manually show the URL Once authenticated, press R to refresh -Backspace Back +Backspace Back └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_connector_auth_show_url.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_connector_auth_show_url.svg index 524922b..a986457 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_connector_auth_show_url.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_connector_auth_show_url.svg @@ -35,11 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #868887 } +.terminal-r4 { fill: #608ab1;font-weight: bold } +.terminal-r5 { fill: #4b4e55;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -173,32 +171,32 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 3 connectors · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 3 connectors · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -Connector: beta +Connector: beta This connector requires authentication   Press enter to open auth in your browser   Copy URL to clipboard -  Manually show the URL +  Manually show the URL https://fake-auth.example.com/beta Once authenticated, press R to refresh -Backspace Back +Backspace Back └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_backspace_returns_to_overview.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_backspace_returns_to_overview.svg index 049e013..c048688 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_backspace_returns_to_overview.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_backspace_returns_to_overview.svg @@ -35,12 +35,11 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } +.terminal-r4 { fill: #608ab1;font-weight: bold } .terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #7b7e82;font-weight: bold } -.terminal-r9 { fill: #868887 } +.terminal-r6 { fill: #4b4e55;font-weight: bold } +.terminal-r7 { fill: #7b7e82;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -181,25 +180,25 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp filesystem +/ mcp filesystem MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers +MCP Servers Local MCP Servers -  filesystem  [stdio]  1 tool -  search      [http]   1 tool -↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close +  filesystem  [stdio]  1 tool +  search      [http]   1 tool +↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_broken_server.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_broken_server.svg index 3c966e5..4a20c20 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_broken_server.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_broken_server.svg @@ -35,12 +35,11 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } +.terminal-r4 { fill: #608ab1;font-weight: bold } .terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #7b7e82;font-weight: bold } -.terminal-r9 { fill: #868887 } +.terminal-r6 { fill: #4b4e55;font-weight: bold } +.terminal-r7 { fill: #7b7e82;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -180,26 +179,26 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 3 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 3 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers +MCP Servers Local MCP Servers -  filesystem     [stdio]  1 tool -  broken-server  [stdio]  no tools -  search         [http]   1 tool -↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close +  filesystem     [stdio]  1 tool +  broken-server  [stdio]  no tools +  search         [http]   1 tool +↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_connector_back_to_overview.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_connector_back_to_overview.svg index 630ba2a..49ea848 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_connector_back_to_overview.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_connector_back_to_overview.svg @@ -35,13 +35,12 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } +.terminal-r4 { fill: #608ab1;font-weight: bold } .terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #7b7e82;font-weight: bold } -.terminal-r9 { fill: #868887 } -.terminal-r10 { fill: #98a84b } +.terminal-r6 { fill: #4b4e55;font-weight: bold } +.terminal-r7 { fill: #7b7e82;font-weight: bold } +.terminal-r8 { fill: #868887 } +.terminal-r9 { fill: #98a84b } @@ -179,28 +178,28 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 connectors · 1 MCP server · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 connectors · 1 MCP server · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers & Connectors +MCP Servers & Connectors Local MCP Servers -  filesystem  [stdio]  1 tool +  filesystem  [stdio]  1 tool Workspace Connectors -  gmail  [connector]  3 tools connected -  slack  [connector]  2 tools connected -↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close +  gmail  [connector]  3 tools connected +  slack  [connector]  2 tools connected +↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_connectors_only.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_connectors_only.svg index 44a1c0d..0db4240 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_connectors_only.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_connectors_only.svg @@ -35,14 +35,13 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } +.terminal-r4 { fill: #608ab1;font-weight: bold } .terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #7b7e82;font-weight: bold } -.terminal-r9 { fill: #98a84b;font-weight: bold } -.terminal-r10 { fill: #868887 } -.terminal-r11 { fill: #98a84b } +.terminal-r6 { fill: #4b4e55;font-weight: bold } +.terminal-r7 { fill: #7b7e82;font-weight: bold } +.terminal-r8 { fill: #98a84b;font-weight: bold } +.terminal-r9 { fill: #868887 } +.terminal-r10 { fill: #98a84b } @@ -183,25 +182,25 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 connectors · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 connectors · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers & Connectors +MCP Servers & Connectors Workspace Connectors -  gmail  [connector]  3 tools connected -  slack  [connector]  2 tools connected -↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close +  gmail  [connector]  3 tools connected +  slack  [connector]  2 tools connected +↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_connectors_sorted_by_status.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_connectors_sorted_by_status.svg index fecab18..e6f2a0c 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_connectors_sorted_by_status.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_connectors_sorted_by_status.svg @@ -35,13 +35,12 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } +.terminal-r4 { fill: #608ab1;font-weight: bold } .terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #7b7e82;font-weight: bold } -.terminal-r9 { fill: #98a84b;font-weight: bold } -.terminal-r10 { fill: #868887 } +.terminal-r6 { fill: #4b4e55;font-weight: bold } +.terminal-r7 { fill: #7b7e82;font-weight: bold } +.terminal-r8 { fill: #98a84b;font-weight: bold } +.terminal-r9 { fill: #868887 } @@ -181,26 +180,26 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 3 connectors · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 3 connectors · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers & Connectors +MCP Servers & Connectors Workspace Connectors -  alpha  [connector]  1 tool   connected -  beta   [connector]  no tools needs auth -  zeta   [connector]  no tools needs setup -↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close +  alpha  [connector]  1 tool   connected +  beta   [connector]  no tools needs auth +  zeta   [connector]  no tools needs setup +↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_drill_into_connector.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_drill_into_connector.svg index 56b8951..19f2c04 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_drill_into_connector.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_drill_into_connector.svg @@ -35,11 +35,10 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #868887 } +.terminal-r4 { fill: #608ab1;font-weight: bold } +.terminal-r5 { fill: #4b4e55;font-weight: bold } +.terminal-r6 { fill: #c5c8c6;font-weight: bold } +.terminal-r7 { fill: #868887 } @@ -181,24 +180,24 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 connectors · 1 MCP server · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 connectors · 1 MCP server · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -Connector: slack +Connector: slack -search_messages  -  Search Slack messages -send_message  -  Send a Slack message -↑↓ Navigate  D Disable  E Enable  Backspace Back  R Refresh  Esc Close +search_messages  -  Search Slack messages +send_message  -  Send a Slack message +↑↓ Navigate  D Disable  E Enable  Backspace Back  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_enter_drills_into_server.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_enter_drills_into_server.svg index d5bef3a..bbdb908 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_enter_drills_into_server.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_enter_drills_into_server.svg @@ -35,11 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #868887 } +.terminal-r4 { fill: #608ab1;font-weight: bold } +.terminal-r5 { fill: #4b4e55;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -182,23 +180,23 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Server: filesystem +MCP Server: filesystem -fake_tool  -  A fake tool for filesystem -↑↓ Navigate  D Disable  E Enable  Backspace Back  R Refresh  Esc Close +fake_tool  -  A fake tool for filesystem +↑↓ Navigate  D Disable  E Enable  Backspace Back  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_escape_closes.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_escape_closes.svg index 343c377..b984cd1 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_escape_closes.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_escape_closes.svg @@ -35,9 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #868887 } +.terminal-r4 { fill: #868887 } @@ -180,23 +178,23 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... MCP servers closed. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_help_bar_shows_connect.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_help_bar_shows_connect.svg index 57b906d..034060c 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_help_bar_shows_connect.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_help_bar_shows_connect.svg @@ -35,13 +35,12 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } +.terminal-r4 { fill: #608ab1;font-weight: bold } .terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #868887 } -.terminal-r8 { fill: #98a84b } -.terminal-r9 { fill: #4b4e55;font-weight: bold } -.terminal-r10 { fill: #7b7e82;font-weight: bold } +.terminal-r6 { fill: #868887 } +.terminal-r7 { fill: #98a84b } +.terminal-r8 { fill: #4b4e55;font-weight: bold } +.terminal-r9 { fill: #7b7e82;font-weight: bold } @@ -181,26 +180,26 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 3 connectors · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 3 connectors · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers & Connectors +MCP Servers & Connectors Workspace Connectors -  alpha  [connector]  1 tool   connected -  beta   [connector]  no tools needs auth -  zeta   [connector]  no tools needs setup -↑↓ Navigate  Enter Connect  D Disable  E Enable  R Refresh  Esc Close +  alpha  [connector]  1 tool   connected +  beta   [connector]  no tools needs auth +  zeta   [connector]  no tools needs setup +↑↓ Navigate  Enter Connect  D Disable  E Enable  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_no_servers.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_no_servers.svg index 4680382..fd5e867 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_no_servers.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_no_servers.svg @@ -35,9 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #868887 } +.terminal-r4 { fill: #868887 } @@ -181,22 +179,22 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp No MCP servers or connectors configured. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_overview.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_overview.svg index 61e9cbc..7969e1e 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_overview.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_overview.svg @@ -35,12 +35,11 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } +.terminal-r4 { fill: #608ab1;font-weight: bold } .terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #7b7e82;font-weight: bold } -.terminal-r9 { fill: #868887 } +.terminal-r6 { fill: #4b4e55;font-weight: bold } +.terminal-r7 { fill: #7b7e82;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -181,25 +180,25 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers +MCP Servers Local MCP Servers -  filesystem  [stdio]  1 tool -  search      [http]   1 tool -↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close +  filesystem  [stdio]  1 tool +  search      [http]   1 tool +↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_overview_navigate_down.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_overview_navigate_down.svg index 03bace9..3514883 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_overview_navigate_down.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_overview_navigate_down.svg @@ -35,12 +35,11 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } +.terminal-r4 { fill: #608ab1;font-weight: bold } .terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #868887 } -.terminal-r8 { fill: #4b4e55;font-weight: bold } -.terminal-r9 { fill: #7b7e82;font-weight: bold } +.terminal-r6 { fill: #868887 } +.terminal-r7 { fill: #4b4e55;font-weight: bold } +.terminal-r8 { fill: #7b7e82;font-weight: bold } @@ -181,25 +180,25 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers +MCP Servers Local MCP Servers -  filesystem  [stdio]  1 tool -  search      [http]   1 tool -↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close +  filesystem  [stdio]  1 tool +  search      [http]   1 tool +↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_refresh_shortcut.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_refresh_shortcut.svg index 5baac2e..d4b3d98 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_refresh_shortcut.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_refresh_shortcut.svg @@ -35,12 +35,11 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } +.terminal-r4 { fill: #608ab1;font-weight: bold } .terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #7b7e82;font-weight: bold } -.terminal-r9 { fill: #868887 } +.terminal-r6 { fill: #4b4e55;font-weight: bold } +.terminal-r7 { fill: #7b7e82;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -181,25 +180,25 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers +MCP Servers Local MCP Servers -  filesystem  [stdio]  1 tool -  search      [http]   1 tool -Refreshed.  ↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close +  filesystem  [stdio]  1 tool +  search      [http]   1 tool +Refreshed.  ↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_server_arg.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_server_arg.svg index 2de16b7..889304a 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_server_arg.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_server_arg.svg @@ -35,11 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #868887 } +.terminal-r4 { fill: #608ab1;font-weight: bold } +.terminal-r5 { fill: #4b4e55;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -182,23 +180,23 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp filesystem +/ mcp filesystem MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Server: filesystem +MCP Server: filesystem -fake_tool  -  A fake tool for filesystem -↑↓ Navigate  D Disable  E Enable  Backspace Back  R Refresh  Esc Close +fake_tool  -  A fake tool for filesystem +↑↓ Navigate  D Disable  E Enable  Backspace Back  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_with_connectors_overview.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_with_connectors_overview.svg index 630ba2a..49ea848 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_with_connectors_overview.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_with_connectors_overview.svg @@ -35,13 +35,12 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } +.terminal-r4 { fill: #608ab1;font-weight: bold } .terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #4b4e55;font-weight: bold } -.terminal-r8 { fill: #7b7e82;font-weight: bold } -.terminal-r9 { fill: #868887 } -.terminal-r10 { fill: #98a84b } +.terminal-r6 { fill: #4b4e55;font-weight: bold } +.terminal-r7 { fill: #7b7e82;font-weight: bold } +.terminal-r8 { fill: #868887 } +.terminal-r9 { fill: #98a84b } @@ -179,28 +178,28 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 connectors · 1 MCP server · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 connectors · 1 MCP server · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/mcp +/ mcp MCP servers opened... ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers & Connectors +MCP Servers & Connectors Local MCP Servers -  filesystem  [stdio]  1 tool +  filesystem  [stdio]  1 tool Workspace Connectors -  gmail  [connector]  3 tools connected -  slack  [connector]  2 tools connected -↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close +  gmail  [connector]  3 tools connected +  slack  [connector]  2 tools connected +↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_escape_cancels.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_escape_cancels.svg index 0386126..cbcc80d 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_escape_cancels.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_escape_cancels.svg @@ -189,11 +189,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_select_different_model.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_select_different_model.svg index ff0327d..ec8f07e 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_select_different_model.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_select_different_model.svg @@ -189,11 +189,11 @@ Configuration reloaded (includes agent instructions and skills). -┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_accept_edits_mode.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_accept_edits_mode.svg index 7fa1792..f54cbd1 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_accept_edits_mode.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_accept_edits_mode.svg @@ -190,11 +190,11 @@ -┌─────────────────────────────────────────────────────────────────────────────────────────────────────── accept edits ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +───────────────────────────────────────────────────────────────────────────────────────────────────────── accept edits ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_auto_approve_mode.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_auto_approve_mode.svg index ceb654d..ce34f5f 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_auto_approve_mode.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_auto_approve_mode.svg @@ -190,11 +190,11 @@ -┌─────────────────────────────────────────────────────────────────────────────────────────────────────── auto approve ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +───────────────────────────────────────────────────────────────────────────────────────────────────────── auto approve ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_plan_mode.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_plan_mode.svg index bf972a8..e7bbf9b 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_plan_mode.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_to_plan_mode.svg @@ -190,11 +190,11 @@ -┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────── plan ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +───────────────────────────────────────────────────────────────────────────────────────────────────────────────── plan ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_wraps_to_default.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_wraps_to_default.svg index 49d41ab..4979a60 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_wraps_to_default.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_cycle_wraps_to_default.svg @@ -189,11 +189,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_default_mode.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_default_mode.svg index 49d41ab..4979a60 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_default_mode.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_modes/test_snapshot_default_mode.svg @@ -189,11 +189,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_narrator_flow/test_snapshot_narrator_idle_after_speaking.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_narrator_flow/test_snapshot_narrator_idle_after_speaking.svg index 8249d93..a1dec6d 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_narrator_flow/test_snapshot_narrator_idle_after_speaking.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_narrator_flow/test_snapshot_narrator_idle_after_speaking.svg @@ -35,9 +35,8 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #868887 } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } @@ -179,24 +178,24 @@ - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + - -Hello +Hello +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Hello! I can help you. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir6% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir6% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_narrator_flow/test_snapshot_narrator_speaking.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_narrator_flow/test_snapshot_narrator_speaking.svg index 7723d34..6d2ab81 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_narrator_flow/test_snapshot_narrator_speaking.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_narrator_flow/test_snapshot_narrator_speaking.svg @@ -35,10 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #ffa500;font-weight: bold } -.terminal-r7 { fill: #868887 } @@ -180,24 +179,24 @@ - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + - -Hello +Hello +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Hello! I can help you. -▂▅▇ speaking Esc/Ctrl+C to stop -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir6% of 200k tokens +▂▅▇ speaking Esc/Ctrl+C to stop +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir6% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_narrator_flow/test_snapshot_narrator_summarizing.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_narrator_flow/test_snapshot_narrator_summarizing.svg index 47150c4..259b22f 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_narrator_flow/test_snapshot_narrator_summarizing.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_narrator_flow/test_snapshot_narrator_summarizing.svg @@ -35,10 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #ffa500;font-weight: bold } -.terminal-r7 { fill: #868887 } @@ -180,24 +179,24 @@ - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + - -Hello +Hello +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Hello! I can help you. - summarizing Esc/Ctrl+C to stop -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir6% of 200k tokens + summarizing Esc/Ctrl+C to stop +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir6% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_plan_file_message/test_snapshot_plan_file_message.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_plan_file_message/test_snapshot_plan_file_message.svg index cee3075..baf9ee4 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_plan_file_message/test_snapshot_plan_file_message.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_plan_file_message/test_snapshot_plan_file_message.svg @@ -191,11 +191,11 @@ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_cancel_discards_changes.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_cancel_discards_changes.svg index 0a04f71..8447796 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_cancel_discards_changes.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_cancel_discards_changes.svg @@ -189,11 +189,11 @@ Proxy setup cancelled. -┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_edit_existing_values.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_edit_existing_values.svg index bfa6298..94d5fd3 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_edit_existing_values.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_edit_existing_values.svg @@ -189,11 +189,11 @@ Proxy settings saved. Restart the CLI for changes to take effect. -┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_save_error.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_save_error.svg index 68a6561..7a81dbb 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_save_error.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_save_error.svg @@ -190,11 +190,11 @@ Error: Failed to save proxy settings: Permission denied -┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_save_new_values.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_save_new_values.svg index b9a8289..ad5fb4d 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_save_new_values.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_save_new_values.svg @@ -189,11 +189,11 @@ Proxy settings saved. Restart the CLI for changes to take effect. -┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_buffered_reasoning_yields_before_content.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_buffered_reasoning_yields_before_content.svg index 835ebbb..205352f 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_buffered_reasoning_yields_before_content.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_buffered_reasoning_yields_before_content.svg @@ -35,10 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #6b753d } -.terminal-r7 { fill: #868887 } @@ -178,26 +177,26 @@ - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + - -Give me an answer +Give me an answer +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -Thought +Thought Here is my carefully considered answer. I hope this helps! -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_interleaved_reasoning.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_interleaved_reasoning.svg index e7d7f7d..c37d188 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_interleaved_reasoning.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_interleaved_reasoning.svg @@ -35,10 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #6b753d } -.terminal-r7 { fill: #868887 } @@ -174,30 +173,30 @@ - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + - -Explain this to me +Explain this to me +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -Thought +Thought Here's the first part of the answer. -Thought +Thought And here's the conclusion! -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_reasoning_content.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_reasoning_content.svg index aaf0990..d370050 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_reasoning_content.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_reasoning_content.svg @@ -35,10 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #6b753d } -.terminal-r7 { fill: #868887 } @@ -178,26 +177,26 @@ - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + - -What is the answer? +What is the answer? +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -Thought +Thought The answer to your question is 42. This is the ultimate answer. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_reasoning_content_expanded.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_reasoning_content_expanded.svg index ae50ee1..8581a3b 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_reasoning_content_expanded.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_reasoning_content/test_snapshot_shows_reasoning_content_expanded.svg @@ -35,10 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #6b753d } -.terminal-r7 { fill: #868887 } @@ -177,27 +176,27 @@ - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + - -What is the answer? +What is the answer? +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -Thought -Let me think about this step by step... First, I need to understand the question. Then I can formulate a response. +Thought +Let me think about this step by step... First, I need to understand the question. Then I can formulate a response. The answer to your question is 42. This is the ultimate answer. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_release_update_notification/test_snapshot_shows_release_update_notification.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_release_update_notification/test_snapshot_shows_release_update_notification.svg index 68f7b2b..ab02dbe 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_release_update_notification/test_snapshot_shows_release_update_notification.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_release_update_notification/test_snapshot_shows_release_update_notification.svg @@ -193,11 +193,11 @@ 1.0.4 => 1000.2.0 Please update mistral-vibe with your package manager -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_error_shows_toast.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_error_shows_toast.svg index a566b12..cc949fd 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_error_shows_toast.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_error_shows_toast.svg @@ -35,12 +35,11 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #292929;font-weight: bold } .terminal-r7 { fill: #cc555a } .terminal-r8 { fill: #608ab1;font-weight: bold } -.terminal-r9 { fill: #868887 } @@ -162,7 +161,7 @@ - + @@ -171,24 +170,24 @@ - - - +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information - +first message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +Hello! How can I help you? -first message - -Hello! How can I help you? + +second message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -second message - -third message + +third message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ┌───────────────────────────────────────────────────────────┐ @@ -197,9 +196,9 @@ › 1. Edit & restore files to this point   2. Edit without restoring files -Alt+↑↓ or Ctrl+P/N browse messages  ↑↓ pick option  Enter confirm  ESC cancel +Alt+↑↓ or Ctrl+P/N browse messages  ↑↓ pick option  Enter confirm  ESC cancel └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_exit_on_escape.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_exit_on_escape.svg index dcdd308..8a869dd 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_exit_on_escape.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_exit_on_escape.svg @@ -35,9 +35,8 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #868887 } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } @@ -171,32 +170,32 @@ - - - +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information - +first message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +Hello! How can I help you? -first message - -Hello! How can I help you? + +second message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -second message - -third message + +third message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_navigate_down.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_navigate_down.svg index 0f8f071..42a01b6 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_navigate_down.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_navigate_down.svg @@ -35,11 +35,10 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #292929;font-weight: bold } .terminal-r7 { fill: #608ab1;font-weight: bold } -.terminal-r8 { fill: #868887 } @@ -161,7 +160,7 @@ - + @@ -170,24 +169,24 @@ - - - +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information - +first message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +Hello! How can I help you? -first message - -Hello! How can I help you? + +second message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -second message - -third message + +third message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -196,9 +195,9 @@ › 1. Edit & restore files to this point   2. Edit without restoring files -Alt+↑↓ or Ctrl+P/N browse messages  ↑↓ pick option  Enter confirm  ESC cancel +Alt+↑↓ or Ctrl+P/N browse messages  ↑↓ pick option  Enter confirm  ESC cancel └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_navigate_up.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_navigate_up.svg index 4093b47..313dbe3 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_navigate_up.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_navigate_up.svg @@ -35,11 +35,10 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #292929;font-weight: bold } .terminal-r7 { fill: #608ab1;font-weight: bold } -.terminal-r8 { fill: #868887 } @@ -161,7 +160,7 @@ - + @@ -170,24 +169,24 @@ - - - +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information - +first message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +Hello! How can I help you? -first message - -Hello! How can I help you? + +second message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -second message - -third message + +third message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -196,9 +195,9 @@ › 1. Edit & restore files to this point   2. Edit without restoring files -Alt+↑↓ or Ctrl+P/N browse messages  ↑↓ pick option  Enter confirm  ESC cancel +Alt+↑↓ or Ctrl+P/N browse messages  ↑↓ pick option  Enter confirm  ESC cancel └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_panel_shown.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_panel_shown.svg index 0f8f071..42a01b6 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_panel_shown.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_rewind/test_snapshot_rewind_panel_shown.svg @@ -35,11 +35,10 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #292929;font-weight: bold } .terminal-r7 { fill: #608ab1;font-weight: bold } -.terminal-r8 { fill: #868887 } @@ -161,7 +160,7 @@ - + @@ -170,24 +169,24 @@ - - - +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information - +first message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +Hello! How can I help you? -first message - -Hello! How can I help you? + +second message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -second message - -third message + +third message +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -196,9 +195,9 @@ › 1. Edit & restore files to this point   2. Edit without restoring files -Alt+↑↓ or Ctrl+P/N browse messages  ↑↓ pick option  Enter confirm  ESC cancel +Alt+↑↓ or Ctrl+P/N browse messages  ↑↓ pick option  Enter confirm  ESC cancel └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_session_resume/test_snapshot_shows_resumed_session_messages.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_session_resume/test_snapshot_shows_resumed_session_messages.svg index 1036fbe..c81c9b2 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_session_resume/test_snapshot_shows_resumed_session_messages.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_session_resume/test_snapshot_shows_resumed_session_messages.svg @@ -35,10 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #98a84b } -.terminal-r7 { fill: #868887 } @@ -178,26 +177,26 @@ - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + - -Hello, how are you? +Hello, how are you? +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── I'm doing well, thank you! Let me read that file for you. read_file -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_session_resume_injected/test_snapshot_session_resume_hides_injected_messages.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_session_resume_injected/test_snapshot_session_resume_hides_injected_messages.svg index 80e7e95..c705fd9 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_session_resume_injected/test_snapshot_session_resume_hides_injected_messages.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_session_resume_injected/test_snapshot_session_resume_hides_injected_messages.svg @@ -35,9 +35,8 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #868887 } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } @@ -173,30 +172,30 @@ - - - +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information - + + +Hello, can you help me? +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── -Hello, can you help me? +Sure! What do you need? -Sure! What do you need? - -Please read my config file. + +Please read my config file. +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Here is the content of your config file. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_code_fence/test_snapshot_streaming_code_fence_preserved.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_code_fence/test_snapshot_streaming_code_fence_preserved.svg index 777dcad..0716448 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_code_fence/test_snapshot_streaming_code_fence_preserved.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_code_fence/test_snapshot_streaming_code_fence_preserved.svg @@ -35,10 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } .terminal-r6 { fill: #868887;font-style: italic; } -.terminal-r7 { fill: #868887 } @@ -164,14 +163,14 @@ - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + - -show diagram +show diagram +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────   Client     | @@ -192,12 +191,12 @@ +---------+ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_command_hidden_for_non_pro_account.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_command_hidden_for_non_pro_account.svg index 1e8ef2f..39d3b27 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_command_hidden_for_non_pro_account.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_command_hidden_for_non_pro_account.svg @@ -195,13 +195,13 @@ - + Keyboard Shortcuts • Enter Submit message • Ctrl+J / Shift+Enter Insert newline -• Escape Interrupt agent or close dialogs -• Ctrl+C Quit (or clear input if text present) +• Escape Interrupt agent or close dialogs +• Ctrl+C Quit (or clear input if text present) • Ctrl+G Edit input in external editor • Ctrl+O Toggle tool output view • Shift+Tab Cycle through agents (default, plan, ...) @@ -239,11 +239,11 @@ • /theme: Select theme -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_command_visible_for_pro_account.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_command_visible_for_pro_account.svg index 1f150fb..1485af9 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_command_visible_for_pro_account.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_command_visible_for_pro_account.svg @@ -195,14 +195,14 @@ - + • Enter Submit message • Ctrl+J / Shift+Enter Insert newline • Escape Interrupt agent or close dialogs • Ctrl+C Quit (or clear input if text present) -• Ctrl+G Edit input in external editor -• Ctrl+O Toggle tool output view +• Ctrl+G Edit input in external editor +• Ctrl+O Toggle tool output view • Shift+Tab Cycle through agents (default, plan, ...) • Alt+↑↓ / Ctrl+P/N Rewind to previous/next message @@ -225,7 +225,7 @@ • /compact: Compact conversation history by summarizing. Optionally pass instructions to guide the summary • /exit: Exit the application • /status: Display agent statistics -• /teleport: Teleport session to Vibe Code +• /teleport: Teleport session to Vibe Code Web • /proxy-setup: Configure proxy and SSL certificate settings • /continue/resume: Browse and resume past sessions • /rename: Rename the current session @@ -239,11 +239,11 @@ • /theme: Select theme -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_complete.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_complete.svg index a40de48..0bdee04 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_complete.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_complete.svg @@ -78,9 +78,9 @@ - + - Teleported to Vibe Code: https://chat.example.com + Teleported to Vibe Code Web: https://chat.example.com diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_indicator_shown.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_indicator_shown.svg index bc38ae4..915ea38 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_indicator_shown.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_indicator_shown.svg @@ -190,11 +190,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -hello                                                                                                              - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +hello                                                                                                                  + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_not_started_when_voice_disabled.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_not_started_when_voice_disabled.svg index 3b5dd41..9efae01 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_not_started_when_voice_disabled.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_not_started_when_voice_disabled.svg @@ -189,11 +189,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ ->hello                                                                                                              - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +>hello                                                                                                                  + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_stopped_after_keypress.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_stopped_after_keypress.svg index 809cc8e..f5b9ec4 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_stopped_after_keypress.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_stopped_after_keypress.svg @@ -189,11 +189,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ ->hello                                                                                                              - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +>hello                                                                                                                  + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_then_conversation.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_then_conversation.svg index 39dd22b..8607ac0 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_then_conversation.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_recording_then_conversation.svg @@ -35,9 +35,8 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #868887 } +.terminal-r4 { fill: #c5c8c6;font-weight: bold } +.terminal-r5 { fill: #868887 } @@ -179,24 +178,24 @@ - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + - -hello +hello +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── I'm ready to help you. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_transcription_populates_text_area.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_transcription_populates_text_area.svg index 16e7e12..6bc3d20 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_transcription_populates_text_area.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_transcription_populates_text_area.svg @@ -190,11 +190,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -Hello world from voice                                                                                             - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +Hello world from voice                                                                                                 + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_voice_disable.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_voice_disable.svg index a584e25..98e21b1 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_voice_disable.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_voice_disable.svg @@ -35,9 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #868887 } +.terminal-r4 { fill: #868887 } @@ -180,23 +178,23 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/voice +/ voice Voice settings opened... Voice mode disabled. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_voice_enable.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_voice_enable.svg index 45a9efc..8c3b592 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_voice_enable.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_voice_mode/test_snapshot_voice_enable.svg @@ -35,9 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #c5c8c6;font-weight: bold } -.terminal-r6 { fill: #868887 } +.terminal-r4 { fill: #868887 } @@ -180,23 +178,23 @@ - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + -/voice +/ voice Voice settings opened... Voice mode enabled. Press ctrl+r to start recording. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_no_plan_message.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_no_plan_message.svg index e111519..e3e7225 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_no_plan_message.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_no_plan_message.svg @@ -191,11 +191,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_switch_message.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_switch_message.svg index 31cda8d..fdd97eb 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_switch_message.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_switch_message.svg @@ -193,11 +193,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_upgrade_message.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_upgrade_message.svg index 69b2250..c956d8a 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_upgrade_message.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_upgrade_message.svg @@ -193,11 +193,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_whats_new_message.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_whats_new_message.svg index 808192d..80688c8 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_whats_new_message.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_whats_new_message.svg @@ -191,11 +191,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +────────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─ +> + + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── /test/workdir0% of 200k tokens diff --git a/tests/snapshots/base_snapshot_test_app.py b/tests/snapshots/base_snapshot_test_app.py index 14d54fb..39362ca 100644 --- a/tests/snapshots/base_snapshot_test_app.py +++ b/tests/snapshots/base_snapshot_test_app.py @@ -36,11 +36,16 @@ class BaseSnapshotTestApp(VibeApp): backend: FakeBackend | None = None, **kwargs, ): + agent_loop_kwargs: dict = {} + if "mcp_registry" in kwargs: + agent_loop_kwargs["mcp_registry"] = kwargs.pop("mcp_registry") + agent_loop = build_test_agent_loop( config=config or default_config(), agent_name=self._current_agent_name, enable_streaming=bool(kwargs.get("enable_streaming", False)), backend=backend or FakeBackend(), + **agent_loop_kwargs, ) plan_offer_gateway = kwargs.pop( diff --git a/tests/snapshots/test_ui_snapshot_mcp_command.py b/tests/snapshots/test_ui_snapshot_mcp_command.py index e5e6f42..c57e2db 100644 --- a/tests/snapshots/test_ui_snapshot_mcp_command.py +++ b/tests/snapshots/test_ui_snapshot_mcp_command.py @@ -1,22 +1,15 @@ from __future__ import annotations -from unittest.mock import patch - from textual.pilot import Pilot from tests.snapshots.base_snapshot_test_app import BaseSnapshotTestApp, default_config from tests.snapshots.snap_compare import SnapCompare from tests.stubs.fake_connector_registry import FakeConnectorRegistry -from tests.stubs.fake_mcp_registry import ( - FakeMCPRegistry, - FakeMCPRegistryWithBrokenServer, -) +from tests.stubs.fake_mcp_registry import FakeMCPRegistryWithBrokenServer from vibe.core.config import MCPHttp, MCPStdio from vibe.core.tools.connectors import ConnectorAuthAction from vibe.core.tools.mcp.tools import RemoteTool -_MCP_PATCH = "vibe.core.agent_loop.MCPRegistry" - _FAKE_CONNECTORS = { "gmail": [ RemoteTool(name="gmail_search", description="Search emails in Gmail"), @@ -55,7 +48,7 @@ class SnapshotTestAppWithBrokenMcpServer(BaseSnapshotTestApp): ), MCPHttp(name="search", transport="http", url="http://localhost:8080"), ] - super().__init__(config=config) + super().__init__(config=config, mcp_registry=FakeMCPRegistryWithBrokenServer()) class SnapshotTestAppWithMcpServers(BaseSnapshotTestApp): @@ -92,24 +85,22 @@ def test_snapshot_mcp_broken_server(snap_compare: SnapCompare) -> None: async def run_before(pilot: Pilot) -> None: await _run_mcp_command(pilot, "/mcp") - with patch(_MCP_PATCH, FakeMCPRegistryWithBrokenServer): - assert snap_compare( - "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithBrokenMcpServer", - terminal_size=(120, 36), - run_before=run_before, - ) + assert snap_compare( + "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithBrokenMcpServer", + terminal_size=(120, 36), + run_before=run_before, + ) def test_snapshot_mcp_overview(snap_compare: SnapCompare) -> None: async def run_before(pilot: Pilot) -> None: await _run_mcp_command(pilot, "/mcp") - with patch(_MCP_PATCH, FakeMCPRegistry): - assert snap_compare( - "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", - terminal_size=(120, 36), - run_before=run_before, - ) + assert snap_compare( + "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", + terminal_size=(120, 36), + run_before=run_before, + ) def test_snapshot_mcp_overview_navigate_down(snap_compare: SnapCompare) -> None: @@ -118,12 +109,11 @@ def test_snapshot_mcp_overview_navigate_down(snap_compare: SnapCompare) -> None: await pilot.press("down") await pilot.pause(0.1) - with patch(_MCP_PATCH, FakeMCPRegistry): - assert snap_compare( - "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", - terminal_size=(120, 36), - run_before=run_before, - ) + assert snap_compare( + "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", + terminal_size=(120, 36), + run_before=run_before, + ) def test_snapshot_mcp_enter_drills_into_server(snap_compare: SnapCompare) -> None: @@ -135,12 +125,11 @@ def test_snapshot_mcp_enter_drills_into_server(snap_compare: SnapCompare) -> Non await pilot.pause(0.1) await pilot.press("enter") - with patch(_MCP_PATCH, FakeMCPRegistry): - assert snap_compare( - "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", - terminal_size=(120, 36), - run_before=run_before, - ) + assert snap_compare( + "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", + terminal_size=(120, 36), + run_before=run_before, + ) def test_snapshot_mcp_server_arg(snap_compare: SnapCompare) -> None: @@ -148,12 +137,11 @@ def test_snapshot_mcp_server_arg(snap_compare: SnapCompare) -> None: await _run_mcp_command(pilot, "/mcp filesystem") await pilot.pause(0.1) - with patch(_MCP_PATCH, FakeMCPRegistry): - assert snap_compare( - "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", - terminal_size=(120, 36), - run_before=run_before, - ) + assert snap_compare( + "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", + terminal_size=(120, 36), + run_before=run_before, + ) def test_snapshot_mcp_backspace_returns_to_overview(snap_compare: SnapCompare) -> None: @@ -162,12 +150,11 @@ def test_snapshot_mcp_backspace_returns_to_overview(snap_compare: SnapCompare) - await pilot.press("backspace") await pilot.pause(0.1) - with patch(_MCP_PATCH, FakeMCPRegistry): - assert snap_compare( - "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", - terminal_size=(120, 36), - run_before=run_before, - ) + assert snap_compare( + "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", + terminal_size=(120, 36), + run_before=run_before, + ) def test_snapshot_mcp_escape_closes(snap_compare: SnapCompare) -> None: @@ -176,12 +163,11 @@ def test_snapshot_mcp_escape_closes(snap_compare: SnapCompare) -> None: await pilot.press("escape") await pilot.pause(0.2) - with patch(_MCP_PATCH, FakeMCPRegistry): - assert snap_compare( - "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", - terminal_size=(120, 36), - run_before=run_before, - ) + assert snap_compare( + "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", + terminal_size=(120, 36), + run_before=run_before, + ) def test_snapshot_mcp_refresh_shortcut(snap_compare: SnapCompare) -> None: @@ -190,12 +176,11 @@ def test_snapshot_mcp_refresh_shortcut(snap_compare: SnapCompare) -> None: await pilot.press("r") await pilot.pause(0.2) - with patch(_MCP_PATCH, FakeMCPRegistry): - assert snap_compare( - "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", - terminal_size=(120, 36), - run_before=run_before, - ) + assert snap_compare( + "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithMcpServers", + terminal_size=(120, 36), + run_before=run_before, + ) # --------------------------------------------------------------------------- @@ -274,12 +259,11 @@ def test_snapshot_mcp_with_connectors_overview(snap_compare: SnapCompare) -> Non async def run_before(pilot: Pilot) -> None: await _run_mcp_command(pilot, "/mcp") - with patch(_MCP_PATCH, FakeMCPRegistry): - assert snap_compare( - "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithConnectors", - terminal_size=(120, 36), - run_before=run_before, - ) + assert snap_compare( + "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithConnectors", + terminal_size=(120, 36), + run_before=run_before, + ) # --------------------------------------------------------------------------- @@ -399,12 +383,11 @@ def test_snapshot_mcp_drill_into_connector(snap_compare: SnapCompare) -> None: await pilot.press("enter") # drill in await pilot.pause(0.1) - with patch(_MCP_PATCH, FakeMCPRegistry): - assert snap_compare( - "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithConnectors", - terminal_size=(120, 36), - run_before=run_before, - ) + assert snap_compare( + "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithConnectors", + terminal_size=(120, 36), + run_before=run_before, + ) def test_snapshot_mcp_connector_back_to_overview(snap_compare: SnapCompare) -> None: @@ -418,9 +401,8 @@ def test_snapshot_mcp_connector_back_to_overview(snap_compare: SnapCompare) -> N await pilot.press("backspace") await pilot.pause(0.1) - with patch(_MCP_PATCH, FakeMCPRegistry): - assert snap_compare( - "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithConnectors", - terminal_size=(120, 36), - run_before=run_before, - ) + assert snap_compare( + "test_ui_snapshot_mcp_command.py:SnapshotTestAppWithConnectors", + terminal_size=(120, 36), + run_before=run_before, + ) diff --git a/tests/test_agent_auto_compact.py b/tests/test_agent_auto_compact.py index 3482ddf..94d4ec0 100644 --- a/tests/test_agent_auto_compact.py +++ b/tests/test_agent_auto_compact.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +from pathlib import Path from typing import cast from unittest.mock import AsyncMock, patch @@ -268,6 +269,28 @@ async def test_compact_appends_extra_instructions_to_prompt() -> None: assert "focus on auth" in compaction_prompt +@pytest.mark.asyncio +async def test_compact_uses_configured_compaction_prompt( + mock_prompts_dirs: tuple[Path, Path], +) -> None: + project_prompts, _ = mock_prompts_dirs + (project_prompts / "theorem_compact.md").write_text("Summarize theorem progress") + + backend = FakeBackend([[mock_llm_chunk(content="")]]) + cfg = build_test_vibe_config( + models=make_test_models(auto_compact_threshold=999), + compaction_prompt_id="theorem_compact", + ) + agent = build_test_agent_loop(config=cfg, backend=backend) + agent.messages.append(LLMMessage(role=Role.user, content="Hello")) + agent.stats.context_tokens = 100 + + await agent.compact() + + compaction_prompt = backend.requests_messages[0][-1].content + assert compaction_prompt == "Summarize theorem progress" + + @pytest.mark.asyncio async def test_compact_without_extra_instructions_has_no_additional_section() -> None: backend = FakeBackend([[mock_llm_chunk(content="")]]) @@ -281,3 +304,39 @@ async def test_compact_without_extra_instructions_has_no_additional_section() -> compaction_prompt = backend.requests_messages[0][-1].content assert compaction_prompt is not None assert "## Additional Instructions" not in compaction_prompt + + +@pytest.mark.asyncio +async def test_compact_message_shape_preserves_prior_user_messages() -> None: + from vibe.core.prompts import UtilityPrompt + + summary_prefix = UtilityPrompt.COMPACT_SUMMARY_PREFIX.read() + backend = FakeBackend([[mock_llm_chunk(content="fresh summary body")]]) + cfg = build_test_vibe_config(models=make_test_models(auto_compact_threshold=999)) + agent = build_test_agent_loop(config=cfg, backend=backend) + system_message_before = agent.messages[0] + + agent.messages.append(LLMMessage(role=Role.user, content="first real ask")) + agent.messages.append( + LLMMessage(role=Role.user, content="middleware ping", injected=True) + ) + agent.messages.append(LLMMessage(role=Role.assistant, content="ack")) + agent.messages.append( + LLMMessage(role=Role.user, content=f"{summary_prefix}\nprior summary blob") + ) + agent.messages.append(LLMMessage(role=Role.user, content="follow-up ask")) + agent.stats.context_tokens = 100 + + await agent.compact() + + final = list(agent.messages) + assert len(final) == 4 # [system, prior_user_1, prior_user_2, wrapped_summary] + assert final[0] is system_message_before + assert [m.role for m in final[1:]] == [Role.user, Role.user, Role.user] + assert final[1].content == "first real ask" + assert final[2].content == "follow-up ask" + # Injected and prior-summary user messages must be filtered out. + assert all("middleware ping" not in (m.content or "") for m in final) + assert sum("prior summary blob" in (m.content or "") for m in final) == 0 + # Final message is the wrapped summary the next agent will read first. + assert final[-1].content == f"{summary_prefix}\nfresh summary body" diff --git a/tests/test_agents.py b/tests/test_agents.py index 60f3a35..e979121 100644 --- a/tests/test_agents.py +++ b/tests/test_agents.py @@ -19,7 +19,7 @@ from vibe.core.agents.models import ( _deep_merge, ) from vibe.core.config import VibeConfig -from vibe.core.config.harness_files import HarnessFilesManager +from vibe.core.prompts import UtilityPrompt from vibe.core.tools.base import ToolPermission from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role @@ -233,35 +233,11 @@ class TestAgentApplyToConfig: assert "ask_user_question" in result.enabled_tools def test_custom_prompt_found_in_global_when_missing_from_project( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + self, mock_prompts_dirs: tuple[Path, Path] ) -> None: - """Regression test for https://github.com/mistralai/mistral-vibe/issues/288 - - When a custom prompt .md file is absent from the project-local prompts - directory, the system_prompt property should fall back to the global - ~/.vibe/prompts/ directory and load the file from there. - """ - project_prompts = tmp_path / "project" / ".vibe" / "prompts" - project_prompts.mkdir(parents=True) - - global_prompts = tmp_path / "home" / ".vibe" / "prompts" - global_prompts.mkdir(parents=True) + _, global_prompts = mock_prompts_dirs (global_prompts / "cc.md").write_text("Global custom prompt") - class _MockManager(HarnessFilesManager): - @property - def project_prompts_dirs(self) -> list[Path]: - return [project_prompts] - - @property - def user_prompts_dirs(self) -> list[Path]: - return [global_prompts] - - mock_manager = _MockManager(sources=("user",)) - monkeypatch.setattr( - "vibe.core.prompts.get_harness_files_manager", lambda: mock_manager - ) - base = VibeConfig(include_project_context=False, include_prompt_detail=False) agent = AgentProfile( name="cc", @@ -275,7 +251,7 @@ class TestAgentApplyToConfig: assert result.system_prompt == "Global custom prompt" def test_custom_prompt_overrides_builtin( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + self, mock_prompts_dirs: tuple[Path, Path] ) -> None: """Custom prompts in .vibe/prompts/ should override built-in prompts. @@ -283,24 +259,9 @@ class TestAgentApplyToConfig: project or user prompts directory must take priority over the bundled SystemPrompt enum. """ - project_prompts = tmp_path / "project" / ".vibe" / "prompts" - project_prompts.mkdir(parents=True) + project_prompts, _ = mock_prompts_dirs (project_prompts / "explore.md").write_text("My custom explore prompt") - class _MockManager(HarnessFilesManager): - @property - def project_prompts_dirs(self) -> list[Path]: - return [project_prompts] - - @property - def user_prompts_dirs(self) -> list[Path]: - return [] - - mock_manager = _MockManager(sources=("user",)) - monkeypatch.setattr( - "vibe.core.prompts.get_harness_files_manager", lambda: mock_manager - ) - config = VibeConfig( system_prompt_id="explore", include_project_context=False, @@ -308,6 +269,71 @@ class TestAgentApplyToConfig: ) assert config.system_prompt == "My custom explore prompt" + def test_custom_compaction_prompt_found_in_global_when_missing_from_project( + self, mock_prompts_dirs: tuple[Path, Path] + ) -> None: + _, global_prompts = mock_prompts_dirs + (global_prompts / "proofs.md").write_text("Global custom compaction prompt") + + config = VibeConfig( + compaction_prompt_id="proofs", + include_project_context=False, + include_prompt_detail=False, + ) + assert config.compaction_prompt == "Global custom compaction prompt" + + def test_custom_compaction_prompt_overrides_builtin( + self, mock_prompts_dirs: tuple[Path, Path] + ) -> None: + project_prompts, _ = mock_prompts_dirs + (project_prompts / "compact.md").write_text("My custom compact prompt") + + config = VibeConfig( + compaction_prompt_id="compact", + include_project_context=False, + include_prompt_detail=False, + ) + assert config.compaction_prompt == "My custom compact prompt" + + def test_default_compaction_prompt_falls_back_to_builtin( + self, mock_prompts_dirs: tuple[Path, Path] + ) -> None: + config = VibeConfig(include_project_context=False, include_prompt_detail=False) + assert config.compaction_prompt == UtilityPrompt.COMPACT.read() + + def test_invalid_compaction_prompt_reports_setting_name( + self, mock_prompts_dirs: tuple[Path, Path] + ) -> None: + project_prompts, user_prompts = mock_prompts_dirs + (project_prompts / "alpha.md").write_text("a") + (user_prompts / "beta.md").write_text("b") + + with pytest.raises(ValueError) as exc_info: + VibeConfig( + compaction_prompt_id="unknown", + include_project_context=False, + include_prompt_detail=False, + ) + + error_text = str(exc_info.value) + assert "Invalid compaction_prompt_id value: 'unknown'" in error_text + assert 'available prompts ("compact")' in error_text + assert '(available: "alpha", "beta")' in error_text + + @pytest.mark.parametrize( + "malicious_id", + ["../../../etc/passwd", "..", ".", "subdir/compact", "back\\slash", ""], + ) + def test_prompt_id_rejects_path_traversal( + self, mock_prompts_dirs: tuple[Path, Path], malicious_id: str + ) -> None: + with pytest.raises(ValueError, match="must be a bare filename"): + VibeConfig( + compaction_prompt_id=malicious_id, + include_project_context=False, + include_prompt_detail=False, + ) + class TestAgentProfileOverrides: def test_default_agent_disables_exit_plan_mode(self) -> None: @@ -679,30 +705,12 @@ class TestAgentManagerFiltering: class TestAgentLoopInitialization: def test_agent_system_prompt_id_is_applied_on_init( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + self, mock_prompts_dirs: tuple[Path, Path], monkeypatch: pytest.MonkeyPatch ) -> None: - project_prompts = tmp_path / "project" / ".vibe" / "prompts" - project_prompts.mkdir(parents=True) - - global_prompts = tmp_path / "home" / ".vibe" / "prompts" - global_prompts.mkdir(parents=True) + _, global_prompts = mock_prompts_dirs custom_prompt_content = "CUSTOM_AGENT_PROMPT_MARKER" (global_prompts / "custom_agent.md").write_text(custom_prompt_content) - class _MockManager(HarnessFilesManager): - @property - def project_prompts_dirs(self) -> list[Path]: - return [project_prompts] - - @property - def user_prompts_dirs(self) -> list[Path]: - return [global_prompts] - - mock_manager = _MockManager(sources=("user",)) - monkeypatch.setattr( - "vibe.core.prompts.get_harness_files_manager", lambda: mock_manager - ) - custom_agent = AgentProfile( name="custom_test_agent", display_name="Custom Test", diff --git a/tests/test_cli_programmatic_preload.py b/tests/test_cli_programmatic_preload.py index 4a0ed41..b0829be 100644 --- a/tests/test_cli_programmatic_preload.py +++ b/tests/test_cli_programmatic_preload.py @@ -7,6 +7,7 @@ from tests.mock.mock_backend_factory import mock_backend_factory from tests.mock.utils import mock_llm_chunk from tests.stubs.fake_backend import FakeBackend from vibe.core import run_programmatic +from vibe.core.agents.models import BuiltinAgentName from vibe.core.types import Backend, LLMMessage, OutputFormat, Role @@ -66,6 +67,7 @@ def test_run_programmatic_preload_streaming_is_batched( prompt="Can you summarize what decorators are?", output_format=OutputFormat.STREAMING, previous_messages=previous, + agent_name=BuiltinAgentName.AUTO_APPROVE, ) roles = [r for r, _ in spy.emitted] @@ -136,6 +138,7 @@ def test_run_programmatic_ignores_system_messages_in_previous( content="Second system message that should be ignored.", ), ], + agent_name=BuiltinAgentName.AUTO_APPROVE, ) roles = [r for r, _ in spy.emitted] @@ -176,6 +179,7 @@ def test_run_programmatic_teleport_ignored_when_nuage_disabled( prompt="Hello", output_format=OutputFormat.STREAMING, teleport=True, + agent_name=BuiltinAgentName.AUTO_APPROVE, ) roles = [r for r, _ in spy.emitted] diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 2aa89c1..30cb4c2 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -14,6 +14,7 @@ from vibe.core.middleware import ( MiddlewarePipeline, ReadOnlyAgentMiddleware, ResetReason, + TokenLimitMiddleware, make_plan_agent_reminder, ) from vibe.core.types import AgentStats, MessageList @@ -634,3 +635,31 @@ class TestReadOnlyAgentMiddlewareIntegration: # 8. Stay in default: no injection r = await plan_middleware.before_turn(_ctx()) assert r.action == MiddlewareAction.CONTINUE + + +class TestTokenLimitMiddleware: + @pytest.mark.asyncio + async def test_stops_when_session_total_tokens_exceeds_limit( + self, ctx: ConversationContext + ) -> None: + middleware = TokenLimitMiddleware(14) + ctx.stats.session_prompt_tokens = 10 + ctx.stats.session_completion_tokens = 5 + + result = await middleware.before_turn(ctx) + + assert result.action == MiddlewareAction.STOP + assert result.reason == "Token limit exceeded: 15 > 14" + + @pytest.mark.asyncio + async def test_allows_when_session_total_tokens_matches_limit( + self, ctx: ConversationContext + ) -> None: + middleware = TokenLimitMiddleware(15) + ctx.stats.session_prompt_tokens = 10 + ctx.stats.session_completion_tokens = 5 + + result = await middleware.before_turn(ctx) + + assert result.action == MiddlewareAction.CONTINUE + assert result.reason is None diff --git a/tests/vscode_extension_promo/__init__.py b/tests/vscode_extension_promo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/vscode_extension_promo/fake_repository.py b/tests/vscode_extension_promo/fake_repository.py new file mode 100644 index 0000000..daf4241 --- /dev/null +++ b/tests/vscode_extension_promo/fake_repository.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from vibe.cli.vscode_extension_promo import ( + VscodeExtensionPromoRepository, + VscodeExtensionPromoState, +) + + +class FakeVscodeExtensionPromoRepository(VscodeExtensionPromoRepository): + def __init__(self, state: VscodeExtensionPromoState | None = None) -> None: + self.state: VscodeExtensionPromoState | None = state + + async def get(self) -> VscodeExtensionPromoState | None: + return self.state + + async def set(self, state: VscodeExtensionPromoState) -> None: + self.state = state diff --git a/tests/vscode_extension_promo/test_ui_vscode_extension_promo.py b/tests/vscode_extension_promo/test_ui_vscode_extension_promo.py new file mode 100644 index 0000000..3d1c590 --- /dev/null +++ b/tests/vscode_extension_promo/test_ui_vscode_extension_promo.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from pathlib import Path +import time +from unittest.mock import patch + +import pytest + +from tests.conftest import build_test_vibe_app, build_test_vibe_config +from tests.update_notifier.adapters.fake_update_cache_repository import ( + FakeUpdateCacheRepository, +) +from tests.update_notifier.adapters.fake_update_gateway import FakeUpdateGateway +from tests.vscode_extension_promo.fake_repository import ( + FakeVscodeExtensionPromoRepository, +) +from vibe.cli.textual_ui.app import VibeApp +from vibe.cli.textual_ui.widgets.messages import ( + VSCODE_EXTENSION_PROMO_WHATS_NEW_SUFFIX, + VscodeExtensionPromoMessage, + WhatsNewMessage, +) +from vibe.cli.update_notifier import UpdateCache +from vibe.cli.vscode_extension_promo import ( + VscodeExtensionPromo, + VscodeExtensionPromoState, +) +from vibe.core.config import VibeConfig + + +@pytest.fixture +def vibe_config() -> VibeConfig: + return build_test_vibe_config(enable_update_checks=True) + + +def _build_app( + *, + config: VibeConfig, + promo: VscodeExtensionPromo | None, + update_cache_repository: FakeUpdateCacheRepository, + current_version: str = "1.0.0", +) -> VibeApp: + return build_test_vibe_app( + config=config, + update_notifier=FakeUpdateGateway(update=None), + update_cache_repository=update_cache_repository, + current_version=current_version, + vscode_extension_promo=promo, + ) + + +async def _wait_for(predicate: Callable[[], bool], pilot, timeout: float = 1.0) -> None: + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout + while loop.time() < deadline: + if predicate(): + return + await pilot.pause(0.05) + pytest.fail("Condition not met within timeout") + + +@pytest.fixture(autouse=True) +def _bypass_promo_gates() -> object: + with ( + patch("vibe.cli.textual_ui.app._is_vscode_family_terminal", return_value=True), + patch("vibe.cli.textual_ui.app.should_show_promo") as mocked_should_show, + ): + mocked_should_show.side_effect = lambda state: ( + state is None or state.shown_count < 10 + ) + yield mocked_should_show + + +@pytest.mark.asyncio +async def test_promo_appears_as_standalone_chat_message_when_no_whats_new( + vibe_config: VibeConfig, tmp_path: Path +) -> None: + repository = FakeVscodeExtensionPromoRepository() + promo = VscodeExtensionPromo(repository=repository, initial_state=None) + cache = UpdateCache( + latest_version="1.0.0", + stored_at_timestamp=int(time.time()), + seen_whats_new_version="1.0.0", + ) + app = _build_app( + config=vibe_config, + promo=promo, + update_cache_repository=FakeUpdateCacheRepository(update_cache=cache), + ) + + with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path): + async with app.run_test() as pilot: + await pilot.pause(0.3) + await _wait_for(lambda: bool(app.query(VscodeExtensionPromoMessage)), pilot) + + assert not app.query(WhatsNewMessage) + assert repository.state == VscodeExtensionPromoState(shown_count=1) + + +@pytest.mark.asyncio +async def test_promo_is_merged_into_whats_new_message_when_both_shown( + vibe_config: VibeConfig, tmp_path: Path +) -> None: + repository = FakeVscodeExtensionPromoRepository() + promo = VscodeExtensionPromo(repository=repository, initial_state=None) + cache = UpdateCache( + latest_version="1.0.0", + stored_at_timestamp=int(time.time()), + seen_whats_new_version=None, + ) + app = _build_app( + config=vibe_config, + promo=promo, + update_cache_repository=FakeUpdateCacheRepository(update_cache=cache), + ) + + whats_new_content = "# What's New\n\n- Feature 1" + with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path): + (tmp_path / "whats_new.md").write_text(whats_new_content) + async with app.run_test() as pilot: + await pilot.pause(0.3) + await _wait_for(lambda: bool(app.query(WhatsNewMessage)), pilot) + message = app.query_one(WhatsNewMessage) + await _wait_for( + lambda: repository.state == VscodeExtensionPromoState(shown_count=1), + pilot, + ) + + assert not app.query(VscodeExtensionPromoMessage) + assert message._content.startswith(whats_new_content) + assert message._content.endswith(VSCODE_EXTENSION_PROMO_WHATS_NEW_SUFFIX) + + +@pytest.mark.asyncio +async def test_promo_does_not_appear_when_no_promo_dependency_provided( + vibe_config: VibeConfig, tmp_path: Path +) -> None: + cache = UpdateCache( + latest_version="1.0.0", + stored_at_timestamp=int(time.time()), + seen_whats_new_version="1.0.0", + ) + app = _build_app( + config=vibe_config, + promo=None, + update_cache_repository=FakeUpdateCacheRepository(update_cache=cache), + ) + + with patch("vibe.cli.update_notifier.whats_new.VIBE_ROOT", tmp_path): + async with app.run_test() as pilot: + await pilot.pause(0.4) + + assert not app.query(VscodeExtensionPromoMessage) + assert not app.query(WhatsNewMessage) diff --git a/tests/vscode_extension_promo/test_vscode_extension_promo.py b/tests/vscode_extension_promo/test_vscode_extension_promo.py new file mode 100644 index 0000000..cd46b37 --- /dev/null +++ b/tests/vscode_extension_promo/test_vscode_extension_promo.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from datetime import UTC, datetime, timedelta +from pathlib import Path +import tomllib + +import pytest +import tomli_w + +from vibe.cli.vscode_extension_promo import ( + MAX_SHOWN_COUNT, + PROMO_START, + FileSystemVscodeExtensionPromoRepository, + VscodeExtensionPromoState, + should_show_promo, +) + + +@pytest.mark.asyncio +async def test_repository_roundtrip_persists_shown_count(tmp_path: Path) -> None: + repository = FileSystemVscodeExtensionPromoRepository(base_path=tmp_path) + + await repository.set(VscodeExtensionPromoState(shown_count=4)) + + assert await repository.get() == VscodeExtensionPromoState(shown_count=4) + + +@pytest.mark.asyncio +async def test_repository_set_preserves_unrelated_cache_sections( + tmp_path: Path, +) -> None: + with (tmp_path / "cache.toml").open("wb") as f: + tomli_w.dump({"update_cache": {"latest_version": "2.0.0"}}, f) + repository = FileSystemVscodeExtensionPromoRepository(base_path=tmp_path) + + await repository.set(VscodeExtensionPromoState(shown_count=1)) + + with (tmp_path / "cache.toml").open("rb") as f: + data = tomllib.load(f) + assert data["update_cache"]["latest_version"] == "2.0.0" + assert data["vscode_extension_promo"]["shown_count"] == 1 + + +@pytest.mark.parametrize( + "state,expected", + [ + (None, True), + (VscodeExtensionPromoState(shown_count=0), True), + (VscodeExtensionPromoState(shown_count=MAX_SHOWN_COUNT - 1), True), + (VscodeExtensionPromoState(shown_count=MAX_SHOWN_COUNT), False), + (VscodeExtensionPromoState(shown_count=MAX_SHOWN_COUNT + 5), False), + ], +) +def test_should_show_promo_respects_threshold( + state: VscodeExtensionPromoState | None, expected: bool +) -> None: + assert should_show_promo(state, now=PROMO_START) is expected + + +@pytest.mark.parametrize( + "now,expected", + [ + (PROMO_START - timedelta(seconds=1), False), + (PROMO_START - timedelta(days=365), False), + (PROMO_START, True), + (PROMO_START + timedelta(seconds=1), True), + ], +) +def test_should_show_promo_gated_by_start_date(now: datetime, expected: bool) -> None: + assert should_show_promo(None, now=now) is expected + + +def test_promo_start_is_2026_05_28_16_utc() -> None: + assert PROMO_START == datetime(2026, 5, 28, 16, 0, tzinfo=UTC) diff --git a/uv.lock b/uv.lock index f10ec97..3811060 100644 --- a/uv.lock +++ b/uv.lock @@ -821,7 +821,7 @@ wheels = [ [[package]] name = "mistral-vibe" -version = "2.11.0" +version = "2.11.1" source = { editable = "." } dependencies = [ { name = "agent-client-protocol" }, diff --git a/vibe/__init__.py b/vibe/__init__.py index d3cdcc6..4ed606a 100644 --- a/vibe/__init__.py +++ b/vibe/__init__.py @@ -3,4 +3,4 @@ from __future__ import annotations from pathlib import Path VIBE_ROOT = Path(__file__).parent -__version__ = "2.11.0" +__version__ = "2.11.1" diff --git a/vibe/acp/acp_agent_loop.py b/vibe/acp/acp_agent_loop.py index 5708f7e..8f07fc5 100644 --- a/vibe/acp/acp_agent_loop.py +++ b/vibe/acp/acp_agent_loop.py @@ -1,7 +1,7 @@ from __future__ import annotations import asyncio -from collections.abc import AsyncGenerator, Callable +from collections.abc import AsyncGenerator, Callable, Mapping from contextlib import aclosing from dataclasses import dataclass from datetime import UTC @@ -125,6 +125,7 @@ from vibe.core.config import ( from vibe.core.data_retention import DATA_RETENTION_MESSAGE from vibe.core.feedback import record_feedback_asked, should_show_feedback from vibe.core.hooks.config import load_hooks_from_fs +from vibe.core.paths import GLOBAL_ENV_FILE from vibe.core.proxy_setup import ( ProxySetupError, parse_proxy_command, @@ -165,14 +166,18 @@ from vibe.core.utils import ( get_user_cancellation_message, ) from vibe.setup.auth import ( + AuthState, + AuthStateKind, BrowserSignInAttempt, BrowserSignInError, BrowserSignInErrorCode, BrowserSignInService, HttpBrowserSignInGateway, + assess_auth_state, ) from vibe.setup.auth.api_key_persistence import ( persist_api_key, + remove_api_key, resolve_api_key_provider, ) from vibe.setup.onboarding.context import OnboardingContext @@ -205,6 +210,22 @@ class TelemetrySendNotification(BaseModel): session_id: str = Field(validation_alias=AliasChoices("session_id", "sessionId")) +class AuthStatusResponse(BaseModel): + model_config = ConfigDict(extra="forbid", populate_by_name=True) + + authenticated: bool + auth_state: AuthStateKind = Field(alias="authState") + sign_out_available: bool = Field(alias="signOutAvailable") + + +def _auth_status_response_from_auth_state(auth_state: AuthState) -> AuthStatusResponse: + return AuthStatusResponse( + authenticated=auth_state.can_use_active_provider, + authState=auth_state.kind, + signOutAvailable=auth_state.sign_out_available, + ) + + def _dispatch_at_mention_inserted( client: TelemetryClient, properties: dict[str, Any] ) -> None: @@ -250,6 +271,7 @@ RETRYABLE_BROWSER_SIGN_IN_COMPLETION_ERRORS = { OnboardingContextLoader = Callable[[], OnboardingContext] ApiKeyPersister = Callable[[ProviderConfig, str], str] +ApiKeyRemover = Callable[[ProviderConfig], None] class BrowserSignInServiceAdapter(Protocol): @@ -274,10 +296,17 @@ class VibeAcpAgentLoop(AcpAgent): onboarding_context_loader: OnboardingContextLoader | None = None, browser_sign_in_service_factory: BrowserSignInServiceFactory | None = None, api_key_persister: ApiKeyPersister = persist_api_key, + api_key_remover: ApiKeyRemover = remove_api_key, + environ_before_dotenv_load: Mapping[str, str] | None = None, ) -> None: self.sessions: dict[str, AcpSessionLoop] = {} self.client_capabilities: ClientCapabilities | None = None self.client_info: Implementation | None = None + self._environ_before_dotenv_load = dict( + environ_before_dotenv_load + if environ_before_dotenv_load is not None + else os.environ + ) self._pending_browser_sign_in_attempts: dict[ str, PendingBrowserSignInAttempt ] = {} @@ -288,11 +317,12 @@ class VibeAcpAgentLoop(AcpAgent): browser_sign_in_service_factory or self._build_browser_sign_in_service ) self._persist_api_key = api_key_persister + self._remove_api_key = api_key_remover def _build_browser_auth_method( self, context: OnboardingContext, method_id: str ) -> AuthMethodAgent | None: - if not context.browser_sign_in_enabled: + if not context.supports_browser_sign_in: return None return AuthMethodAgent( @@ -340,7 +370,7 @@ class VibeAcpAgentLoop(AcpAgent): def _load_enabled_browser_sign_in_context(self) -> OnboardingContext: context = self._load_onboarding_context() - if not context.browser_sign_in_enabled: + if not context.supports_browser_sign_in: raise InvalidRequestError( "Browser sign-in is not available for the configured provider." ) @@ -1474,8 +1504,46 @@ class VibeAcpAgentLoop(AcpAgent): ) return {} + def _assess_current_auth_state(self) -> tuple[ProviderConfig, AuthState]: + load_dotenv_values(env_path=GLOBAL_ENV_FILE.path) + provider = self._load_onboarding_context().provider + auth_state = assess_auth_state( + provider, + process_env_had_value_before_dotenv_load=bool( + provider.api_key_env_var + and self._environ_before_dotenv_load.get(provider.api_key_env_var) + ), + ) + return provider, auth_state + + def _handle_auth_status(self) -> dict[str, Any]: + _, auth_state = self._assess_current_auth_state() + return _auth_status_response_from_auth_state(auth_state).model_dump( + mode="json", by_alias=True + ) + + def _handle_auth_sign_out(self) -> dict[str, Any]: + provider, auth_state = self._assess_current_auth_state() + if not auth_state.sign_out_available: + raise InvalidRequestError( + f"Sign out is not available for auth state: {auth_state.kind.value}" + ) + + try: + self._remove_api_key(provider) + except (OSError, ValueError) as exc: + raise InternalError(f"Failed to sign out: {exc}") from exc + + return {} + @override async def ext_method(self, method: str, params: dict) -> dict: + if method == "auth/status": + return self._handle_auth_status() + + if method == "auth/signOut": + return self._handle_auth_sign_out() + if method == "session/set_title": return await self._handle_session_set_title(params) @@ -1744,8 +1812,10 @@ class VibeAcpAgentLoop(AcpAgent): SESSION_CLOSED_FLUSH_TIMEOUT_SECONDS = 1.0 -def run_acp_server() -> None: - agent = VibeAcpAgentLoop() +def run_acp_server( + *, environ_before_dotenv_load: Mapping[str, str] | None = None +) -> None: + agent = VibeAcpAgentLoop(environ_before_dotenv_load=environ_before_dotenv_load) install_sigterm_flush = TelemetryClient(config_getter=VibeConfig.load).is_active() received_sigterm = False previous_sigterm_handler = signal.getsignal(signal.SIGTERM) diff --git a/vibe/acp/entrypoint.py b/vibe/acp/entrypoint.py index a362b24..d3df744 100644 --- a/vibe/acp/entrypoint.py +++ b/vibe/acp/entrypoint.py @@ -84,6 +84,7 @@ def main() -> None: from vibe.core.tracing import setup_tracing from vibe.setup.onboarding import run_onboarding + environ_before_dotenv_load = os.environ.copy() load_dotenv_values() bootstrap_config_files() args = parse_arguments() @@ -104,7 +105,7 @@ def main() -> None: except Exception: pass # tracing disabled - run_acp_server() + run_acp_server(environ_before_dotenv_load=environ_before_dotenv_load) if __name__ == "__main__": diff --git a/vibe/cli/cli.py b/vibe/cli/cli.py index 6752a64..c8d0867 100644 --- a/vibe/cli/cli.py +++ b/vibe/cli/cli.py @@ -11,13 +11,7 @@ import tomli_w from vibe import __version__ from vibe.cli.textual_ui.app import StartupOptions, run_textual_ui from vibe.core.agent_loop import AgentLoop, TeleportError -from vibe.core.agents.models import BuiltinAgentName -from vibe.core.config import ( - MissingAPIKeyError, - MissingPromptFileError, - VibeConfig, - load_dotenv_values, -) +from vibe.core.config import MissingAPIKeyError, VibeConfig, load_dotenv_values from vibe.core.config.harness_files import get_harness_files_manager from vibe.core.hooks.config import load_hooks_from_fs from vibe.core.logger import logger @@ -44,8 +38,6 @@ def _build_cli_entrypoint_metadata() -> EntrypointMetadata: def get_initial_agent_name(args: argparse.Namespace, config: VibeConfig) -> str: - if args.prompt is not None and args.agent is None: - return BuiltinAgentName.AUTO_APPROVE return args.agent or config.default_agent @@ -77,9 +69,6 @@ def load_config_or_exit(*, interactive: bool) -> VibeConfig: sys.exit(1) run_onboarding(entrypoint_metadata=_build_cli_entrypoint_metadata()) return VibeConfig.load() - except MissingPromptFileError as e: - rprint(f"[yellow]Invalid system prompt id: {e}[/]") - sys.exit(1) except ValueError as e: rprint(f"[yellow]{e}[/]") sys.exit(1) @@ -238,6 +227,7 @@ def run_cli(args: argparse.Namespace) -> None: prompt=programmatic_prompt or "", max_turns=args.max_turns, max_price=args.max_price, + max_session_tokens=args.max_tokens, output_format=output_format, previous_messages=loaded_session[0] if loaded_session else None, agent_name=initial_agent_name, diff --git a/vibe/cli/commands.py b/vibe/cli/commands.py index caf8dcd..500ff17 100644 --- a/vibe/cli/commands.py +++ b/vibe/cli/commands.py @@ -114,7 +114,7 @@ class CommandRegistry: ), "teleport": Command( aliases=frozenset(["/teleport"]), - description="Teleport session to Vibe Code", + description="Teleport session to Vibe Code Web", handler="_teleport_command", is_available=CommandAvailabilityContext.is_teleport_available, ), diff --git a/vibe/cli/entrypoint.py b/vibe/cli/entrypoint.py index 5ad01ce..4118b6f 100644 --- a/vibe/cli/entrypoint.py +++ b/vibe/cli/entrypoint.py @@ -44,8 +44,9 @@ def parse_arguments() -> argparse.Namespace: nargs="?", const="", metavar="TEXT", - help="Run in programmatic mode: send prompt, auto-approve all tools, " - "output response, and exit.", + help="Run in programmatic mode: send prompt, output response, and exit. " + "Tool approval follows the selected --agent (or 'default_agent' config); " + "pass --agent auto-approve for the previous YOLO behavior.", ) parser.add_argument( "--max-turns", @@ -61,6 +62,14 @@ def parse_arguments() -> argparse.Namespace: help="Maximum cost in dollars (only applies in programmatic mode with -p). " "Session will be interrupted if cost exceeds this limit.", ) + parser.add_argument( + "--max-tokens", + type=int, + metavar="N", + help="Maximum total prompt + completion tokens across the session " + "(only applies in programmatic mode with -p). " + "Session will be interrupted if usage exceeds this limit.", + ) parser.add_argument( "--enabled-tools", action="append", @@ -84,10 +93,10 @@ def parse_arguments() -> argparse.Namespace: metavar="NAME", default=None, help="Agent to use (builtin: default, plan, accept-edits, auto-approve, " - "or custom from ~/.vibe/agents/NAME.toml). In interactive mode, " - "defaults to the 'default_agent' config setting. In programmatic " - "mode (-p/--prompt), defaults to auto-approve and 'default_agent' " - "is ignored.", + "or custom from ~/.vibe/agents/NAME.toml). Defaults to the " + "'default_agent' config setting in both interactive and programmatic " + "(-p/--prompt) mode. Pass --agent auto-approve for non-interactive " + "automation that needs tools to run without approval.", ) parser.add_argument("--setup", action="store_true", help="Setup API key and exit") parser.add_argument( diff --git a/vibe/cli/textual_ui/app.py b/vibe/cli/textual_ui/app.py index 51b3afa..de8139f 100644 --- a/vibe/cli/textual_ui/app.py +++ b/vibe/cli/textual_ui/app.py @@ -44,6 +44,7 @@ from vibe.cli.plan_offer.decide_plan_offer import ( resolve_api_key_for_plan, ) from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIGateway, WhoAmIPlanType +from vibe.cli.terminal_detect import Terminal, detect_terminal from vibe.cli.textual_ui.handlers.event_handler import EventHandler from vibe.cli.textual_ui.notifications import ( NotificationContext, @@ -73,13 +74,17 @@ from vibe.cli.textual_ui.widgets.loading import ( ) from vibe.cli.textual_ui.widgets.mcp_app import MCPApp, MCPSourceKind from vibe.cli.textual_ui.widgets.messages import ( + VSCODE_EXTENSION_PROMO_WHATS_NEW_SUFFIX, AssistantMessage, BashOutputMessage, ErrorMessage, InterruptMessage, + SlashCommandMessage, StreamingMessageBase, + TeleportUserMessage, UserCommandMessage, UserMessage, + VscodeExtensionPromoMessage, WarningMessage, WhatsNewMessage, ) @@ -121,6 +126,12 @@ from vibe.cli.update_notifier import ( from vibe.cli.update_notifier.update import do_update from vibe.cli.voice_manager import VoiceManager, VoiceManagerPort from vibe.cli.voice_manager.voice_manager_port import TranscribeState +from vibe.cli.vscode_extension_promo import ( + FileSystemVscodeExtensionPromoRepository, + VscodeExtensionPromo, + VscodeExtensionPromoState, + should_show_promo, +) from vibe.core.agent_loop import AgentLoop, TeleportError from vibe.core.agents import AgentProfile from vibe.core.audio_player.audio_player import AudioPlayer @@ -186,6 +197,12 @@ from vibe.core.utils import ( is_dangerous_directory, ) +_VSCODE_FAMILY_TERMINALS = {Terminal.VSCODE, Terminal.VSCODE_INSIDERS, Terminal.CURSOR} + + +def _is_vscode_family_terminal() -> bool: + return detect_terminal() in _VSCODE_FAMILY_TERMINALS + def _compute_connector_counts( config: VibeConfig, connector_registry: ConnectorRegistry | None @@ -362,6 +379,7 @@ class VibeApp(App): # noqa: PLR0904 terminal_notifier: NotificationPort | None = None, voice_manager: VoiceManagerPort | None = None, narrator_manager: NarratorManagerPort | None = None, + vscode_extension_promo: VscodeExtensionPromo | None = None, **kwargs: Any, ) -> None: super().__init__(**kwargs) @@ -404,6 +422,12 @@ class VibeApp(App): # noqa: PLR0904 self._update_cache_repository = update_cache_repository self._current_version = current_version self._plan_offer_gateway = plan_offer_gateway + self._vscode_extension_promo = vscode_extension_promo + self._show_vscode_extension_promo = ( + vscode_extension_promo is not None + and _is_vscode_family_terminal() + and should_show_promo(vscode_extension_promo.initial_state) + ) self._configure_startup_options(startup) self._last_escape_time: float | None = None self._quit_manager = QuitManager(self) @@ -643,6 +667,9 @@ class VibeApp(App): # noqa: PLR0904 if self._agent_running: await self._interrupt_agent_loop() + await self._dispatch_submitted_input(value) + + async def _dispatch_submitted_input(self, value: str) -> None: if value.startswith("!"): self._bash_task = asyncio.create_task(self._handle_bash_command(value[1:])) return @@ -956,7 +983,7 @@ class VibeApp(App): # noqa: PLR0904 self.agent_loop.telemetry_client.send_slash_command_used( cmd_name, "builtin" ) - await self._mount_and_scroll(UserMessage(user_input)) + await self._mount_and_scroll(SlashCommandMessage(user_input[1:])) handler = getattr(self, command.handler) if asyncio.iscoroutinefunction(handler): await handler(cmd_args=cmd_args) @@ -1510,7 +1537,7 @@ class VibeApp(App): # noqa: PLR0904 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(UserMessage("/teleport")) + await self._mount_and_scroll(SlashCommandMessage("teleport")) if not has_history: send_teleport_early_failure_telemetry( self.agent_loop.telemetry_client, @@ -1526,7 +1553,7 @@ class VibeApp(App): # noqa: PLR0904 ) return elif show_message: - await self._mount_and_scroll(UserMessage(value)) + await self._mount_and_scroll(TeleportUserMessage(value)) self.run_worker(self._teleport(value), exclusive=False) async def _teleport(self, prompt: str | None = None) -> None: @@ -2106,7 +2133,7 @@ class VibeApp(App): # noqa: PLR0904 messages_area = self._cached_messages_area or self.query_one("#messages") await messages_area.remove_children() - await messages_area.mount(UserMessage("/clear")) + await messages_area.mount(SlashCommandMessage("clear")) await self._mount_and_scroll( UserCommandMessage("Conversation history cleared!") ) @@ -2986,23 +3013,45 @@ class VibeApp(App): # noqa: PLR0904 ) await self._mount_and_scroll(WarningMessage(warning, show_border=False)) + async def _record_vscode_extension_promo_shown(self) -> None: + if self._vscode_extension_promo is None: + return + previous_count = ( + self._vscode_extension_promo.initial_state.shown_count + if self._vscode_extension_promo.initial_state is not None + else 0 + ) + try: + await self._vscode_extension_promo.repository.set( + VscodeExtensionPromoState(shown_count=previous_count + 1) + ) + except Exception: + logger.warning( + "Failed to persist VSCode extension promo shown count", exc_info=True + ) + async def _check_and_show_whats_new(self) -> None: if self._update_cache_repository is None: + await self._maybe_show_vscode_extension_promo() return if not await should_show_whats_new( self._current_version, self._update_cache_repository ): + await self._maybe_show_vscode_extension_promo() return content = load_whats_new_content() if content is not None: - whats_new_message = WhatsNewMessage(content) + body = content plan_offer = plan_offer_cta( self._plan_info, console_base_url=self.config.console_base_url ) if plan_offer is not None: - whats_new_message = WhatsNewMessage(f"{content}\n\n{plan_offer}") + body = f"{body}\n\n{plan_offer}" + if self._show_vscode_extension_promo: + body = f"{body}{VSCODE_EXTENSION_PROMO_WHATS_NEW_SUFFIX}" + whats_new_message = WhatsNewMessage(body) if self._history_widget_indices: whats_new_message.add_class("after-history") messages_area = self._cached_messages_area or self.query_one("#messages") @@ -3012,8 +3061,28 @@ class VibeApp(App): # noqa: PLR0904 self._whats_new_message = whats_new_message if should_anchor: chat.anchor() + if self._show_vscode_extension_promo: + self.run_worker( + self._record_vscode_extension_promo_shown(), exclusive=False + ) + else: + await self._maybe_show_vscode_extension_promo() await mark_version_as_seen(self._current_version, self._update_cache_repository) + async def _maybe_show_vscode_extension_promo(self) -> None: + if not self._show_vscode_extension_promo: + return + promo_message = VscodeExtensionPromoMessage() + if self._history_widget_indices: + promo_message.add_class("after-history") + messages_area = self._cached_messages_area or self.query_one("#messages") + chat = self._cached_chat or self.query_one("#chat", ChatScroll) + should_anchor = chat.is_at_bottom + await chat.mount(promo_message, after=messages_area) + if should_anchor: + chat.anchor() + self.run_worker(self._record_vscode_extension_promo_shown(), exclusive=False) + async def _resolve_plan(self) -> None: if self._plan_offer_gateway is None: self._plan_info = None @@ -3192,6 +3261,11 @@ def run_textual_ui( update_notifier = PyPIUpdateGateway(project_name="mistral-vibe") update_cache_repository = FileSystemUpdateCacheRepository() plan_offer_gateway = HttpWhoAmIGateway(base_url=agent_loop.config.console_base_url) + vscode_extension_promo_repository = FileSystemVscodeExtensionPromoRepository() + vscode_extension_promo = VscodeExtensionPromo( + repository=vscode_extension_promo_repository, + initial_state=asyncio.run(vscode_extension_promo_repository.get()), + ) with stderr_guard(): app = VibeApp( @@ -3200,6 +3274,7 @@ def run_textual_ui( update_notifier=update_notifier, update_cache_repository=update_cache_repository, plan_offer_gateway=plan_offer_gateway, + vscode_extension_promo=vscode_extension_promo, ) session_id = app.run() diff --git a/vibe/cli/textual_ui/app.tcss b/vibe/cli/textual_ui/app.tcss index 3a3ce76..8fb134b 100644 --- a/vibe/cli/textual_ui/app.tcss +++ b/vibe/cli/textual_ui/app.tcss @@ -154,33 +154,39 @@ OptionList, OptionList:focus { height: auto; width: 100%; background: transparent; - border: solid $foreground-muted; + border: none; + border-top: solid $foreground-muted; + border-bottom: solid $foreground-muted; border-title-align: right; border-title-color: $text-muted; - padding: 0 1; &.border-warning { - border: solid $warning; + border-top: solid $warning; + border-bottom: solid $warning; border-title-color: $warning; } &.border-safe { - border: solid $success; + border-top: solid $success; + border-bottom: solid $success; border-title-color: $success; } &.border-error { - border: solid $error; + border-top: solid $error; + border-bottom: solid $error; border-title-color: $error; } &.border-recording { - border: solid $mistral_orange; + border-top: solid $mistral_orange; + border-bottom: solid $mistral_orange; border-title-color: $mistral_orange; } &.border-remote { - border: solid $mistral_orange; + border-top: solid $mistral_orange; + border-bottom: solid $mistral_orange; border-title-color: $mistral_orange; } } @@ -275,6 +281,12 @@ Markdown { } } +.user-message-wrapper { + margin-top: 1; + width: 100%; + height: auto; +} + .user-message-container { width: 100%; height: auto; @@ -282,7 +294,8 @@ Markdown { .user-message-prompt { width: auto; - height: 100%; + height: auto; + color: $mistral_orange; text-style: bold; } @@ -290,8 +303,22 @@ Markdown { width: 1fr; height: auto; text-style: bold; - padding-left: 1; - border-left: heavy $mistral_orange; +} + +.slash-command-message { + .user-message-content { + color: $mistral_orange; + } +} + +.user-message-separator { + width: 100%; + height: 1; + color: $text-muted; + + &:ansi { + text-style: dim; + } } .assistant-message { @@ -1135,7 +1162,7 @@ NarratorStatus { #banner-container { align: left middle; - padding: 0 1 0 0; + padding: 1 1 0 0; width: auto; } @@ -1231,6 +1258,33 @@ NarratorStatus { margin-top: 1; } +.vscode-extension-promo-message { + border-left: heavy $mistral_orange; + margin-bottom: 1; + + Markdown { + padding: 0 0 0 1; + margin: 0; + + & > MarkdownBlock:first-child { + margin-top: 0; + } + + & > MarkdownBlock:last-child { + margin-bottom: 0; + } + } + + link-style: none; + link-background-hover: transparent; + link-style-hover: underline; + link-color-hover: $mistral_orange; +} + +.vscode-extension-promo-message.after-history { + margin-top: 1; +} + #sessionpicker-app { width: 100%; height: auto; diff --git a/vibe/cli/textual_ui/widgets/chat_input/container.py b/vibe/cli/textual_ui/widgets/chat_input/container.py index 7c85929..9af2bcf 100644 --- a/vibe/cli/textual_ui/widgets/chat_input/container.py +++ b/vibe/cli/textual_ui/widgets/chat_input/container.py @@ -263,9 +263,8 @@ class ChatInputContainer(Vertical): return SAFETY_BORDER_CLASSES.get(self._safety, "") def _get_border_title(self) -> str: - if self._custom_border_label is not None: - return self._custom_border_label - return self._agent_name + label = self._custom_border_label or self._agent_name + return f" {label} " if label else "" def _apply_input_box_chrome(self) -> None: try: diff --git a/vibe/cli/textual_ui/widgets/messages.py b/vibe/cli/textual_ui/widgets/messages.py index cb2223f..f09405c 100644 --- a/vibe/cli/textual_ui/widgets/messages.py +++ b/vibe/cli/textual_ui/widgets/messages.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio from pathlib import Path -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, ClassVar, cast from vibe.core.hooks.models import HookMessageSeverity from vibe.core.logger import logger @@ -47,7 +47,20 @@ class ExpandingBorder(NonSelectableStatic): self.refresh() +# Mimic a border bottom with this component in order to have dimmed colors in ANSI themes +# Move back to border when Textual supports dimmed borders or foreground-muted in ANSI themes +class ExpandingSeparator(NonSelectableStatic): + def render(self) -> str: + return "─" * max(self.size.width, 1) + + def on_resize(self) -> None: + self.refresh() + + class UserMessage(Static): + PROMPT_CHAR: ClassVar[str] = ">" + SHOW_SEPARATOR: ClassVar[bool] = True + def __init__( self, content: str, pending: bool = False, message_index: int | None = None ) -> None: @@ -61,10 +74,16 @@ class UserMessage(Static): return self._content def compose(self) -> ComposeResult: - with Horizontal(classes="user-message-container"): - yield NoMarkupStatic(self._content, classes="user-message-content") - if self._pending: - self.add_class("pending") + with Vertical(classes="user-message-wrapper"): + with Horizontal(classes="user-message-container"): + yield NonSelectableStatic( + f"{self.PROMPT_CHAR} ", classes="user-message-prompt" + ) + yield NoMarkupStatic(self._content, classes="user-message-content") + if self.SHOW_SEPARATOR: + yield ExpandingSeparator(classes="user-message-separator") + if self._pending: + self.add_class("pending") async def set_pending(self, pending: bool) -> None: if pending == self._pending: @@ -79,6 +98,19 @@ class UserMessage(Static): self.remove_class("pending") +class SlashCommandMessage(UserMessage): + PROMPT_CHAR = "/" + SHOW_SEPARATOR = False + + def __init__(self, content: str) -> None: + super().__init__(content) + self.add_class("slash-command-message") + + +class TeleportUserMessage(UserMessage): + PROMPT_CHAR = "&" + + class StreamingMessageBase(Static): def __init__(self, content: str) -> None: super().__init__() @@ -247,6 +279,15 @@ class UserCommandMessage(Static): yield Markdown(self._content) +VSCODE_EXTENSION_URI = "vscode:extension/mistralai.vibe-code" +VSCODE_EXTENSION_LINK_LABEL = "VS Code extension" +VSCODE_EXTENSION_PROMO_STANDALONE = f"We now have a [{VSCODE_EXTENSION_LINK_LABEL}]({VSCODE_EXTENSION_URI}) with a rich UI. Check it out!" +VSCODE_EXTENSION_PROMO_WHATS_NEW_SUFFIX = ( + f"\n\n_Btw, we also have a new [{VSCODE_EXTENSION_LINK_LABEL}]" + f"({VSCODE_EXTENSION_URI}). Check it out!_" +) + + class WhatsNewMessage(Static): def __init__(self, content: str) -> None: super().__init__() @@ -257,6 +298,16 @@ class WhatsNewMessage(Static): yield Markdown(self._content) +class VscodeExtensionPromoMessage(Static): + def __init__(self, content: str = VSCODE_EXTENSION_PROMO_STANDALONE) -> None: + super().__init__() + self.add_class("vscode-extension-promo-message") + self._content = content + + def compose(self) -> ComposeResult: + yield Markdown(self._content) + + class InterruptMessage(Static): def __init__(self) -> None: super().__init__() diff --git a/vibe/cli/textual_ui/widgets/teleport_message.py b/vibe/cli/textual_ui/widgets/teleport_message.py index eb98640..9016cbc 100644 --- a/vibe/cli/textual_ui/widgets/teleport_message.py +++ b/vibe/cli/textual_ui/widgets/teleport_message.py @@ -15,7 +15,7 @@ class TeleportMessage(StatusMessage): if self._error: return f"Teleport failed: {self._error}" if self._final_url: - return f"Teleported to Vibe Code: {self._final_url}" + return f"Teleported to Vibe Code Web: {self._final_url}" return self._status def set_status(self, status: str) -> None: diff --git a/vibe/cli/vscode_extension_promo/__init__.py b/vibe/cli/vscode_extension_promo/__init__.py new file mode 100644 index 0000000..50fe632 --- /dev/null +++ b/vibe/cli/vscode_extension_promo/__init__.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import UTC, datetime + +from vibe.cli.vscode_extension_promo._port import ( + VscodeExtensionPromoRepository, + VscodeExtensionPromoState, +) +from vibe.cli.vscode_extension_promo.adapters.filesystem_repository import ( + FileSystemVscodeExtensionPromoRepository, +) + +__all__ = [ + "MAX_SHOWN_COUNT", + "PROMO_START", + "FileSystemVscodeExtensionPromoRepository", + "VscodeExtensionPromo", + "VscodeExtensionPromoRepository", + "VscodeExtensionPromoState", + "should_show_promo", +] + + +MAX_SHOWN_COUNT = 10 +PROMO_START = datetime(2026, 5, 28, 16, 0, tzinfo=UTC) + + +def should_show_promo( + state: VscodeExtensionPromoState | None, now: datetime | None = None +) -> bool: + current = now or datetime.now(UTC) + if current < PROMO_START: + return False + if state is None: + return True + return state.shown_count < MAX_SHOWN_COUNT + + +@dataclass(frozen=True, slots=True) +class VscodeExtensionPromo: + repository: VscodeExtensionPromoRepository + initial_state: VscodeExtensionPromoState | None diff --git a/vibe/cli/vscode_extension_promo/_port.py b/vibe/cli/vscode_extension_promo/_port.py new file mode 100644 index 0000000..3b32148 --- /dev/null +++ b/vibe/cli/vscode_extension_promo/_port.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol + + +@dataclass(frozen=True, slots=True) +class VscodeExtensionPromoState: + shown_count: int + + +class VscodeExtensionPromoRepository(Protocol): + async def get(self) -> VscodeExtensionPromoState | None: ... + async def set(self, state: VscodeExtensionPromoState) -> None: ... diff --git a/vibe/cli/vscode_extension_promo/adapters/__init__.py b/vibe/cli/vscode_extension_promo/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vibe/cli/vscode_extension_promo/adapters/filesystem_repository.py b/vibe/cli/vscode_extension_promo/adapters/filesystem_repository.py new file mode 100644 index 0000000..3714ced --- /dev/null +++ b/vibe/cli/vscode_extension_promo/adapters/filesystem_repository.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import asyncio +from pathlib import Path + +from vibe.cli.cache import read_cache, write_cache +from vibe.cli.vscode_extension_promo._port import ( + VscodeExtensionPromoRepository, + VscodeExtensionPromoState, +) +from vibe.core.paths import VIBE_HOME + +_CACHE_SECTION = "vscode_extension_promo" + + +class FileSystemVscodeExtensionPromoRepository(VscodeExtensionPromoRepository): + def __init__(self, base_path: Path | str | None = None) -> None: + self._base_path = Path(base_path) if base_path is not None else VIBE_HOME.path + self._cache_file = self._base_path / "cache.toml" + + async def get(self) -> VscodeExtensionPromoState | None: + data = await asyncio.to_thread(self._read_section) + if data is None: + return None + shown_count = data.get("shown_count") + if not isinstance(shown_count, int): + return None + return VscodeExtensionPromoState(shown_count=shown_count) + + async def set(self, state: VscodeExtensionPromoState) -> None: + await asyncio.to_thread( + write_cache, + self._cache_file, + _CACHE_SECTION, + {"shown_count": state.shown_count}, + ) + + def _read_section(self) -> dict | None: + cache = read_cache(self._cache_file) + section = cache.get(_CACHE_SECTION) + if isinstance(section, dict): + return section + return None diff --git a/vibe/core/agent_loop.py b/vibe/core/agent_loop.py index a67b2b3..bcd5f46 100644 --- a/vibe/core/agent_loop.py +++ b/vibe/core/agent_loop.py @@ -22,6 +22,7 @@ from pydantic import BaseModel from vibe.cli.terminal_detect import detect_terminal from vibe.core.agents.manager import AgentManager from vibe.core.agents.models import AgentProfile, BuiltinAgentName +from vibe.core.compaction import collect_prior_user_messages from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig from vibe.core.experiments import ExperimentManager from vibe.core.experiments.client import RemoteEvalClient @@ -53,6 +54,7 @@ from vibe.core.middleware import ( PriceLimitMiddleware, ReadOnlyAgentMiddleware, ResetReason, + TokenLimitMiddleware, TurnLimitMiddleware, make_plan_agent_reminder, ) @@ -237,6 +239,7 @@ class AgentLoop: # noqa: PLR0904 message_observer: Callable[[LLMMessage], None] | None = None, max_turns: int | None = None, max_price: float | None = None, + max_session_tokens: int | None = None, backend: BackendLike | None = None, enable_streaming: bool = False, entrypoint_metadata: EntrypointMetadata | None = None, @@ -245,6 +248,7 @@ class AgentLoop: # noqa: PLR0904 headless: bool = False, hook_config_result: HookConfigResult | None = None, permission_store: PermissionStore | None = None, + mcp_registry: MCPRegistry | None = None, ) -> None: self._base_config = config self._headless = headless @@ -260,7 +264,7 @@ class AgentLoop: # noqa: PLR0904 self._permission_store = permission_store or PermissionStore() - self.mcp_registry = MCPRegistry() + self.mcp_registry = mcp_registry or MCPRegistry() self.connector_registry = self._create_connector_registry() self.agent_manager = AgentManager( lambda: self._base_config, @@ -278,6 +282,7 @@ class AgentLoop: # noqa: PLR0904 self.message_observer = message_observer self._max_turns = max_turns self._max_price = max_price + self._max_session_tokens = max_session_tokens self._plan_session = PlanSession() self.format_handler = APIToolFormatHandler() @@ -738,6 +743,9 @@ class AgentLoop: # noqa: PLR0904 if self._max_price is not None: self.middleware_pipeline.add(PriceLimitMiddleware(self._max_price)) + if self._max_session_tokens is not None: + self.middleware_pipeline.add(TokenLimitMiddleware(self._max_session_tokens)) + self.middleware_pipeline.add(AutoCompactMiddleware()) if self.config.context_warnings: self.middleware_pipeline.add(ContextWarningMiddleware(0.5)) @@ -1698,7 +1706,12 @@ class AgentLoop: # noqa: PLR0904 self.agent_profile, ) - summary_request = UtilityPrompt.COMPACT.read() + summary_prefix = UtilityPrompt.COMPACT_SUMMARY_PREFIX.read() + prior_user_messages = collect_prior_user_messages( + list(self.messages), summary_prefix + ) + + summary_request = self.config.compaction_prompt if extra_instructions: summary_request += ( f"\n\n## Additional Instructions\n{extra_instructions}" @@ -1717,11 +1730,14 @@ class AgentLoop: # noqa: PLR0904 raise AgentLoopLLMResponseError( "Usage data missing in compaction summary response" ) - summary_content = summary_result.message.content or "" + summary_content = (summary_result.message.content or "").strip() + if not summary_content: + summary_content = "(no summary available)" system_message = self.messages[0] - summary_message = LLMMessage(role=Role.user, content=summary_content) - self.messages.reset([system_message, summary_message]) + wrapped_summary = f"{summary_prefix}\n{summary_content}" + summary_message = LLMMessage(role=Role.user, content=wrapped_summary) + self.messages.reset([system_message, *prior_user_messages, summary_message]) active_model = self.config.get_active_model() await self._reset_session() @@ -1745,7 +1761,7 @@ class AgentLoop: # noqa: PLR0904 self.middleware_pipeline.reset(reset_reason=ResetReason.COMPACT) - return summary_content or "" + return summary_content except Exception: await self.session_logger.save_interaction( diff --git a/vibe/core/compaction.py b/vibe/core/compaction.py new file mode 100644 index 0000000..5c9bfa4 --- /dev/null +++ b/vibe/core/compaction.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from vibe.core.types import LLMMessage, Role +from vibe.core.utils.tokens import approx_token_count, truncate_middle_to_tokens + +COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000 + + +def collect_prior_user_messages( + messages: list[LLMMessage], + summary_prefix: str, + max_tokens: int = COMPACT_USER_MESSAGE_MAX_TOKENS, +) -> list[LLMMessage]: + """Pick user messages to preserve through compaction. + + Walks newest-first within a token budget, dropping system-internal + injections and prior compaction summaries, middle-truncating the message + that spills over. Returns kept messages in chronological order. + """ + candidates = [ + m + for m in messages + if m.role == Role.user + and not m.injected + and m.content + and not m.content.startswith(summary_prefix) + ] + + selected: list[LLMMessage] = [] + remaining = max_tokens + for m in reversed(candidates): + if remaining <= 0: + break + content = m.content or "" + cost = approx_token_count(content) + if cost <= remaining: + selected.append(LLMMessage(role=Role.user, content=content)) + remaining -= cost + else: + truncated = truncate_middle_to_tokens(content, remaining) + selected.append(LLMMessage(role=Role.user, content=truncated)) + remaining = 0 + + selected.reverse() + return selected diff --git a/vibe/core/config/__init__.py b/vibe/core/config/__init__.py index e6666fd..1efa34a 100644 --- a/vibe/core/config/__init__.py +++ b/vibe/core/config/__init__.py @@ -40,6 +40,7 @@ from vibe.core.config.layer import ( EmptyLayerError, LayerImplementationError, RawConfig, + TrustNotResolvedError, TrustResolutionError, UntrustedLayerError, ) @@ -113,6 +114,7 @@ __all__ = [ "TranscribeClient", "TranscribeModelConfig", "TranscribeProviderConfig", + "TrustNotResolvedError", "TrustResolutionError", "UntrustedLayerError", "VibeConfig", diff --git a/vibe/core/config/_settings.py b/vibe/core/config/_settings.py index cea7554..06702f9 100644 --- a/vibe/core/config/_settings.py +++ b/vibe/core/config/_settings.py @@ -30,7 +30,7 @@ from vibe.core.agents.models import BuiltinAgentName from vibe.core.config.harness_files import get_harness_files_manager from vibe.core.logger import logger from vibe.core.paths import GLOBAL_ENV_FILE, SESSION_LOG_DIR -from vibe.core.prompts import load_system_prompt +from vibe.core.prompts import UtilityPrompt, load_prompt, load_system_prompt from vibe.core.types import Backend from vibe.core.utils import get_server_url_from_api_base @@ -512,6 +512,7 @@ class VibeConfig(BaseSettings): enable_telemetry: bool = True experiment_overrides: dict[str, str] = Field(default_factory=dict) system_prompt_id: str = "cli" + compaction_prompt_id: str = "compact" include_commit_signature: bool = True include_model_info: bool = True include_project_context: bool = True @@ -537,7 +538,6 @@ class VibeConfig(BaseSettings): console_base_url: str = Field(default=DEFAULT_CONSOLE_BASE_URL, exclude=True) enable_experimental_hooks: bool = Field(default=False, exclude=True) - enable_experimental_browser_sign_in: bool = Field(default=False, exclude=True) providers: list[ProviderConfig] = Field( default_factory=lambda: list(DEFAULT_PROVIDERS) @@ -634,10 +634,9 @@ class VibeConfig(BaseSettings): default_agent: str = Field( default=BuiltinAgentName.DEFAULT, description=( - "Agent profile to use when no --agent flag is passed in interactive " - "mode. Builtin: default, plan, accept-edits, auto-approve. " - "Ignored in programmatic mode (-p/--prompt), which falls back to " - "auto-approve when --agent is not provided." + "Agent profile to use when no --agent flag is passed. " + "Builtin: default, plan, accept-edits, auto-approve. " + "Applies in both interactive and programmatic (-p/--prompt) mode." ), ) skill_paths: list[Path] = Field( @@ -717,6 +716,14 @@ class VibeConfig(BaseSettings): def system_prompt(self) -> str: return load_system_prompt(self.system_prompt_id) + @property + def compaction_prompt(self) -> str: + return load_prompt( + self.compaction_prompt_id, + setting_name="compaction_prompt_id", + builtins={"compact": UtilityPrompt.COMPACT.path}, + ) + def get_active_model(self) -> ModelConfig: for model in self.models: if model.alias == self.active_model: @@ -933,6 +940,11 @@ class VibeConfig(BaseSettings): _ = self.system_prompt return self + @model_validator(mode="after") + def _check_compaction_prompt(self) -> VibeConfig: + _ = self.compaction_prompt + return self + def set_thinking(self, level: ThinkingLevel) -> None: model = self.get_active_model() diff --git a/vibe/core/config/layer.py b/vibe/core/config/layer.py index e0d7216..4396894 100644 --- a/vibe/core/config/layer.py +++ b/vibe/core/config/layer.py @@ -36,6 +36,15 @@ class EmptyLayerError(ConfigLayerError): super().__init__(layer_name, f"Layer '{layer_name}' has no data after load") +class TrustNotResolvedError(ConfigLayerError): + """Raised when grant_trust/revoke_trust is called before trust has been resolved.""" + + def __init__(self, layer_name: str) -> None: + super().__init__( + layer_name, f"Layer '{layer_name}': trust has not been resolved yet" + ) + + class TrustResolutionError(ConfigLayerError): """Raised when trust status is not resolvable.""" @@ -171,6 +180,9 @@ class ConfigLayer[S: BaseModel](ABC): return new_state async def _handle_grant_trust(self, state: _LayerState[S]) -> _LayerState[S]: + if state.is_trusted is None: + raise TrustNotResolvedError(self.name) + if state.is_trusted is True: return state @@ -179,6 +191,9 @@ class ConfigLayer[S: BaseModel](ABC): return _LayerState(is_trusted=True, data=state.data) async def _handle_revoke_trust(self, state: _LayerState[S]) -> _LayerState[S]: + if state.is_trusted is None: + raise TrustNotResolvedError(self.name) + if state.is_trusted is False: return state diff --git a/vibe/core/config/layers/__init__.py b/vibe/core/config/layers/__init__.py index e79996f..eec79b9 100644 --- a/vibe/core/config/layers/__init__.py +++ b/vibe/core/config/layers/__init__.py @@ -1,5 +1,7 @@ from __future__ import annotations +from vibe.core.config.layers.overrides import OverridesLayer +from vibe.core.config.layers.project import ProjectConfigLayer from vibe.core.config.layers.user import UserConfigLayer -__all__ = ["UserConfigLayer"] +__all__ = ["OverridesLayer", "ProjectConfigLayer", "UserConfigLayer"] diff --git a/vibe/core/config/layers/overrides.py b/vibe/core/config/layers/overrides.py new file mode 100644 index 0000000..49392a4 --- /dev/null +++ b/vibe/core/config/layers/overrides.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import copy +from typing import Any + +from vibe.core.config.layer import ConfigLayer, RawConfig + + +class OverridesLayer(ConfigLayer[RawConfig]): + """Highest-priority layer wrapping an arbitrary dict passed at construction. + + Always trusted and read-only. + Used by CLI and ACP entry points to inject runtime overrides. + """ + + def __init__(self, *, data: dict[str, Any], name: str = "overrides") -> None: + super().__init__(name=name) + self._data = data + + async def _check_trust(self) -> bool: + return True + + async def _read_config(self) -> dict[str, Any]: + return copy.deepcopy(self._data) + + async def apply(self, patch: Any, *, on_conflict: str = "cancel") -> None: + raise NotImplementedError("OverridesLayer.apply() is not implemented (M2)") diff --git a/vibe/core/config/layers/project.py b/vibe/core/config/layers/project.py new file mode 100644 index 0000000..8b26f05 --- /dev/null +++ b/vibe/core/config/layers/project.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import asyncio +from pathlib import Path +import tomllib +from typing import Any + +from vibe.core.config.layer import ConfigLayer, RawConfig +from vibe.core.paths._vibe_home import VIBE_HOME +from vibe.core.trusted_folders import trusted_folders_manager + + +class ProjectConfigLayer(ConfigLayer[RawConfig]): + """Reads a project-level TOML config file. + If no file is found in the current working directory, walks up parent directories + until a trusted .vibe/config.toml is found. + """ + + def __init__(self, *, path: Path | None = None, name: str = "project-toml") -> None: + super().__init__(name=name) + self._root = path or Path.cwd() + self._config_file_path: Path | None = None + self._is_set = False + self._find_lock = asyncio.Lock() + + @property + def config_file_path(self) -> Path | None: + return self._config_file_path + + async def _check_trust(self) -> bool: + await self._find_config_file() + + if self._config_file_path is None: + return True + + return bool(trusted_folders_manager.is_trusted(self._config_file_path.parent)) + + async def _read_config(self) -> dict[str, Any]: + if self._config_file_path is None: + return {} + with self._config_file_path.open("rb") as f: + return tomllib.load(f) + + async def _on_trust_changed(self, old: bool | None, new: bool | None) -> None: + if new is None or self._config_file_path is None: + return + + if new: + trusted_folders_manager.add_trusted(self._config_file_path.parent) + else: + trusted_folders_manager.add_untrusted(self._config_file_path.parent) + + async def grant_trust(self) -> None: + await self._find_config_file() + if self._config_file_path is None: + return + + await super().grant_trust() + + async def revoke_trust(self) -> None: + await self._find_config_file() + if self._config_file_path is None: + return + + await super().revoke_trust() + + async def apply(self, patch: Any, *, on_conflict: str = "cancel") -> None: + raise NotImplementedError("ProjectConfigLayer.apply() is not implemented (M2)") + + async def _find_config_file(self) -> None: + async with self._find_lock: + if self._is_set: + return + + for directory in [self._root, *self._root.parents]: + if directory == VIBE_HOME.path.parent: + break + + candidate = directory / ".vibe" / "config.toml" + if candidate.is_file(): + self._config_file_path = candidate + break + + self._is_set = True diff --git a/vibe/core/middleware.py b/vibe/core/middleware.py index cf98e0e..f25998b 100644 --- a/vibe/core/middleware.py +++ b/vibe/core/middleware.py @@ -78,6 +78,25 @@ class PriceLimitMiddleware: pass +class TokenLimitMiddleware: + def __init__(self, max_tokens: int) -> None: + self.max_tokens = max_tokens + + async def before_turn(self, context: ConversationContext) -> MiddlewareResult: + if context.stats.session_total_llm_tokens > self.max_tokens: + return MiddlewareResult( + action=MiddlewareAction.STOP, + reason=( + "Token limit exceeded: " + f"{context.stats.session_total_llm_tokens:,} > {self.max_tokens:,}" + ), + ) + return MiddlewareResult() + + def reset(self, reset_reason: ResetReason = ResetReason.STOP) -> None: + pass + + class AutoCompactMiddleware: async def before_turn(self, context: ConversationContext) -> MiddlewareResult: threshold = context.config.get_active_model().auto_compact_threshold diff --git a/vibe/core/programmatic.py b/vibe/core/programmatic.py index bf2c14d..a643fda 100644 --- a/vibe/core/programmatic.py +++ b/vibe/core/programmatic.py @@ -29,9 +29,10 @@ def run_programmatic( # noqa: PLR0913, PLR0917 prompt: str, max_turns: int | None = None, max_price: float | None = None, + max_session_tokens: int | None = None, output_format: OutputFormat = OutputFormat.TEXT, previous_messages: list[LLMMessage] | None = None, - agent_name: str = BuiltinAgentName.AUTO_APPROVE, + agent_name: str = BuiltinAgentName.DEFAULT, client_metadata: ClientMetadata = _DEFAULT_CLIENT_METADATA, teleport: bool = False, headless: bool = False, @@ -45,6 +46,7 @@ def run_programmatic( # noqa: PLR0913, PLR0917 message_observer=formatter.on_message_added, max_turns=max_turns, max_price=max_price, + max_session_tokens=max_session_tokens, enable_streaming=False, headless=headless, entrypoint_metadata=build_entrypoint_metadata( diff --git a/vibe/core/prompts/__init__.py b/vibe/core/prompts/__init__.py index 732a924..27264fb 100644 --- a/vibe/core/prompts/__init__.py +++ b/vibe/core/prompts/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Iterable, Mapping from enum import StrEnum, auto from pathlib import Path @@ -30,48 +31,82 @@ class SystemPrompt(Prompt): class UtilityPrompt(Prompt): AGENTS_DOC = auto() COMPACT = auto() + COMPACT_SUMMARY_PREFIX = auto() DANGEROUS_DIRECTORY = auto() PROJECT_CONTEXT = auto() TURN_SUMMARY = auto() -class MissingPromptFileError(RuntimeError): - def __init__(self, system_prompt_id: str, *prompt_dirs: str) -> None: - dirs_str = " or ".join(prompt_dirs) if prompt_dirs else "" +class MissingPromptFileError(ValueError): + def __init__( + self, + setting_name: str, + prompt_id: str, + builtin_ids: Iterable[str], + custom_dirs: Iterable[Path], + custom_ids: Iterable[str], + ) -> None: + builtin_hint = ", ".join('"' + i + '"' for i in builtin_ids) + dirs_hint = " or ".join(str(d) for d in custom_dirs) or "" + custom_hint = ", ".join('"' + i + '"' for i in custom_ids) or "" super().__init__( - f"Invalid system_prompt_id value: '{system_prompt_id}'. " - f"Must be one of the available prompts ({', '.join(p.name.lower() for p in SystemPrompt)}), " - f"or correspond to a .md file in {dirs_str}" + f"Invalid {setting_name} value: '{prompt_id}'. " + f"Must be one of the available prompts ({builtin_hint}), " + f"or correspond to a .md file in {dirs_hint} (available: {custom_hint})" ) - self.system_prompt_id = system_prompt_id + self.setting_name = setting_name + self.prompt_id = prompt_id + + +def _validate_prompt_id(prompt_id: str, setting_name: str) -> None: + if ( + not prompt_id + or prompt_id in {".", ".."} + or "/" in prompt_id + or "\\" in prompt_id + ): + raise ValueError( + f"Invalid {setting_name} value: '{prompt_id}' must be a bare filename " + "without path separators" + ) + + +def load_prompt( + prompt_id: str, *, setting_name: str, builtins: Mapping[str, Path] +) -> str: + _validate_prompt_id(prompt_id, setting_name) + mgr = get_harness_files_manager() + custom_dirs = mgr.project_prompts_dirs + mgr.user_prompts_dirs + for d in custom_dirs: + path = (d / prompt_id).with_suffix(".md") + if path.is_file(): + return read_safe(path).text.strip() + + builtin_path = builtins.get(prompt_id.lower()) + if builtin_path is not None and builtin_path.is_file(): + return read_safe(builtin_path).text.strip() + + custom_ids = sorted({p.stem for d in custom_dirs for p in d.glob("*.md")}) + raise MissingPromptFileError( + setting_name, prompt_id, tuple(builtins), custom_dirs, custom_ids + ) def load_system_prompt(prompt_id: str) -> str: - mgr = get_harness_files_manager() - prompt_dirs = mgr.project_prompts_dirs + mgr.user_prompts_dirs - for current_prompt_dir in prompt_dirs: - custom_sp_path = (current_prompt_dir / prompt_id).with_suffix(".md") - if custom_sp_path.is_file(): - return read_safe(custom_sp_path).text - - try: - return SystemPrompt[prompt_id.upper()].read() - except KeyError: - pass - - builtin_path = (PROMPTS_DIR / prompt_id).with_suffix(".md") - if builtin_path.is_file(): - return read_safe(builtin_path).text.strip() - - raise MissingPromptFileError( - prompt_id, *(str(d) for d in [*prompt_dirs, PROMPTS_DIR]) - ) + builtins: dict[str, Path] = {p.name.lower(): p.path for p in SystemPrompt} + # Experiment variants may reference bundled .md files not in the enum. + fallback = (PROMPTS_DIR / prompt_id).with_suffix(".md") + if fallback.is_file(): + builtins.setdefault(prompt_id.lower(), fallback) + return load_prompt(prompt_id, setting_name="system_prompt_id", builtins=builtins) __all__ = [ "PROMPTS_DIR", "MissingPromptFileError", + "Prompt", "SystemPrompt", "UtilityPrompt", + "load_prompt", "load_system_prompt", ] diff --git a/vibe/core/prompts/compact.md b/vibe/core/prompts/compact.md index 9e3aaf3..f31f8e0 100644 --- a/vibe/core/prompts/compact.md +++ b/vibe/core/prompts/compact.md @@ -1,48 +1,12 @@ -Create a comprehensive summary of our entire conversation that will serve as complete context for continuing this work. Structure your summary to capture both the narrative flow and technical details necessary for seamless continuation. +You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume this task. -Your summary must include these sections in order: +Include: +- The user's current goal and any explicit constraints or preferences +- Key decisions made and their rationale +- Files touched and the current state of in-progress work (paths + one-line status) +- What remains to be done — the concrete next step +- Any data, identifiers, or references the next LLM needs to continue -## 1. User's Primary Goals and Intent -Capture ALL explicit requests and objectives stated by the user throughout the conversation, preserving their exact priorities and constraints. +Be concise and structured. One line per modified file unless a snippet is load-bearing. Do not repeat information already captured. Do not include a "Final Answer" section — the entire response IS the handoff. -## 2. Conversation Timeline and Progress -Chronologically document the key phases of our work: -- Initial requests and how they were addressed -- Major decisions made and their rationale -- Problems encountered and solutions applied -- Current state of the work - -## 3. Technical Context and Decisions -- Technologies, frameworks, and tools being used -- Architectural patterns and design decisions made -- Key technical constraints or requirements identified -- Important code patterns or conventions established - -## 4. Files and Code Changes -For each file created, modified, or examined: -- Full file path/name -- Purpose and importance of the file -- Specific changes made (with key code snippets where critical) -- Current state of the file - -## 5. Active Work and Last Actions -CRITICAL: Detail EXACTLY what was being worked on in the most recent exchanges: -- The specific task or problem being addressed -- Last completed action -- Any partial work or mid-implementation state -- Include relevant code snippets from the most recent work - -## 6. Unresolved Issues and Pending Tasks -- Any errors or issues still requiring attention -- Tasks explicitly requested but not yet started -- Decisions waiting for user input - -## 7. Immediate Next Step -State the SPECIFIC next action to take based on: -- The user's most recent request -- The current state of implementation -- Any ongoing work that was interrupted - -Important: Be precise with technical details, file names, and code. The next agent reading this should be able to continue exactly where we left off without asking clarifying questions. Include enough detail that no context is lost, but remain focused on actionable information. - -Respond with ONLY the summary text following this structure - no additional commentary or meta-discussion. +Respond with the summary text only — no preamble, no meta-discussion, no tool calls. diff --git a/vibe/core/prompts/compact_summary_prefix.md b/vibe/core/prompts/compact_summary_prefix.md new file mode 100644 index 0000000..008b0ca --- /dev/null +++ b/vibe/core/prompts/compact_summary_prefix.md @@ -0,0 +1 @@ +Another language model started to solve this problem and produced a summary of its work so far. Use this summary to continue the task without redoing work already done. Summary follows: diff --git a/vibe/core/skills/builtins/vibe.py b/vibe/core/skills/builtins/vibe.py index 11d65ab..61873c2 100644 --- a/vibe/core/skills/builtins/vibe.py +++ b/vibe/core/skills/builtins/vibe.py @@ -28,7 +28,7 @@ agents, prompts, logs, and session data live here. vibehistory # Command history trusted_folders.toml # Trust database for project folders agents/ # Custom agent profiles (*.toml) - prompts/ # Custom system prompts (*.md) + prompts/ # Custom prompts (*.md) skills/ # User-level skills (each skill is a subdirectory with SKILL.md) tools/ # Custom tool definitions logs/ @@ -56,6 +56,9 @@ When in a trusted folder, Vibe also looks for project-local configuration: The configuration file uses TOML format. Settings can also be overridden via environment variables with the `VIBE_` prefix (e.g., `VIBE_ACTIVE_MODEL=local`). +Custom prompt IDs are resolved from project-local `.vibe/prompts/` first, then +from `~/.vibe/prompts/`, and finally from the built-in bundled prompts. + ### Key Settings ```toml @@ -71,6 +74,7 @@ file_watcher_for_autocomplete = false # Behavior bypass_tool_permissions = false # Skip tool approval prompts system_prompt_id = "cli" # System prompt: "cli", "lean", or custom .md filename +compaction_prompt_id = "compact" # Compaction prompt: built-in "compact" or custom .md filename enable_telemetry = true enable_update_checks = true enable_auto_update = true @@ -251,18 +255,9 @@ save_dir = "" # Defaults to ~/.vibe/logs/session session_prefix = "session" ``` -### Browser Sign-In (Experimental) +### Browser Sign-In Browser sign-in lets users authenticate through the browser during onboarding. -The feature is **experimental** and must be enabled first: - -```toml -# In config.toml -enable_experimental_browser_sign_in = true -``` - -Or via the environment variable `VIBE_ENABLE_EXPERIMENTAL_BROWSER_SIGN_IN=true`. - Mistral providers use default browser sign-in URLs. Custom or renamed providers must configure both URLs: @@ -382,6 +377,7 @@ vibe -v / --version # Show version vibe --setup # Run onboarding/setup vibe --max-turns N # Max assistant turns (programmatic mode) vibe --max-price DOLLARS # Max cost limit (programmatic mode) +vibe --max-tokens N # Max total session tokens (programmatic mode) vibe --enabled-tools TOOL # Enable specific tools (repeatable) vibe --output text|json|streaming # Output format (programmatic mode) ``` @@ -441,7 +437,7 @@ Custom agents are TOML files in `~/.vibe/agents/NAME.toml`. - `/leanstall` - Install the Lean 4 agent (leanstral) - `/unleanstall` - Uninstall the Lean 4 agent - `/data-retention` - Show data retention information -- `/teleport` - Teleport session to Vibe Code (only available when Vibe Code is enabled) +- `/teleport` - Teleport session to Vibe Code Web (only available when Vibe Code is enabled) - `/exit` - Exit the application ## Skills System diff --git a/vibe/core/teleport/experimental_nuage.py b/vibe/core/teleport/experimental_nuage.py index fc9d220..9cdae46 100644 --- a/vibe/core/teleport/experimental_nuage.py +++ b/vibe/core/teleport/experimental_nuage.py @@ -1,6 +1,7 @@ from __future__ import annotations import types +from typing import Literal import httpx from pydantic import BaseModel, ConfigDict, Field, ValidationError @@ -23,11 +24,22 @@ class ExperimentalNuageMessage(BaseModel): parts: list[ExperimentalNuageTextPart] +class ExperimentalNuageDiff(BaseModel): + model_config = ConfigDict(extra="forbid") + + format: Literal["git-diff"] = "git-diff" + encoding: Literal["base64"] = "base64" + compression: Literal["zstd"] = "zstd" + content: str + + class ExperimentalNuageRepository(BaseModel): model_config = ConfigDict(extra="forbid") repo_url: str = Field(serialization_alias="repoUrl") branch: str | None = None + commit_sha: str | None = Field(default=None, serialization_alias="commitSha") + diff: ExperimentalNuageDiff | None = None class ExperimentalNuageContext(BaseModel): diff --git a/vibe/core/teleport/teleport.py b/vibe/core/teleport/teleport.py index addb8a3..0d3e7a9 100644 --- a/vibe/core/teleport/teleport.py +++ b/vibe/core/teleport/teleport.py @@ -16,6 +16,7 @@ from vibe.core.teleport.errors import ServiceTeleportError from vibe.core.teleport.experimental_nuage import ( ExperimentalNuageClient, ExperimentalNuageContext, + ExperimentalNuageDiff, ExperimentalNuageMessage, ExperimentalNuageRepository, ExperimentalNuageRequest, @@ -282,6 +283,13 @@ class TeleportService: "Experimental Nuage teleport requires a checked-out branch." ) + compressed = self._compress_diff(git_info.diff) + diff = ( + ExperimentalNuageDiff(content=compressed.decode("ascii")) + if compressed is not None + else None + ) + return ExperimentalNuageRequest( idempotency_key=str(uuid4()), message=ExperimentalNuageMessage( @@ -290,7 +298,10 @@ class TeleportService: context=ExperimentalNuageContext( repositories=[ ExperimentalNuageRepository( - repo_url=git_info.remote_url, branch=git_info.branch + repo_url=git_info.remote_url, + branch=git_info.branch, + commit_sha=git_info.commit, + diff=diff, ) ] ), diff --git a/vibe/core/utils/tokens.py b/vibe/core/utils/tokens.py new file mode 100644 index 0000000..fd52476 --- /dev/null +++ b/vibe/core/utils/tokens.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import math + +_APPROX_BYTES_PER_TOKEN = 4 +_TRUNCATION_MARKER = "\n\n[... truncated ...]\n\n" + + +def approx_token_count(text: str) -> int: + return math.ceil(len(text) / _APPROX_BYTES_PER_TOKEN) + + +def truncate_middle_to_tokens(text: str, max_tokens: int) -> str: + """Shrink ``text`` to fit in ``max_tokens`` by dropping the middle. + + Keeps head + tail (intent and constraints usually live at the ends of user + messages) and inserts a marker where the middle was removed. + """ + if max_tokens <= 0: + return "" + max_chars = max_tokens * _APPROX_BYTES_PER_TOKEN + if len(text) <= max_chars: + return text + available = max_chars - len(_TRUNCATION_MARKER) + if available <= 0: + return text[:max_chars] + head = available // 2 + tail = available - head + return text[:head] + _TRUNCATION_MARKER + text[-tail:] diff --git a/vibe/setup/auth/__init__.py b/vibe/setup/auth/__init__.py index b8f7e18..3bdfe92 100644 --- a/vibe/setup/auth/__init__.py +++ b/vibe/setup/auth/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +from vibe.setup.auth.auth_state import AuthState, AuthStateKind, assess_auth_state from vibe.setup.auth.browser_sign_in import ( BrowserSignInAttempt, BrowserSignInService, @@ -15,6 +16,8 @@ from vibe.setup.auth.browser_sign_in_gateway import ( from vibe.setup.auth.http_browser_sign_in_gateway import HttpBrowserSignInGateway __all__ = [ + "AuthState", + "AuthStateKind", "BrowserSignInAttempt", "BrowserSignInError", "BrowserSignInErrorCode", @@ -24,4 +27,5 @@ __all__ = [ "BrowserSignInService", "BrowserSignInStatus", "HttpBrowserSignInGateway", + "assess_auth_state", ] diff --git a/vibe/setup/auth/api_key_persistence.py b/vibe/setup/auth/api_key_persistence.py index 681b7be..973022f 100644 --- a/vibe/setup/auth/api_key_persistence.py +++ b/vibe/setup/auth/api_key_persistence.py @@ -2,7 +2,7 @@ from __future__ import annotations import os -from dotenv import set_key +from dotenv import set_key, unset_key from vibe.core.config import DEFAULT_PROVIDERS, ProviderConfig, VibeConfig from vibe.core.paths import GLOBAL_ENV_FILE @@ -16,6 +16,12 @@ def _save_api_key_to_env_file(env_key: str, api_key: str) -> None: set_key(GLOBAL_ENV_FILE.path, env_key, api_key) +def _remove_api_key_from_env_file(env_key: str) -> None: + if not GLOBAL_ENV_FILE.path.exists(): + return + unset_key(GLOBAL_ENV_FILE.path, env_key) + + def _get_mistral_provider() -> ProviderConfig: return next( provider for provider in DEFAULT_PROVIDERS if provider.name == "mistral" @@ -62,3 +68,11 @@ def persist_api_key( except Exception: pass return "completed" + + +def remove_api_key(provider: ProviderConfig) -> None: + env_key = provider.api_key_env_var + if not env_key: + raise ValueError("Cannot remove API key without an environment variable name") + _remove_api_key_from_env_file(env_key) + os.environ.pop(env_key, None) diff --git a/vibe/setup/auth/auth_state.py b/vibe/setup/auth/auth_state.py new file mode 100644 index 0000000..ad60693 --- /dev/null +++ b/vibe/setup/auth/auth_state.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from enum import StrEnum, auto +import os +from pathlib import Path + +from dotenv import dotenv_values + +from vibe.core.config import DEFAULT_MISTRAL_API_ENV_KEY, ProviderConfig +from vibe.core.paths import GLOBAL_ENV_FILE + + +class AuthStateKind(StrEnum): + SIGNED_OUT = auto() + AUTH_NOT_REQUIRED = auto() + VIBE_HOME_ENV_FILE = auto() + PROCESS_ENV = auto() + VIBE_HOME_ENV_FILE_OVERRIDES_PROCESS_ENV = auto() + UNSUPPORTED_PROVIDER = auto() + + +@dataclass(frozen=True, slots=True) +class _AuthEnvSnapshot: + env_key: str + current_process_has_value: bool + dotenv_has_value: bool + process_env_had_value_before_dotenv_load: bool + + +@dataclass(frozen=True, slots=True) +class AuthState: + kind: AuthStateKind + can_use_active_provider: bool + sign_out_available: bool + env_key: str | None + + +def _has_value(value: str | None) -> bool: + return bool(value) + + +def _dotenv_has_value(env_path: Path, env_key: str) -> bool: + if not env_path.is_file() and not env_path.is_fifo(): + return False + + value = dotenv_values(env_path).get(env_key) + if not isinstance(value, str): + return False + return _has_value(value) + + +def _supports_vibe_owned_sign_out(provider: ProviderConfig) -> bool: + return provider.api_key_env_var == DEFAULT_MISTRAL_API_ENV_KEY + + +def _auth_state( + kind: AuthStateKind, + *, + can_use_active_provider: bool, + sign_out_available: bool = False, + env_key: str | None = None, +) -> AuthState: + return AuthState( + kind=kind, + can_use_active_provider=can_use_active_provider, + sign_out_available=sign_out_available, + env_key=env_key, + ) + + +def _capture_auth_env_snapshot( + env_key: str, + *, + env_path: Path | None = None, + environ: Mapping[str, str] | None = None, + process_env_had_value_before_dotenv_load: bool = False, +) -> _AuthEnvSnapshot: + resolved_env_path = env_path if env_path is not None else GLOBAL_ENV_FILE.path + resolved_environ = environ if environ is not None else os.environ + return _AuthEnvSnapshot( + env_key=env_key, + current_process_has_value=_has_value(resolved_environ.get(env_key)), + dotenv_has_value=_dotenv_has_value(resolved_env_path, env_key), + process_env_had_value_before_dotenv_load=process_env_had_value_before_dotenv_load, + ) + + +def assess_auth_state( + provider: ProviderConfig, + *, + env_path: Path | None = None, + environ: Mapping[str, str] | None = None, + process_env_had_value_before_dotenv_load: bool = False, +) -> AuthState: + env_key = provider.api_key_env_var + if not env_key: + return _auth_state( + AuthStateKind.AUTH_NOT_REQUIRED, can_use_active_provider=True + ) + + auth_snapshot = _capture_auth_env_snapshot( + env_key, + env_path=env_path, + environ=environ, + process_env_had_value_before_dotenv_load=process_env_had_value_before_dotenv_load, + ) + if ( + not auth_snapshot.current_process_has_value + and not auth_snapshot.dotenv_has_value + ): + return _auth_state( + AuthStateKind.SIGNED_OUT, can_use_active_provider=False, env_key=env_key + ) + + if not _supports_vibe_owned_sign_out(provider): + return _auth_state( + AuthStateKind.UNSUPPORTED_PROVIDER, + can_use_active_provider=True, + env_key=env_key, + ) + + if ( + auth_snapshot.dotenv_has_value + and auth_snapshot.process_env_had_value_before_dotenv_load + ): + return _auth_state( + AuthStateKind.VIBE_HOME_ENV_FILE_OVERRIDES_PROCESS_ENV, + can_use_active_provider=True, + env_key=env_key, + ) + + if auth_snapshot.dotenv_has_value: + return _auth_state( + AuthStateKind.VIBE_HOME_ENV_FILE, + can_use_active_provider=True, + sign_out_available=True, + env_key=env_key, + ) + + if auth_snapshot.current_process_has_value: + return _auth_state( + AuthStateKind.PROCESS_ENV, can_use_active_provider=True, env_key=env_key + ) + + raise AssertionError("assess_auth_state reached unreachable state") + + +__all__ = ["AuthState", "AuthStateKind", "assess_auth_state"] diff --git a/vibe/setup/onboarding/__init__.py b/vibe/setup/onboarding/__init__.py index 17caacb..97cb93a 100644 --- a/vibe/setup/onboarding/__init__.py +++ b/vibe/setup/onboarding/__init__.py @@ -96,7 +96,7 @@ class OnboardingApp(App[str | None]): def _resolve_browser_sign_in_factory( self, browser_sign_in_service_factory: Callable[[], BrowserSignInService] | None ) -> Callable[[], BrowserSignInService] | None: - if not self._config.browser_sign_in_enabled: + if not self._config.supports_browser_sign_in: return None return ( diff --git a/vibe/setup/onboarding/context.py b/vibe/setup/onboarding/context.py index ecfe065..1eaadfc 100644 --- a/vibe/setup/onboarding/context.py +++ b/vibe/setup/onboarding/context.py @@ -29,7 +29,6 @@ def _default_model_payloads() -> list[dict[str, Any]]: class _OnboardingSnapshot(BaseModel): active_model: str = DEFAULT_ACTIVE_MODEL - enable_experimental_browser_sign_in: bool = False providers: list[Any] = Field(default_factory=_default_provider_payloads) models: list[Any] = Field(default_factory=_default_model_payloads) @@ -98,19 +97,6 @@ def _load_onboarding_env_payload_for_fields( and (models := _find_env_value("VIBE_MODELS")) is not None ): payload["models"] = _ONBOARDING_LIST_ADAPTER.validate_json(models) - # Onboarding uses this lightweight snapshot before full VibeConfig loading, - # so env-backed config fields must be mirrored here when onboarding needs them. - if ( - "enable_experimental_browser_sign_in" in field_names - and ( - enable_browser_sign_in := _find_env_value( - "VIBE_ENABLE_EXPERIMENTAL_BROWSER_SIGN_IN" - ) - ) - is not None - ): - payload["enable_experimental_browser_sign_in"] = enable_browser_sign_in - return payload @@ -194,27 +180,14 @@ def _resolve_provider( @dataclass(frozen=True) class OnboardingContext: provider: ProviderConfig - enable_experimental_browser_sign_in: bool = False @property def supports_browser_sign_in(self) -> bool: return self.provider.supports_browser_sign_in - @property - def browser_sign_in_enabled(self) -> bool: - return ( - self.enable_experimental_browser_sign_in - and self.provider.supports_browser_sign_in - ) - @classmethod def from_config(cls, config: VibeConfig) -> OnboardingContext: - return cls( - provider=config.get_active_provider(), - enable_experimental_browser_sign_in=( - config.enable_experimental_browser_sign_in - ), - ) + return cls(provider=config.get_active_provider()) @classmethod def load(cls, **overrides: Any) -> OnboardingContext: @@ -225,10 +198,7 @@ class OnboardingContext: return cls( provider=_resolve_provider( active_model=snapshot.active_model, snapshot=snapshot - ), - enable_experimental_browser_sign_in=( - snapshot.enable_experimental_browser_sign_in - ), + ) ) except (RuntimeError, ValidationError, ValueError): logger.warning( diff --git a/vibe/whats_new.md b/vibe/whats_new.md index 6af9474..e8ee7c7 100644 --- a/vibe/whats_new.md +++ b/vibe/whats_new.md @@ -1,5 +1,4 @@ -# What's new in v2.11.0 +# What's new in v2.11.1 -- **Shared skills**: Vibe now loads skills from `~/.agents/skills` so they can be reused across agents -- **Connector onboarding from the CLI**: `/mcp` lists unauthenticated connectors and lets you start the OAuth flow without leaving Vibe -- **Theme picker is back**: Pick a theme during onboarding (`vibe --setup`) or switch any time with `/theme` +- **Custom compaction prompts**: Override the default `/compact` prompt by setting `compaction_prompt_id` and dropping a markdown file in `~/.vibe/prompts/` or `.vibe/prompts/`. +- **Safer programmatic mode**: `-p` no longer auto-approves tool calls by default — pass `--auto-approve` to restore the previous behavior.