diff --git a/AGENTS.md b/AGENTS.md index fb1ad7a..523a2fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,6 +76,11 @@ Always go through `uv` — never invoke bare `python` or `pip`. - They return `ReadSafeResult(text, encoding)` and try UTF-8, then BOM detection, then locale, then `charset_normalizer` lazily. - Pass `raise_on_error=True` only when callers must distinguish corrupt files from valid ones; the default replaces undecodable bytes with U+FFFD. +## TCSS + +- When a rule sets `color: $text-muted;`, pair it with a nested `&:ansi { text-style: dim; }` so the muted intent survives under ANSI themes. +- Never use `ansi_*` colors (e.g. `ansi_red`, `ansi_bright_blue`). Use Textual theme variables like `$primary`, `$foreground`, `$surface`, `$error`, etc. — see https://textual.textualize.io/guide/design/. ANSI themes are derived from these variables automatically. + ## Tests - Stack: `pytest` + `pytest-asyncio` + `pytest-textual-snapshot` + `respx`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7df74d9..46f653c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ 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.0] - 2026-05-25 + +### Added + +- Load skills from `~/.agents/skills` so they can be shared across agents +- Restore the textual theme selection system with an onboarding theme picker +- Surface unauthenticated connectors in `/mcp` with `needs auth` / `needs setup` labels and start the OAuth flow from inside the CLI +- Current date injected into the system prompt +- `minimal` system prompt variant for eval and training + +### Changed + +- Refreshed manual API key onboarding screen to match the new onboarding panel layout +- Newly discovered connectors are now disabled by default; existing user choices are preserved + +### Fixed + +- Preserve original line endings in ACP `search_replace` +- Show MCP/connectors count as `enabled/total` in the banner +- ACP grep tool displays the search path as a chip and no longer drops the filename + + ## [2.10.1] - 2026-05-20 ### Added diff --git a/README.md b/README.md index b8949a9..9d7e6c7 100644 --- a/README.md +++ b/README.md @@ -372,7 +372,7 @@ Vibe discovers skills from multiple locations: 1. **Custom paths**: Configured in `config.toml` via `skill_paths` 2. **Standard Agent Skills path** (project root, trusted folders only): `.agents/skills/` — [Agent Skills](https://agentskills.io) standard 3. **Local project skills** (project root, trusted folders only): `.vibe/skills/` in your project -4. **Global skills directory**: `~/.vibe/skills/` +4. **Global skills directories**: `~/.vibe/skills/` and `~/.agents/skills/` ```toml skill_paths = ["/path/to/custom/skills"] diff --git a/distribution/zed/extension.toml b/distribution/zed/extension.toml index 336d461..33aab8f 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.10.1" +version = "2.11.0" 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.10.1/vibe-acp-darwin-aarch64-2.10.1.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.0/vibe-acp-darwin-aarch64-2.11.0.zip" cmd = "./vibe-acp" [agent_servers.mistral-vibe.targets.darwin-x86_64] -archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.10.1/vibe-acp-darwin-x86_64-2.10.1.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.0/vibe-acp-darwin-x86_64-2.11.0.zip" cmd = "./vibe-acp" [agent_servers.mistral-vibe.targets.linux-aarch64] -archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.10.1/vibe-acp-linux-aarch64-2.10.1.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.0/vibe-acp-linux-aarch64-2.11.0.zip" cmd = "./vibe-acp" [agent_servers.mistral-vibe.targets.linux-x86_64] -archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.10.1/vibe-acp-linux-x86_64-2.10.1.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.0/vibe-acp-linux-x86_64-2.11.0.zip" cmd = "./vibe-acp" [agent_servers.mistral-vibe.targets.windows-x86_64] -archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.10.1/vibe-acp-windows-x86_64-2.10.1.zip" +archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.11.0/vibe-acp-windows-x86_64-2.11.0.zip" cmd = "./vibe-acp.exe" diff --git a/pyproject.toml b/pyproject.toml index a218d1d..d4f0760 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mistral-vibe" -version = "2.10.1" +version = "2.11.0" description = "Minimal CLI coding agent by Mistral" readme = "README.md" requires-python = ">=3.12" @@ -106,7 +106,7 @@ dependencies = [ "soupsieve==2.8.3", "sse-starlette==3.4.1", "starlette==1.0.0", - "textual==8.2.4", + "textual==8.2.7", "textual-speedups==0.2.1", "tomli-w==1.2.0", "tree-sitter==0.25.2", @@ -146,7 +146,7 @@ vibe-acp = "vibe.acp.entrypoint:main" [tool.uv] exclude-newer = "7 days" -exclude-newer-package = { agent-client-protocol = false, mistralai = false } +exclude-newer-package = { agent-client-protocol = false, mistralai = false, textual = false } package = true required-version = ">=0.8.0" @@ -225,8 +225,7 @@ ignore = ["D203", "D205", "D213", "ANN401", "PLR6301"] [tool.ruff.lint.per-file-ignores] "tests/*" = ["ANN", "PLR"] -# Textual parent classes don't use ClassVar, so we can't either without breaking pyright -"vibe/cli/textual_ui/ansi_markdown.py" = ["RUF012"] + [tool.ruff.lint.flake8-tidy-imports] ban-relative-imports = "all" diff --git a/tests/acp/test_bash.py b/tests/acp/test_bash.py index ac19b52..3724ed5 100644 --- a/tests/acp/test_bash.py +++ b/tests/acp/test_bash.py @@ -11,6 +11,7 @@ from vibe.acp.tools.builtins.bash import AcpBashState, Bash from vibe.acp.tools.events import ToolTerminalOpenedEvent from vibe.core.tools.base import InvokeContext, ToolError from vibe.core.tools.builtins.bash import BashArgs, BashResult, BashToolConfig +from vibe.core.types import ToolResultEvent class MockTerminalHandle: @@ -483,3 +484,42 @@ class TestAcpBashCleanup: assert result is not None assert result.stdout == "test output" + + +class TestAcpBashToolResultSessionUpdate: + def test_success_reports_completed(self) -> None: + event = ToolResultEvent( + tool_name="bash", + tool_call_id="call_1", + tool_class=Bash, + result=BashResult(command="echo ok", stdout="ok", stderr="", returncode=0), + ) + + update = Bash.tool_result_session_update(event) + + assert update is not None + assert update.status == "completed" + + def test_user_rejection_reports_failed(self) -> None: + event = ToolResultEvent( + tool_name="bash", + tool_call_id="call_1", + tool_class=Bash, + skipped=True, + skip_reason="User rejected the tool call, provide an alternative plan", + ) + + update = Bash.tool_result_session_update(event) + + assert update is not None + assert update.status == "failed" + + def test_error_reports_failed(self) -> None: + event = ToolResultEvent( + tool_name="bash", tool_call_id="call_1", tool_class=Bash, error="boom" + ) + + update = Bash.tool_result_session_update(event) + + assert update is not None + assert update.status == "failed" diff --git a/tests/acp/test_initialize.py b/tests/acp/test_initialize.py index 629a127..0409ffc 100644 --- a/tests/acp/test_initialize.py +++ b/tests/acp/test_initialize.py @@ -65,7 +65,7 @@ class TestACPInitialize: ), ) assert response.agent_info == Implementation( - name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.10.1" + name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.11.0" ) assert response.auth_methods is not None @@ -97,7 +97,7 @@ class TestACPInitialize: ), ) assert response.agent_info == Implementation( - name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.10.1" + name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.11.0" ) assert response.auth_methods is not None diff --git a/tests/acp/test_search_replace.py b/tests/acp/test_search_replace.py index d3e35fe..75c40ac 100644 --- a/tests/acp/test_search_replace.py +++ b/tests/acp/test_search_replace.py @@ -16,6 +16,13 @@ from vibe.core.tools.builtins.search_replace import ( from vibe.core.types import ToolCallEvent, ToolResultEvent +def _make_block(search: str, replace: str) -> str: + head = "<" * 7 + " SEARCH" + sep = "=" * 7 + tail = ">" * 7 + " REPLACE" + return f"{head}\n{search}\n{sep}\n{replace}\n{tail}" + + class MockClient: def __init__( self, @@ -153,11 +160,74 @@ class TestAcpSearchReplaceExecution: result = await collect_result(tool.run(args)) assert result.blocks_applied == 1 - # Should have written the main file and the backup - assert len(mock_client._write_calls) >= 1 - # Check if backup was written (it should be written to .bak file) assert sum(w["path"].endswith(".bak") for w in mock_client._write_calls) == 1 + @pytest.mark.asyncio + @pytest.mark.parametrize("newline", ["\r\n", "\r", "\n"]) + async def test_run_preserves_line_endings( + self, + newline: str, + mock_client: MockClient, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.chdir(tmp_path) + mock_client._file_content = newline.join([ + "original line 1", + "original line 2", + "original line 3", + ]) + + tool = SearchReplace( + config_getter=lambda: SearchReplaceConfig(), + state=AcpSearchReplaceState.model_construct( + client=mock_client, session_id="test_session" + ), + ) + + test_file = tmp_path / "test_file.txt" + test_file.touch() + args = SearchReplaceArgs( + file_path=str(test_file), + content=_make_block("original line 2", "modified line 2"), + ) + await collect_result(tool.run(args)) + + assert mock_client._last_write_params["content"] == newline.join([ + "original line 1", + "modified line 2", + "original line 3", + ]) + + @pytest.mark.asyncio + async def test_run_backup_preserves_crlf_byte_for_byte( + self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.chdir(tmp_path) + original = "original line 1\r\noriginal line 2\r\noriginal line 3" + mock_client._file_content = original + + tool = SearchReplace( + config_getter=lambda: SearchReplaceConfig(create_backup=True), + state=AcpSearchReplaceState.model_construct( + client=mock_client, session_id="test_session" + ), + ) + + test_file = tmp_path / "test_file.txt" + test_file.touch() + args = SearchReplaceArgs( + file_path=str(test_file), + content=_make_block("original line 1", "modified line 1"), + ) + await collect_result(tool.run(args)) + + backup_calls = [ + w for w in mock_client._write_calls if w["path"].endswith(".bak") + ] + assert len(backup_calls) == 1 + assert backup_calls[0]["content"] == original + @pytest.mark.asyncio async def test_run_read_error( self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch diff --git a/tests/acp/test_tool_field_meta.py b/tests/acp/test_tool_field_meta.py index 03e7c6d..922961c 100644 --- a/tests/acp/test_tool_field_meta.py +++ b/tests/acp/test_tool_field_meta.py @@ -53,22 +53,24 @@ def _result_event( class TestGrepFieldMeta: - def test_call_meta_contains_query_and_tool_name(self) -> None: + def test_call_meta_contains_query_tool_name_and_search_path(self) -> None: event = _call_event("grep", Grep, GrepArgs(pattern="TODO", path="src")) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) - assert update.field_meta == {"tool_name": "grep", "query": "TODO"} + assert update.field_meta == { + "tool_name": "grep", + "query": "TODO", + "search_path": str(Path("src").resolve()), + } assert update.kind == "search" - def test_call_location_is_resolved_search_path(self) -> None: + def test_call_has_no_locations_so_result_matches_are_not_replaced(self) -> None: event = _call_event("grep", Grep, GrepArgs(pattern="TODO", path="src")) update = tool_call_session_update(event) assert isinstance(update, ToolCallStart) - assert update.locations is not None - assert len(update.locations) == 1 - assert update.locations[0].path == str(Path("src").resolve()) + assert update.locations is None def test_result_locations_from_parsed_matches(self) -> None: result = GrepResult( diff --git a/tests/acp/test_write_file.py b/tests/acp/test_write_file.py index f9e666c..be1ec43 100644 --- a/tests/acp/test_write_file.py +++ b/tests/acp/test_write_file.py @@ -118,6 +118,61 @@ class TestAcpWriteFileExecution: assert params["path"] == str(test_file) assert params["content"] == "New content" + @pytest.mark.asyncio + @pytest.mark.parametrize("input_newline", ["\r\n", "\r", "\n"]) + @pytest.mark.parametrize("os_newline", ["\n", "\r\n"]) + async def test_run_writes_with_os_linesep( + self, + input_newline: str, + os_newline: str, + mock_client: MockClient, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.chdir(tmp_path) + monkeypatch.setattr("vibe.acp.tools.builtins.write_file.os.linesep", os_newline) + tool = WriteFile( + config_getter=lambda: WriteFileConfig(), + state=AcpWriteFileState.model_construct( + client=mock_client, session_id="test_session" + ), + ) + + test_file = tmp_path / "test.txt" + content = input_newline.join(["line 1", "line 2", "line 3"]) + args = WriteFileArgs(path=str(test_file), content=content) + await collect_result(tool.run(args)) + + assert mock_client._last_write_params["content"] == os_newline.join([ + "line 1", + "line 2", + "line 3", + ]) + + @pytest.mark.asyncio + async def test_run_normalizes_mixed_newlines( + self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.chdir(tmp_path) + monkeypatch.setattr("vibe.acp.tools.builtins.write_file.os.linesep", "\n") + tool = WriteFile( + config_getter=lambda: WriteFileConfig(), + state=AcpWriteFileState.model_construct( + client=mock_client, session_id="test_session" + ), + ) + + test_file = tmp_path / "test.txt" + args = WriteFileArgs( + path=str(test_file), content="line 1\r\nline 2\nline 3\rline 4" + ) + await collect_result(tool.run(args)) + + assert ( + mock_client._last_write_params["content"] + == "line 1\nline 2\nline 3\nline 4" + ) + @pytest.mark.asyncio async def test_run_write_error( self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch @@ -209,6 +264,24 @@ class TestAcpWriteFileSessionUpdates: assert len(update.locations) == 1 assert update.locations[0].path == str(Path("/tmp/test.txt").resolve()) + def test_tool_call_session_update_passes_content_through( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setattr("vibe.acp.tools.builtins.write_file.os.linesep", "\r\n") + content = "line 1\r\nline 2\nline 3\rline 4" + event = ToolCallEvent( + tool_name="write_file", + tool_call_id="test_call_123", + args=WriteFileArgs(path="/tmp/test.txt", content=content), + tool_class=WriteFile, + ) + + update = WriteFile.tool_call_session_update(event) + assert update is not None + assert update.content is not None + assert isinstance(update.content, list) + assert update.content[0].new_text == content + def test_tool_call_session_update_invalid_args(self) -> None: from vibe.core.types import FunctionCall, ToolCall diff --git a/tests/banner/test_banner_initial_state.py b/tests/banner/test_banner_initial_state.py index 262167b..6a8797c 100644 --- a/tests/banner/test_banner_initial_state.py +++ b/tests/banner/test_banner_initial_state.py @@ -6,16 +6,17 @@ from vibe.cli.textual_ui.widgets.banner.banner import Banner, BannerState, _plur from vibe.core.config import VibeConfig from vibe.core.config._settings import ModelConfig, ThinkingLevel from vibe.core.skills.manager import SkillManager -from vibe.core.tools.mcp.registry import MCPRegistry def _make_mock_config( - active_model: str = "test-model", thinking: ThinkingLevel = "off" + active_model: str = "test-model", + thinking: ThinkingLevel = "off", + mcp_servers: list | None = None, ) -> Mock: config = Mock(spec=VibeConfig) config.active_model = active_model config.models = [active_model] - config.mcp_servers = [] + config.mcp_servers = mcp_servers or [] config.connectors = [] config.disable_welcome_banner_animation = False config.get_active_model.return_value = ModelConfig( @@ -40,48 +41,38 @@ class TestBannerInitialState: skill_manager = Mock(spec=SkillManager) skill_manager.custom_skills_count = 0 - mcp_registry = Mock(spec=MCPRegistry) - mcp_registry.count_loaded.return_value = 0 - banner = Banner( config=_make_mock_config(), skill_manager=skill_manager, - mcp_registry=mcp_registry, - connectors_count=5, + connectors_enabled=5, + connectors_total=5, ) assert banner._initial_state.active_model == "test-model[off]" assert banner._initial_state.models_count == 1 - assert banner._initial_state.mcp_servers_count == 0 - assert banner._initial_state.connectors_count == 5 + assert ( + banner._initial_state.mcp_servers_enabled == 0 + ) # No MCP servers configured + assert banner._initial_state.mcp_servers_total == 0 + assert banner._initial_state.connectors_enabled == 5 + assert banner._initial_state.connectors_total == 5 assert banner._initial_state.skills_count == 0 def test_banner_initial_state_with_no_connectors(self) -> None: skill_manager = Mock(spec=SkillManager) skill_manager.custom_skills_count = 0 - mcp_registry = Mock(spec=MCPRegistry) - mcp_registry.count_loaded.return_value = 0 + banner = Banner(config=_make_mock_config(), skill_manager=skill_manager) - banner = Banner( - config=_make_mock_config(), - skill_manager=skill_manager, - mcp_registry=mcp_registry, - ) - - assert banner._initial_state.connectors_count == 0 + assert banner._initial_state.connectors_enabled == 0 + assert banner._initial_state.connectors_total == 0 def test_banner_shows_thinking_level(self) -> None: skill_manager = Mock(spec=SkillManager) skill_manager.custom_skills_count = 0 - mcp_registry = Mock(spec=MCPRegistry) - mcp_registry.count_loaded.return_value = 0 - banner = Banner( - config=_make_mock_config(thinking="max"), - skill_manager=skill_manager, - mcp_registry=mcp_registry, + config=_make_mock_config(thinking="max"), skill_manager=skill_manager ) assert banner._initial_state.active_model == "test-model[max]" @@ -90,49 +81,150 @@ class TestBannerInitialState: skill_manager = Mock(spec=SkillManager) skill_manager.custom_skills_count = 0 - mcp_registry = Mock(spec=MCPRegistry) - mcp_registry.count_loaded.return_value = 0 + banner = Banner(config=_make_mock_config(), skill_manager=skill_manager) - banner = Banner( - config=_make_mock_config(), - skill_manager=skill_manager, - mcp_registry=mcp_registry, - ) - - # Now test _format_meta_counts by setting state + # Now test _format_meta_counts by setting state with x/y format banner.state = BannerState( - models_count=2, mcp_servers_count=1, connectors_count=3, skills_count=5 + models_count=2, + mcp_servers_enabled=1, + mcp_servers_total=2, + connectors_enabled=3, + connectors_total=3, + skills_count=5, ) result = banner._format_meta_counts() assert "2 models" in result - assert "3 connectors" in result - assert "1 MCP server" in result + assert "3 connectors" in result # When enabled == total, just show count + assert "1/2 MCP servers" in result assert "5 skills" in result # Test without connectors banner.state = BannerState( - models_count=2, mcp_servers_count=1, connectors_count=0, skills_count=5 + models_count=2, + mcp_servers_enabled=1, + mcp_servers_total=2, + connectors_enabled=0, + connectors_total=0, + skills_count=5, ) result = banner._format_meta_counts() assert "2 models" in result assert "connectors" not in result # Should not appear when 0 - assert "1 MCP server" in result + assert "1/2 MCP servers" in result assert "5 skills" in result +class TestBannerMCPServersCount: + """Test that banner correctly counts MCP servers regardless of tool discovery.""" + + def test_banner_counts_enabled_mcp_servers(self) -> None: + """Test that banner counts all enabled MCP servers, not just those with tools.""" + from vibe.core.config import MCPServer + + # Create mock MCP servers - even if they have no tools, they should be counted + mock_server1 = Mock(spec=MCPServer) + mock_server1.name = "server1" + mock_server1.disabled = False + + mock_server2 = Mock(spec=MCPServer) + mock_server2.name = "server2" + mock_server2.disabled = False + + # Create a disabled server that should NOT be counted + mock_server3 = Mock(spec=MCPServer) + mock_server3.name = "server3" + mock_server3.disabled = True + + skill_manager = Mock(spec=SkillManager) + skill_manager.custom_skills_count = 0 + + # Note: we don't need to mock count_loaded anymore since it's not used + + banner = Banner( + config=_make_mock_config( + mcp_servers=[mock_server1, mock_server2, mock_server3] + ), + skill_manager=skill_manager, + ) + + # Should count only enabled servers (server1 and server2, not server3) + assert banner._initial_state.mcp_servers_enabled == 2 + assert banner._initial_state.mcp_servers_total == 3 + + def test_banner_shows_zero_mcp_servers(self) -> None: + """Test that banner correctly shows 0 when no MCP servers are configured.""" + skill_manager = Mock(spec=SkillManager) + skill_manager.custom_skills_count = 0 + + banner = Banner( + config=_make_mock_config(mcp_servers=[]), skill_manager=skill_manager + ) + + assert banner._initial_state.mcp_servers_enabled == 0 + assert banner._initial_state.mcp_servers_total == 0 + + def test_banner_shows_disabled_count_in_xy_format(self) -> None: + """Test that banner shows x/y format with disabled servers.""" + from vibe.core.config import MCPServer + + skill_manager = Mock(spec=SkillManager) + skill_manager.custom_skills_count = 0 + + banner = Banner( + config=_make_mock_config( + mcp_servers=[ + Mock(spec=MCPServer, name="s1", disabled=False), + Mock(spec=MCPServer, name="s2", disabled=False), + Mock(spec=MCPServer, name="s3", disabled=True), + ] + ), + skill_manager=skill_manager, + ) + + assert banner._initial_state.mcp_servers_enabled == 2 + assert banner._initial_state.mcp_servers_total == 3 + # Test the formatted output using the initial state + banner.state = banner._initial_state + result = banner._format_meta_counts() + assert "2/3 MCP servers" in result + + def test_banner_shows_simple_count_when_all_enabled(self) -> None: + """Test that banner shows simple count when all MCP servers are enabled.""" + from vibe.core.config import MCPServer + + skill_manager = Mock(spec=SkillManager) + skill_manager.custom_skills_count = 0 + + banner = Banner( + config=_make_mock_config( + mcp_servers=[ + Mock(spec=MCPServer, name="s1", disabled=False), + Mock(spec=MCPServer, name="s2", disabled=False), + ] + ), + skill_manager=skill_manager, + ) + + assert banner._initial_state.mcp_servers_enabled == 2 + assert banner._initial_state.mcp_servers_total == 2 + banner.state = banner._initial_state + result = banner._format_meta_counts() + # When all are enabled, show simple count not x/y + assert "2 MCP servers" in result + assert "/" not in result # No slash when all enabled + + class TestBannerConnectorsCount: def test_connectors_count_passed_through(self) -> None: skill_manager = Mock(spec=SkillManager) skill_manager.custom_skills_count = 0 - mcp_registry = Mock(spec=MCPRegistry) - mcp_registry.count_loaded.return_value = 0 - banner = Banner( config=_make_mock_config(), skill_manager=skill_manager, - mcp_registry=mcp_registry, - connectors_count=5, + connectors_enabled=3, + connectors_total=5, ) - assert banner._initial_state.connectors_count == 5 + assert banner._initial_state.connectors_enabled == 3 + assert banner._initial_state.connectors_total == 5 diff --git a/tests/cli/test_feedback_bar_manager.py b/tests/cli/test_feedback_bar_manager.py index c4bf91d..af8a165 100644 --- a/tests/cli/test_feedback_bar_manager.py +++ b/tests/cli/test_feedback_bar_manager.py @@ -5,12 +5,12 @@ import time import tomllib from unittest.mock import MagicMock, patch -from vibe.cli.textual_ui.widgets.feedback_bar_manager import ( +from vibe.cli.textual_ui.widgets.feedback_bar_manager import FeedbackBarManager +from vibe.core.feedback import ( _CACHE_SECTION, _LAST_SHOWN_KEY, FEEDBACK_COOLDOWN_SECONDS, MIN_USER_MESSAGES_FOR_FEEDBACK, - FeedbackBarManager, ) from vibe.core.types import LLMMessage, Role @@ -19,15 +19,12 @@ def _patch_cache_file(tmp_path: Path): from vibe.core.paths._vibe_home import GlobalPath return patch( - "vibe.cli.textual_ui.widgets.feedback_bar_manager.CACHE_FILE", - GlobalPath(lambda: tmp_path / "cache.toml"), + "vibe.core.feedback.CACHE_FILE", GlobalPath(lambda: tmp_path / "cache.toml") ) def _patch_probability(value: float): - return patch( - "vibe.cli.textual_ui.widgets.feedback_bar_manager.FEEDBACK_PROBABILITY", value - ) + return patch("vibe.core.feedback.FEEDBACK_PROBABILITY", value) def _make_agent_loop( @@ -50,10 +47,7 @@ class TestShouldShow: with ( _patch_cache_file(tmp_path), _patch_probability(0.2), - patch( - "vibe.cli.textual_ui.widgets.feedback_bar_manager.random.random", - return_value=0.0, - ), + patch("vibe.core.feedback.random.random", return_value=0.0), ): assert manager.should_show(_make_agent_loop()) is True @@ -62,10 +56,7 @@ class TestShouldShow: with ( _patch_cache_file(tmp_path), _patch_probability(0.2), - patch( - "vibe.cli.textual_ui.widgets.feedback_bar_manager.random.random", - return_value=1.0, - ), + patch("vibe.core.feedback.random.random", return_value=1.0), ): assert manager.should_show(_make_agent_loop()) is False @@ -77,10 +68,7 @@ class TestShouldShow: with ( _patch_cache_file(tmp_path), _patch_probability(0.2), - patch( - "vibe.cli.textual_ui.widgets.feedback_bar_manager.random.random", - return_value=0.0, - ), + patch("vibe.core.feedback.random.random", return_value=0.0), ): assert manager.should_show(_make_agent_loop()) is False @@ -92,10 +80,7 @@ class TestShouldShow: with ( _patch_cache_file(tmp_path), _patch_probability(0.2), - patch( - "vibe.cli.textual_ui.widgets.feedback_bar_manager.random.random", - return_value=0.0, - ), + patch("vibe.core.feedback.random.random", return_value=0.0), ): assert manager.should_show(_make_agent_loop()) is True @@ -111,10 +96,7 @@ class TestShouldShow: with ( _patch_cache_file(tmp_path), _patch_probability(0.2), - patch( - "vibe.cli.textual_ui.widgets.feedback_bar_manager.random.random", - return_value=0.0, - ), + patch("vibe.core.feedback.random.random", return_value=0.0), ): assert manager.should_show(_make_agent_loop(user_message_count=1)) is False @@ -129,10 +111,7 @@ class TestShouldShow: with ( _patch_cache_file(tmp_path), _patch_probability(0.2), - patch( - "vibe.cli.textual_ui.widgets.feedback_bar_manager.random.random", - return_value=0.0, - ), + patch("vibe.core.feedback.random.random", return_value=0.0), ): # Only 1 non-injected user message, below MIN_USER_MESSAGES_FOR_FEEDBACK assert manager.should_show(loop) is False diff --git a/tests/cli/test_mcp_app.py b/tests/cli/test_mcp_app.py index 6b9c80e..66607d5 100644 --- a/tests/cli/test_mcp_app.py +++ b/tests/cli/test_mcp_app.py @@ -5,6 +5,7 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock import pytest +from textual.widgets.option_list import Option from tests.stubs.fake_connector_registry import FakeConnectorRegistry from vibe.cli.textual_ui.widgets.mcp_app import ( @@ -17,7 +18,10 @@ from vibe.cli.textual_ui.widgets.mcp_app import ( ) from vibe.core.config import MCPStdio from vibe.core.tools.base import InvokeContext -from vibe.core.tools.connectors.connector_registry import RemoteTool +from vibe.core.tools.connectors.connector_registry import ( + ConnectorAuthAction, + RemoteTool, +) from vibe.core.tools.mcp.tools import MCPTool, MCPToolResult, _OpenArgs from vibe.core.types import ToolStreamEvent @@ -271,7 +275,8 @@ class TestHelpBarChanges: connectors={ "gmail": [RemoteTool(name="search", description="Search")], "slack": [], - } + }, + auth_actions={"slack": ConnectorAuthAction.OAUTH}, ) mgr = _make_tool_manager({}) return MCPApp( @@ -282,33 +287,35 @@ class TestHelpBarChanges: def test_help_shows_show_tools_for_server(self) -> None: app = self._make_app_with_connectors() - option = MagicMock() - option.id = "server:srv" + option = Option("", id="server:srv") assert app._list_help_for_option(option) == _LIST_VIEW_HELP_TOOLS def test_help_shows_show_tools_for_connected_connector(self) -> None: app = self._make_app_with_connectors() - option = MagicMock() - option.id = "connector:gmail" + option = Option("", id="connector:gmail") assert app._list_help_for_option(option) == _LIST_VIEW_HELP_TOOLS def test_help_shows_authenticate_for_disconnected_connector(self) -> None: app = self._make_app_with_connectors() - option = MagicMock() - option.id = "connector:slack" + option = Option("", id="connector:slack") assert app._list_help_for_option(option) == _LIST_VIEW_HELP_AUTH + def test_help_shows_tools_for_degraded_connector(self) -> None: + registry = FakeConnectorRegistry(connectors={"slack": []}) + mgr = _make_tool_manager({}) + app = MCPApp(mcp_servers=[], tool_manager=mgr, connector_registry=registry) + option = Option("", id="connector:slack") + assert app._list_help_for_option(option) == _LIST_VIEW_HELP_TOOLS + def test_help_defaults_to_tools_for_unknown_option(self) -> None: app = self._make_app_with_connectors() - option = MagicMock() - option.id = "" + option = Option("") assert app._list_help_for_option(option) == _LIST_VIEW_HELP_TOOLS def test_help_shows_tools_without_registry(self) -> None: mgr = _make_tool_manager({}) app = MCPApp(mcp_servers=[], tool_manager=mgr) - option = MagicMock() - option.id = "connector:slack" + option = Option("", id="connector:slack") assert app._list_help_for_option(option) == _LIST_VIEW_HELP_TOOLS @@ -316,7 +323,9 @@ class TestConnectorAuthRequested: """Clicking a disconnected connector posts ConnectorAuthRequested.""" def test_disconnected_connector_posts_auth_requested(self) -> None: - registry = FakeConnectorRegistry(connectors={"slack": []}) + registry = FakeConnectorRegistry( + connectors={"slack": []}, auth_actions={"slack": ConnectorAuthAction.OAUTH} + ) mgr = _make_tool_manager({}) app = MCPApp(mcp_servers=[], tool_manager=mgr, connector_registry=registry) app.query_one = MagicMock() @@ -334,6 +343,24 @@ class TestConnectorAuthRequested: assert msg.connector_registry is registry assert msg.tool_manager is mgr + def test_degraded_connector_shows_refresh_message(self) -> None: + registry = FakeConnectorRegistry(connectors={"slack": []}) + mgr = _make_tool_manager({}) + app = MCPApp(mcp_servers=[], tool_manager=mgr, connector_registry=registry) + app.query_one = MagicMock() + app.post_message = MagicMock() + + option_list = MagicMock() + app._show_detail_view( + "slack", option_list, app._index, kind=MCPSourceKind.CONNECTOR + ) + + app.post_message.assert_not_called() + option_list.add_option.assert_called_once() + assert "press R to refresh" in str( + option_list.add_option.call_args.args[0].prompt + ) + def test_connected_connector_with_no_indexed_tools_shows_message(self) -> None: registry = MagicMock() registry.get_connector_names.return_value = ["slack"] diff --git a/tests/cli/test_ui_theme_picker.py b/tests/cli/test_ui_theme_picker.py new file mode 100644 index 0000000..975a134 --- /dev/null +++ b/tests/cli/test_ui_theme_picker.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from tests.conftest import build_test_vibe_app, build_test_vibe_config +from vibe.cli.textual_ui.app import BottomApp +from vibe.cli.textual_ui.widgets.theme_picker import ThemePickerApp + + +@pytest.mark.asyncio +async def test_theme_opens_theme_picker() -> None: + app = build_test_vibe_app(config=build_test_vibe_config()) + async with app.run_test() as pilot: + await pilot.pause(0.1) + await app._show_theme() + await pilot.pause(0.2) + + assert app._current_bottom_app == BottomApp.ThemePicker + assert len(app.query(ThemePickerApp)) == 1 + + +@pytest.mark.asyncio +async def test_theme_picker_lists_themes_and_marks_current() -> None: + config = build_test_vibe_config() + config.theme = "dracula" + app = build_test_vibe_app(config=config) + async with app.run_test() as pilot: + await pilot.pause(0.1) + await app._show_theme() + await pilot.pause(0.2) + + picker = app.query_one(ThemePickerApp) + assert "dracula" in picker._theme_names + assert "ansi-dark" in picker._theme_names + assert picker._current_theme == "dracula" + + +@pytest.mark.asyncio +async def test_theme_picker_escape_restores_original_theme() -> None: + config = build_test_vibe_config() + config.theme = "ansi-dark" + app = build_test_vibe_app(config=config) + async with app.run_test() as pilot: + await pilot.pause(0.1) + await app._show_theme() + await pilot.pause(0.2) + + # Move highlight to a different theme to trigger preview. + await pilot.press("down") + await pilot.pause(0.2) + + with patch("vibe.cli.textual_ui.app.VibeConfig.save_updates") as mock_save: + await pilot.press("escape") + await pilot.pause(0.2) + + mock_save.assert_not_called() + + assert app._current_bottom_app == BottomApp.Input + assert len(app.query(ThemePickerApp)) == 0 + assert app.config.theme == "ansi-dark" + assert app.theme == "ansi-dark" + + +@pytest.mark.asyncio +async def test_theme_picker_select_persists_and_applies() -> None: + config = build_test_vibe_config() + config.theme = "ansi-dark" + app = build_test_vibe_app(config=config) + async with app.run_test() as pilot: + await pilot.pause(0.1) + await app._show_theme() + await pilot.pause(0.2) + + picker = app.query_one(ThemePickerApp) + names = picker._theme_names + current_index = names.index("ansi-dark") + target_index = (current_index + 1) % len(names) + target = names[target_index] + + await pilot.press("down") + + with patch("vibe.cli.textual_ui.app.VibeConfig.save_updates") as mock_save: + await pilot.press("enter") + await pilot.pause(0.2) + + mock_save.assert_called_once_with({"theme": target}) + + assert app._current_bottom_app == BottomApp.Input + assert len(app.query(ThemePickerApp)) == 0 + assert app.config.theme == target + assert app.theme == target diff --git a/tests/cli/textual_ui/test_message_queue.py b/tests/cli/textual_ui/test_message_queue.py new file mode 100644 index 0000000..8e68610 --- /dev/null +++ b/tests/cli/textual_ui/test_message_queue.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +import pytest + +from vibe.cli.textual_ui.message_queue import MessageQueue, QueuedItemKind + + +def test_empty_queue_is_falsy() -> None: + queue = MessageQueue() + assert not queue + assert len(queue) == 0 + assert not queue.paused + + +def test_append_prompt_increases_length() -> None: + queue = MessageQueue() + queue.append_prompt("hello") + assert len(queue) == 1 + assert queue.items[0].kind == QueuedItemKind.PROMPT + assert queue.items[0].content == "hello" + + +def test_append_bash_marks_kind() -> None: + queue = MessageQueue() + queue.append_bash("ls") + assert queue.items[0].kind == QueuedItemKind.BASH + + +def test_pop_last_returns_newest() -> None: + queue = MessageQueue() + queue.append_prompt("a") + queue.append_prompt("b") + queue.append_prompt("c") + + popped = queue.pop_last() + assert popped is not None + assert popped.content == "c" + assert [item.content for item in queue.items] == ["a", "b"] + + +def test_pop_first_returns_oldest() -> None: + queue = MessageQueue() + queue.append_prompt("a") + queue.append_bash("ls") + queue.append_prompt("c") + + first = queue.pop_first() + assert first is not None + assert first.content == "a" + assert first.kind == QueuedItemKind.PROMPT + + second = queue.pop_first() + assert second is not None + assert second.content == "ls" + assert second.kind == QueuedItemKind.BASH + + +def test_pop_from_empty_returns_none() -> None: + queue = MessageQueue() + assert queue.pop_first() is None + assert queue.pop_last() is None + + +def test_pause_and_resume() -> None: + queue = MessageQueue() + queue.append_prompt("a") + + queue.pause() + assert queue.paused + + queue.resume() + assert not queue.paused + + +def test_pause_is_idempotent() -> None: + queue = MessageQueue() + calls = [] + queue.set_change_listener(lambda: calls.append(None)) + queue.pause() + queue.pause() + assert len(calls) == 1 + + +def test_clear_resets_state() -> None: + queue = MessageQueue() + queue.append_prompt("a") + queue.pause() + queue.clear() + assert not queue + assert not queue.paused + + +def test_change_listener_fires_on_mutations() -> None: + queue = MessageQueue() + calls: list[None] = [] + queue.set_change_listener(lambda: calls.append(None)) + + queue.append_prompt("a") + assert len(calls) == 1 + + queue.append_bash("ls") + assert len(calls) == 2 + + queue.pop_last() + assert len(calls) == 3 + + queue.pause() + assert len(calls) == 4 + + queue.resume() + assert len(calls) == 5 + + +def test_change_listener_can_be_cleared() -> None: + queue = MessageQueue() + calls: list[None] = [] + queue.set_change_listener(lambda: calls.append(None)) + queue.set_change_listener(None) + queue.append_prompt("a") + assert calls == [] + + +def test_items_returns_copy() -> None: + queue = MessageQueue() + queue.append_prompt("a") + snapshot = queue.items + queue.append_prompt("b") + assert len(snapshot) == 1 + + +@pytest.mark.parametrize( + "kind,content", + [(QueuedItemKind.PROMPT, "hello world"), (QueuedItemKind.BASH, "echo 'hi'")], +) +def test_item_kinds_round_trip(kind: QueuedItemKind, content: str) -> None: + queue = MessageQueue() + if kind == QueuedItemKind.PROMPT: + queue.append_prompt(content) + else: + queue.append_bash(content) + item = queue.pop_first() + assert item is not None + assert item.kind == kind + assert item.content == content diff --git a/tests/conftest.py b/tests/conftest.py index 2824333..2fd7530 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -77,6 +77,9 @@ def config_dir( config_file.write_text(tomli_w.dumps(get_base_config()), encoding="utf-8") monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", config_dir) + agents_dir = tmp_path / ".agents" + agents_dir.mkdir(parents=True, exist_ok=True) + monkeypatch.setattr("vibe.core.paths._agents_home._DEFAULT_AGENTS_HOME", agents_dir) # Re-evaluate PLAN agent overrides so the allowlist uses the monkeypatched path from vibe.core.agents.models import PLAN, _plan_overrides @@ -159,9 +162,7 @@ def _mock_update_commands(monkeypatch: pytest.MonkeyPatch) -> None: @pytest.fixture(autouse=True) def _disable_feedback_bar(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr( - "vibe.cli.textual_ui.widgets.feedback_bar_manager.FEEDBACK_PROBABILITY", 0 - ) + monkeypatch.setattr("vibe.core.feedback.FEEDBACK_PROBABILITY", 0) @pytest.fixture(autouse=True) diff --git a/tests/core/experiments/test_system_prompt_variant.py b/tests/core/experiments/test_system_prompt_variant.py index 93a0244..7db1582 100644 --- a/tests/core/experiments/test_system_prompt_variant.py +++ b/tests/core/experiments/test_system_prompt_variant.py @@ -1,5 +1,8 @@ from __future__ import annotations +from datetime import date +from string import Template + import pytest from tests.conftest import build_test_vibe_config @@ -14,6 +17,14 @@ from vibe.core.system_prompt import get_universal_system_prompt from vibe.core.tools.manager import ToolManager +def _expected_prompt(prompt_id: str) -> str: + today = date.today() + current_date = f"{today.isoformat()} ({today.strftime('%A')})" + return Template(load_system_prompt(prompt_id)).safe_substitute( + current_date=current_date + ) + + class _StubClient(RemoteEvalClient): def __init__(self, response: EvalResponse | None) -> None: self._response = response @@ -125,7 +136,7 @@ def test_system_prompt_honors_user_config_when_manager_uninitialized() -> None: prompt = get_universal_system_prompt( tool_manager, config, skill_manager, agent_manager, experiment_manager=manager ) - assert prompt == load_system_prompt("lean") + assert prompt == _expected_prompt("lean") @pytest.mark.asyncio @@ -151,7 +162,7 @@ async def test_system_prompt_honors_user_config_when_no_remote_assignment() -> N prompt = get_universal_system_prompt( tool_manager, config, skill_manager, agent_manager, experiment_manager=manager ) - assert prompt == load_system_prompt("lean") + assert prompt == _expected_prompt("lean") @pytest.mark.asyncio @@ -182,5 +193,5 @@ async def test_user_config_overrides_assigned_experiment_variant() -> None: prompt = get_universal_system_prompt( tool_manager, config, skill_manager, agent_manager, experiment_manager=manager ) - assert prompt == load_system_prompt("lean") - assert prompt != load_system_prompt("explore") + assert prompt == _expected_prompt("lean") + assert prompt != _expected_prompt("explore") diff --git a/tests/core/test_config_patch_ops.py b/tests/core/test_config_patch_ops.py index c0089d9..06b268e 100644 --- a/tests/core/test_config_patch_ops.py +++ b/tests/core/test_config_patch_ops.py @@ -13,6 +13,7 @@ from vibe.core.config import ( RemoveFromList, SetField, ) +from vibe.core.config.patch import ConfigPatch def test_patch_op_union_contains_all_operations() -> None: @@ -122,3 +123,135 @@ def test_scenario_mini_vibe_patch_operations() -> None: RemoveFromList("models.available_models", ("codestral-latest",)), DeleteField("tools.deprecated_setting"), ] + + +# --- ConfigPatch --- + + +def test_config_patch_stores_operations_and_metadata() -> None: + op = SetField("active_model", "devstral-small") + patch = ConfigPatch(op, fingerprint="fp-1", reason="test") + + assert patch.operations == [op] + assert patch.fingerprint == "fp-1" + assert patch.reason == "test" + + +def test_config_patch_defaults() -> None: + patch = ConfigPatch(fingerprint="fp-1") + + assert patch.reason == "" + assert patch.operations == [] + + +def test_config_patch_accepts_multiple_operations() -> None: + ops: list[PatchOp] = [ + SetField("active_model", "devstral-small"), + AppendToList("tools.disabled_tools", ("bash",)), + ] + patch = ConfigPatch(*ops, fingerprint="fp-1") + + assert patch.operations == ops + + +def test_config_patch_add_appends_operations() -> None: + patch = ConfigPatch(SetField("active_model", "devstral-small"), fingerprint="fp-1") + patch.add(DeleteField("tools.deprecated_setting")) + + assert patch.operations == [ + SetField("active_model", "devstral-small"), + DeleteField("tools.deprecated_setting"), + ] + + +def test_config_patch_add_returns_self() -> None: + patch = ConfigPatch(fingerprint="fp-1") + result = patch.add(SetField("active_model", "devstral-small")) + + assert result is patch + + +def test_config_patch_add_accepts_multiple_operations() -> None: + patch = ConfigPatch(fingerprint="fp-1") + patch.add( + SetField("active_model", "devstral-small"), + AppendToList("tools.disabled_tools", ("bash",)), + ) + + assert len(patch.operations) == 2 + + +def test_config_patch_add_is_chainable() -> None: + patch = ( + ConfigPatch(fingerprint="fp-1") + .add(SetField("active_model", "devstral-small")) + .add(DeleteField("tools.deprecated_setting")) + ) + + assert len(patch.operations) == 2 + + +def test_config_patch_describe_set_field() -> None: + patch = ConfigPatch(SetField("active_model", "devstral-small"), fingerprint="fp-1") + + assert patch.describe() == ["set 'active_model' = 'devstral-small'"] + + +def test_config_patch_describe_append_to_list() -> None: + patch = ConfigPatch( + AppendToList("tools.disabled_tools", ("bash", "python")), fingerprint="fp-1" + ) + + assert patch.describe() == ["append to 'tools.disabled_tools': ['bash', 'python']"] + + +def test_config_patch_describe_remove_from_list() -> None: + patch = ConfigPatch( + RemoveFromList("models.available_models", ("codestral-latest",)), + fingerprint="fp-1", + ) + + assert patch.describe() == [ + "remove from 'models.available_models': ['codestral-latest']" + ] + + +def test_config_patch_describe_delete_field() -> None: + patch = ConfigPatch(DeleteField("tools.deprecated_setting"), fingerprint="fp-1") + + assert patch.describe() == ["delete 'tools.deprecated_setting'"] + + +def test_config_patch_describe_empty_returns_empty_list() -> None: + patch = ConfigPatch(fingerprint="fp-1") + + assert patch.describe() == [] + + +def test_config_patch_describe_multiple_operations() -> None: + patch = ConfigPatch( + SetField("active_model", "devstral-small"), + AppendToList("tools.disabled_tools", ("bash",)), + DeleteField("tools.deprecated_setting"), + fingerprint="fp-1", + ) + + assert patch.describe() == [ + "set 'active_model' = 'devstral-small'", + "append to 'tools.disabled_tools': ['bash']", + "delete 'tools.deprecated_setting'", + ] + + +def test_scenario_build_patch_incrementally() -> None: + patch = ConfigPatch(fingerprint="fp-abc", reason="/model command") + patch.add(SetField("active_model", "devstral-small")) + patch.add(AppendToList("tools.disabled_tools", ("bash",))) + + assert patch.fingerprint == "fp-abc" + assert patch.reason == "/model command" + assert len(patch.operations) == 2 + assert patch.describe() == [ + "set 'active_model' = 'devstral-small'", + "append to 'tools.disabled_tools': ['bash']", + ] diff --git a/tests/core/test_config_paths.py b/tests/core/test_config_paths.py index 80a1201..42f2486 100644 --- a/tests/core/test_config_paths.py +++ b/tests/core/test_config_paths.py @@ -176,18 +176,14 @@ class TestUserToolsDirs: mgr = HarnessFilesManager(sources=("project",)) assert mgr.user_tools_dirs == [] - def test_returns_empty_when_dir_does_not_exist( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch - ) -> None: - monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path) + def test_returns_empty_when_dir_does_not_exist(self) -> None: mgr = HarnessFilesManager(sources=("user",)) assert mgr.user_tools_dirs == [] def test_returns_path_when_user_in_sources_and_dir_exists( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + self, config_dir: Path ) -> None: - monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path) - tools_dir = tmp_path / "tools" + tools_dir = config_dir / "tools" tools_dir.mkdir() mgr = HarnessFilesManager(sources=("user",)) assert mgr.user_tools_dirs == [tools_dir] @@ -198,40 +194,50 @@ class TestUserSkillsDirs: mgr = HarnessFilesManager(sources=("project",)) assert mgr.user_skills_dirs == [] - def test_returns_empty_when_dir_does_not_exist( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch - ) -> None: - monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path) + def test_returns_empty_when_dir_does_not_exist(self) -> None: mgr = HarnessFilesManager(sources=("user",)) assert mgr.user_skills_dirs == [] def test_returns_path_when_user_in_sources_and_dir_exists( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + self, config_dir: Path ) -> None: - monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path) - skills_dir = tmp_path / "skills" + skills_dir = config_dir / "skills" skills_dir.mkdir() mgr = HarnessFilesManager(sources=("user",)) assert mgr.user_skills_dirs == [skills_dir] + def test_returns_agents_skills_dir_when_only_it_exists( + self, config_dir: Path + ) -> None: + agents_dir = config_dir.parent / ".agents" + agents_skills = agents_dir / "skills" + agents_skills.mkdir(parents=True) + mgr = HarnessFilesManager(sources=("user",)) + assert mgr.user_skills_dirs == [agents_skills] + + def test_returns_both_skills_dirs_when_both_exist(self, config_dir: Path) -> None: + vibe_skills_dir = config_dir / "skills" + vibe_skills_dir.mkdir() + agents_dir = config_dir.parent / ".agents" + agents_skills = agents_dir / "skills" + agents_skills.mkdir(parents=True) + mgr = HarnessFilesManager(sources=("user",)) + assert mgr.user_skills_dirs == [vibe_skills_dir, agents_skills] + class TestUserAgentsDirs: def test_returns_empty_when_user_not_in_sources(self) -> None: mgr = HarnessFilesManager(sources=("project",)) assert mgr.user_agents_dirs == [] - def test_returns_empty_when_dir_does_not_exist( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch - ) -> None: - monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path) + def test_returns_empty_when_dir_does_not_exist(self) -> None: mgr = HarnessFilesManager(sources=("user",)) assert mgr.user_agents_dirs == [] def test_returns_path_when_user_in_sources_and_dir_exists( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + self, config_dir: Path ) -> None: - monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path) - agents_dir = tmp_path / "agents" + agents_dir = config_dir / "agents" agents_dir.mkdir() mgr = HarnessFilesManager(sources=("user",)) assert mgr.user_agents_dirs == [agents_dir] @@ -569,26 +575,19 @@ class TestLoadUserDoc: mgr = HarnessFilesManager(sources=("project",)) assert mgr.load_user_doc() == "" - def test_returns_empty_when_file_does_not_exist( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch - ) -> None: - monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path) + def test_returns_empty_when_file_does_not_exist(self) -> None: mgr = HarnessFilesManager(sources=("user",)) assert mgr.load_user_doc() == "" - def test_returns_file_content_when_file_exists( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch - ) -> None: - monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path) - (tmp_path / "AGENTS.md").write_text("# User instructions", encoding="utf-8") + def test_returns_file_content_when_file_exists(self, config_dir: Path) -> None: + (config_dir / "AGENTS.md").write_text("# User instructions", encoding="utf-8") mgr = HarnessFilesManager(sources=("user",)) assert mgr.load_user_doc() == "# User instructions" def test_returns_empty_string_for_whitespace_only_file( - self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + self, config_dir: Path ) -> None: # load_user_doc strips — consistent with _collect_agents_md - monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path) - (tmp_path / "AGENTS.md").write_text(" \n ", encoding="utf-8") + (config_dir / "AGENTS.md").write_text(" \n ", encoding="utf-8") mgr = HarnessFilesManager(sources=("user",)) assert mgr.load_user_doc() == "" diff --git a/tests/core/test_teleport_experimental_nuage.py b/tests/core/test_teleport_experimental_nuage.py new file mode 100644 index 0000000..3559ffb --- /dev/null +++ b/tests/core/test_teleport_experimental_nuage.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +import json + +import httpx +import pytest + +from vibe.core.teleport.errors import ServiceTeleportError +from vibe.core.teleport.experimental_nuage import ( + ExperimentalNuageClient, + ExperimentalNuageContext, + ExperimentalNuageMessage, + ExperimentalNuageRepository, + ExperimentalNuageRequest, + ExperimentalNuageTextPart, +) + + +def _request() -> ExperimentalNuageRequest: + return ExperimentalNuageRequest( + idempotency_key="idem-1", + message=ExperimentalNuageMessage( + parts=[ExperimentalNuageTextPart(text="continue from here")] + ), + context=ExperimentalNuageContext( + repositories=[ + ExperimentalNuageRepository( + repo_url="https://github.com/owner/repo", branch="main" + ) + ] + ), + ) + + +@pytest.mark.asyncio +async def test_start_posts_experimental_nuage_request() -> None: + seen_request: httpx.Request | None = None + + async def handler(request: httpx.Request) -> httpx.Response: + nonlocal seen_request + seen_request = request + return httpx.Response( + 200, + json={ + "sessionId": "controller-session-id", + "webSessionId": "web-session-id", + "projectId": "project-id", + "status": "running", + "url": "https://chat.example.com/code/project-id/web-session-id", + }, + ) + + async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + experimental_nuage = ExperimentalNuageClient( + "https://chat.example.com/", "api-key", client=client + ) + response = await experimental_nuage.start(_request()) + + assert seen_request is not None + assert str(seen_request.url) == "https://chat.example.com/api/v1/code/sessions" + assert seen_request.headers["authorization"] == "Bearer api-key" + assert seen_request.headers["content-type"] == "application/json" + assert json.loads(seen_request.content) == { + "project_name": "Vibe CLI", + "source": "vibe_code_cli", + "idempotencyKey": "idem-1", + "message": { + "role": "user", + "parts": [{"type": "text", "text": "continue from here"}], + }, + "context": { + "repositories": [ + {"repoUrl": "https://github.com/owner/repo", "branch": "main"} + ] + }, + } + assert response.nuage_session_id == "controller-session-id" + assert response.nuage_web_session_id == "web-session-id" + assert response.nuage_project_id == "project-id" + assert response.status == "running" + assert response.url == "https://chat.example.com/code/project-id/web-session-id" + + +@pytest.mark.asyncio +async def test_start_omits_empty_branch() -> 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": "controller-session-id", + "webSessionId": "web-session-id", + "projectId": "project-id", + "status": "running", + "url": "https://chat.example.com/code/project-id/web-session-id", + }, + ) + + request = ExperimentalNuageRequest( + idempotency_key="idem-1", + message=ExperimentalNuageMessage( + parts=[ExperimentalNuageTextPart(text="prompt")] + ), + context=ExperimentalNuageContext( + repositories=[ + ExperimentalNuageRepository(repo_url="https://github.com/owner/repo") + ] + ), + ) + async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + experimental_nuage = ExperimentalNuageClient( + "https://chat.example.com", "api-key", client=client + ) + await experimental_nuage.start(request) + + assert seen_body == { + "project_name": "Vibe CLI", + "source": "vibe_code_cli", + "idempotencyKey": "idem-1", + "message": {"role": "user", "parts": [{"type": "text", "text": "prompt"}]}, + "context": {"repositories": [{"repoUrl": "https://github.com/owner/repo"}]}, + } + + +@pytest.mark.asyncio +async def test_start_raises_for_unsuccessful_response() -> None: + async def handler(request: httpx.Request) -> httpx.Response: + return httpx.Response(401, text="Unauthorized") + + async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + experimental_nuage = ExperimentalNuageClient( + "https://chat.example.com", "api-key", client=client + ) + with pytest.raises(ServiceTeleportError, match="Nuage start"): + await experimental_nuage.start(_request()) + + +@pytest.mark.asyncio +async def test_start_raises_for_invalid_response() -> None: + async def handler(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, json={"url": "https://chat.example.com/code/1/2"}) + + async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + experimental_nuage = ExperimentalNuageClient( + "https://chat.example.com", "api-key", client=client + ) + with pytest.raises(ServiceTeleportError, match="response was invalid"): + await experimental_nuage.start(_request()) diff --git a/tests/core/test_teleport_service.py b/tests/core/test_teleport_service.py index fe3f2ff..52752ea 100644 --- a/tests/core/test_teleport_service.py +++ b/tests/core/test_teleport_service.py @@ -2,15 +2,18 @@ from __future__ import annotations import base64 import importlib +import json import os from pathlib import Path import sys from typing import Any from unittest.mock import AsyncMock, MagicMock, patch +import httpx import pytest import zstandard +from vibe.core.config import VibeConfig from vibe.core.teleport.errors import ( ServiceTeleportError, ServiceTeleportNotSupportedError, @@ -455,6 +458,225 @@ class TestTeleportServiceExecute: call_args = mock_nuage.start_workflow.call_args assert "teleported" in call_args[0][0].prompt.lower() + @pytest.mark.asyncio + async def test_execute_uses_experimental_nuage_when_enabled( + self, tmp_path: Path + ) -> None: + seen_body: dict[str, object] | None = None + seen_url: str | None = None + + async def handler(request: httpx.Request) -> httpx.Response: + nonlocal seen_body, seen_url + seen_url = str(request.url) + seen_body = json.loads(request.content) + return httpx.Response( + 200, + json={ + "sessionId": "controller-session-id", + "webSessionId": "web-session-id", + "projectId": "project-id", + "status": "running", + "url": "https://chat.example.com/code/project-id/web-session-id", + }, + ) + + config = VibeConfig(vibe_code_experimental_nuage_enabled=True) + async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + service = TeleportService( + session_logger=MagicMock(), + vibe_code_base_url="https://chat.example.com", + vibe_code_workflow_id="workflow-id", + vibe_code_api_key="api-key", + workdir=tmp_path, + vibe_config=config, + client=client, + ) + service._git.fetch = AsyncMock() + service._git.get_info = AsyncMock( + return_value=GitRepoInfo( + remote_url="https://github.com/owner/repo", + owner="owner", + repo="repo", + branch="main", + commit="abc123", + diff="local diff is intentionally not sent", + ) + ) + service._git.is_commit_pushed = AsyncMock(return_value=True) + service._git.is_branch_pushed = AsyncMock(return_value=True) + + events = [ + event + async for event in service.execute("test prompt", TeleportSession()) + ] + + assert isinstance(events[0], TeleportCheckingGitEvent) + assert isinstance(events[1], TeleportStartingWorkflowEvent) + assert isinstance(events[2], TeleportCompleteEvent) + assert ( + events[2].url == "https://chat.example.com/code/project-id/web-session-id" + ) + assert service._nuage_client_instance is None + assert seen_url == "https://chat.example.com/api/v1/code/sessions" + assert seen_body is not None + assert seen_body["message"] == { + "role": "user", + "parts": [{"type": "text", "text": "test prompt"}], + } + assert seen_body["context"] == { + "repositories": [ + {"repoUrl": "https://github.com/owner/repo", "branch": "main"} + ] + } + assert "idempotencyKey" in seen_body + + @pytest.mark.asyncio + async def test_execute_experimental_nuage_uses_last_message_when_prompt_missing( + self, tmp_path: Path + ) -> 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": "controller-session-id", + "webSessionId": "web-session-id", + "projectId": "project-id", + "status": "running", + "url": "https://chat.example.com/code/project-id/web-session-id", + }, + ) + + config = VibeConfig(vibe_code_experimental_nuage_enabled=True) + async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + service = TeleportService( + session_logger=MagicMock(), + vibe_code_base_url="https://api.example.com", + vibe_code_workflow_id="workflow-id", + vibe_code_api_key="api-key", + workdir=tmp_path, + vibe_config=config, + client=client, + ) + service._git.fetch = AsyncMock() + service._git.get_info = AsyncMock( + return_value=GitRepoInfo( + remote_url="https://github.com/owner/repo", + owner="owner", + repo="repo", + branch="main", + commit="abc123", + diff="", + ) + ) + service._git.is_commit_pushed = AsyncMock(return_value=True) + service._git.is_branch_pushed = AsyncMock(return_value=True) + + session = TeleportSession( + messages=[{"role": "user", "content": "help me refactor"}] + ) + events = [event async for event in service.execute(None, session)] + + assert isinstance(events[-1], TeleportCompleteEvent) + assert seen_body is not None + assert seen_body["message"] == { + "role": "user", + "parts": [{"type": "text", "text": "help me refactor (continue)"}], + } + + @pytest.mark.asyncio + async def test_execute_experimental_nuage_requires_branch( + self, tmp_path: Path + ) -> None: + config = VibeConfig(vibe_code_experimental_nuage_enabled=True) + service = TeleportService( + session_logger=MagicMock(), + vibe_code_base_url="https://api.example.com", + vibe_code_workflow_id="workflow-id", + vibe_code_api_key="api-key", + workdir=tmp_path, + vibe_config=config, + ) + service._git.fetch = AsyncMock() + service._git.get_info = AsyncMock( + return_value=GitRepoInfo( + remote_url="https://github.com/owner/repo", + owner="owner", + repo="repo", + branch=None, + commit="abc123", + diff="", + ) + ) + + with pytest.raises(ServiceTeleportError, match="checked-out branch"): + async for _ in service.execute("test prompt", TeleportSession()): + pass + + service._git.fetch.assert_not_awaited() + + @pytest.mark.asyncio + async def test_execute_experimental_nuage_keeps_push_confirmation( + self, tmp_path: Path + ) -> None: + async def handler(request: httpx.Request) -> httpx.Response: + return httpx.Response( + 200, + json={ + "sessionId": "controller-session-id", + "webSessionId": "web-session-id", + "projectId": "project-id", + "status": "running", + "url": "https://chat.example.com/code/project-id/web-session-id", + }, + ) + + config = VibeConfig(vibe_code_experimental_nuage_enabled=True) + async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + service = TeleportService( + session_logger=MagicMock(), + vibe_code_base_url="https://api.example.com", + vibe_code_workflow_id="workflow-id", + vibe_code_api_key="api-key", + workdir=tmp_path, + vibe_config=config, + client=client, + ) + service._git.fetch = AsyncMock() + service._git.get_info = AsyncMock( + return_value=GitRepoInfo( + remote_url="https://github.com/owner/repo", + owner="owner", + repo="repo", + branch="main", + commit="abc123", + diff="", + ) + ) + service._git.is_commit_pushed = AsyncMock(return_value=False) + service._git.is_branch_pushed = AsyncMock(return_value=False) + service._git.get_unpushed_commit_count = AsyncMock(return_value=2) + service._git.push_current_branch = AsyncMock(return_value=True) + + gen = service.execute("test prompt", TeleportSession()) + assert isinstance(await gen.asend(None), TeleportCheckingGitEvent) + push_event = await gen.asend(None) + assert isinstance(push_event, TeleportPushRequiredEvent) + assert push_event.unpushed_count == 2 + assert push_event.branch_not_pushed is True + assert isinstance( + await gen.asend(TeleportPushResponseEvent(approved=True)), + TeleportPushingEvent, + ) + events = [event async for event in gen] + + service._git.push_current_branch.assert_awaited_once() + assert isinstance(events[0], TeleportStartingWorkflowEvent) + assert isinstance(events[1], TeleportCompleteEvent) + class TestTeleportServiceContextManager: @pytest.mark.asyncio diff --git a/tests/onboarding/test_ui_onboarding.py b/tests/onboarding/test_ui_onboarding.py index e46b327..9246f48 100644 --- a/tests/onboarding/test_ui_onboarding.py +++ b/tests/onboarding/test_ui_onboarding.py @@ -3,14 +3,20 @@ from __future__ import annotations import asyncio from collections.abc import Callable from pathlib import Path +import tomllib from typing import cast import pytest +from textual.events import Resize +from textual.geometry import Size from textual.pilot import Pilot +from textual.screen import Screen +from textual.widget import Widget from textual.widgets import Input from tests.browser_sign_in.stubs import build_browser_sign_in_service_factory from tests.conftest import build_test_vibe_config +from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig from vibe.core.config._settings import ( DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL, @@ -20,7 +26,7 @@ from vibe.core.config.harness_files import ( init_harness_files_manager, reset_harness_files_manager, ) -from vibe.core.paths import GLOBAL_ENV_FILE +from vibe.core.paths import GLOBAL_ENV_FILE, VIBE_HOME from vibe.core.telemetry.build_metadata import build_entrypoint_metadata from vibe.core.telemetry.send import TelemetryClient from vibe.core.types import Backend @@ -33,14 +39,11 @@ from vibe.setup.auth import ( from vibe.setup.auth.api_key_persistence import persist_api_key import vibe.setup.onboarding as onboarding_module from vibe.setup.onboarding import OnboardingApp +from vibe.setup.onboarding.context import OnboardingContext from vibe.setup.onboarding.screens.api_key import ApiKeyScreen from vibe.setup.onboarding.screens.auth_method import AuthMethodScreen -from vibe.setup.onboarding.screens.browser_sign_in import ( - ERROR_HINT, - PENDING_HINT, - UNEXPECTED_ERROR_MESSAGE, - BrowserSignInScreen, -) +from vibe.setup.onboarding.screens.browser_sign_in import BrowserSignInScreen +from vibe.setup.onboarding.screens.theme_selection import THEMES, ThemeSelectionScreen CONSOLE_URL = "https://console.mistral.ai" BROWSER_AUTH_API_URL = "https://console.mistral.ai/api" @@ -90,7 +93,9 @@ def _build_onboarding_config( def _build_browser_onboarding_app( - *, browser_sign_in_service_factory: Callable[[], BrowserSignInService] | None = None + *, + browser_sign_in_service_factory: Callable[[], BrowserSignInService] | None = None, + browser_sign_in_success_delay: float = 0, ) -> OnboardingApp: return OnboardingApp( config=_build_onboarding_config( @@ -99,6 +104,7 @@ def _build_browser_onboarding_app( 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, ) @@ -130,6 +136,30 @@ def _saved_env_contents() -> str: return GLOBAL_ENV_FILE.path.read_text(encoding="utf-8") +def _browser_sign_in_step_cards(screen: Screen) -> list[Widget]: + return list(screen.query(".browser-sign-in-step")) + + +def _browser_sign_in_step_card(screen: Screen, index: int) -> Widget: + return _browser_sign_in_step_cards(screen)[index] + + +def _active_browser_sign_in_step_card(screen: Screen) -> Widget: + active_cards = [ + card for card in _browser_sign_in_step_cards(screen) if card.has_class("active") + ] + if len(active_cards) != 1: + msg = "Expected exactly one active browser sign-in step." + raise AssertionError(msg) + return active_cards[0] + + +def _browser_sign_in_step_text(card: Widget) -> str: + title = card.query_one(".browser-sign-in-step-title", NoMarkupStatic) + detail = card.query_one(".browser-sign-in-step-detail", NoMarkupStatic) + return f"{title.render()}\n{detail.render()}" + + def _build_unexpected_browser_sign_in_service_factory( outcomes: list[str], *, @@ -193,10 +223,16 @@ async def _pass_welcome_screen(pilot: Pilot) -> None: lambda: not welcome_screen.query_one("#enter-hint").has_class("hidden"), pilot ) await pilot.press("enter") + await _wait_for(lambda: isinstance(pilot.app.screen, ThemeSelectionScreen), pilot) + + +async def _pass_theme_selection_screen(pilot: Pilot) -> None: + await pilot.press("enter") async def _show_auth_method(pilot: Pilot) -> None: await _pass_welcome_screen(pilot) + await _pass_theme_selection_screen(pilot) await _wait_for(lambda: isinstance(pilot.app.screen, AuthMethodScreen), pilot) @@ -219,6 +255,7 @@ async def test_ui_keeps_manual_flow_when_browser_sign_in_is_unsupported() -> Non 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) input_widget = app.screen.query_one("#key", Input) await pilot.press(*api_key_value) @@ -247,6 +284,7 @@ async def test_ui_hides_browser_sign_in_when_experimental_flag_is_disabled() -> 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) @@ -307,6 +345,54 @@ async def test_ui_allows_manual_path_when_browser_sign_in_is_supported() -> None assert api_key_value in _saved_env_contents() +@pytest.mark.asyncio +async def test_ui_does_not_show_browser_opened_before_attempt_starts() -> None: + authenticate_started = asyncio.Event() + finish_authenticate = asyncio.Event() + keep_authenticate_running = asyncio.Event() + + class DelayedBrowserSignInService: + async def authenticate( + self, status_callback: Callable[[BrowserSignInStatus], None] | None = None + ) -> str: + authenticate_started.set() + await finish_authenticate.wait() + if status_callback is not None: + status_callback(BrowserSignInStatus.OPENING_BROWSER) + await keep_authenticate_running.wait() + return "sk-never-reached" + + async def aclose(self) -> None: + return None + + app = _build_browser_onboarding_app( + browser_sign_in_service_factory=lambda: cast( + BrowserSignInService, DelayedBrowserSignInService() + ) + ) + + async with app.run_test() as pilot: + await _show_browser_sign_in(pilot) + await _wait_for(authenticate_started.is_set, pilot) + + active_step = _active_browser_sign_in_step_card(app.screen) + active_step_text = _browser_sign_in_step_text(active_step) + assert "Open browser" in active_step_text + assert "Getting things ready..." in active_step_text + assert "Browser opened" not in active_step_text + + finish_authenticate.set() + await _wait_for( + lambda: ( + "Opening your browser..." + in _browser_sign_in_step_text( + _active_browser_sign_in_step_card(app.screen) + ) + ), + pilot, + ) + + @pytest.mark.asyncio async def test_ui_completes_browser_sign_in_and_retries_after_failure() -> None: gateway, browser_sign_in_service_factory, created_services = ( @@ -321,7 +407,9 @@ async def test_ui_completes_browser_sign_in_and_retries_after_failure() -> None: await _wait_for( lambda: ( "expired" - in str(app.screen.query_one("#browser-sign-in-status").render()) + in _browser_sign_in_step_text( + _active_browser_sign_in_step_card(app.screen) + ) ), pilot, ) @@ -335,6 +423,73 @@ async def test_ui_completes_browser_sign_in_and_retries_after_failure() -> None: assert "sk-browser-onboarding-test-key" in _saved_env_contents() +@pytest.mark.asyncio +async def test_ui_preserves_completed_browser_sign_in_during_success_delay() -> None: + _, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory( + outcomes=["completed"] + ) + app = _build_browser_onboarding_app( + browser_sign_in_service_factory=browser_sign_in_service_factory, + browser_sign_in_success_delay=0.5, + ) + + async with app.run_test() as pilot: + await _show_browser_sign_in(pilot) + await _wait_for( + lambda: ( + "Sign-in complete" + in _browser_sign_in_step_text( + _active_browser_sign_in_step_card(app.screen) + ) + ), + pilot, + ) + assert isinstance(app.screen, BrowserSignInScreen) + assert app.screen.state.variant == "success" + hint = str(app.screen.query_one("#browser-sign-in-hint").render()) + assert "Finishing setup..." in hint + assert "Press M to enter API key manually - Esc to cancel" not in hint + assert app.return_value is None + assert "sk-browser-onboarding-test-key" in _saved_env_contents() + await pilot.press("m", "escape") + assert isinstance(app.screen, BrowserSignInScreen) + assert app.return_value is None + await _wait_for(lambda: app.return_value is not None, pilot, timeout=2.0) + + assert app.return_value == "completed" + assert "sk-browser-onboarding-test-key" in _saved_env_contents() + + +@pytest.mark.asyncio +async def test_ui_skips_success_delay_when_browser_api_key_cannot_be_persisted() -> ( + None +): + _, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory( + outcomes=["completed"] + ) + provider = ProviderConfig( + name="mistral", + api_base="https://api.mistral.ai/v1", + api_key_env_var="BAD=NAME", + browser_auth_base_url=CONSOLE_URL, + browser_auth_api_base_url=BROWSER_AUTH_API_URL, + backend=Backend.MISTRAL, + ) + app = OnboardingApp( + config=OnboardingContext( + provider=provider, enable_experimental_browser_sign_in=True + ), + browser_sign_in_service_factory=browser_sign_in_service_factory, + browser_sign_in_success_delay=2.0, + ) + + async with app.run_test() as pilot: + await _show_browser_sign_in(pilot) + await _wait_for(lambda: app.return_value is not None, pilot, timeout=0.5) + + assert app.return_value == "env_var_error:BAD=NAME" + + @pytest.mark.asyncio async def test_ui_browser_sign_in_falls_back_to_mistral_env_var_when_missing() -> None: _, browser_sign_in_service_factory, _ = build_browser_sign_in_service_factory( @@ -375,7 +530,9 @@ async def test_ui_shows_human_message_when_polling_fails() -> None: await _wait_for( lambda: ( "We couldn't complete sign-in. Please try again." - in str(app.screen.query_one("#browser-sign-in-status").render()) + in _browser_sign_in_step_text( + _active_browser_sign_in_step_card(app.screen) + ) ), pilot, ) @@ -395,17 +552,21 @@ async def test_ui_shows_retryable_error_when_browser_sign_in_fails_unexpectedly( await _show_browser_sign_in(pilot) await _wait_for( lambda: ( - UNEXPECTED_ERROR_MESSAGE - in str(app.screen.query_one("#browser-sign-in-status").render()) + "Something went wrong during browser sign-in. Please try again." + in _browser_sign_in_step_text( + _active_browser_sign_in_step_card(app.screen) + ) ), pilot, ) assert isinstance(app.screen, BrowserSignInScreen) - status_widget = app.screen.query_one("#browser-sign-in-status") - assert status_widget.has_class("error") - assert not status_widget.has_class("pending") - assert ERROR_HINT in str(app.screen.query_one("#browser-sign-in-hint").render()) + assert app.screen.state.variant == "error" + assert _active_browser_sign_in_step_card(app.screen).has_class("active") + assert ( + "Press R to retry - Press M to enter API key manually - Esc to cancel" + in str(app.screen.query_one("#browser-sign-in-hint").render()) + ) assert app.return_value is None @@ -422,8 +583,10 @@ async def test_ui_retries_after_unexpected_browser_sign_in_failure() -> None: await _show_browser_sign_in(pilot) await _wait_for( lambda: ( - UNEXPECTED_ERROR_MESSAGE - in str(app.screen.query_one("#browser-sign-in-status").render()) + "Something went wrong during browser sign-in. Please try again." + in _browser_sign_in_step_text( + _active_browser_sign_in_step_card(app.screen) + ) ), pilot, ) @@ -450,25 +613,50 @@ async def test_ui_waits_for_browser_sign_in_cleanup_before_retrying() -> None: await _show_browser_sign_in(pilot) await _wait_for(close_started.is_set, pilot) - status_widget = app.screen.query_one("#browser-sign-in-status") hint_widget = app.screen.query_one("#browser-sign-in-hint") await _wait_for( - lambda: "Getting things ready..." in str(status_widget.render()), pilot + lambda: ( + "Getting things ready..." + in _browser_sign_in_step_text( + _active_browser_sign_in_step_card(app.screen) + ) + ), + pilot, + ) + await _wait_for( + lambda: ( + "Press M to enter API key manually - Esc to cancel" + in str(hint_widget.render()) + ), + pilot, ) - await _wait_for(lambda: PENDING_HINT in str(hint_widget.render()), pilot) await pilot.press("r") await _wait_for( - lambda: "Getting things ready..." in str(status_widget.render()), pilot + lambda: ( + "Getting things ready..." + in _browser_sign_in_step_text( + _active_browser_sign_in_step_card(app.screen) + ) + ), + pilot, + ) + await _wait_for( + lambda: ( + "Press M to enter API key manually - Esc to cancel" + in str(hint_widget.render()) + ), + pilot, ) - await _wait_for(lambda: PENDING_HINT in str(hint_widget.render()), pilot) assert app.return_value is None close_blocker.set() await _wait_for( lambda: ( - UNEXPECTED_ERROR_MESSAGE - in str(app.screen.query_one("#browser-sign-in-status").render()) + "Something went wrong during browser sign-in. Please try again." + in _browser_sign_in_step_text( + _active_browser_sign_in_step_card(app.screen) + ) ), pilot, ) @@ -530,22 +718,27 @@ async def test_ui_switches_to_manual_path_while_browser_sign_in_is_running() -> await _show_browser_sign_in(pilot) await _wait_for( lambda: ( - "Waiting for you to finish signing in..." - in str(app.screen.query_one("#browser-sign-in-status").render()) + "Waiting for authentication..." + in _browser_sign_in_step_text( + _active_browser_sign_in_step_card(app.screen) + ) ), pilot, ) - status_widget = app.screen.query_one("#browser-sign-in-status") - assert status_widget.has_class("pending") - assert not status_widget.has_class("error") - step_widgets = list(app.screen.query(".browser-sign-in-step")) - assert len(step_widgets) == 3 - assert step_widgets[0].has_class("done") - assert "Open your browser" in str(step_widgets[0].render()) - assert step_widgets[1].has_class("active") - assert "Sign in and return here" in str(step_widgets[1].render()) - assert step_widgets[2].has_class("idle") - assert "Finish setup" in str(step_widgets[2].render()) + assert isinstance(app.screen, BrowserSignInScreen) + assert app.screen.state.variant == "pending" + step_cards = _browser_sign_in_step_cards(app.screen) + assert len(step_cards) == 3 + assert step_cards[0].has_class("done") + assert "Open browser" in _browser_sign_in_step_text(step_cards[0]) + assert "Browser opened" in _browser_sign_in_step_text(step_cards[0]) + assert step_cards[1].has_class("active") + assert "Complete sign-in" in _browser_sign_in_step_text(step_cards[1]) + assert "Waiting for authentication..." in _browser_sign_in_step_text( + step_cards[1] + ) + assert step_cards[2].has_class("idle") + assert "Finished setup" in _browser_sign_in_step_text(step_cards[2]) await pilot.press("m") await _wait_for(lambda: isinstance(pilot.app.screen, ApiKeyScreen), pilot) await pilot.press(*api_key_value) @@ -621,6 +814,7 @@ async def test_ui_keeps_browser_sign_in_disabled_from_false_env_var( 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) @@ -706,6 +900,37 @@ async def test_ui_falls_back_to_default_onboarding_context_with_invalid_active_m await _show_auth_method(pilot) +@pytest.mark.asyncio +async def test_ui_can_pick_a_theme_and_saves_selection() -> None: + app = OnboardingApp() + + async with app.run_test() as pilot: + await _pass_welcome_screen(pilot) + + theme_screen = app.screen + assert isinstance(theme_screen, ThemeSelectionScreen) + app.post_message(Resize(Size(40, 10), Size(40, 10))) + preview = theme_screen.query_one("#preview") + assert preview.styles.max_height is not None + + target_theme = "gruvbox" + assert target_theme in THEMES + start_index = THEMES.index(app.theme) + target_index = THEMES.index(target_theme) + steps_down = (target_index - start_index) % len(THEMES) + await pilot.press(*["down"] * steps_down) + assert app.theme == target_theme + + await pilot.press("enter") + await _wait_for(lambda: isinstance(app.screen, ApiKeyScreen), pilot) + + config_path = VIBE_HOME.path / "config.toml" + assert config_path.is_file() + config_contents = config_path.read_text(encoding="utf-8") + config_dict = tomllib.loads(config_contents) + assert config_dict.get("theme") == target_theme + + def test_api_key_screen_falls_back_to_mistral_for_provider_without_env_key() -> None: screen = ApiKeyScreen( provider=ProviderConfig( 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 8f1f6d2..cf8eb44 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 @@ -35,7 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #868887 } @@ -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 1a8f27f..3ac35fc 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 @@ -35,7 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #868887 } @@ -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 1da5a37..76ab5f1 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 @@ -36,7 +36,8 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -185,17 +186,17 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆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 0c42f10..f11becd 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 @@ -35,13 +35,14 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99;font-style: italic; } +.terminal-r4 { fill: #868887;font-style: italic; } .terminal-r5 { fill: #98a84b } .terminal-r6 { fill: #d2d2d2 } -.terminal-r7 { fill: #292929 } -.terminal-r8 { fill: #4b4e55 } -.terminal-r9 { fill: #98a84b;font-weight: bold } -.terminal-r10 { fill: #9a9b99 } +.terminal-r7 { fill: #4b4e55 } +.terminal-r8 { fill: #292929 } +.terminal-r9 { fill: #608ab1 } +.terminal-r10 { fill: #98a84b;font-weight: bold } +.terminal-r11 { fill: #868887 } @@ -163,7 +164,7 @@ - + @@ -180,27 +181,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 -Here's a very long print instruction: - -(): -ery long line (Lorem Ipsum) -m ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna  -▋            - -The print statement includes a very long line of Lorem Ipsum text to demonstrate a lengthy output. + +Here's a very long print instruction: + +(): +ery long line (Lorem Ipsum) +m ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna  + +The print statement includes a very long line of Lorem Ipsum text to demonstrate a lengthy output. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── 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 acf2b79..f805425 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 @@ -35,7 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #868887 } @@ -185,15 +185,15 @@  ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Configuration opened... -Configuration closed (no changes saved). +Configuration opened... +Configuration closed (no changes saved). -┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐ +> + + +└──────────────────────────────────────────────────────────────────────────────────────────────────┘ /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_initial.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_initial.svg index 0088172..1f296f1 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_initial.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_initial.svg @@ -35,8 +35,8 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } -.terminal-r5 { fill: #608ab1;font-weight: bold } +.terminal-r4 { fill: #608ab1;font-weight: bold } +.terminal-r5 { fill: #4b4e55;font-weight: bold } .terminal-r6 { fill: #c5c8c6;font-weight: bold } .terminal-r7 { fill: #98a84b;font-weight: bold } .terminal-r8 { fill: #868887 } @@ -161,7 +161,7 @@ - + @@ -185,20 +185,20 @@  ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Configuration opened... +Configuration opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ -Settings - -Model: devstral-latest -Thinking: Off -Auto-copy: On -Autocomplete watcher (may delay first autocompletion): Off - -↑↓ Navigate  Enter Select/Toggle  Esc Exit -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ +Settings + +Model: devstral-latest +Thinking: Off +Auto-copy: On +Autocomplete watcher (may delay first autocompletion): Off + +↑↓ Navigate  Enter Select/Toggle  Esc Exit +└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_navigate_down.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_navigate_down.svg index ef4e936..0d2b619 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_navigate_down.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_navigate_down.svg @@ -35,9 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } -.terminal-r5 { fill: #608ab1;font-weight: bold } -.terminal-r6 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #608ab1;font-weight: bold } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #4b4e55;font-weight: bold } .terminal-r7 { fill: #98a84b;font-weight: bold } .terminal-r8 { fill: #868887 } @@ -161,7 +161,7 @@ - + @@ -185,20 +185,20 @@  ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Configuration opened... +Configuration opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ -Settings - -Model: devstral-latest -Thinking: Off -Auto-copy: On -Autocomplete watcher (may delay first autocompletion): Off - -↑↓ Navigate  Enter Select/Toggle  Esc Exit -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ +Settings + +Model: devstral-latest +Thinking: Off +Auto-copy: On +Autocomplete watcher (may delay first autocompletion): Off + +↑↓ Navigate  Enter Select/Toggle  Esc Exit +└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_toggle_autocopy.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_toggle_autocopy.svg index eac385f..d96320e 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_toggle_autocopy.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_config_app/test_snapshot_config_toggle_autocopy.svg @@ -35,10 +35,10 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } -.terminal-r5 { fill: #608ab1;font-weight: bold } -.terminal-r6 { fill: #c5c8c6;font-weight: bold } -.terminal-r7 { fill: #9cafbd;font-weight: bold } +.terminal-r4 { fill: #608ab1;font-weight: bold } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #4b4e55;font-weight: bold } +.terminal-r7 { fill: #7b7e82;font-weight: bold } .terminal-r8 { fill: #868887 } @@ -161,7 +161,7 @@ - + @@ -185,20 +185,20 @@  ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Configuration opened... +Configuration opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ -Settings - -Model: devstral-latest -Thinking: Off -Auto-copy: Off -Autocomplete watcher (may delay first autocompletion): Off - -↑↓ Navigate  Enter Select/Toggle  Esc Exit -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ +Settings + +Model: devstral-latest +Thinking: Off +Auto-copy: Off +Autocomplete watcher (may delay first autocompletion): Off + +↑↓ Navigate  Enter Select/Toggle  Esc Exit +└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 a670b4e..059d47b 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 @@ -35,9 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #1984e9;text-decoration: underline; } .terminal-r5 { fill: #608ab1;text-decoration: underline; } -.terminal-r6 { fill: #c5c8c6;text-decoration: underline; } +.terminal-r6 { fill: #868887 } @@ -183,20 +183,20 @@  ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Your Data Helps Improve Mistral AI -At Mistral AI, we're committed to delivering the best possible experience. When you use Mistral -models on our API, your interactions may be collected to improve our models, ensuring they stay -cutting-edge, accurate, and helpful. - -Manage your data settings here +Your Data Helps Improve Mistral AI +At Mistral AI, we're committed to delivering the best possible experience. When you use Mistral +models on our API, your interactions may be collected to improve our models, ensuring they stay +cutting-edge, accurate, and helpful. + +Manage your data settings here -┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────── 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 be678bb..dbecc6e 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 @@ -35,7 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #868887 } @@ -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 0f266a9..01a2032 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 @@ -33,13 +33,12 @@ } .terminal-r1 { fill: #c5c8c6 } -.terminal-r2 { fill: #9a9b99 } -.terminal-r3 { fill: #868887 } -.terminal-r4 { fill: #68a0b3 } -.terminal-r5 { fill: #d0b344 } -.terminal-r6 { fill: #cc555a } -.terminal-r7 { fill: #cc555a;font-weight: bold } -.terminal-r8 { fill: #ff8205;font-weight: bold } +.terminal-r2 { fill: #868887 } +.terminal-r3 { fill: #68a0b3 } +.terminal-r4 { fill: #d0b344 } +.terminal-r5 { fill: #cc555a } +.terminal-r6 { fill: #cc555a;font-weight: bold } +.terminal-r7 { fill: #ff8205;font-weight: bold } @@ -163,42 +162,42 @@ - Debug Console  (ctrl+\ to close) - -2026-02-21 10:28:51DEBUG    Initializing     -model registry -2026-02-21 10:28:51INFO     Server started   -on port 8080 -2026-02-21 10:28:51INFO     Loading          -configuration from /etc/vibe/config.yaml -2026-02-21 10:28:51WARNING  Cache miss for   -key user:42 -2026-02-21 10:28:51DEBUG    Processing       -request GET /api/v1/models -2026-02-21 10:28:51INFO     Request          -completed in 45ms -2026-02-21 10:28:51ERROR    Connection       -refused to upstream service -2026-02-21 10:28:51INFO     Retrying         -connection attempt 1/3 -2026-02-21 10:28:51WARNING  Rate limit       -approaching for client api-key-abc -2026-02-21 10:28:52INFO     Health check     -passed -2026-02-21 10:28:53CRITICAL Out of memory    -error detected -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information - - - -┌──────────────────────────────────────────────────────────── default ─┐│ ->││ -││ -││ -└──────────────────────────────────────────────────────────────────────┘│ -/test/workdir0% of 200k tokens + Debug Console  (ctrl+\ to close) + +2026-02-21 10:28:51DEBUG    Initializing     +model registry +2026-02-21 10:28:51INFO     Server started   +on port 8080 +2026-02-21 10:28:51INFO     Loading          +configuration from /etc/vibe/config.yaml +2026-02-21 10:28:51WARNING  Cache miss for   +key user:42 +2026-02-21 10:28:51DEBUG    Processing       +request GET /api/v1/models +2026-02-21 10:28:51INFO     Request          +completed in 45ms +2026-02-21 10:28:51ERROR    Connection       +refused to upstream service +2026-02-21 10:28:51INFO     Retrying         +connection attempt 1/3 +2026-02-21 10:28:51WARNING  Rate limit       +approaching for client api-key-abc +2026-02-21 10:28:52INFO     Health check     +passed +2026-02-21 10:28:53CRITICAL Out of memory    +error detected +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + + +┌──────────────────────────────────────────────────────────── 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 adb6011..0abf547 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 @@ -33,12 +33,11 @@ } .terminal-r1 { fill: #c5c8c6 } -.terminal-r2 { fill: #9a9b99 } -.terminal-r3 { fill: #868887 } -.terminal-r4 { fill: #68a0b3 } -.terminal-r5 { fill: #d0b344 } -.terminal-r6 { fill: #cc555a } -.terminal-r7 { fill: #ff8205;font-weight: bold } +.terminal-r2 { fill: #868887 } +.terminal-r3 { fill: #68a0b3 } +.terminal-r4 { fill: #d0b344 } +.terminal-r5 { fill: #cc555a } +.terminal-r6 { fill: #ff8205;font-weight: bold } @@ -162,42 +161,42 @@ - Debug Console  (ctrl+\ to close) - -2026-02-21 10:28:51DEBUG    Initializing     -model registry -2026-02-21 10:28:51INFO     Server started   -on port 8080 -2026-02-21 10:28:51INFO     Loading          -configuration from /etc/vibe/config.yaml -2026-02-21 10:28:51WARNING  Cache miss for   -key user:42 -2026-02-21 10:28:51DEBUG    Processing       -request GET /api/v1/models -2026-02-21 10:28:51INFO     Request          -completed in 45ms -2026-02-21 10:28:51ERROR    Connection       -refused to upstream service -2026-02-21 10:28:51INFO     Retrying         -connection attempt 1/3 -2026-02-21 10:28:51WARNING  Rate limit       -approaching for client api-key-abc -2026-02-21 10:28:52INFO     Health check     -passed - - -  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills -  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information - - - -┌──────────────────────────────────────────────────────────── default ─┐│ ->││ -││ -││ -└──────────────────────────────────────────────────────────────────────┘│ -/test/workdir0% of 200k tokens + Debug Console  (ctrl+\ to close) + +2026-02-21 10:28:51DEBUG    Initializing     +model registry +2026-02-21 10:28:51INFO     Server started   +on port 8080 +2026-02-21 10:28:51INFO     Loading          +configuration from /etc/vibe/config.yaml +2026-02-21 10:28:51WARNING  Cache miss for   +key user:42 +2026-02-21 10:28:51DEBUG    Processing       +request GET /api/v1/models +2026-02-21 10:28:51INFO     Request          +completed in 45ms +2026-02-21 10:28:51ERROR    Connection       +refused to upstream service +2026-02-21 10:28:51INFO     Retrying         +connection attempt 1/3 +2026-02-21 10:28:51WARNING  Rate limit       +approaching for client api-key-abc +2026-02-21 10:28:52INFO     Health check     +passed + + +  ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills +  ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information + + + +┌──────────────────────────────────────────────────────────── 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 742237a..69206b4 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 @@ -36,9 +36,9 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #98a84b } -.terminal-r6 { fill: #9a9b99;font-style: italic; } -.terminal-r7 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #6b753d } +.terminal-r7 { fill: #868887 } @@ -185,18 +185,18 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -What is the answer? +What is the answer? -Thought +Thought The answer is 42. -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── 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 928c2d2..680777a 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 @@ -36,8 +36,9 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #608ab1 } -.terminal-r6 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #608ab1 } +.terminal-r7 { fill: #868887 } @@ -186,17 +187,17 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆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 +How is Vibe doing so far?  1: good  2: fine  3: bad +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── 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 926cabd..cab9511 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 @@ -36,10 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } -.terminal-r8 { fill: #9cafbd;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 } @@ -163,7 +163,7 @@ - + @@ -186,21 +186,21 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... -MCP servers opened... +/mcp +MCP servers opened... +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers & Connectors - -Workspace Connectors -  alpha  [connector]  1 tool   connected -  beta   [connector]  no tools not connected -  zeta   [connector]  no tools not connected -↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 9d60d6e..13088ff 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 @@ -36,9 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } +.terminal-r7 { fill: #4b4e55;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -160,7 +161,7 @@ - + @@ -180,24 +181,24 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... +/mcp +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -Connector: beta - -This connector requires authentication - -  Press enter to open auth in your browser -  Copy URL to clipboard -  Manually show the URL - -Once authenticated, press R to refresh - -Backspace Back -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +Connector: beta + +This connector requires authentication + +  Press enter to open auth in your browser +  Copy URL to clipboard +  Manually show the URL + +Once authenticated, press R to refresh + +Backspace Back +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 09bc1f9..524922b 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 @@ -36,9 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } +.terminal-r7 { fill: #4b4e55;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -160,7 +161,7 @@ - + @@ -178,26 +179,26 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... +/mcp +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -Connector: beta - -This connector requires authentication - -  Press enter to open auth in your browser -  Copy URL to clipboard -  Manually show the URL - -https://fake-auth.example.com/beta - -Once authenticated, press R to refresh - -Backspace Back -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +Connector: beta + +This connector requires authentication + +  Press enter to open auth in your browser +  Copy URL to clipboard +  Manually show the URL + +https://fake-auth.example.com/beta + +Once authenticated, press R to refresh + +Backspace Back +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 687a88f..049e013 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 @@ -36,10 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } -.terminal-r8 { fill: #9cafbd;font-weight: bold } +.terminal-r7 { fill: #4b4e55;font-weight: bold } +.terminal-r8 { fill: #7b7e82;font-weight: bold } .terminal-r9 { fill: #868887 } @@ -162,7 +162,7 @@ - + @@ -187,19 +187,19 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp filesystem -MCP servers opened... +/mcp filesystem +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -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 -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 04a3ed6..3c966e5 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 @@ -36,10 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } -.terminal-r8 { fill: #9cafbd;font-weight: bold } +.terminal-r7 { fill: #4b4e55;font-weight: bold } +.terminal-r8 { fill: #7b7e82;font-weight: bold } .terminal-r9 { fill: #868887 } @@ -162,7 +162,7 @@ - + @@ -182,24 +182,24 @@   ⡠⣒⠄  ⡔⢄⠔⡄Mistral Vibe v0.0.0 · devstral-latest[off] · [Subscription] Pro - ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 2 MCP servers · 0 skills + ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 3 MCP servers · 0 skills   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... +/mcp +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -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 -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 7a3c667..630ba2a 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 @@ -36,10 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } -.terminal-r8 { fill: #9cafbd;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 } @@ -163,7 +163,7 @@ - + @@ -185,22 +185,22 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... +/mcp +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers & Connectors - -Local MCP Servers -  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 -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +MCP Servers & Connectors + +Local MCP Servers +  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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 3cd6643..44a1c0d 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 @@ -36,10 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } -.terminal-r8 { fill: #9cafbd;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 } @@ -164,7 +164,7 @@ - + @@ -189,19 +189,19 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... +/mcp +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -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 -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 1f59f1e..fecab18 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 @@ -36,10 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } -.terminal-r8 { fill: #9cafbd;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 } @@ -163,7 +163,7 @@ - + @@ -187,20 +187,20 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... +/mcp +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers & Connectors - -Workspace Connectors -  alpha  [connector]  1 tool   connected -  beta   [connector]  no tools not connected -  zeta   [connector]  no tools not connected -↑↓ Navigate  Enter Show tools  D Disable  E Enable  R Refresh  Esc Close -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 4d85cf0..56b8951 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 @@ -36,9 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } +.terminal-r7 { fill: #4b4e55;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -160,7 +161,7 @@ - + @@ -186,18 +187,18 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... +/mcp +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -Connector: slack - -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 +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +Connector: slack + +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 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 43212c7..d5bef3a 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 @@ -36,9 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } +.terminal-r7 { fill: #4b4e55;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -160,7 +161,7 @@ - + @@ -187,17 +188,17 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... +/mcp +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Server: filesystem - -fake_tool  -  A fake tool for filesystem -↑↓ Navigate  D Disable  E Enable  Backspace Back  R Refresh  Esc Close -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +MCP Server: filesystem + +fake_tool  -  A fake tool for filesystem +↑↓ Navigate  D Disable  E Enable  Backspace Back  R Refresh  Esc Close +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 fda8223..343c377 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 @@ -36,7 +36,8 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -185,17 +186,17 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... -MCP servers closed. +/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_authenticate.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_help_bar_shows_connect.svg similarity index 79% rename from tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_help_bar_shows_authenticate.svg rename to tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_help_bar_shows_connect.svg index 802fa4c..57b906d 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_help_bar_shows_authenticate.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_mcp_command/test_snapshot_mcp_help_bar_shows_connect.svg @@ -36,12 +36,12 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } -.terminal-r8 { fill: #868887 } -.terminal-r9 { fill: #98a84b } -.terminal-r10 { fill: #9cafbd;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 } @@ -163,7 +163,7 @@ - + @@ -187,20 +187,20 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... +/mcp +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers & Connectors - -Workspace Connectors -  alpha  [connector]  1 tool   connected -  beta   [connector]  no tools not connected -  zeta   [connector]  no tools not connected -↑↓ Navigate  Enter Authenticate  D Disable  E Enable  R Refresh  Esc Close -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 ba48d18..4680382 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 @@ -36,7 +36,8 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -186,16 +187,16 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -No MCP servers or connectors configured. +/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 48cd5ea..61e9cbc 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 @@ -36,10 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } -.terminal-r8 { fill: #9cafbd;font-weight: bold } +.terminal-r7 { fill: #4b4e55;font-weight: bold } +.terminal-r8 { fill: #7b7e82;font-weight: bold } .terminal-r9 { fill: #868887 } @@ -162,7 +162,7 @@ - + @@ -187,19 +187,19 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... +/mcp +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -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 -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 26ecc77..03bace9 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 @@ -36,11 +36,11 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } -.terminal-r8 { fill: #868887 } -.terminal-r9 { fill: #9cafbd;font-weight: bold } +.terminal-r7 { fill: #868887 } +.terminal-r8 { fill: #4b4e55;font-weight: bold } +.terminal-r9 { fill: #7b7e82;font-weight: bold } @@ -162,7 +162,7 @@ - + @@ -187,19 +187,19 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... +/mcp +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -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 -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 1327316..5baac2e 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 @@ -36,10 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } -.terminal-r8 { fill: #9cafbd;font-weight: bold } +.terminal-r7 { fill: #4b4e55;font-weight: bold } +.terminal-r8 { fill: #7b7e82;font-weight: bold } .terminal-r9 { fill: #868887 } @@ -162,7 +162,7 @@ - + @@ -187,19 +187,19 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... +/mcp +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -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 -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 27b442e..2de16b7 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 @@ -36,9 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } +.terminal-r7 { fill: #4b4e55;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -160,7 +161,7 @@ - + @@ -187,17 +188,17 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp filesystem -MCP servers opened... +/mcp filesystem +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Server: filesystem - -fake_tool  -  A fake tool for filesystem -↑↓ Navigate  D Disable  E Enable  Backspace Back  R Refresh  Esc Close -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +MCP Server: filesystem + +fake_tool  -  A fake tool for filesystem +↑↓ Navigate  D Disable  E Enable  Backspace Back  R Refresh  Esc Close +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 7a3c667..630ba2a 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 @@ -36,10 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } .terminal-r6 { fill: #608ab1;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } -.terminal-r8 { fill: #9cafbd;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 } @@ -163,7 +163,7 @@ - + @@ -185,22 +185,22 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/mcp -MCP servers opened... +/mcp +MCP servers opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -MCP Servers & Connectors - -Local MCP Servers -  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 -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +MCP Servers & Connectors + +Local MCP Servers +  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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 2ecc5ba..0386126 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 @@ -35,7 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #868887 } @@ -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_initial.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_initial.svg index cad57c6..05ad8e4 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_initial.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_initial.svg @@ -35,10 +35,10 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } -.terminal-r5 { fill: #608ab1;font-weight: bold } -.terminal-r6 { fill: #98a84b;font-weight: bold } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #608ab1;font-weight: bold } +.terminal-r5 { fill: #98a84b;font-weight: bold } +.terminal-r6 { fill: #4b4e55;font-weight: bold } +.terminal-r7 { fill: #868887 } @@ -160,7 +160,7 @@ - + @@ -187,17 +187,17 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ -Select Model -  mistral-large -› devstral -  codestral -  mistral-small -  local - -↑↓ Navigate  Enter Select  Esc Cancel -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ +Select Model +  mistral-large +› devstral +  codestral +  mistral-small +  local + +↑↓ Navigate  Enter Select  Esc Cancel +└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_navigate_down.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_navigate_down.svg index 1945313..5ad4635 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_navigate_down.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_model_picker/test_snapshot_model_picker_navigate_down.svg @@ -35,10 +35,11 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } -.terminal-r5 { fill: #608ab1;font-weight: bold } -.terminal-r6 { fill: #98a84b } -.terminal-r7 { fill: #c5c8c6;font-weight: bold } +.terminal-r4 { fill: #608ab1;font-weight: bold } +.terminal-r5 { fill: #98a84b } +.terminal-r6 { fill: #c5c8c6;font-weight: bold } +.terminal-r7 { fill: #4b4e55;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -160,7 +161,7 @@ - + @@ -187,17 +188,17 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ -Select Model -  mistral-large -› devstral -  codestral -  mistral-small -  local - -↑↓ Navigate  Enter Select  Esc Cancel -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ +Select Model +  mistral-large +› devstral +  codestral +  mistral-small +  local + +↑↓ Navigate  Enter Select  Esc Cancel +└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 7cab259..ff0327d 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 @@ -35,7 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #868887 } @@ -186,14 +186,14 @@  ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Configuration reloaded (includes agent instructions and skills). +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 bdd506e..7fa1792 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 @@ -36,7 +36,7 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #d0b344 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #868887 } 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 1b42b8a..ceb654d 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 @@ -36,7 +36,7 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #cc555a } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #868887 } 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 d558a13..bf972a8 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 @@ -36,7 +36,7 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #98a84b } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #868887 } 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 1f6be05..49d41ab 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 @@ -35,7 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #868887 } @@ -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 1f6be05..49d41ab 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 @@ -35,7 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #868887 } @@ -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 e714aad..8249d93 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 @@ -36,7 +36,8 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -185,17 +186,17 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆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 ace43eb..7723d34 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 @@ -36,9 +36,9 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #ffa500;font-weight: bold } -.terminal-r6 { fill: #868887 } -.terminal-r7 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #ffa500;font-weight: bold } +.terminal-r7 { fill: #868887 } @@ -187,16 +187,16 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Hello +Hello Hello! I can help you. -▂▅▇ speaking Esc/Ctrl+C to stop -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +▂▅▇ 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 1a9e5b2..47150c4 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 @@ -36,9 +36,9 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #ffa500;font-weight: bold } -.terminal-r6 { fill: #868887 } -.terminal-r7 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #ffa500;font-weight: bold } +.terminal-r7 { fill: #868887 } @@ -187,16 +187,16 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Hello +Hello Hello! I can help you. - summarizing Esc/Ctrl+C to stop -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + summarizing Esc/Ctrl+C to stop +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ +> + + +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ /test/workdir6% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_onboarding_api_key/test_snapshot_onboarding_api_key_with_valid_input.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_onboarding_api_key/test_snapshot_onboarding_api_key_with_valid_input.svg new file mode 100644 index 0000000..8dd73b0 --- /dev/null +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_onboarding_api_key/test_snapshot_onboarding_api_key_with_valid_input.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ApiKeyScreenSnapshotApp + + + + + + + + + + + + + ⢠⢢    ⡔⢄⠔⡄ + ⢸⢸⣀⡔⢉⠱⣃⡐⣔⡣ + ⠈⠒⠒⠣⠤⠵⠤⠬⠮⠆ +Get your Mistral API key +Visit AI Studio to generate or copy your Vibe key + +https://console.mistral.ai/codestral/cli + + +┌─ Paste API key ────────────────────────────────────────────────────────┐ +••••••••••••••• +└────────────────────────────────────────────────────────────────────────┘ + +Press Enter to submit ↵ + +Learn more about Vibe configurations +https://github.com/mistralai/mistral-vibe?tab=readme-ov-file#configuration + + + + + + + diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_onboarding_theme/test_snapshot_onboarding_theme_selection[ansi-dark].svg b/tests/snapshots/__snapshots__/test_ui_snapshot_onboarding_theme/test_snapshot_onboarding_theme_selection[ansi-dark].svg new file mode 100644 index 0000000..560dd3a --- /dev/null +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_onboarding_theme/test_snapshot_onboarding_theme_selection[ansi-dark].svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OnboardingApp + + + + + + + + + + +Select your preferred theme + + +rose-pine-dawn +solarized-light +textual-light +╭────────────────────────────╮ +Navigate ↑ ↓ ansi-dark Press Enter ↵ +╰────────────────────────────╯ +atom-one-dark +catppuccin-frappe +catppuccin-macchiato + + +╭───────────────────────────── Preview ──────────────────────────────╮ + +Heading + +Bolditalic, and inline code. +▄▄ +• Bullet point +• Another bullet point + + 1. First item + 2. Second item + +╰────────────────────────────────────────────────────────────────────╯ + + + + + diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_onboarding_theme/test_snapshot_onboarding_theme_selection[ansi-light].svg b/tests/snapshots/__snapshots__/test_ui_snapshot_onboarding_theme/test_snapshot_onboarding_theme_selection[ansi-light].svg new file mode 100644 index 0000000..b6cb2fd --- /dev/null +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_onboarding_theme/test_snapshot_onboarding_theme_selection[ansi-light].svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OnboardingApp + + + + + + + + + + +Select your preferred theme + + +rose-pine-dawn +solarized-light +textual-light +╭────────────────────────────╮ +Navigate ↑ ↓ ansi-dark Press Enter ↵ +╰────────────────────────────╯ +atom-one-dark +catppuccin-frappe +catppuccin-macchiato + + +╭───────────────────────────── Preview ──────────────────────────────╮ + +Heading + +Bolditalic, and inline code. +▄▄ +• Bullet point +• Another bullet point + + 1. First item + 2. Second item + +╰────────────────────────────────────────────────────────────────────╯ + + + + + diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_onboarding_theme/test_snapshot_onboarding_theme_selection[dracula].svg b/tests/snapshots/__snapshots__/test_ui_snapshot_onboarding_theme/test_snapshot_onboarding_theme_selection[dracula].svg new file mode 100644 index 0000000..0db5576 --- /dev/null +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_onboarding_theme/test_snapshot_onboarding_theme_selection[dracula].svg @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OnboardingApp + + + + + + + + + + +Select your preferred theme + + +rose-pine-dawn +solarized-light +textual-light +╭────────────────────────────╮ +Navigate ↑ ↓ ansi-dark Press Enter ↵ +╰────────────────────────────╯ +atom-one-dark +catppuccin-frappe +catppuccin-macchiato + + +╭───────────────────────────── Preview ──────────────────────────────╮ + +Heading + +Bolditalic, and inline code. +▄▄ +• Bullet point +• Another bullet point + + 1. First item + 2. Second item + +╰────────────────────────────────────────────────────────────────────╯ + + + + + diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_parallel_tool_calls/test_snapshot_parallel_tool_calls_pending.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_parallel_tool_calls/test_snapshot_parallel_tool_calls_pending.svg index b84b444..95f42d9 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_parallel_tool_calls/test_snapshot_parallel_tool_calls_pending.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_parallel_tool_calls/test_snapshot_parallel_tool_calls_pending.svg @@ -33,7 +33,7 @@ } .terminal-r1 { fill: #c5c8c6 } -.terminal-r2 { fill: #d9d9d9 } +.terminal-r2 { fill: #e0e0e0 } diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_parallel_tool_calls/test_snapshot_parallel_tool_calls_resolved.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_parallel_tool_calls/test_snapshot_parallel_tool_calls_resolved.svg index 209e55e..f018d6c 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_parallel_tool_calls/test_snapshot_parallel_tool_calls_resolved.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_parallel_tool_calls/test_snapshot_parallel_tool_calls_resolved.svg @@ -33,10 +33,9 @@ } .terminal-r1 { fill: #c5c8c6 } -.terminal-r2 { fill: #98e024 } -.terminal-r3 { fill: #d9d9d9 } -.terminal-r4 { fill: #625e4c } -.terminal-r5 { fill: #625e4c;font-style: italic; } +.terminal-r2 { fill: #4ebf71 } +.terminal-r3 { fill: #e0e0e0 } +.terminal-r4 { fill: #a0a0a0 } @@ -117,15 +116,15 @@ Read 1 line from file_0.py Path: /src/file_0.py -# content of file_0.py +# content of file_0.py Read 1 line from file_1.py Path: /src/file_1.py -# content of file_1.py +# content of file_1.py Read 1 line from file_2.py Path: /src/file_2.py -# content of file_2.py +# content of file_2.py 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 5c2ef7e..cee3075 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 @@ -35,9 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } -.terminal-r5 { fill: #608ab1;font-weight: bold } -.terminal-r6 { fill: #608ab1;text-decoration: underline; } +.terminal-r4 { fill: #98729f;font-weight: bold } +.terminal-r5 { fill: #1984e9;text-decoration: underline; } +.terminal-r6 { fill: #868887 } @@ -174,29 +174,29 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -Implementation Plan - - -1. Add user authentication - -• JWT token validation -• Session management - - -2. Database migrations - -• Create users table -• Add indexes -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +Implementation Plan + + +1. Add user authentication + +• JWT token validation +• Session management + + +2. Database migrations + +• Create users table +• Add indexes +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── 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 4663f2a..0a04f71 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 @@ -35,7 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #868887 } @@ -185,15 +185,15 @@  ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Proxy setup opened... -Proxy setup cancelled. +Proxy setup opened... +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 bd9ae0c..bfa6298 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 @@ -35,7 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #868887 } @@ -185,15 +185,15 @@  ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Proxy setup opened... -Proxy settings saved. Restart the CLI for changes to take effect. +Proxy setup opened... +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_initial_empty.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_initial_empty.svg index 49d37fc..e7b5040 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_initial_empty.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_initial_empty.svg @@ -35,10 +35,9 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } -.terminal-r5 { fill: #608ab1;font-weight: bold } -.terminal-r6 { fill: #949798 } -.terminal-r7 { fill: #868887 } +.terminal-r4 { fill: #608ab1;font-weight: bold } +.terminal-r5 { fill: #7b7e82 } +.terminal-r6 { fill: #868887 } @@ -160,7 +159,7 @@ - + @@ -178,26 +177,26 @@  ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Proxy setup opened... +Proxy setup opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ -Proxy Configuration -HTTP_PROXY -Proxy URL for HTTP requests -HTTPS_PROXY -Proxy URL for HTTPS requests -ALL_PROXY -Proxy URL for all requests (fallback) -NO_PROXY -Comma-separated list of hosts to bypass proxy -SSL_CERT_FILE -Path to custom SSL certificate file -SSL_CERT_DIR -Path to directory containing SSL certificates -↑↓ navigate  Enter save & exit  ESC cancel -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ +Proxy Configuration +HTTP_PROXY +Proxy URL for HTTP requests +HTTPS_PROXY +Proxy URL for HTTPS requests +ALL_PROXY +Proxy URL for all requests (fallback) +NO_PROXY +Comma-separated list of hosts to bypass proxy +SSL_CERT_FILE +Path to custom SSL certificate file +SSL_CERT_DIR +Path to directory containing SSL certificates +↑↓ navigate  Enter save & exit  ESC cancel +└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +/test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_initial_with_values.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_initial_with_values.svg index fe937cc..1668dcf 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_initial_with_values.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_proxy_setup/test_snapshot_proxy_setup_initial_with_values.svg @@ -35,8 +35,8 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } -.terminal-r5 { fill: #608ab1;font-weight: bold } +.terminal-r4 { fill: #608ab1;font-weight: bold } +.terminal-r5 { fill: #4b4e55 } .terminal-r6 { fill: #868887 } @@ -159,7 +159,7 @@ - + @@ -177,26 +177,26 @@  ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Proxy setup opened... +Proxy setup opened... -┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ -Proxy Configuration -HTTP_PROXY -http://old-proxy:8080 -HTTPS_PROXY -https://old-proxy:8443                                                                         -ALL_PROXY -Proxy URL for all requests (fallback) -NO_PROXY -Comma-separated list of hosts to bypass proxy -SSL_CERT_FILE -Path to custom SSL certificate file -SSL_CERT_DIR -Path to directory containing SSL certificates -↑↓ navigate  Enter save & exit  ESC cancel -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ +Proxy Configuration +HTTP_PROXY +http://old-proxy:8080 +HTTPS_PROXY +https://old-proxy:8443                                                                         +ALL_PROXY +Proxy URL for all requests (fallback) +NO_PROXY +Comma-separated list of hosts to bypass proxy +SSL_CERT_FILE +Path to custom SSL certificate file +SSL_CERT_DIR +Path to directory containing SSL certificates +↑↓ navigate  Enter save & exit  ESC cancel +└──────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 1bd51c1..68a6561 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 @@ -35,8 +35,8 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } -.terminal-r5 { fill: #cc555a;font-weight: bold } +.terminal-r4 { fill: #cc555a;font-weight: bold } +.terminal-r5 { fill: #868887 } @@ -186,16 +186,16 @@  ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Proxy setup opened... -Error: Failed to save proxy settings: Permission denied +Proxy setup opened... +Error: Failed to save proxy settings: Permission denied -┌──────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────── 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 abe2d81..b9a8289 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 @@ -35,7 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #868887 } @@ -185,15 +185,15 @@  ⢸⠸⣀⡔⢉⠱⣃⡢⣂⡣1 model · 0 MCP servers · 0 skills   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Proxy setup opened... -Proxy settings saved. Restart the CLI for changes to take effect. +Proxy setup opened... +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_question_app/test_snapshot_multi_question_answer_first_advance.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_answer_first_advance.svg index 8d63062..fe0f5fc 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_answer_first_advance.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_answer_first_advance.svg @@ -32,11 +32,12 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff } -.terminal-r4 { fill: #9d65ff;font-weight: bold } -.terminal-r5 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4 } +.terminal-r4 { fill: #0178d4;font-weight: bold } +.terminal-r5 { fill: #e0e0e0 } +.terminal-r6 { fill: #a0a0a0 } @@ -119,9 +120,9 @@ › 1. FastAPI   2. Django -  3. Type your answer... +  3. Type your answer... -←→ questions  ↑↓ navigate  Enter select  Esc cancel +←→ questions  ↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_first_answered_checkmark.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_first_answered_checkmark.svg index 8d63062..fe0f5fc 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_first_answered_checkmark.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_first_answered_checkmark.svg @@ -32,11 +32,12 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff } -.terminal-r4 { fill: #9d65ff;font-weight: bold } -.terminal-r5 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4 } +.terminal-r4 { fill: #0178d4;font-weight: bold } +.terminal-r5 { fill: #e0e0e0 } +.terminal-r6 { fill: #a0a0a0 } @@ -119,9 +120,9 @@ › 1. FastAPI   2. Django -  3. Type your answer... +  3. Type your answer... -←→ questions  ↑↓ navigate  Enter select  Esc cancel +←→ questions  ↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_initial.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_initial.svg index cf35a14..3f522d1 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_initial.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_initial.svg @@ -32,11 +32,12 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff } -.terminal-r4 { fill: #9d65ff;font-weight: bold } -.terminal-r5 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4 } +.terminal-r4 { fill: #0178d4;font-weight: bold } +.terminal-r5 { fill: #e0e0e0 } +.terminal-r6 { fill: #a0a0a0 } @@ -119,9 +120,9 @@ › 1. PostgreSQL   2. MongoDB -  3. Type your answer... +  3. Type your answer... -←→ questions  ↑↓ navigate  Enter select  Esc cancel +←→ questions  ↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_navigate_left_wraps.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_navigate_left_wraps.svg index 646e09a..b9a832f 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_navigate_left_wraps.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_navigate_left_wraps.svg @@ -32,11 +32,12 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff } -.terminal-r4 { fill: #9d65ff;font-weight: bold } -.terminal-r5 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4 } +.terminal-r4 { fill: #0178d4;font-weight: bold } +.terminal-r5 { fill: #e0e0e0 } +.terminal-r6 { fill: #a0a0a0 } @@ -119,9 +120,9 @@ › 1. FastAPI   2. Django -  3. Type your answer... +  3. Type your answer... -←→ questions  ↑↓ navigate  Enter select  Esc cancel +←→ questions  ↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_navigate_right.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_navigate_right.svg index 646e09a..b9a832f 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_navigate_right.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_navigate_right.svg @@ -32,11 +32,12 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff } -.terminal-r4 { fill: #9d65ff;font-weight: bold } -.terminal-r5 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4 } +.terminal-r4 { fill: #0178d4;font-weight: bold } +.terminal-r5 { fill: #e0e0e0 } +.terminal-r6 { fill: #a0a0a0 } @@ -119,9 +120,9 @@ › 1. FastAPI   2. Django -  3. Type your answer... +  3. Type your answer... -←→ questions  ↑↓ navigate  Enter select  Esc cancel +←→ questions  ↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_tab_to_second.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_tab_to_second.svg index cf35a14..3f522d1 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_tab_to_second.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_question_tab_to_second.svg @@ -32,11 +32,12 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff } -.terminal-r4 { fill: #9d65ff;font-weight: bold } -.terminal-r5 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4 } +.terminal-r4 { fill: #0178d4;font-weight: bold } +.terminal-r5 { fill: #e0e0e0 } +.terminal-r6 { fill: #a0a0a0 } @@ -119,9 +120,9 @@ › 1. PostgreSQL   2. MongoDB -  3. Type your answer... +  3. Type your answer... -←→ questions  ↑↓ navigate  Enter select  Esc cancel +←→ questions  ↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_initial.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_initial.svg index 548b2c2..6000640 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_initial.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_initial.svg @@ -32,10 +32,11 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #a0a0a0 } @@ -117,11 +118,11 @@ › 1. [ ] Authentication - User login/logout   2. [ ] Caching - Redis caching layer   3. [ ] Logging - Structured logging -  4. [ ] Type your answer... +  4. [ ] Type your answer...      Submit → -↑↓ navigate  Enter toggle  Esc cancel +↑↓ navigate  Enter toggle  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_mixed_selection.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_mixed_selection.svg index 429b0ca..4f59125 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_mixed_selection.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_mixed_selection.svg @@ -32,13 +32,13 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } -.terminal-r5 { fill: #d9d9d9;font-weight: bold } -.terminal-r6 { fill: #e0e0e0 } -.terminal-r7 { fill: #121212 } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #e0e0e0;font-weight: bold } +.terminal-r6 { fill: #121212 } +.terminal-r7 { fill: #a0a0a0 } @@ -120,11 +120,11 @@   1. [x] Authentication - User login/logout   2. [ ] Caching - Redis caching layer   3. [x] Logging - Structured logging -› 4. [x] Extra +› 4. [x] Extra      Submit → -↑↓ navigate  Enter toggle  Esc cancel +↑↓ navigate  Enter toggle  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_navigate_to_submit.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_navigate_to_submit.svg index b98ed4c..12c1c53 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_navigate_to_submit.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_navigate_to_submit.svg @@ -32,11 +32,12 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } -.terminal-r5 { fill: #d9d9d9;font-weight: bold } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #a0a0a0 } +.terminal-r6 { fill: #e0e0e0;font-weight: bold } @@ -118,11 +119,11 @@   1. [ ] Authentication - User login/logout   2. [ ] Caching - Redis caching layer   3. [ ] Logging - Structured logging -  4. [ ] Type your answer... +  4. [ ] Type your answer... -›    Submit → +›    Submit → -↑↓ navigate  Enter toggle  Esc cancel +↑↓ navigate  Enter toggle  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_other_with_text.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_other_with_text.svg index d8e1c39..bcbdacb 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_other_with_text.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_other_with_text.svg @@ -32,13 +32,13 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } -.terminal-r5 { fill: #d9d9d9;font-weight: bold } -.terminal-r6 { fill: #e0e0e0 } -.terminal-r7 { fill: #121212 } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #e0e0e0;font-weight: bold } +.terminal-r6 { fill: #121212 } +.terminal-r7 { fill: #a0a0a0 } @@ -120,11 +120,11 @@   1. [ ] Authentication - User login/logout   2. [ ] Caching - Redis caching layer   3. [ ] Logging - Structured logging -› 4. [x] Custom feature +› 4. [x] Custom feature      Submit → -↑↓ navigate  Enter toggle  Esc cancel +↑↓ navigate  Enter toggle  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_toggle_first.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_toggle_first.svg index 063283e..5f66515 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_toggle_first.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_toggle_first.svg @@ -32,10 +32,11 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #a0a0a0 } @@ -117,11 +118,11 @@ › 1. [x] Authentication - User login/logout   2. [ ] Caching - Redis caching layer   3. [ ] Logging - Structured logging -  4. [ ] Type your answer... +  4. [ ] Type your answer...      Submit → -↑↓ navigate  Enter toggle  Esc cancel +↑↓ navigate  Enter toggle  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_toggle_multiple.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_toggle_multiple.svg index 1b1fcbe..473f784 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_toggle_multiple.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_toggle_multiple.svg @@ -32,10 +32,11 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #a0a0a0 } @@ -117,11 +118,11 @@   1. [x] Authentication - User login/logout   2. [ ] Caching - Redis caching layer › 3. [x] Logging - Structured logging -  4. [ ] Type your answer... +  4. [ ] Type your answer...      Submit → -↑↓ navigate  Enter toggle  Esc cancel +↑↓ navigate  Enter toggle  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_untoggle.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_untoggle.svg index 548b2c2..6000640 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_untoggle.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_multi_select_untoggle.svg @@ -32,10 +32,11 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #a0a0a0 } @@ -117,11 +118,11 @@ › 1. [ ] Authentication - User login/logout   2. [ ] Caching - Redis caching layer   3. [ ] Logging - Structured logging -  4. [ ] Type your answer... +  4. [ ] Type your answer...      Submit → -↑↓ navigate  Enter toggle  Esc cancel +↑↓ navigate  Enter toggle  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_initial.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_initial.svg index cef48fd..585680e 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_initial.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_initial.svg @@ -32,10 +32,11 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #a0a0a0 } @@ -117,9 +118,9 @@ › 1. PostgreSQL - Relational database   2. MongoDB - Document database   3. Redis - In-memory store -  4. Type your answer... +  4. Type your answer... -↑↓ navigate  Enter select  Esc cancel +↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_down.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_down.svg index 8105a30..8cd0d3b 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_down.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_down.svg @@ -32,10 +32,11 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #a0a0a0 } @@ -117,9 +118,9 @@   1. PostgreSQL - Relational database › 2. MongoDB - Document database   3. Redis - In-memory store -  4. Type your answer... +  4. Type your answer... -↑↓ navigate  Enter select  Esc cancel +↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_to_other.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_to_other.svg index cf5cb0b..6993ff6 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_to_other.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_to_other.svg @@ -32,14 +32,14 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } -.terminal-r5 { fill: #d9d9d9;font-weight: bold } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #e0e0e0;font-weight: bold } .terminal-r6 { fill: #121212 } .terminal-r7 { fill: #6c6c6c } -.terminal-r8 { fill: #e0e0e0 } +.terminal-r8 { fill: #a0a0a0 } @@ -123,7 +123,7 @@   3. Redis - In-memory store › 4. Type your answer... -↑↓ navigate  Enter select  Esc cancel +↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_to_third_option.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_to_third_option.svg index 66c56ce..c0b3e71 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_to_third_option.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_to_third_option.svg @@ -32,10 +32,11 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #a0a0a0 } @@ -117,9 +118,9 @@   1. PostgreSQL - Relational database   2. MongoDB - Document database › 3. Redis - In-memory store -  4. Type your answer... +  4. Type your answer... -↑↓ navigate  Enter select  Esc cancel +↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_up_wraps.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_up_wraps.svg index cf5cb0b..6993ff6 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_up_wraps.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_navigate_up_wraps.svg @@ -32,14 +32,14 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } -.terminal-r5 { fill: #d9d9d9;font-weight: bold } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #e0e0e0;font-weight: bold } .terminal-r6 { fill: #121212 } .terminal-r7 { fill: #6c6c6c } -.terminal-r8 { fill: #e0e0e0 } +.terminal-r8 { fill: #a0a0a0 } @@ -123,7 +123,7 @@   3. Redis - In-memory store › 4. Type your answer... -↑↓ navigate  Enter select  Esc cancel +↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_other_typing.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_other_typing.svg index a375a86..d1236fa 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_other_typing.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_question_app/test_snapshot_question_app_other_typing.svg @@ -32,13 +32,13 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } -.terminal-r5 { fill: #d9d9d9;font-weight: bold } -.terminal-r6 { fill: #e0e0e0 } -.terminal-r7 { fill: #121212 } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #e0e0e0;font-weight: bold } +.terminal-r6 { fill: #121212 } +.terminal-r7 { fill: #a0a0a0 } @@ -120,9 +120,9 @@   1. PostgreSQL - Relational database   2. MongoDB - Document database   3. Redis - In-memory store -› 4. SQLite +› 4. SQLite -↑↓ navigate  Enter select  Esc cancel +↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ 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 2033eb6..835ebbb 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 @@ -36,9 +36,9 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #98a84b } -.terminal-r6 { fill: #9a9b99;font-style: italic; } -.terminal-r7 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #6b753d } +.terminal-r7 { fill: #868887 } @@ -185,18 +185,18 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆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 ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── 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 e727d97..e7d7f7d 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 @@ -36,9 +36,9 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #98a84b } -.terminal-r6 { fill: #9a9b99;font-style: italic; } -.terminal-r7 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #6b753d } +.terminal-r7 { fill: #868887 } @@ -181,22 +181,22 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆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 ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── 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 db95365..aaf0990 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 @@ -36,9 +36,9 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #98a84b } -.terminal-r6 { fill: #9a9b99;font-style: italic; } -.terminal-r7 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #6b753d } +.terminal-r7 { fill: #868887 } @@ -185,18 +185,18 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆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 ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── 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 a8fb074..ae50ee1 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 @@ -36,9 +36,9 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #98a84b } -.terminal-r6 { fill: #9a9b99;font-style: italic; } -.terminal-r7 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #6b753d } +.terminal-r7 { fill: #868887 } @@ -184,19 +184,19 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆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 ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── 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 78f4812..68f7b2b 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 @@ -36,10 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #608ab1;font-weight: bold } +.terminal-r5 { fill: #98729f;font-weight: bold } .terminal-r6 { fill: #98a84b } -.terminal-r7 { fill: #56ab56;font-weight: bold } -.terminal-r8 { fill: #9a9b99 } +.terminal-r7 { fill: #98a84b;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -161,7 +161,7 @@ - + @@ -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 a0218df..a566b12 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 @@ -36,10 +36,11 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #292929;font-weight: bold } -.terminal-r6 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #292929;font-weight: bold } .terminal-r7 { fill: #cc555a } .terminal-r8 { fill: #608ab1;font-weight: bold } +.terminal-r9 { fill: #868887 } @@ -161,7 +162,7 @@ - + @@ -181,24 +182,24 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -first message +first message Hello! How can I help you? -second message +second message -third message +third message -┌───────────────────────────────────────────────────────────┐ -Rewind to: third messageInvalid message index: 99 - -› 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 -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌───────────────────────────────────────────────────────────┐ +Rewind to: third messageInvalid message index: 99 + +› 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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 18a6bcb..dcdd308 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 @@ -36,7 +36,8 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -181,21 +182,21 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -first message +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 7ef16ba..0f8f071 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 @@ -36,9 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #292929;font-weight: bold } -.terminal-r6 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #292929;font-weight: bold } .terminal-r7 { fill: #608ab1;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -160,7 +161,7 @@ - + @@ -180,24 +181,24 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -first message +first message Hello! How can I help you? -second message +second message -third message +third message -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -Rewind to: third message - -› 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 -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +Rewind to: third message + +› 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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 82a97d9..4093b47 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 @@ -36,9 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #292929;font-weight: bold } -.terminal-r6 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #292929;font-weight: bold } .terminal-r7 { fill: #608ab1;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -160,7 +161,7 @@ - + @@ -180,24 +181,24 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -first message +first message Hello! How can I help you? -second message +second message -third message +third message -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -Rewind to: second message - -› 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 -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +Rewind to: second message + +› 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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 7ef16ba..0f8f071 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 @@ -36,9 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #292929;font-weight: bold } -.terminal-r6 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #292929;font-weight: bold } .terminal-r7 { fill: #608ab1;font-weight: bold } +.terminal-r8 { fill: #868887 } @@ -160,7 +161,7 @@ - + @@ -180,24 +181,24 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -first message +first message Hello! How can I help you? -second message +second message -third message +third message -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -Rewind to: third message - -› 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 -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ -/test/workdir0% of 200k tokens +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +Rewind to: third message + +› 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 +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +/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 add7611..1036fbe 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 @@ -36,8 +36,9 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #98a84b } -.terminal-r6 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #98a84b } +.terminal-r7 { fill: #868887 } @@ -184,19 +185,19 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆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 +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 360758f..80e7e95 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 @@ -36,7 +36,8 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -181,21 +182,21 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -Hello, can you help me? +Hello, can you help me? 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 3affece..777dcad 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 @@ -36,9 +36,9 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99;font-style: italic; } -.terminal-r6 { fill: #d2d2d2 } -.terminal-r7 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #868887;font-style: italic; } +.terminal-r7 { fill: #868887 } @@ -171,32 +171,32 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -show diagram +show diagram -  Client -    | -    |  POST /api/orders -    v +  Client +    | +    |  POST /api/orders +    v +---------+ -| Gateway | +| Gateway | +---------+ -    | -    v +    | +    v +---------+ -| Service | +| Service | +---------+ -    | -    v +    | +    v +----+----+ -|   DB    | +|   DB    | +---------+ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ +> + + +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ /test/workdir0% of 200k tokens diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_tool_call/test_snapshot_tool_call_partial.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_tool_call/test_snapshot_tool_call_partial.svg index 9b74bc6..fe4fd95 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_tool_call/test_snapshot_tool_call_partial.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_tool_call/test_snapshot_tool_call_partial.svg @@ -33,7 +33,7 @@ } .terminal-r1 { fill: #c5c8c6 } -.terminal-r2 { fill: #d9d9d9 } +.terminal-r2 { fill: #e0e0e0 } diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_tool_call/test_snapshot_tool_call_updated.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_tool_call/test_snapshot_tool_call_updated.svg index 1fc616e..3289eae 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_tool_call/test_snapshot_tool_call_updated.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_streaming_tool_call/test_snapshot_tool_call_updated.svg @@ -33,7 +33,7 @@ } .terminal-r1 { fill: #c5c8c6 } -.terminal-r2 { fill: #d9d9d9 } +.terminal-r2 { fill: #e0e0e0 } 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 844042b..1e8ef2f 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 @@ -33,12 +33,11 @@ } .terminal-r1 { fill: #c5c8c6 } -.terminal-r2 { fill: #9a9b99 } -.terminal-r3 { fill: #608ab1;font-weight: bold } -.terminal-r4 { fill: #98a84b;font-weight: bold } -.terminal-r5 { fill: #4b4e55 } -.terminal-r6 { fill: #292929 } -.terminal-r7 { fill: #ff8205;font-weight: bold } +.terminal-r2 { fill: #608ab1 } +.terminal-r3 { fill: #98a84b;font-weight: bold } +.terminal-r4 { fill: #292929 } +.terminal-r5 { fill: #ff8205;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -196,56 +195,56 @@ - + - -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) -• 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 - -Special Features - -• !<command> Execute bash command directly -• @path/to/file/ Autocompletes file paths - -Commands - -• /help: Show help message -• /config: Edit config settings -• /model: Select active model -• /thinking: Select thinking level -• /reload: Reload configuration, agent instructions, and skills from disk -• /clear: Clear conversation history -• /copy: Copy the last agent message to the clipboard -• /log: Show path to current interaction log file -• /debug: Toggle debug console -• /compact: Compact conversation history by summarizing. Optionally pass instructions to guide the summary -• /exit: Exit the application -• /status: Display agent statistics -• /proxy-setup: Configure proxy and SSL certificate settings -• /continue/resume: Browse and resume past sessions -• /rename: Rename the current session -• /connectors/mcp: Display available MCP servers and connectors. Pass a name to list its tools -• /voice: Configure voice settings -• /leanstall: Install the Lean 4 agent (leanstral) -• /unleanstall: Uninstall the Lean 4 agent -• /rewind: Rewind to a previous message -• /loop: Schedule a recurring prompt. Use /loop <interval> <prompt>/loop list, or /loop cancel <id|all> -• /data-retention: Show data retention information + 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) +• 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 + +Special Features + +• !<command> Execute bash command directly +• @path/to/file/ Autocompletes file paths + +Commands + +• /help: Show help message +• /config: Edit config settings +• /model: Select active model +• /thinking: Select thinking level +• /reload: Reload configuration, agent instructions, and skills from disk +• /clear: Clear conversation history +• /copy: Copy the last agent message to the clipboard +• /log: Show path to current interaction log file +• /debug: Toggle debug console +• /compact: Compact conversation history by summarizing. Optionally pass instructions to guide the summary +• /exit: Exit the application +• /status: Display agent statistics +• /proxy-setup: Configure proxy and SSL certificate settings +• /continue/resume: Browse and resume past sessions +• /rename: Rename the current session +• /connectors/mcp: Display available MCP servers and connectors. Pass a name to list its tools +• /voice: Configure voice settings +• /leanstall: Install the Lean 4 agent (leanstral) +• /unleanstall: Uninstall the Lean 4 agent +• /rewind: Rewind to a previous message +• /loop: Schedule a recurring prompt. Use /loop <interval> <prompt>/loop list, or /loop cancel <id|all> +• /data-retention: Show data retention information +• /theme: Select theme -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── 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_visible_for_pro_account.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_command_visible_for_pro_account.svg index 64512d0..1f150fb 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 @@ -33,12 +33,11 @@ } .terminal-r1 { fill: #c5c8c6 } -.terminal-r2 { fill: #9a9b99 } -.terminal-r3 { fill: #608ab1;font-weight: bold } -.terminal-r4 { fill: #98a84b;font-weight: bold } -.terminal-r5 { fill: #4b4e55 } -.terminal-r6 { fill: #292929 } -.terminal-r7 { fill: #ff8205;font-weight: bold } +.terminal-r2 { fill: #98a84b;font-weight: bold } +.terminal-r3 { fill: #608ab1 } +.terminal-r4 { fill: #292929 } +.terminal-r5 { fill: #ff8205;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -196,56 +195,56 @@ - + - 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) -• 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 - -Special Features - -• !<command> Execute bash command directly -• @path/to/file/ Autocompletes file paths - -Commands - -• /help: Show help message -• /config: Edit config settings -• /model: Select active model -• /thinking: Select thinking level -• /reload: Reload configuration, agent instructions, and skills from disk -• /clear: Clear conversation history -• /copy: Copy the last agent message to the clipboard -• /log: Show path to current interaction log file -• /debug: Toggle debug console -• /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 -• /proxy-setup: Configure proxy and SSL certificate settings -• /continue/resume: Browse and resume past sessions -• /rename: Rename the current session -• /connectors/mcp: Display available MCP servers and connectors. Pass a name to list its tools -• /voice: Configure voice settings -• /leanstall: Install the Lean 4 agent (leanstral) -• /unleanstall: Uninstall the Lean 4 agent -• /rewind: Rewind to a previous message -• /loop: Schedule a recurring prompt. Use /loop <interval> <prompt>/loop list, or /loop cancel <id|all> -• /data-retention: Show data retention information + • 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 +• Shift+Tab Cycle through agents (default, plan, ...) +• Alt+↑↓ / Ctrl+P/N Rewind to previous/next message + +Special Features + +• !<command> Execute bash command directly +• @path/to/file/ Autocompletes file paths + +Commands + +• /help: Show help message +• /config: Edit config settings +• /model: Select active model +• /thinking: Select thinking level +• /reload: Reload configuration, agent instructions, and skills from disk +• /clear: Clear conversation history +• /copy: Copy the last agent message to the clipboard +• /log: Show path to current interaction log file +• /debug: Toggle debug console +• /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 +• /proxy-setup: Configure proxy and SSL certificate settings +• /continue/resume: Browse and resume past sessions +• /rename: Rename the current session +• /connectors/mcp: Display available MCP servers and connectors. Pass a name to list its tools +• /voice: Configure voice settings +• /leanstall: Install the Lean 4 agent (leanstral) +• /unleanstall: Uninstall the Lean 4 agent +• /rewind: Rewind to a previous message +• /loop: Schedule a recurring prompt. Use /loop <interval> <prompt>/loop list, or /loop cancel <id|all> +• /data-retention: Show data retention information +• /theme: Select theme -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── 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_push_confirmation_cancel_selected.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_cancel_selected.svg index 0cb4a50..bbe32b3 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_cancel_selected.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_cancel_selected.svg @@ -32,10 +32,11 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #a0a0a0 } @@ -93,7 +94,7 @@   1. Push and continue › 2. Cancel -↑↓ navigate  Enter select  Esc cancel +↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_multiple_commits.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_multiple_commits.svg index 33d1334..26c184d 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_multiple_commits.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_multiple_commits.svg @@ -32,10 +32,11 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #a0a0a0 } @@ -93,7 +94,7 @@ › 1. Push and continue   2. Cancel -↑↓ navigate  Enter select  Esc cancel +↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_single_commit.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_single_commit.svg index 6757c58..ddcadbc 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_single_commit.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_push_confirmation_single_commit.svg @@ -32,10 +32,11 @@ font-family: arial; } - .terminal-r1 { fill: #625e4c } + .terminal-r1 { fill: #8d8d8d } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #9d65ff;font-weight: bold } -.terminal-r4 { fill: #d9d9d9 } +.terminal-r3 { fill: #0178d4;font-weight: bold } +.terminal-r4 { fill: #e0e0e0 } +.terminal-r5 { fill: #a0a0a0 } @@ -93,7 +94,7 @@ › 1. Push and continue   2. Cancel -↑↓ navigate  Enter select  Esc cancel +↑↓ navigate  Enter select  Esc cancel └──────────────────────────────────────────────────────────────────────────────┘ diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_auth_complete.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_auth_complete.svg index 2c03617..ad90139 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_auth_complete.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_auth_complete.svg @@ -32,7 +32,7 @@ font-family: arial; } - .terminal-r1 { fill: #d9d9d9 } + .terminal-r1 { fill: #e0e0e0 } .terminal-r2 { fill: #c5c8c6 } diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_auth_required.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_auth_required.svg index 0286c4a..3b7879c 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_auth_required.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_auth_required.svg @@ -32,7 +32,7 @@ font-family: arial; } - .terminal-r1 { fill: #d9d9d9 } + .terminal-r1 { fill: #e0e0e0 } .terminal-r2 { fill: #c5c8c6 } diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_checking_git.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_checking_git.svg index 0c713cd..627ba1d 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_checking_git.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_checking_git.svg @@ -32,7 +32,7 @@ font-family: arial; } - .terminal-r1 { fill: #d9d9d9 } + .terminal-r1 { fill: #e0e0e0 } .terminal-r2 { fill: #c5c8c6 } 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 d3bdf13..a40de48 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 @@ -32,9 +32,9 @@ font-family: arial; } - .terminal-r1 { fill: #98e024 } + .terminal-r1 { fill: #4ebf71 } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #d9d9d9 } +.terminal-r3 { fill: #e0e0e0 } diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_error.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_error.svg index 5b34dec..ea4f1aa 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_error.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_error.svg @@ -32,9 +32,9 @@ font-family: arial; } - .terminal-r1 { fill: #f4005f } + .terminal-r1 { fill: #b93c5b } .terminal-r2 { fill: #c5c8c6 } -.terminal-r3 { fill: #d9d9d9 } +.terminal-r3 { fill: #e0e0e0 } diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_pushing.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_pushing.svg index 052a717..f554b3d 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_pushing.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_pushing.svg @@ -32,7 +32,7 @@ font-family: arial; } - .terminal-r1 { fill: #d9d9d9 } + .terminal-r1 { fill: #e0e0e0 } .terminal-r2 { fill: #c5c8c6 } diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_sending_token.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_sending_token.svg index 66ec512..60b7073 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_sending_token.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_sending_token.svg @@ -32,7 +32,7 @@ font-family: arial; } - .terminal-r1 { fill: #d9d9d9 } + .terminal-r1 { fill: #e0e0e0 } .terminal-r2 { fill: #c5c8c6 } diff --git a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_starting_workflow.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_starting_workflow.svg index 161a89f..d8ac257 100644 --- a/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_starting_workflow.svg +++ b/tests/snapshots/__snapshots__/test_ui_snapshot_teleport/test_snapshot_teleport_status_starting_workflow.svg @@ -32,7 +32,7 @@ font-family: arial; } - .terminal-r1 { fill: #d9d9d9 } + .terminal-r1 { fill: #e0e0e0 } .terminal-r2 { fill: #c5c8c6 } 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 ecf49d7..bc38ae4 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 @@ -36,7 +36,7 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #868887 } 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 4f31150..3b5dd41 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 @@ -35,7 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #868887 } @@ -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 2113207..809cc8e 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 @@ -35,7 +35,7 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } -.terminal-r4 { fill: #9a9b99 } +.terminal-r4 { fill: #868887 } @@ -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 d4067d0..39dd22b 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 @@ -36,7 +36,8 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -185,17 +186,17 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆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 8b7fef6..16e7e12 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 @@ -36,7 +36,7 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #868887 } 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 30c54d8..a584e25 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 @@ -36,7 +36,8 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -185,17 +186,17 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/voice -Voice settings opened... -Voice mode disabled. +/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 ebee884..45a9efc 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 @@ -36,7 +36,8 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #9a9b99 } +.terminal-r5 { fill: #c5c8c6;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -185,17 +186,17 @@   ⠉⠒⠣⠤⠵⠤⠬⠮⠆Type /help for more information -/voice -Voice settings opened... -Voice mode enabled. Press ctrl+r to start recording. +/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 3028486..e111519 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 @@ -36,8 +36,8 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #608ab1;font-weight: bold } -.terminal-r6 { fill: #9a9b99 } +.terminal-r5 { fill: #98729f;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -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 3ab2300..31cda8d 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 @@ -36,9 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #608ab1;font-weight: bold } -.terminal-r6 { fill: #c5c8c6;font-weight: bold;text-decoration: underline; } -.terminal-r7 { fill: #9a9b99 } +.terminal-r5 { fill: #98729f;font-weight: bold } +.terminal-r6 { fill: #608ab1 } +.terminal-r7 { fill: #608ab1;text-decoration: underline; } +.terminal-r8 { fill: #868887 } @@ -188,16 +189,16 @@ • Feature 2 • Feature 3 -Switch to your Le Chat Pro API key +Switch to your Le Chat Pro API key -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── 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_upgrade_message.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_upgrade_message.svg index cbcca0d..69b2250 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 @@ -36,9 +36,10 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #608ab1;font-weight: bold } -.terminal-r6 { fill: #c5c8c6;font-weight: bold;text-decoration: underline; } -.terminal-r7 { fill: #9a9b99 } +.terminal-r5 { fill: #98729f;font-weight: bold } +.terminal-r6 { fill: #608ab1 } +.terminal-r7 { fill: #608ab1;text-decoration: underline; } +.terminal-r8 { fill: #868887 } @@ -188,16 +189,16 @@ • Feature 2 • Feature 3 -Unlock more with Vibe - Upgrade to Le Chat Pro +Unlock more with Vibe - Upgrade to Le Chat Pro -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── 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_whats_new_message.svg b/tests/snapshots/__snapshots__/test_ui_snapshot_whats_new/test_snapshot_shows_whats_new_message.svg index 57494bd..808192d 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 @@ -36,8 +36,8 @@ .terminal-r2 { fill: #ff8205;font-weight: bold } .terminal-r3 { fill: #68a0b3 } .terminal-r4 { fill: #ff8205 } -.terminal-r5 { fill: #608ab1;font-weight: bold } -.terminal-r6 { fill: #9a9b99 } +.terminal-r5 { fill: #98729f;font-weight: bold } +.terminal-r6 { fill: #868887 } @@ -191,11 +191,11 @@ -┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ -> - - -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────────────────────────────────────────────────────── default ─┐ +> + + +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ /test/workdir0% of 200k tokens diff --git a/tests/snapshots/test_ui_snapshot_feedback_bar.py b/tests/snapshots/test_ui_snapshot_feedback_bar.py index 371b6d7..8b601f8 100644 --- a/tests/snapshots/test_ui_snapshot_feedback_bar.py +++ b/tests/snapshots/test_ui_snapshot_feedback_bar.py @@ -14,13 +14,8 @@ from vibe.core.telemetry.send import TelemetryClient @pytest.fixture(autouse=True) def _enable_feedback_bar(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr( - "vibe.cli.textual_ui.widgets.feedback_bar_manager.FEEDBACK_PROBABILITY", 1 - ) - monkeypatch.setattr( - "vibe.cli.textual_ui.widgets.feedback_bar_manager.MIN_USER_MESSAGES_FOR_FEEDBACK", - 1, - ) + monkeypatch.setattr("vibe.core.feedback.FEEDBACK_PROBABILITY", 1) + monkeypatch.setattr("vibe.core.feedback.MIN_USER_MESSAGES_FOR_FEEDBACK", 1) monkeypatch.setattr(TelemetryClient, "is_active", lambda self: True) @@ -38,10 +33,7 @@ class FeedbackBarSnapshotApp(BaseSnapshotTestApp): def test_snapshot_feedback_bar_visible(snap_compare: SnapCompare) -> None: async def run_before(pilot: Pilot) -> None: - with patch( - "vibe.cli.textual_ui.widgets.feedback_bar_manager.random.random", - return_value=0, - ): + with patch("vibe.core.feedback.random.random", return_value=0): await pilot.press(*"Hello") await pilot.press("enter") await pilot.pause(0.4) diff --git a/tests/snapshots/test_ui_snapshot_mcp_command.py b/tests/snapshots/test_ui_snapshot_mcp_command.py index 483232b..e5e6f42 100644 --- a/tests/snapshots/test_ui_snapshot_mcp_command.py +++ b/tests/snapshots/test_ui_snapshot_mcp_command.py @@ -12,6 +12,7 @@ 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" @@ -33,6 +34,10 @@ _FAKE_CONNECTORS_MIXED_CONNECTION = { "alpha": [RemoteTool(name="lookup", description="Lookup Alpha records")], "beta": [], } +_FAKE_CONNECTOR_AUTH_ACTIONS = { + "beta": ConnectorAuthAction.OAUTH, + "zeta": ConnectorAuthAction.CREDENTIALS_SETUP, +} class SnapshotTestAppNoMcpServers(BaseSnapshotTestApp): @@ -200,10 +205,17 @@ def test_snapshot_mcp_refresh_shortcut(snap_compare: SnapCompare) -> None: class SnapshotTestAppWithConnectors(BaseSnapshotTestApp): def __init__(self) -> None: + from vibe.core.config import ConnectorConfig + config = default_config() config.mcp_servers = [ MCPStdio(name="filesystem", transport="stdio", command="npx") ] + # Explicitly enable all fake connectors so they appear enabled in snapshots + config.connectors = [ + ConnectorConfig(name="gmail", disabled=False), + ConnectorConfig(name="slack", disabled=False), + ] super().__init__(config=config) registry = FakeConnectorRegistry(connectors=_FAKE_CONNECTORS) self.agent_loop.connector_registry = registry @@ -213,8 +225,15 @@ class SnapshotTestAppWithConnectors(BaseSnapshotTestApp): class SnapshotTestAppConnectorsOnly(BaseSnapshotTestApp): def __init__(self) -> None: + from vibe.core.config import ConnectorConfig + config = default_config() config.mcp_servers = [] + # Explicitly enable all fake connectors so they appear enabled in snapshots + config.connectors = [ + ConnectorConfig(name="gmail", disabled=False), + ConnectorConfig(name="slack", disabled=False), + ] super().__init__(config=config) registry = FakeConnectorRegistry(connectors=_FAKE_CONNECTORS) self.agent_loop.connector_registry = registry @@ -224,10 +243,22 @@ class SnapshotTestAppConnectorsOnly(BaseSnapshotTestApp): class SnapshotTestAppConnectorsMixedState(BaseSnapshotTestApp): def __init__(self) -> None: + from vibe.core.config import ConnectorConfig + config = default_config() config.mcp_servers = [] + # Explicitly enable connectors that should appear connected in snapshots + # alpha is connected, beta and zeta are disconnected + config.connectors = [ + ConnectorConfig(name="alpha", disabled=False), + ConnectorConfig(name="beta", disabled=False), + ConnectorConfig(name="zeta", disabled=False), + ] super().__init__(config=config) - registry = FakeConnectorRegistry(connectors=_FAKE_CONNECTORS_MIXED_CONNECTION) + registry = FakeConnectorRegistry( + connectors=_FAKE_CONNECTORS_MIXED_CONNECTION, + auth_actions=_FAKE_CONNECTOR_AUTH_ACTIONS, + ) self.agent_loop.connector_registry = registry self.agent_loop.tool_manager._connector_registry = registry self.agent_loop.tool_manager.integrate_connectors() @@ -318,9 +349,7 @@ def test_snapshot_connector_auth_back_to_mcp(snap_compare: SnapCompare) -> None: ) -def test_snapshot_mcp_help_bar_shows_authenticate(snap_compare: SnapCompare) -> None: - """Help bar shows 'Enter Authenticate' when a disconnected connector is highlighted.""" - +def test_snapshot_mcp_help_bar_shows_connect(snap_compare: SnapCompare) -> None: async def run_before(pilot: Pilot) -> None: await _run_mcp_command(pilot, "/mcp") # Navigate to a disconnected connector (beta or zeta) diff --git a/tests/snapshots/test_ui_snapshot_onboarding_api_key.py b/tests/snapshots/test_ui_snapshot_onboarding_api_key.py new file mode 100644 index 0000000..47ea91f --- /dev/null +++ b/tests/snapshots/test_ui_snapshot_onboarding_api_key.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from textual.app import App +from textual.pilot import Pilot + +from tests.snapshots.snap_compare import SnapCompare +from vibe.core.config import ProviderConfig +from vibe.core.types import Backend +from vibe.setup.onboarding.screens.api_key import ApiKeyScreen + + +class ApiKeyScreenSnapshotApp(App[str | None]): + CSS_PATH = "../../vibe/setup/onboarding/onboarding.tcss" + + def on_mount(self) -> None: + self.theme = "ansi-dark" + self.push_screen( + ApiKeyScreen( + ProviderConfig( + name="mistral", + api_base="https://api.mistral.ai/v1", + api_key_env_var="MISTRAL_API_KEY", + backend=Backend.MISTRAL, + ) + ) + ) + + +def test_snapshot_onboarding_api_key_with_valid_input( + snap_compare: SnapCompare, +) -> None: + async def run_before(pilot: Pilot) -> None: + await pilot.pause(0.2) + await pilot.press(*"sk-test-api-key") + await pilot.pause(0.1) + + assert snap_compare( + "test_ui_snapshot_onboarding_api_key.py:ApiKeyScreenSnapshotApp", + terminal_size=(80, 24), + run_before=run_before, + ) diff --git a/tests/snapshots/test_ui_snapshot_onboarding_theme.py b/tests/snapshots/test_ui_snapshot_onboarding_theme.py new file mode 100644 index 0000000..1f6d0c6 --- /dev/null +++ b/tests/snapshots/test_ui_snapshot_onboarding_theme.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import pytest +from textual.pilot import Pilot + +from tests.snapshots.snap_compare import SnapCompare +from vibe.setup.onboarding import OnboardingApp +from vibe.setup.onboarding.screens.theme_selection import ThemeSelectionScreen + + +async def _advance_to_theme_screen(pilot: Pilot) -> None: + welcome = pilot.app.get_screen("welcome") + for _ in range(40): + await pilot.pause(0.1) + if not welcome.query_one("#enter-hint").has_class("hidden"): + break + await pilot.press("enter") + for _ in range(20): + if isinstance(pilot.app.screen, ThemeSelectionScreen): + return + await pilot.pause(0.1) + + +@pytest.mark.parametrize("theme", ["ansi-dark", "ansi-light", "dracula"]) +def test_snapshot_onboarding_theme_selection( + snap_compare: SnapCompare, theme: str +) -> None: + async def run_before(pilot: Pilot) -> None: + await _advance_to_theme_screen(pilot) + pilot.app.theme = theme + await pilot.pause(0.2) + + assert snap_compare(OnboardingApp(), terminal_size=(80, 30), run_before=run_before) diff --git a/tests/stubs/fake_connector_registry.py b/tests/stubs/fake_connector_registry.py index f03749b..95ed298 100644 --- a/tests/stubs/fake_connector_registry.py +++ b/tests/stubs/fake_connector_registry.py @@ -2,6 +2,7 @@ from __future__ import annotations from vibe.core.tools.base import BaseTool from vibe.core.tools.connectors.connector_registry import ( + ConnectorAuthAction, ConnectorRegistry, RemoteTool, _normalize_name, @@ -12,15 +13,21 @@ from vibe.core.tools.connectors.connector_registry import ( class FakeConnectorRegistry(ConnectorRegistry): """Test double that returns canned connector tools without hitting the API.""" - def __init__(self, connectors: dict[str, list[RemoteTool]] | None = None) -> None: + def __init__( + self, + connectors: dict[str, list[RemoteTool]] | None = None, + auth_actions: dict[str, ConnectorAuthAction] | None = None, + ) -> None: super().__init__(api_key="fake-key") self._fake_connectors = connectors or {} + self._fake_auth_actions = auth_actions or {} self._build_cache() def _build_cache(self) -> None: self._cache = {} self._connector_names = [] self._connector_connected = {} + self._connector_auth_action = {} self._alias_to_id = {} for connector_name, tools in self._fake_connectors.items(): alias = _normalize_name(connector_name) @@ -39,6 +46,9 @@ class FakeConnectorRegistry(ConnectorRegistry): self._cache[connector_id] = tool_map self._connector_names.append(alias) self._connector_connected[alias] = bool(tool_map) + self._connector_auth_action[alias] = self._fake_auth_actions.get( + connector_name, ConnectorAuthAction.NONE + ) def get_tools(self) -> dict[str, type[BaseTool]]: if self._cache is None: diff --git a/tests/test_system_prompt.py b/tests/test_system_prompt.py index 3252920..cd25c6b 100644 --- a/tests/test_system_prompt.py +++ b/tests/test_system_prompt.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import date import sys import pytest @@ -123,3 +124,25 @@ def test_headless_section_absent_by_default() -> None: ) assert "Headless Mode" not in prompt + + +def test_current_date_placeholder_substituted_in_prompt() -> None: + config = build_test_vibe_config( + system_prompt_id="cli", + include_project_context=False, + include_prompt_detail=False, + include_model_info=False, + include_commit_signature=False, + ) + tool_manager = ToolManager(lambda: config) + skill_manager = SkillManager(lambda: config) + agent_manager = AgentManager(lambda: config) + + prompt = get_universal_system_prompt( + tool_manager, config, skill_manager, agent_manager + ) + + today = date.today() + expected = f"Today's date is {today.isoformat()} ({today.strftime('%A')})." + assert expected in prompt + assert "$current_date" not in prompt diff --git a/tests/tools/test_connectors.py b/tests/tools/test_connectors.py index 7e89b83..dae9784 100644 --- a/tests/tools/test_connectors.py +++ b/tests/tools/test_connectors.py @@ -13,6 +13,7 @@ from tests.stubs.fake_mcp_registry import FakeMCPRegistry from vibe.core.config import ConnectorConfig, VibeConfig from vibe.core.tools.base import BaseToolConfig, ToolError from vibe.core.tools.connectors.connector_registry import ( + ConnectorAuthAction, ConnectorRegistry, RemoteTool, _connector_error_message, @@ -424,7 +425,7 @@ class TestConnectorDisableFiltering: assert "connector_mail_send" not in tm.available_tools assert "connector_mail_read" in tm.available_tools - def test_no_config_means_all_enabled(self) -> None: + def test_no_config_means_all_disabled_by_default(self) -> None: registry = FakeConnectorRegistry( connectors={"wiki": [RemoteTool(name="search", description="Search")]} ) @@ -434,7 +435,10 @@ class TestConnectorDisableFiltering: mcp_registry=FakeMCPRegistry(), connector_registry=registry, ) - assert "connector_wiki_search" in tm.available_tools + # Connectors without config entries are disabled by default + assert "connector_wiki_search" not in tm.available_tools + # But still registered (discoverable for UI) + assert "connector_wiki_search" in tm.registered_tools def test_unrelated_config_does_not_affect_other_connectors(self) -> None: registry = FakeConnectorRegistry( @@ -443,8 +447,12 @@ class TestConnectorDisableFiltering: "mail": [RemoteTool(name="send", description="Send")], } ) + # Explicitly enable wiki, disable mail config = self._make_config( - connectors=[ConnectorConfig(name="mail", disabled=True)] + connectors=[ + ConnectorConfig(name="mail", disabled=True), + ConnectorConfig(name="wiki", disabled=False), + ] ) tm = ToolManager( config_getter=lambda: config, @@ -475,6 +483,7 @@ def _make_connector_payload( is_ready: bool = True, tools: list[dict[str, Any]] | None = None, bootstrap_errors: list[str] | None = None, + auth_action: dict[str, Any] | None = None, ) -> dict[str, Any]: return { "id": connector_id, @@ -484,6 +493,7 @@ def _make_connector_payload( "status": {"is_ready": is_ready}, "tools": tools or [], "bootstrap_errors": bootstrap_errors, + "auth_action": auth_action, } @@ -656,3 +666,219 @@ class TestBootstrapDiscovery: refreshed = await registry.refresh_connector_async("wiki") assert "connector_wiki_search" in refreshed assert "connector_wiki_write" in refreshed + + +# --------------------------------------------------------------------------- +# Auth-actionable connector discovery +# --------------------------------------------------------------------------- + + +class TestAuthActionablediscovery: + @respx.mock + @pytest.mark.asyncio + async def test_bootstrap_url_opts_into_auth_actionable_connectors(self) -> None: + payload = _make_bootstrap_response([]) + route = respx.get(_BOOTSTRAP_URL).mock( + return_value=httpx.Response(200, json=payload) + ) + + registry = ConnectorRegistry(api_key="test-key") + await registry.get_tools_async() + + assert route.called + called_url = str(route.calls.last.request.url) + assert "include_auth_actionable_connectors=true" in called_url + + @respx.mock + @pytest.mark.asyncio + async def test_oauth_connector_is_discovered_but_disconnected(self) -> None: + payload = _make_bootstrap_response([ + _make_connector_payload( + name="linear", is_ready=False, tools=[], auth_action={"type": "oauth"} + ) + ]) + respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload)) + + registry = ConnectorRegistry(api_key="test-key") + tools = await registry.get_tools_async() + + assert tools == {} + assert "linear" in registry.get_connector_names() + assert not registry.is_connected("linear") + assert registry.get_auth_action("linear") == ConnectorAuthAction.OAUTH + + @respx.mock + @pytest.mark.asyncio + async def test_credentials_setup_connector_is_discovered_but_disconnected( + self, + ) -> None: + payload = _make_bootstrap_response([ + _make_connector_payload( + name="custom_crm", + is_ready=False, + tools=[], + auth_action={"type": "credentials_setup"}, + ) + ]) + respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload)) + + registry = ConnectorRegistry(api_key="test-key") + await registry.get_tools_async() + + assert "custom_crm" in registry.get_connector_names() + assert not registry.is_connected("custom_crm") + assert ( + registry.get_auth_action("custom_crm") + == ConnectorAuthAction.CREDENTIALS_SETUP + ) + + @respx.mock + @pytest.mark.asyncio + async def test_ready_connector_has_no_auth_action(self) -> None: + payload = _make_bootstrap_response([ + _make_connector_payload(name="wiki", tools=[_make_tool_payload("search")]) + ]) + respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload)) + + registry = ConnectorRegistry(api_key="test-key") + await registry.get_tools_async() + + assert registry.is_connected("wiki") + assert registry.get_auth_action("wiki") == ConnectorAuthAction.NONE + + @respx.mock + @pytest.mark.asyncio + async def test_degraded_connector_has_no_auth_action(self) -> None: + payload = _make_bootstrap_response([ + _make_connector_payload( + name="degraded", + is_ready=False, + tools=[], + bootstrap_errors=["tools_or_system_prompt_failed: timeout"], + auth_action=None, + ) + ]) + respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload)) + + registry = ConnectorRegistry(api_key="test-key") + await registry.get_tools_async() + + assert "degraded" in registry.get_connector_names() + assert not registry.is_connected("degraded") + assert registry.get_auth_action("degraded") == ConnectorAuthAction.NONE + + @respx.mock + @pytest.mark.asyncio + async def test_unknown_auth_action_type_is_treated_as_none(self) -> None: + payload = _make_bootstrap_response([ + _make_connector_payload( + name="weird", + is_ready=False, + tools=[], + auth_action={"type": "magic_link"}, + ) + ]) + respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload)) + + registry = ConnectorRegistry(api_key="test-key") + await registry.get_tools_async() + + assert registry.get_auth_action("weird") == ConnectorAuthAction.NONE + + @respx.mock + @pytest.mark.asyncio + async def test_refresh_picks_up_oauth_completed(self) -> None: + payload = _make_bootstrap_response([ + _make_connector_payload( + connector_id="c-1", + name="linear", + is_ready=False, + tools=[], + auth_action={"type": "oauth"}, + ) + ]) + respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload)) + + registry = ConnectorRegistry(api_key="test-key") + await registry.get_tools_async() + assert registry.get_auth_action("linear") == ConnectorAuthAction.OAUTH + + refresh_payload = _make_bootstrap_response([ + _make_connector_payload( + connector_id="c-1", + name="linear", + tools=[_make_tool_payload("search_issues")], + ) + ]) + respx.get(_BOOTSTRAP_URL).mock( + return_value=httpx.Response(200, json=refresh_payload) + ) + + refreshed = await registry.refresh_connector_async("linear") + assert "connector_linear_search_issues" in refreshed + assert registry.is_connected("linear") + assert registry.get_auth_action("linear") == ConnectorAuthAction.NONE + + def test_get_auth_action_unknown_alias_returns_none(self) -> None: + registry = ConnectorRegistry(api_key="test-key") + assert registry.get_auth_action("nobody") == ConnectorAuthAction.NONE + + @respx.mock + @pytest.mark.asyncio + async def test_refresh_drops_connector_when_server_no_longer_lists_it(self) -> None: + payload = _make_bootstrap_response([ + _make_connector_payload( + connector_id="c-1", + name="linear", + is_ready=False, + tools=[], + auth_action={"type": "oauth"}, + ) + ]) + respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload)) + + registry = ConnectorRegistry(api_key="test-key") + await registry.get_tools_async() + assert "linear" in registry.get_connector_names() + + # Server now returns an empty connector list — the connector was + # deleted or revoked. Local state must drop it entirely. + respx.get(_BOOTSTRAP_URL).mock( + return_value=httpx.Response(200, json=_make_bootstrap_response([])) + ) + + refreshed = await registry.refresh_connector_async("linear") + + assert refreshed == {} + assert "linear" not in registry.get_connector_names() + assert not registry.is_connected("linear") + assert registry.get_auth_action("linear") == ConnectorAuthAction.NONE + assert registry.get_connector_id("linear") is None + + @respx.mock + @pytest.mark.asyncio + async def test_refresh_keeps_cached_auth_action_when_fetch_fails(self) -> None: + payload = _make_bootstrap_response([ + _make_connector_payload( + connector_id="c-1", + name="linear", + is_ready=False, + tools=[], + auth_action={"type": "oauth"}, + ) + ]) + respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(200, json=payload)) + + registry = ConnectorRegistry(api_key="test-key") + await registry.get_tools_async() + assert registry.get_auth_action("linear") == ConnectorAuthAction.OAUTH + + # Bootstrap fails on refresh — cached state must survive. + respx.get(_BOOTSTRAP_URL).mock(return_value=httpx.Response(500)) + + refreshed = await registry.refresh_connector_async("linear") + + assert refreshed == {} + assert "linear" in registry.get_connector_names() + assert registry.get_auth_action("linear") == ConnectorAuthAction.OAUTH + assert registry.get_connector_id("linear") == "c-1" diff --git a/tests/tools/test_grep.py b/tests/tools/test_grep.py index a727502..1605e87 100644 --- a/tests/tools/test_grep.py +++ b/tests/tools/test_grep.py @@ -198,6 +198,21 @@ async def test_uses_effective_workdir(tmp_path, monkeypatch): assert "test.py" in result.matches +@pytest.mark.asyncio +async def test_single_file_match_includes_filename_in_output(grep, tmp_path): + # Without --with-filename / -H, rg and grep omit the filename when + # searching a single file, causing GrepMatch.from_output_line to + # misinterpret the line number as a path. See VIBE-2772. + (tmp_path / "only.py").write_text("hit one\nnope\nhit two\n") + + result = await collect_result(grep.run(GrepArgs(pattern="hit", path="only.py"))) + + assert result.match_count == 2 + for parsed in result.parsed_matches: + assert parsed.path.endswith("only.py") + assert parsed.line is not None + + @pytest.mark.skipif(not shutil.which("grep"), reason="GNU grep not available") class TestGnuGrepBackend: @pytest.mark.asyncio diff --git a/uv.lock b/uv.lock index 14a21fb..f10ec97 100644 --- a/uv.lock +++ b/uv.lock @@ -3,11 +3,12 @@ revision = 3 requires-python = ">=3.12" [options] -exclude-newer = "2026-05-13T08:56:52.756107Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P7D" [options.exclude-newer-package] mistralai = false +textual = false agent-client-protocol = false [[package]] @@ -820,7 +821,7 @@ wheels = [ [[package]] name = "mistral-vibe" -version = "2.10.1" +version = "2.11.0" source = { editable = "." } dependencies = [ { name = "agent-client-protocol" }, @@ -1029,7 +1030,7 @@ requires-dist = [ { name = "soupsieve", specifier = "==2.8.3" }, { name = "sse-starlette", specifier = "==3.4.1" }, { name = "starlette", specifier = "==1.0.0" }, - { name = "textual", specifier = "==8.2.4" }, + { name = "textual", specifier = "==8.2.7" }, { name = "textual-speedups", specifier = "==0.2.1" }, { name = "tomli-w", specifier = "==1.2.0" }, { name = "tree-sitter", specifier = "==0.25.2" }, @@ -2060,7 +2061,7 @@ wheels = [ [[package]] name = "textual" -version = "8.2.4" +version = "8.2.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", extra = ["linkify"] }, @@ -2070,9 +2071,9 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/89/bec5709fb759f9c784bbcb30b2e3497df3f901691d13c2b864dbf6694a17/textual-8.2.4.tar.gz", hash = "sha256:d4e2b2ddd7157191d00b228592b7c739ea080b7d792fd410f23ca75f05ea76c4", size = 1848933, upload-time = "2026-04-19T04:20:45.845Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/7a/c519db0aba5024f86e71e9631810bfdd6866ed2c8695bd7fa34b90e7ef59/textual-8.2.7.tar.gz", hash = "sha256:658f568ff81e30ed43890c3e07520390e5cf1b4763822006e060656b0a88f105", size = 1859249, upload-time = "2026-05-19T10:52:49.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/32/02932f0d597cdbb34e34bf24266ff0f2cf292ccb3aafc37dd9efcb0cc416/textual-8.2.4-py3-none-any.whl", hash = "sha256:a83bd3f0cc7125ca203845af753f9d6b6be030025ecd1b05cc75ebe645b9c4ba", size = 724390, upload-time = "2026-04-19T04:20:49.968Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f5/c1e18bc0707300a0e90204343abbf7d7acd6fb7ebe03a6d4893b99a234b8/textual-8.2.7-py3-none-any.whl", hash = "sha256:4caaa13a90bc4cf9c6c862c067ccd34fe84e9c161710a2a907a8026313b6bd73", size = 731129, upload-time = "2026-05-19T10:52:51.773Z" }, ] [[package]] diff --git a/vibe/__init__.py b/vibe/__init__.py index 972866a..d3cdcc6 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.10.1" +__version__ = "2.11.0" diff --git a/vibe/acp/acp_agent_loop.py b/vibe/acp/acp_agent_loop.py index 2dc9c97..5708f7e 100644 --- a/vibe/acp/acp_agent_loop.py +++ b/vibe/acp/acp_agent_loop.py @@ -123,6 +123,7 @@ from vibe.core.config import ( load_dotenv_values, ) 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.proxy_setup import ( ProxySetupError, @@ -464,9 +465,11 @@ class VibeAcpAgentLoop(AcpAgent): field_meta={ "browser-auth-delegated": { "attemptId": attempt.process_id, - "expiresAt": attempt.expires_at.astimezone(UTC) - .isoformat() - .replace("+00:00", "Z"), + "expiresAt": ( + attempt.expires_at.astimezone(UTC) + .isoformat() + .replace("+00:00", "Z") + ), "signInUrl": attempt.sign_in_url, } } @@ -1102,12 +1105,28 @@ class VibeAcpAgentLoop(AcpAgent): raise InternalError(str(e)) from e self._send_usage_update(session) + meta = self._build_end_turn_meta(session) return PromptResponse( stop_reason="end_turn", usage=self._build_usage(session), user_message_id=resolved_message_id, + field_meta=meta or None, ) + def _build_end_turn_meta(self, session: AcpSessionLoop) -> dict[str, Any] | None: + agent_loop = session.agent_loop + user_message_count = ( + sum(m.role == Role.user and not m.injected for m in agent_loop.messages) + 1 + ) # +1 for the message just sent + if not should_show_feedback( + telemetry_active=agent_loop.telemetry_client.is_active(), + is_mistral_model=agent_loop.config.is_active_model_mistral(), + user_message_count=user_message_count, + ): + return None + record_feedback_asked() + return {"show_feedback_prompt": True} + def _build_text_prompt(self, acp_prompt: list[ContentBlock]) -> str: def _is_automatic_resource(block: ContentBlock) -> bool: return block.type == "resource" and bool( diff --git a/vibe/acp/tools/builtins/bash.py b/vibe/acp/tools/builtins/bash.py index bdaaf15..b950806 100644 --- a/vibe/acp/tools/builtins/bash.py +++ b/vibe/acp/tools/builtins/bash.py @@ -15,7 +15,7 @@ from acp.schema import ( from vibe import VIBE_ROOT from vibe.acp.tools.base import AcpToolState, BaseAcpTool from vibe.acp.tools.events import ToolTerminalOpenedEvent -from vibe.acp.tools.session_update import resolve_kind +from vibe.acp.tools.session_update import failed_tool_result, resolve_kind from vibe.core.logger import logger from vibe.core.tools.base import BaseToolState, InvokeContext, ToolError from vibe.core.tools.builtins.bash import Bash as CoreBashTool, BashArgs, BashResult @@ -118,9 +118,13 @@ class Bash(CoreBashTool, BaseAcpTool[AcpBashState]): @classmethod def tool_call_session_update(cls, event: ToolCallEvent) -> ToolCallStart | None: if event.args is None: + # Title is left empty until args resolve so clients don't briefly + # render the tool name (e.g. "bash") as a stand-in command while + # the LLM is still streaming BashArgs. ACP requires title to be a + # str, so we use "" as the "unknown yet" sentinel. return ToolCallStart( session_update="tool_call", - title="bash", + title="", tool_call_id=event.tool_call_id, kind=resolve_kind(event.tool_name), content=None, @@ -144,10 +148,13 @@ class Bash(CoreBashTool, BaseAcpTool[AcpBashState]): def tool_result_session_update( cls, event: ToolResultEvent ) -> ToolCallProgress | None: + if failure := failed_tool_result(event, BashResult): + return failure + return ToolCallProgress( session_update="tool_call_update", tool_call_id=event.tool_call_id, - status="failed" if event.error else "completed", + status="completed", content=[ ContentToolCallContent( type="content", diff --git a/vibe/acp/tools/builtins/grep.py b/vibe/acp/tools/builtins/grep.py index e2cf37b..0cabbc2 100644 --- a/vibe/acp/tools/builtins/grep.py +++ b/vibe/acp/tools/builtins/grep.py @@ -41,8 +41,11 @@ class Grep( tool_call_id=event.tool_call_id, kind=resolve_kind(event.tool_name), raw_input=event.args.model_dump_json(), - locations=[ToolCallLocation(path=search_path)], - field_meta={"tool_name": event.tool_name, "query": event.args.pattern}, + field_meta={ + "tool_name": event.tool_name, + "query": event.args.pattern, + "search_path": search_path, + }, ) @classmethod diff --git a/vibe/acp/tools/builtins/search_replace.py b/vibe/acp/tools/builtins/search_replace.py index ff82698..0145802 100644 --- a/vibe/acp/tools/builtins/search_replace.py +++ b/vibe/acp/tools/builtins/search_replace.py @@ -24,13 +24,11 @@ from vibe.core.tools.builtins.search_replace import ( SearchReplaceResult, ) from vibe.core.types import ToolCallEvent, ToolResultEvent -from vibe.core.utils.io import ReadSafeResult +from vibe.core.utils.io import ReadSafeResult, normalize_newlines class AcpSearchReplaceState(BaseToolState, AcpToolState): file_backup_content: str | None = None - file_backup_encoding: str = "utf-8" - file_backup_newline: str = "\n" class SearchReplace(CoreSearchReplaceTool, BaseAcpTool[AcpSearchReplaceState]): @@ -54,24 +52,24 @@ class SearchReplace(CoreSearchReplaceTool, BaseAcpTool[AcpSearchReplaceState]): raise ToolError(f"Unexpected error reading {file_path}: {e}") from e self.state.file_backup_content = response.content - self.state.file_backup_encoding = "utf-8" - self.state.file_backup_newline = "\n" - return ReadSafeResult(response.content, "utf-8", "\n") + text, newline = normalize_newlines(response.content) + return ReadSafeResult(text, "utf-8", newline) async def _backup_file(self, file_path: Path) -> None: if self.state.file_backup_content is None: return - await self._write_file( + await self._write_via_client( file_path.with_suffix(file_path.suffix + ".bak"), self.state.file_backup_content, - self.state.file_backup_encoding, - self.state.file_backup_newline, ) async def _write_file( self, file_path: Path, content: str, encoding: str, newline: str ) -> None: + await self._write_via_client(file_path, content.replace("\n", newline)) + + async def _write_via_client(self, file_path: Path, content: str) -> None: client, session_id = self._load_state() try: diff --git a/vibe/acp/tools/builtins/write_file.py b/vibe/acp/tools/builtins/write_file.py index 3584d38..d73e635 100644 --- a/vibe/acp/tools/builtins/write_file.py +++ b/vibe/acp/tools/builtins/write_file.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os from pathlib import Path from acp.helpers import SessionUpdate @@ -24,6 +25,7 @@ from vibe.core.tools.builtins.write_file import ( WriteFileResult, ) from vibe.core.types import ToolCallEvent, ToolResultEvent +from vibe.core.utils.io import normalize_newlines class AcpWriteFileState(BaseToolState, AcpToolState): @@ -42,10 +44,12 @@ class WriteFile(CoreWriteFileTool, BaseAcpTool[AcpWriteFileState]): async def _write_file(self, args: WriteFileArgs, file_path: Path) -> None: client, session_id = self._load_state() + normalized_text, _ = normalize_newlines(args.content) + content = normalized_text.replace("\n", os.linesep) try: await client.write_text_file( - session_id=session_id, path=str(file_path), content=args.content + session_id=session_id, path=str(file_path), content=content ) except Exception as e: raise ToolError(f"Error writing {file_path}: {e}") from e diff --git a/vibe/cli/commands.py b/vibe/cli/commands.py index 1d310b3..caf8dcd 100644 --- a/vibe/cli/commands.py +++ b/vibe/cli/commands.py @@ -174,6 +174,11 @@ class CommandRegistry: description="Show data retention information", handler="_show_data_retention", ), + "theme": Command( + aliases=frozenset(["/theme"]), + description="Select theme", + handler="_show_theme", + ), } @property diff --git a/vibe/cli/textual_ui/ansi_markdown.py b/vibe/cli/textual_ui/ansi_markdown.py deleted file mode 100644 index b07c2ce..0000000 --- a/vibe/cli/textual_ui/ansi_markdown.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - -from pygments.token import Token -from textual.content import Content -from textual.highlight import HighlightTheme, highlight -from textual.widgets import Markdown -from textual.widgets._markdown import MarkdownFence - - -class AnsiHighlightTheme(HighlightTheme): - STYLES = { - Token.Comment: "ansi_bright_black italic", - Token.Error: "ansi_red", - Token.Generic.Strong: "bold", - Token.Generic.Emph: "italic", - Token.Generic.Error: "ansi_red", - Token.Generic.Heading: "ansi_blue underline", - Token.Generic.Subheading: "ansi_blue", - Token.Keyword: "ansi_magenta", - Token.Keyword.Constant: "ansi_cyan", - Token.Keyword.Namespace: "ansi_magenta", - Token.Keyword.Type: "ansi_cyan", - Token.Literal.Number: "ansi_yellow", - Token.Literal.String.Backtick: "ansi_bright_black", - Token.Literal.String: "ansi_green", - Token.Literal.String.Doc: "ansi_green italic", - Token.Literal.String.Double: "ansi_green", - Token.Name: "ansi_default", - Token.Name.Attribute: "ansi_yellow", - Token.Name.Builtin: "ansi_cyan", - Token.Name.Builtin.Pseudo: "italic", - Token.Name.Class: "ansi_yellow", - Token.Name.Constant: "ansi_red", - Token.Name.Decorator: "ansi_blue", - Token.Name.Function: "ansi_blue", - Token.Name.Function.Magic: "ansi_blue", - Token.Name.Tag: "ansi_blue", - Token.Name.Variable: "ansi_default", - Token.Number: "ansi_yellow", - Token.Operator: "ansi_default", - Token.Operator.Word: "ansi_magenta", - Token.String: "ansi_green", - Token.Whitespace: "", - } - - -class AnsiMarkdownFence(MarkdownFence): - @classmethod - def highlight(cls, code: str, language: str) -> Content: - return highlight(code, language=language or None, theme=AnsiHighlightTheme) - - -class AnsiMarkdown(Markdown): - BLOCKS = { - **Markdown.BLOCKS, - "fence": AnsiMarkdownFence, - "code_block": AnsiMarkdownFence, - } diff --git a/vibe/cli/textual_ui/app.py b/vibe/cli/textual_ui/app.py index 79e208c..51b3afa 100644 --- a/vibe/cli/textual_ui/app.py +++ b/vibe/cli/textual_ui/app.py @@ -23,6 +23,7 @@ from textual.binding import Binding, BindingType from textual.containers import Horizontal, VerticalGroup, VerticalScroll from textual.driver import Driver from textual.events import AppBlur, AppFocus, MouseUp +from textual.theme import BUILTIN_THEMES from textual.widget import Widget from textual.widgets import Static @@ -91,6 +92,7 @@ from vibe.cli.textual_ui.widgets.question_app import QuestionApp from vibe.cli.textual_ui.widgets.rewind_app import RewindApp from vibe.cli.textual_ui.widgets.session_picker import SessionPickerApp from vibe.cli.textual_ui.widgets.teleport_message import TeleportMessage +from vibe.cli.textual_ui.widgets.theme_picker import ThemePickerApp, sorted_theme_names from vibe.cli.textual_ui.widgets.thinking_picker import ThinkingPickerApp from vibe.cli.textual_ui.widgets.tools import ToolResultMessage from vibe.cli.textual_ui.widgets.voice_app import VoiceApp @@ -128,7 +130,7 @@ from vibe.core.autocompletion.path_prompt import ( build_title_segments, ) from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt -from vibe.core.config import VibeConfig +from vibe.core.config import DEFAULT_THEME, VibeConfig from vibe.core.data_retention import DATA_RETENTION_MESSAGE from vibe.core.hooks.models import HookStartEvent from vibe.core.log_reader import LogReader @@ -185,17 +187,18 @@ from vibe.core.utils import ( ) -def _compute_connectors_count( +def _compute_connector_counts( config: VibeConfig, connector_registry: ConnectorRegistry | None -) -> int: +) -> tuple[int, int]: total = connector_registry.connector_count if connector_registry else 0 if total == 0: - return 0 + return (0, 0) disabled_names = {c.name for c in config.connectors if c.disabled} known_names = set( connector_registry.get_connector_names() if connector_registry else [] ) - return total - len(disabled_names & known_names) + enabled = total - len(disabled_names & known_names) + return (enabled, total) class BottomApp(StrEnum): @@ -214,6 +217,7 @@ class BottomApp(StrEnum): ModelPicker = auto() ProxySetup = auto() Question = auto() + ThemePicker = auto() ThinkingPicker = auto() Rewind = auto() SessionPicker = auto() @@ -464,13 +468,14 @@ class VibeApp(App): # noqa: PLR0904 def compose(self) -> ComposeResult: with ChatScroll(id="chat"): + connectors_enabled, connectors_total = _compute_connector_counts( + self.config, self.agent_loop.connector_registry + ) self._banner = Banner( config=self.config, skill_manager=self.agent_loop.skill_manager, - mcp_registry=self.agent_loop.mcp_registry, - connectors_count=_compute_connectors_count( - self.config, self.agent_loop.connector_registry - ), + connectors_enabled=connectors_enabled, + connectors_total=connectors_total, ) yield self._banner yield VerticalGroup(id="messages") @@ -498,7 +503,7 @@ class VibeApp(App): # noqa: PLR0904 yield ContextProgress() async def on_mount(self) -> None: - self.theme = "textual-ansi" + self._apply_theme(self.config.theme) self._terminal_notifier.restore() self._cached_messages_area = self.query_one("#messages") @@ -853,6 +858,25 @@ class VibeApp(App): # noqa: PLR0904 ) -> None: await self._switch_to_input_app() + async def on_theme_picker_app_theme_previewed( + self, message: ThemePickerApp.ThemePreviewed + ) -> None: + self._apply_theme(message.theme) + + async def on_theme_picker_app_theme_selected( + self, message: ThemePickerApp.ThemeSelected + ) -> None: + self._apply_theme(message.theme) + self.config.theme = message.theme + VibeConfig.save_updates({"theme": message.theme}) + await self._switch_to_input_app() + + async def on_theme_picker_app_cancelled( + self, message: ThemePickerApp.Cancelled + ) -> None: + self._apply_theme(message.original_theme) + await self._switch_to_input_app() + async def on_mcpapp_mcpclosed(self, _message: MCPApp.MCPClosed) -> None: await self._mount_and_scroll(UserCommandMessage("MCP servers closed.")) await self._switch_to_input_app() @@ -1751,6 +1775,11 @@ class VibeApp(App): # noqa: PLR0904 return await self._switch_to_thinking_picker_app() + async def _show_theme(self, **kwargs: Any) -> None: + if self._current_bottom_app == BottomApp.ThemePicker: + return + await self._switch_to_theme_picker_app() + async def _show_proxy_setup(self, **kwargs: Any) -> None: if self._current_bottom_app == BottomApp.ProxySetup: return @@ -2017,13 +2046,14 @@ class VibeApp(App): # noqa: PLR0904 self._narrator_manager.sync() if self._banner: + ce, ct = _compute_connector_counts( + base_config, self.agent_loop.connector_registry + ) self._banner.set_state( base_config, self.agent_loop.skill_manager, - self.agent_loop.mcp_registry, - connectors_count=_compute_connectors_count( - base_config, self.agent_loop.connector_registry - ), + connectors_enabled=ce, + connectors_total=ct, plan_description=plan_title(self._plan_info), ) await self._mount_and_scroll( @@ -2287,6 +2317,23 @@ class VibeApp(App): # noqa: PLR0904 ) ) + async def _switch_to_theme_picker_app(self) -> None: + if self._current_bottom_app == BottomApp.ThemePicker: + return + + await self._switch_from_input( + ThemePickerApp( + theme_names=sorted_theme_names(), current_theme=self.config.theme + ) + ) + + def _apply_theme(self, theme: str) -> None: + if theme not in BUILTIN_THEMES: + logger.warning("Unknown theme=%s; falling back to %s", theme, DEFAULT_THEME) + self.theme = DEFAULT_THEME + return + self.theme = theme + async def _switch_to_proxy_setup_app(self) -> None: if self._current_bottom_app == BottomApp.ProxySetup: return @@ -2340,6 +2387,8 @@ class VibeApp(App): # noqa: PLR0904 self.query_one(ConfigApp).focus() case BottomApp.ModelPicker: self.query_one(ModelPickerApp).focus() + case BottomApp.ThemePicker: + self.query_one(ThemePickerApp).focus() case BottomApp.ThinkingPicker: self.query_one(ThinkingPickerApp).focus() case BottomApp.ProxySetup: @@ -2411,6 +2460,16 @@ class VibeApp(App): # noqa: PLR0904 pass self._last_escape_time = None + def _handle_theme_picker_app_escape(self) -> None: + try: + theme_picker = self.query_one(ThemePickerApp) + theme_picker.post_message( + ThemePickerApp.Cancelled(original_theme=self.config.theme) + ) + except Exception: + pass + self._last_escape_time = None + def _handle_thinking_picker_app_escape(self) -> None: try: thinking_picker = self.query_one(ThinkingPickerApp) @@ -2661,6 +2720,8 @@ class VibeApp(App): # noqa: PLR0904 self._handle_question_app_escape() elif self._current_bottom_app == BottomApp.ModelPicker: self._handle_model_picker_app_escape() + elif self._current_bottom_app == BottomApp.ThemePicker: + self._handle_theme_picker_app_escape() elif self._current_bottom_app == BottomApp.ThinkingPicker: self._handle_thinking_picker_app_escape() elif self._current_bottom_app == BottomApp.SessionPicker: @@ -2781,13 +2842,14 @@ class VibeApp(App): # noqa: PLR0904 def _refresh_banner(self) -> None: if self._banner: + ce, ct = _compute_connector_counts( + self.config, self.agent_loop.connector_registry + ) self._banner.set_state( self.config, self.agent_loop.skill_manager, - self.agent_loop.mcp_registry, - connectors_count=_compute_connectors_count( - self.config, self.agent_loop.connector_registry - ), + connectors_enabled=ce, + connectors_total=ct, plan_description=plan_title(self._plan_info), ) diff --git a/vibe/cli/textual_ui/app.tcss b/vibe/cli/textual_ui/app.tcss index a7af3a8..3a3ce76 100644 --- a/vibe/cli/textual_ui/app.tcss +++ b/vibe/cli/textual_ui/app.tcss @@ -1,12 +1,6 @@ $mistral_orange: #FF8205; * { - scrollbar-color: ansi_black; - scrollbar-color-hover: ansi_bright_black; - scrollbar-color-active: ansi_bright_black; - scrollbar-background: transparent; - scrollbar-background-hover: transparent; - scrollbar-background-active: transparent; scrollbar-size: 1 1; } @@ -20,7 +14,7 @@ Horizontal { } TextArea > .text-area--cursor { - color: ansi_default; + color: $foreground; } #chat { @@ -54,6 +48,55 @@ TextArea > .text-area--cursor { background: transparent; } +#queued-messages { + height: auto; + width: 100%; + background: transparent; + padding: 0 0 1 0; +} + +#queued-messages-box { + height: auto; + width: 100%; + border: round $surface-lighten-2; + border-title-color: $text-muted; + border-title-style: bold; + padding: 0 1; +} + +#queued-messages-list { + height: auto; + max-height: 8; + width: 100%; + padding: 0; + background: transparent; +} + +#queued-messages-hint { + width: 100%; + height: auto; + color: $text-muted; + padding: 0; +} + +.queued-message-item { + width: 100%; + height: auto; + padding: 0; + color: $text-muted; + text-style: italic; +} + +.queued-message-bash { + color: $accent; +} + +.queued-message-content { + width: 100%; + height: auto; + padding: 0; +} + #bottom-bar { height: auto; width: 100%; @@ -89,14 +132,19 @@ TextArea > .text-area--cursor { display: none; width: auto; height: auto; - color: ansi_default; - background: $surface; - border: solid ansi_bright_black; + color: $foreground; + background: $background; + border: solid $foreground-muted; overflow-y: auto; scrollbar-size-vertical: 1; /* max-height, max-width and padding set in completion_popup.py */ } +OptionList, OptionList:focus { + background: $background; + background-tint: transparent; +} + #completion-popup _CompletionItem { height: auto; width: 1fr; @@ -106,24 +154,24 @@ TextArea > .text-area--cursor { height: auto; width: 100%; background: transparent; - border: solid ansi_bright_black; + border: solid $foreground-muted; border-title-align: right; - border-title-color: ansi_bright_black; + border-title-color: $text-muted; padding: 0 1; &.border-warning { - border: solid ansi_yellow; - border-title-color: ansi_yellow; + border: solid $warning; + border-title-color: $warning; } &.border-safe { - border: solid ansi_green; - border-title-color: ansi_green; + border: solid $success; + border-title-color: $success; } &.border-error { - border: solid ansi_red; - border-title-color: ansi_red; + border: solid $error; + border-title-color: $error; } &.border-recording { @@ -157,13 +205,17 @@ TextArea > .text-area--cursor { min-height: 3; max-height: 50vh; background: transparent; - color: ansi_default; + color: $foreground; border: none; padding: 0; scrollbar-visibility: hidden; &.recording { - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } } @@ -174,10 +226,10 @@ ToastRack { } Markdown { - color: ansi_default; + color: $foreground; .code_inline { - color: ansi_green; + color: $success; background: transparent; text-style: bold; } @@ -199,13 +251,13 @@ Markdown { MarkdownBlockQuote { background: transparent; - border-left: outer ansi_bright_black; + border-left: outer $foreground-muted; } MarkdownBullet, MarkdownBulletList, MarkdownOrderedList { - color: ansi_default; + color: $foreground; } } @@ -240,7 +292,6 @@ Markdown { text-style: bold; padding-left: 1; border-left: heavy $mistral_orange; - color: $mistral_orange; } .assistant-message { @@ -277,11 +328,15 @@ Markdown { .reasoning-indicator { width: auto; height: auto; - color: ansi_bright_black; + color: $text-muted; margin-right: 1; &.success { - color: ansi_green; + color: $success; + } + + &:ansi { + text-style: dim; } } @@ -289,8 +344,12 @@ Markdown { .reasoning-triangle { width: auto; height: auto; - color: ansi_bright_black; + color: $text-muted; text-style: italic; + + &:ansi { + text-style: dim; + } } .reasoning-triangle { @@ -303,12 +362,20 @@ Markdown { height: auto; padding: 0 0 0 2; margin: 0; - color: ansi_bright_black; + color: $text-muted; text-style: italic; + &:ansi { + text-style: dim; + } + * { - color: ansi_bright_black; + color: $text-muted; text-style: italic; + + &:ansi { + text-style: dim; + } } & > MarkdownBlock:last-child { @@ -335,7 +402,7 @@ Markdown { .plan-file-wrapper { height: auto; width: 100%; - border: solid ansi_bright_black; + border: solid $foreground-muted; padding: 0 1; } @@ -372,21 +439,20 @@ Markdown { width: auto; height: 100%; padding: 0 1 0 2; - color: ansi_bright_black; } .interrupt-content { width: 1fr; height: auto; margin: 0; - color: ansi_yellow; + color: $warning; } .error-content { width: 1fr; height: auto; margin: 0; - color: ansi_red; + color: $error; text-style: bold; } @@ -394,7 +460,7 @@ Markdown { width: 1fr; height: auto; margin: 0; - color: ansi_yellow; + color: $warning; } .user-command-content { @@ -441,15 +507,15 @@ Markdown { } &.bash-success { - color: ansi_green; + color: $success; } &.bash-error { - color: ansi_red; + color: $error; } &.bash-interrupted { - color: ansi_yellow; + color: $warning; } } @@ -468,7 +534,11 @@ Markdown { width: auto; height: 100%; padding: 0 1 0 2; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } .bash-output { @@ -478,7 +548,11 @@ Markdown { .unknown-event { height: auto; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } StatusMessage { @@ -494,22 +568,22 @@ StatusMessage { .status-indicator-icon { width: auto; height: auto; - color: ansi_default; + color: $foreground; margin-right: 1; &.success { - color: ansi_green; + color: $success; } &.error { - color: ansi_red; + color: $error; } } .status-indicator-text { width: 1fr; height: auto; - color: ansi_default; + color: $foreground; } .compact-message, @@ -531,8 +605,12 @@ StatusMessage { .tool-stream-message { width: 100%; height: auto; - color: ansi_bright_black; + color: $text-muted; padding-left: 2; + + &:ansi { + text-style: dim; + } } .tool-result { @@ -542,14 +620,14 @@ StatusMessage { margin-left: 0; padding: 0; background: transparent; - color: ansi_default; + color: $foreground; &.error-text { - color: ansi_red; + color: $error; } &.warning-text { - color: ansi_yellow; + color: $warning; } } @@ -564,7 +642,11 @@ StatusMessage { width: auto; height: 100%; padding: 0 1 0 2; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } .tool-result-content { @@ -577,7 +659,7 @@ StatusMessage { .tool-result-widget { width: 100%; height: auto; - color: ansi_default; + color: $foreground; } .tool-result-widget Static { @@ -605,7 +687,11 @@ StatusMessage { .tool-call-detail, .tool-result-detail { height: auto; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } .tool-result-detail { @@ -613,64 +699,76 @@ StatusMessage { } .tool-result-error { - color: ansi_red; + color: $error; } .tool-result-warning { - color: ansi_yellow; + color: $warning; } .diff-header { height: auto; - color: ansi_bright_black; + color: $text-muted; text-style: bold; + + &:ansi { + text-style: dim; + } } .diff-removed { height: auto; - color: ansi_red; + color: $error; } .diff-added { height: auto; - color: ansi_green; + color: $success; } .diff-range { height: auto; - color: ansi_blue; + color: $primary; } .diff-context { height: auto; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } .todo-empty, .todo-cancelled { height: auto; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } .todo-pending { height: auto; - color: ansi_default; + color: $foreground; } .todo-in_progress { height: auto; - color: ansi_yellow; + color: $warning; } .todo-completed { height: auto; - color: ansi_green; + color: $success; } .loading-widget { width: 100%; height: auto; - color: ansi_bright_black; + color: $text-muted; } .loading-container, @@ -698,7 +796,11 @@ StatusMessage { .loading-hint { width: auto; height: auto; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } .loading-debounce { @@ -724,7 +826,7 @@ StatusMessage { .history-load-more-button { background: transparent; border: none; - color: ansi_yellow; + color: $warning; text-style: bold; pointer: pointer; padding: 1; @@ -738,7 +840,7 @@ StatusMessage { width: 100%; height: auto; background: transparent; - border: solid ansi_bright_black; + border: solid $foreground-muted; padding: 0 1; margin: 0; } @@ -751,7 +853,7 @@ StatusMessage { .settings-title { height: auto; text-style: bold; - color: ansi_blue; + color: $primary; } #config-options, #mcp-options, #connectorauth-options { @@ -770,14 +872,18 @@ StatusMessage { .settings-help { height: auto; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } #proxysetup-app { width: 100%; height: auto; background: transparent; - border: solid ansi_bright_black; + border: solid $foreground-muted; padding: 0 1; margin: 0; } @@ -795,7 +901,7 @@ StatusMessage { width: 100%; height: auto; border: none; - border-left: wide ansi_bright_black; + border-left: wide $foreground-muted; padding: 0 0 0 1; } @@ -804,7 +910,7 @@ StatusMessage { height: auto; max-height: 70vh; background: transparent; - border: solid ansi_bright_black; + border: solid $foreground-muted; padding: 0 1; margin: 0; } @@ -829,7 +935,7 @@ StatusMessage { .approval-title { height: auto; text-style: bold; - color: ansi_bright_yellow; + color: $warning; } .approval-tool-info-container { @@ -850,7 +956,7 @@ StatusMessage { .approval-option { height: auto; - color: ansi_default; + color: $foreground; margin-bottom: 1; &:last-of-type { @@ -860,39 +966,47 @@ StatusMessage { .approval-cursor-selected { &.approval-option-yes { - color: ansi_green; + color: $success; text-style: bold; } &.approval-option-no { - color: ansi_bright_red; + color: $error; text-style: bold; } } .approval-option-selected { &.approval-option-yes { - color: ansi_white; + color: $foreground; } &.approval-option-no { - color: ansi_red; + color: $error; } } .approval-help { height: auto; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } .approval-description { height: auto; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } .code-block { height: auto; - color: ansi_default; + color: $foreground; background: transparent; padding: 1; } @@ -902,7 +1016,7 @@ StatusMessage { height: auto; max-height: 70vh; background: transparent; - border: solid ansi_bright_black; + border: solid $foreground-muted; padding: 0 1; margin: 0; } @@ -914,24 +1028,24 @@ StatusMessage { .question-tabs { height: auto; - color: ansi_blue; + color: $primary; margin-bottom: 1; } .question-title { height: auto; text-style: bold; - color: ansi_blue; + color: $primary; margin-bottom: 1; } .question-option { height: auto; - color: ansi_default; + color: $foreground; } .question-option-selected { - color: ansi_blue; + color: $primary; text-style: bold; } @@ -943,7 +1057,7 @@ StatusMessage { .question-other-prefix { width: auto; height: auto; - color: ansi_default; + color: $foreground; } .question-other-input { @@ -957,26 +1071,38 @@ StatusMessage { .question-other-static { width: auto; height: auto; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } .question-submit { height: auto; - color: ansi_default; + color: $foreground; margin-top: 1; } .question-footer-note { height: auto; - color: ansi_bright_black; + color: $text-muted; text-style: italic; margin-top: 1; + + &:ansi { + text-style: dim; + } } .question-help { height: auto; - color: ansi_bright_black; + color: $text-muted; margin-top: 1; + + &:ansi { + text-style: dim; + } } ExpandingBorder { @@ -992,7 +1118,11 @@ ContextProgress { background: transparent; padding: 0; margin: 0; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } NarratorStatus { @@ -1037,12 +1167,12 @@ NarratorStatus { } .banner-meta { - color: ansi_default; + color: $foreground; width: auto; } #banner-model { - color: ansi_cyan; + color: $secondary; width: auto; } @@ -1055,12 +1185,12 @@ NarratorStatus { } .banner-cmd { - color: ansi_cyan; + color: $secondary; width: auto; } .petit-chat { - color: ansi_default; + color: $foreground; width: 12; } @@ -1105,7 +1235,7 @@ NarratorStatus { width: 100%; height: auto; background: transparent; - border: solid ansi_bright_black; + border: solid $foreground-muted; padding: 0 1; margin: 0; } @@ -1130,8 +1260,12 @@ NarratorStatus { .sessionpicker-help { width: 100%; height: auto; - color: ansi_bright_black; + color: $text-muted; margin-top: 1; + + &:ansi { + text-style: dim; + } } /* Debug Console */ @@ -1143,14 +1277,14 @@ NarratorStatus { min-width: 40; height: 100%; background: transparent; - border-left: solid ansi_bright_black; + border-left: solid $foreground-muted; padding: 0 1; } #debug-console-header { width: 100%; height: auto; - color: ansi_default; + color: $foreground; padding: 0 0 1 0; text-align: left; } @@ -1167,7 +1301,7 @@ NarratorStatus { width: 100%; height: auto; background: transparent; - border: solid ansi_bright_black; + border: solid $foreground-muted; padding: 0 1; margin: 0; } @@ -1180,7 +1314,7 @@ NarratorStatus { .modelpicker-title { height: auto; text-style: bold; - color: ansi_blue; + color: $primary; } #modelpicker-options { @@ -1198,15 +1332,19 @@ NarratorStatus { .modelpicker-help { width: 100%; height: auto; - color: ansi_bright_black; + color: $text-muted; margin-top: 1; + + &:ansi { + text-style: dim; + } } #thinkingpicker-app { width: 100%; height: auto; background: transparent; - border: solid ansi_bright_black; + border: solid $foreground-muted; padding: 0 1; margin: 0; } @@ -1219,7 +1357,7 @@ NarratorStatus { .thinkingpicker-title { height: auto; text-style: bold; - color: ansi_blue; + color: $primary; } #thinkingpicker-options { @@ -1237,8 +1375,55 @@ NarratorStatus { .thinkingpicker-help { width: 100%; height: auto; - color: ansi_bright_black; + color: $text-muted; margin-top: 1; + + &:ansi { + text-style: dim; + } +} + +#themepicker-app { + width: 100%; + height: auto; + background: transparent; + border: solid $foreground-muted; + padding: 0 1; + margin: 0; +} + +#themepicker-content { + width: 100%; + height: auto; +} + +.themepicker-title { + height: auto; + text-style: bold; + color: $primary; +} + +#themepicker-options { + width: 100%; + max-height: 50vh; + text-wrap: nowrap; + text-overflow: ellipsis; + border: none; +} + +#themepicker-options:focus { + border: none; +} + +.themepicker-help { + width: 100%; + height: auto; + color: $text-muted; + margin-top: 1; + + &:ansi { + text-style: dim; + } } FeedbackBar { @@ -1257,7 +1442,7 @@ FeedbackBar { width: 100%; height: auto; background: transparent; - border: solid ansi_bright_black; + border: solid $foreground-muted; padding: 0 1; margin: 0; } @@ -1270,32 +1455,36 @@ FeedbackBar { .rewind-title { height: auto; text-style: bold; - color: ansi_blue; + color: $primary; } .rewind-option { height: auto; - color: ansi_default; + color: $foreground; } .rewind-cursor-selected { - color: ansi_blue; + color: $primary; text-style: bold; } .rewind-option-unselected { - color: ansi_default; + color: $foreground; } .rewind-help { height: auto; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } #feedback-text { width: auto; height: auto; - color: ansi_default; + color: $foreground; } .hook-system-message { @@ -1333,17 +1522,21 @@ FeedbackBar { height: auto; max-height: 3; overflow: hidden; - color: ansi_bright_black; + color: $text-muted; + + &:ansi { + text-style: dim; + } } .hook-severity-ok .hook-system-icon { - color: ansi_green; + color: $success; } .hook-severity-warning .hook-system-icon { - color: ansi_yellow; + color: $warning; } .hook-severity-error .hook-system-icon { - color: ansi_red; + color: $error; } diff --git a/vibe/cli/textual_ui/message_queue.py b/vibe/cli/textual_ui/message_queue.py new file mode 100644 index 0000000..7dc984c --- /dev/null +++ b/vibe/cli/textual_ui/message_queue.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, field +from enum import StrEnum, auto + + +class QueuedItemKind(StrEnum): + PROMPT = auto() + BASH = auto() + + +@dataclass(frozen=True, slots=True) +class QueuedItem: + kind: QueuedItemKind + content: str + + +@dataclass(slots=True) +class MessageQueue: + _items: list[QueuedItem] = field(default_factory=list) + _paused: bool = False + _on_change: Callable[[], None] | None = None + + def set_change_listener(self, listener: Callable[[], None] | None) -> None: + self._on_change = listener + + def __len__(self) -> int: + return len(self._items) + + def __bool__(self) -> bool: + return bool(self._items) + + @property + def items(self) -> list[QueuedItem]: + return list(self._items) + + @property + def paused(self) -> bool: + return self._paused + + def append_prompt(self, content: str) -> None: + self._items.append(QueuedItem(QueuedItemKind.PROMPT, content)) + self._notify() + + def append_bash(self, content: str) -> None: + self._items.append(QueuedItem(QueuedItemKind.BASH, content)) + self._notify() + + def pop_last(self) -> QueuedItem | None: + if not self._items: + return None + item = self._items.pop() + self._notify() + return item + + def pop_first(self) -> QueuedItem | None: + if not self._items: + return None + item = self._items.pop(0) + self._notify() + return item + + def pause(self) -> None: + if self._paused: + return + self._paused = True + self._notify() + + def resume(self) -> None: + if not self._paused: + return + self._paused = False + self._notify() + + def clear(self) -> None: + if not self._items and not self._paused: + return + self._items.clear() + self._paused = False + self._notify() + + def _notify(self) -> None: + if self._on_change is not None: + self._on_change() diff --git a/vibe/cli/textual_ui/quit_manager.py b/vibe/cli/textual_ui/quit_manager.py index 3df7fda..89845cb 100644 --- a/vibe/cli/textual_ui/quit_manager.py +++ b/vibe/cli/textual_ui/quit_manager.py @@ -31,15 +31,18 @@ class QuitManager: and (time.monotonic() - self._confirm_time) < QUIT_CONFIRM_DELAY ) - def request_confirmation(self, key: QuitConfirmKey) -> None: + def request_confirmation(self, key: QuitConfirmKey, extra: str = "") -> None: if self._confirm_timer is not None: self._confirm_timer.stop() self._confirm_timer = None self._confirm_time = time.monotonic() self._confirm_key = key + prompt = f"Press {key} again to quit" + if extra: + prompt = f"{prompt} ({extra})" try: path_display = self._app.query_one(PathDisplay) - path_display.update(f"Press {key} again to quit") + path_display.update(prompt) except Exception: pass self._confirm_timer = self._app.set_timer( diff --git a/vibe/cli/textual_ui/widgets/banner/banner.py b/vibe/cli/textual_ui/widgets/banner/banner.py index 8c2cc4e..dfadae3 100644 --- a/vibe/cli/textual_ui/widgets/banner/banner.py +++ b/vibe/cli/textual_ui/widgets/banner/banner.py @@ -13,7 +13,6 @@ from vibe.cli.textual_ui.widgets.banner.petit_chat import PetitChat from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.core.config import VibeConfig from vibe.core.skills.manager import SkillManager -from vibe.core.tools.mcp.registry import MCPRegistry def _pluralize(count: int, singular: str) -> str: @@ -24,8 +23,10 @@ def _pluralize(count: int, singular: str) -> str: class BannerState: active_model: str = "" models_count: int = 0 - mcp_servers_count: int = 0 - connectors_count: int = 0 + mcp_servers_enabled: int = 0 + mcp_servers_total: int = 0 + connectors_enabled: int = 0 + connectors_total: int = 0 skills_count: int = 0 plan_description: str | None = None @@ -37,8 +38,8 @@ class Banner(Static): self, config: VibeConfig, skill_manager: SkillManager, - mcp_registry: MCPRegistry, - connectors_count: int = 0, + connectors_enabled: int = 0, + connectors_total: int = 0, **kwargs: Any, ) -> None: super().__init__(**kwargs) @@ -46,8 +47,8 @@ class Banner(Static): self._initial_state = self._build_state( config=config, skill_manager=skill_manager, - mcp_registry=mcp_registry, - connectors_count=connectors_count, + connectors_enabled=connectors_enabled, + connectors_total=connectors_total, plan_description=None, ) self._animated = not config.disable_welcome_banner_animation @@ -90,40 +91,62 @@ class Banner(Static): self, config: VibeConfig, skill_manager: SkillManager, - mcp_registry: MCPRegistry, - connectors_count: int = 0, + connectors_enabled: int = 0, + connectors_total: int = 0, plan_description: str | None = None, ) -> None: self.state = self._build_state( - config, skill_manager, mcp_registry, connectors_count, plan_description + config, + skill_manager, + connectors_enabled, + connectors_total, + plan_description, ) @staticmethod def _build_state( config: VibeConfig, skill_manager: SkillManager, - mcp_registry: MCPRegistry, - connectors_count: int = 0, + connectors_enabled: int = 0, + connectors_total: int = 0, plan_description: str | None = None, ) -> BannerState: - enabled_servers = [s for s in config.mcp_servers if not s.disabled] - mcp_count = mcp_registry.count_loaded(enabled_servers) + all_servers = config.mcp_servers + enabled_servers = [s for s in all_servers if not s.disabled] active_model = config.get_active_model() return BannerState( active_model=f"{active_model.alias}[{active_model.thinking}]", models_count=len(config.models), - mcp_servers_count=mcp_count, - connectors_count=connectors_count, + mcp_servers_enabled=len(enabled_servers), + mcp_servers_total=len(all_servers), + connectors_enabled=connectors_enabled, + connectors_total=connectors_total, skills_count=skill_manager.custom_skills_count, plan_description=plan_description, ) def _format_meta_counts(self) -> str: parts = [_pluralize(self.state.models_count, "model")] - if self.state.connectors_count > 0: - parts.append(_pluralize(self.state.connectors_count, "connector")) - parts.append(_pluralize(self.state.mcp_servers_count, "MCP server")) + # Format as x/y for MCP servers and connectors (only when enabled != total) + if self.state.connectors_total > 0: + if self.state.connectors_enabled != self.state.connectors_total: + connector_str = ( + f"{self.state.connectors_enabled}/{self.state.connectors_total} connector" + + ("s" if self.state.connectors_total != 1 else "") + ) + else: + connector_str = _pluralize(self.state.connectors_enabled, "connector") + parts.append(connector_str) + # Always show MCP servers count (even if 0/0) + if self.state.mcp_servers_enabled != self.state.mcp_servers_total: + mcp_str = ( + f"{self.state.mcp_servers_enabled}/{self.state.mcp_servers_total} MCP server" + + ("s" if self.state.mcp_servers_total != 1 else "") + ) + else: + mcp_str = _pluralize(self.state.mcp_servers_enabled, "MCP server") + parts.append(mcp_str) parts.append(_pluralize(self.state.skills_count, "skill")) return " · ".join(parts) diff --git a/vibe/cli/textual_ui/widgets/banner/petit_chat.py b/vibe/cli/textual_ui/widgets/banner/petit_chat.py index 9e49436..0b6c462 100644 --- a/vibe/cli/textual_ui/widgets/banner/petit_chat.py +++ b/vibe/cli/textual_ui/widgets/banner/petit_chat.py @@ -163,7 +163,9 @@ TRANSITIONS = [ class PetitChat(Static): def __init__(self, animate: bool = True, **kwargs: Any) -> None: - super().__init__(**kwargs, classes="banner-chat") + classes = kwargs.pop("classes", None) + merged_classes = "banner-chat" if classes is None else f"banner-chat {classes}" + super().__init__(**kwargs, classes=merged_classes) self._dots = {1j * y + x for y, row in enumerate(STARTING_DOTS) for x in row} self._transition_index = 0 self._do_animate = animate diff --git a/vibe/cli/textual_ui/widgets/feedback_bar_manager.py b/vibe/cli/textual_ui/widgets/feedback_bar_manager.py index eeb54b9..bd652c9 100644 --- a/vibe/cli/textual_ui/widgets/feedback_bar_manager.py +++ b/vibe/cli/textual_ui/widgets/feedback_bar_manager.py @@ -1,49 +1,23 @@ from __future__ import annotations -import random -import time - -from vibe.cli.cache import read_cache, write_cache from vibe.core.agent_loop import AgentLoop -from vibe.core.paths import CACHE_FILE +from vibe.core.feedback import record_feedback_asked, should_show_feedback from vibe.core.types import Role -FEEDBACK_PROBABILITY = 0.2 -FEEDBACK_COOLDOWN_SECONDS = 3600 -_CACHE_SECTION = "user_feedback" -_LAST_SHOWN_KEY = "last_shown_at" -MIN_USER_MESSAGES_FOR_FEEDBACK = 3 - class FeedbackBarManager: """Decides whether to show the feedback bar and records when feedback is given.""" def should_show(self, agent_loop: AgentLoop) -> bool: - if not agent_loop.telemetry_client.is_active(): - return False - - if not agent_loop.config.is_active_model_mistral(): - return False - - if ( + user_message_count = ( sum(m.role == Role.user and not m.injected for m in agent_loop.messages) + 1 # +1 for the message the user just sent - < MIN_USER_MESSAGES_FOR_FEEDBACK - ): - return False - - last_ts = ( - read_cache(CACHE_FILE.path).get(_CACHE_SECTION, {}).get(_LAST_SHOWN_KEY, 0) ) - if not isinstance(last_ts, int): - return False - - return ( - time.time() - last_ts >= FEEDBACK_COOLDOWN_SECONDS - and random.random() <= FEEDBACK_PROBABILITY + return should_show_feedback( + telemetry_active=agent_loop.telemetry_client.is_active(), + is_mistral_model=agent_loop.config.is_active_model_mistral(), + user_message_count=user_message_count, ) def record_feedback_asked(self) -> None: - write_cache( - CACHE_FILE.path, _CACHE_SECTION, {_LAST_SHOWN_KEY: int(time.time())} - ) + record_feedback_asked() diff --git a/vibe/cli/textual_ui/widgets/mcp_app.py b/vibe/cli/textual_ui/widgets/mcp_app.py index 1eb4315..a84273e 100644 --- a/vibe/cli/textual_ui/widgets/mcp_app.py +++ b/vibe/cli/textual_ui/widgets/mcp_app.py @@ -16,7 +16,7 @@ from textual.worker import Worker from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.core.config import ConnectorConfig -from vibe.core.tools.connectors import ConnectorRegistry +from vibe.core.tools.connectors import ConnectorAuthAction, ConnectorRegistry from vibe.core.tools.mcp.tools import MCPTool from vibe.core.tools.mcp_settings import updated_tool_list @@ -66,7 +66,7 @@ _LIST_VIEW_HELP_TOOLS = ( "↑↓ Navigate Enter Show tools D Disable E Enable R Refresh Esc Close" ) _LIST_VIEW_HELP_AUTH = ( - "↑↓ Navigate Enter Authenticate D Disable E Enable R Refresh Esc Close" + "↑↓ Navigate Enter Connect D Disable E Enable R Refresh Esc Close" ) _DETAIL_VIEW_HELP = ( "↑↓ Navigate D Disable E Enable Backspace Back R Refresh Esc Close" @@ -246,7 +246,11 @@ class MCPApp(Container): option_id = option.id or "" if option_id.startswith("connector:") and self._connector_registry: name = option_id.removeprefix("connector:") - if not self._connector_registry.is_connected(name): + if ( + not self._connector_registry.is_connected(name) + and self._connector_registry.get_auth_action(name) + == ConnectorAuthAction.OAUTH + ): return _LIST_VIEW_HELP_AUTH return _LIST_VIEW_HELP_TOOLS @@ -461,9 +465,7 @@ class MCPApp(Container): label.append(f" {type_tag:<{max_type}}", style="dim") label.append(f" {_tool_count_text(enabled, total)}", style="dim") if srv.disabled: - label.append(" ") - label.append("○", style="dim") - label.append(" disabled", style="dim") + _append_status(label, "○", "dim", "disabled") option_list.add_option(Option(label, id=f"server:{srv.name}")) def _list_connectors(self, option_list: OptionList, index: MCPToolIndex) -> None: @@ -485,7 +487,7 @@ class MCPApp(Container): ) for cname in ordered_connector_names: cfg = self._find_connector_config(cname) - is_disabled = cfg.disabled if cfg else False + is_disabled = cfg.disabled if cfg else True connected = ( self._connector_registry.is_connected(cname) if self._connector_registry @@ -495,18 +497,24 @@ class MCPApp(Container): label.append(f" {cname:<{max_name}}") label.append(f" {type_tag:<{type_width}}", style="dim") label.append(f" {tool_texts[cname]:<{max_tools}}", style="dim") + auth_action = ( + self._connector_registry.get_auth_action(cname) + if self._connector_registry + else ConnectorAuthAction.NONE + ) if is_disabled: - label.append(" ") - label.append("○", style="dim") - label.append(" disabled", style="dim") + _append_status(label, "○", "dim", "disabled") elif connected: - label.append(" ") - label.append("●", style="green") - label.append(" connected", style="dim") + _append_status(label, "●", "green", "connected") else: - label.append(" ") - label.append("○", style="dim") - label.append(" not connected", style="dim") + match auth_action: + case ConnectorAuthAction.OAUTH: + text = "needs auth" + case ConnectorAuthAction.CREDENTIALS_SETUP: + text = "needs setup" + case _: + text = "error - try refreshing" + _append_status(label, "○", "dim", text) option_list.add_option(Option(label, id=f"connector:{cname}")) # ── detail view ────────────────────────────────────────────────── @@ -537,13 +545,31 @@ class MCPApp(Container): and self._connector_registry and not self._connector_registry.is_connected(server_name) ): - self.post_message( - self.ConnectorAuthRequested( - connector_name=server_name, - connector_registry=self._connector_registry, - tool_manager=self._tool_manager, - ) - ) + auth_action = self._connector_registry.get_auth_action(server_name) + match auth_action: + case ConnectorAuthAction.CREDENTIALS_SETUP: + option_list.add_option( + Option( + "Set up credentials in the Mistral dashboard, " + "then press R to refresh.", + disabled=True, + ) + ) + case ConnectorAuthAction.OAUTH: + self.post_message( + self.ConnectorAuthRequested( + connector_name=server_name, + connector_registry=self._connector_registry, + tool_manager=self._tool_manager, + ) + ) + case _: + option_list.add_option( + Option( + "Connector unavailable; press R to refresh.", + disabled=True, + ) + ) else: option_list.add_option( Option("No tools discovered for this server", disabled=True) @@ -569,6 +595,12 @@ class MCPApp(Container): option_list.highlighted = 0 +def _append_status(label: Text, symbol: str, symbol_style: str, text: str) -> None: + label.append(" ") + label.append(symbol, style=symbol_style) + label.append(f" {text}", style="dim") + + def _tool_count_text(enabled: int, total: int | None = None) -> str: if total is not None and enabled < total: noun = "tool" if total == 1 else "tools" diff --git a/vibe/cli/textual_ui/widgets/messages.py b/vibe/cli/textual_ui/widgets/messages.py index c3aa059..cb2223f 100644 --- a/vibe/cli/textual_ui/widgets/messages.py +++ b/vibe/cli/textual_ui/widgets/messages.py @@ -17,11 +17,10 @@ from textual.containers import Horizontal, Vertical from textual.css.query import NoMatches from textual.reactive import reactive from textual.widget import Widget -from textual.widgets import Static +from textual.widgets import Markdown, Static from textual.widgets._markdown import MarkdownStream from watchfiles import awatch -from vibe.cli.textual_ui.ansi_markdown import AnsiMarkdown as Markdown from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.cli.textual_ui.widgets.spinner import SpinnerMixin, SpinnerType diff --git a/vibe/cli/textual_ui/widgets/proxy_setup_app.py b/vibe/cli/textual_ui/widgets/proxy_setup_app.py index 99783d0..ab496a1 100644 --- a/vibe/cli/textual_ui/widgets/proxy_setup_app.py +++ b/vibe/cli/textual_ui/widgets/proxy_setup_app.py @@ -46,7 +46,7 @@ class ProxySetupApp(Container): yield NoMarkupStatic("Proxy Configuration", classes="settings-title") for key, description in SUPPORTED_PROXY_VARS.items(): - yield Static(f"[bold ansi_blue]{key}[/]", classes="proxy-label-line") + yield Static(f"[bold $primary]{key}[/]", classes="proxy-label-line") initial_value = self.initial_values.get(key) or "" input_widget = VscodeCompatInput( diff --git a/vibe/cli/textual_ui/widgets/queued_messages.py b/vibe/cli/textual_ui/widgets/queued_messages.py new file mode 100644 index 0000000..6515256 --- /dev/null +++ b/vibe/cli/textual_ui/widgets/queued_messages.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from textual.app import ComposeResult +from textual.containers import Vertical, VerticalScroll +from textual.widget import Widget +from textual.widgets import Static + +from vibe.cli.textual_ui.message_queue import MessageQueue, QueuedItem, QueuedItemKind +from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic + + +class QueuedMessageItem(Static): + def __init__(self, item: QueuedItem) -> None: + super().__init__() + self.add_class("queued-message-item") + if item.kind == QueuedItemKind.BASH: + self.add_class("queued-message-bash") + self._item = item + + @property + def item(self) -> QueuedItem: + return self._item + + def compose(self) -> ComposeResult: + prefix = "$ " if self._item.kind == QueuedItemKind.BASH else "" + yield NoMarkupStatic( + f"{prefix}{self._item.content}", classes="queued-message-content" + ) + + +class QueuedMessages(Widget): + DEFAULT_CSS = "" + ID = "queued-messages" + ID_LIST = "queued-messages-list" + ID_HINT = "queued-messages-hint" + ID_BOX = "queued-messages-box" + + def __init__(self) -> None: + super().__init__(id=self.ID) + self._queue: MessageQueue | None = None + self._is_job_running: bool = False + + def bind(self, queue: MessageQueue) -> None: + self._queue = queue + self._queue.set_change_listener(self._on_queue_changed) + + def compose(self) -> ComposeResult: + with Vertical(id=self.ID_BOX) as box: + box.border_title = "Queue" + yield VerticalScroll(id=self.ID_LIST) + yield NoMarkupStatic("", id=self.ID_HINT) + + def on_mount(self) -> None: + self._refresh() + + def set_job_running(self, running: bool) -> None: + if self._is_job_running == running: + return + self._is_job_running = running + self._refresh() + + def _on_queue_changed(self) -> None: + if self.is_mounted: + self._refresh() + + def _refresh(self) -> None: + queue = self._queue + if queue is None or len(queue) == 0: + self.display = False + return + + self.display = True + + try: + box = self.query_one(f"#{self.ID_BOX}", Vertical) + list_widget = self.query_one(f"#{self.ID_LIST}", VerticalScroll) + hint = self.query_one(f"#{self.ID_HINT}", NoMarkupStatic) + except Exception: + return + + title = f"Queue ({len(queue)})" + if queue.paused: + title = f"{title} — paused" + box.border_title = title + + existing = list(list_widget.query(QueuedMessageItem)) + for widget in existing: + widget.remove() + for item in queue.items: + list_widget.mount(QueuedMessageItem(item)) + + if queue.paused: + hint.update("Enter send queue • Ctrl+C drop last queued") + elif self._is_job_running: + hint.update("Esc cancel job + pause • Ctrl+C drop last queued") + else: + hint.update("") diff --git a/vibe/cli/textual_ui/widgets/theme_picker.py b/vibe/cli/textual_ui/widgets/theme_picker.py new file mode 100644 index 0000000..b550238 --- /dev/null +++ b/vibe/cli/textual_ui/widgets/theme_picker.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +from typing import Any, ClassVar + +from rich.text import Text +from textual.app import ComposeResult +from textual.binding import Binding, BindingType +from textual.containers import Container, Vertical +from textual.message import Message +from textual.theme import BUILTIN_THEMES +from textual.timer import Timer +from textual.widgets import OptionList +from textual.widgets.option_list import Option + +from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic + +PREVIEW_DEBOUNCE_SECONDS = 0.1 + + +def sorted_theme_names() -> list[str]: + light = sorted(name for name, t in BUILTIN_THEMES.items() if not t.dark) + dark = sorted(name for name, t in BUILTIN_THEMES.items() if t.dark) + return light + dark + + +def _build_option_text(theme: str, is_current: bool) -> Text: + text = Text(no_wrap=True) + marker = "› " if is_current else " " + text.append(marker, style="green" if is_current else "") + text.append(theme, style="bold" if is_current else "") + return text + + +class ThemePickerApp(Container): + can_focus_children = True + + BINDINGS: ClassVar[list[BindingType]] = [ + Binding("escape", "cancel", "Cancel", show=False) + ] + + class ThemeSelected(Message): + def __init__(self, theme: str) -> None: + self.theme = theme + super().__init__() + + class ThemePreviewed(Message): + def __init__(self, theme: str) -> None: + self.theme = theme + super().__init__() + + class Cancelled(Message): + def __init__(self, original_theme: str) -> None: + self.original_theme = original_theme + super().__init__() + + def __init__( + self, theme_names: list[str], current_theme: str, **kwargs: Any + ) -> None: + super().__init__(id="themepicker-app", **kwargs) + self._theme_names = theme_names + self._current_theme = current_theme + self._preview_timer: Timer | None = None + self._pending_preview: str | None = None + + def compose(self) -> ComposeResult: + options = [ + Option(_build_option_text(name, name == self._current_theme), id=name) + for name in self._theme_names + ] + with Vertical(id="themepicker-content"): + yield NoMarkupStatic("Select Theme", classes="themepicker-title") + yield OptionList(*options, id="themepicker-options") + yield NoMarkupStatic( + "↑↓ Preview Enter Select Esc Cancel", classes="themepicker-help" + ) + + def on_mount(self) -> None: + option_list = self.query_one(OptionList) + for i, name in enumerate(self._theme_names): + if name == self._current_theme: + option_list.highlighted = i + break + option_list.focus() + + def on_option_list_option_highlighted( + self, event: OptionList.OptionHighlighted + ) -> None: + if not event.option.id: + return + self._pending_preview = event.option.id + if self._preview_timer is not None: + self._preview_timer.stop() + self._preview_timer = self.set_timer( + PREVIEW_DEBOUNCE_SECONDS, self._flush_preview + ) + + def _flush_preview(self) -> None: + if self._pending_preview is None: + return + self.post_message(self.ThemePreviewed(self._pending_preview)) + self._pending_preview = None + + def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None: + if self._preview_timer is not None: + self._preview_timer.stop() + self._pending_preview = None + if event.option.id: + self.post_message(self.ThemeSelected(event.option.id)) + + def action_cancel(self) -> None: + if self._preview_timer is not None: + self._preview_timer.stop() + self._pending_preview = None + self.post_message(self.Cancelled(self._current_theme)) diff --git a/vibe/cli/textual_ui/widgets/tool_widgets.py b/vibe/cli/textual_ui/widgets/tool_widgets.py index 00e2de6..021b72f 100644 --- a/vibe/cli/textual_ui/widgets/tool_widgets.py +++ b/vibe/cli/textual_ui/widgets/tool_widgets.py @@ -6,9 +6,8 @@ from pathlib import Path from pydantic import BaseModel from textual.app import ComposeResult from textual.containers import Vertical -from textual.widgets import Static +from textual.widgets import Markdown, Static -from vibe.cli.textual_ui.ansi_markdown import AnsiMarkdown as Markdown from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.core.tools.builtins.ask_user_question import AskUserQuestionResult from vibe.core.tools.builtins.bash import BashArgs, BashResult diff --git a/vibe/core/agent_loop.py b/vibe/core/agent_loop.py index 1dd9604..a67b2b3 100644 --- a/vibe/core/agent_loop.py +++ b/vibe/core/agent_loop.py @@ -624,8 +624,17 @@ class AgentLoop: # noqa: PLR0904 ) @requires_init - async def inject_user_context(self, content: str) -> None: - self.messages.append(LLMMessage(role=Role.user, content=content, injected=True)) + async def inject_user_context( + self, content: str, *, as_message: bool = False + ) -> None: + if as_message: + self.messages.append( + LLMMessage(role=Role.user, content=content, message_id=str(uuid4())) + ) + else: + self.messages.append( + LLMMessage(role=Role.user, content=content, injected=True) + ) await self._save_messages() @requires_init diff --git a/vibe/core/config/__init__.py b/vibe/core/config/__init__.py index 587cba4..e6666fd 100644 --- a/vibe/core/config/__init__.py +++ b/vibe/core/config/__init__.py @@ -5,6 +5,7 @@ from vibe.core.config._settings import ( DEFAULT_MISTRAL_API_ENV_KEY, DEFAULT_MODELS, DEFAULT_PROVIDERS, + DEFAULT_THEME, DEFAULT_TRANSCRIBE_MODELS, DEFAULT_TRANSCRIBE_PROVIDERS, DEFAULT_TTS_MODELS, @@ -44,6 +45,7 @@ from vibe.core.config.layer import ( ) from vibe.core.config.patch import ( AppendToList, + ConfigPatch, DeleteField, PatchOp, RemoveFromList, @@ -68,6 +70,7 @@ __all__ = [ "DEFAULT_MISTRAL_API_ENV_KEY", "DEFAULT_MODELS", "DEFAULT_PROVIDERS", + "DEFAULT_THEME", "DEFAULT_TRANSCRIBE_MODELS", "DEFAULT_TRANSCRIBE_PROVIDERS", "DEFAULT_TTS_MODELS", @@ -78,6 +81,7 @@ __all__ = [ "ConfigFragment", "ConfigLayer", "ConfigLayerError", + "ConfigPatch", "ConfigSchema", "ConnectorConfig", "DeleteField", diff --git a/vibe/core/config/_settings.py b/vibe/core/config/_settings.py index 7f2fddc..cea7554 100644 --- a/vibe/core/config/_settings.py +++ b/vibe/core/config/_settings.py @@ -23,6 +23,7 @@ from pydantic_settings import ( PydanticBaseSettingsSource, SettingsConfigDict, ) +from textual.theme import BUILTIN_THEMES import tomli_w from vibe.core.agents.models import BuiltinAgentName @@ -491,10 +492,13 @@ DEFAULT_TTS_MODELS = [ ) ] +DEFAULT_THEME = "ansi-dark" + class VibeConfig(BaseSettings): active_model: str = DEFAULT_ACTIVE_MODEL vim_keybindings: bool = False + theme: str = DEFAULT_THEME disable_welcome_banner_animation: bool = False autocopy_to_clipboard: bool = True file_watcher_for_autocomplete: bool = False @@ -524,6 +528,7 @@ class VibeConfig(BaseSettings): vibe_code_task_queue: str | None = Field(default="shared-vibe-nuage", exclude=True) vibe_code_api_key_env_var: str = Field(default="MISTRAL_API_KEY", exclude=True) vibe_code_project_name: str | None = Field(default=None, exclude=True) + vibe_code_experimental_nuage_enabled: bool = Field(default=False, exclude=True) # TODO(otel): remove exclude=True once the feature is publicly available enable_otel: bool = Field(default=False, exclude=True) @@ -849,6 +854,18 @@ class VibeConfig(BaseSettings): pass return self + @field_validator("theme", mode="before") + @classmethod + def _validate_theme(cls, v: Any) -> str: + if not isinstance(v, str) or not v: + return DEFAULT_THEME + if v not in BUILTIN_THEMES: + logger.warning( + "Unknown theme=%s in config; falling back to %s", v, DEFAULT_THEME + ) + return DEFAULT_THEME + return v + @field_validator("tool_paths", mode="before") @classmethod def _expand_tool_paths(cls, v: Any) -> list[Path]: diff --git a/vibe/core/config/harness_files/_harness_manager.py b/vibe/core/config/harness_files/_harness_manager.py index 7b351ce..e3c0c7d 100644 --- a/vibe/core/config/harness_files/_harness_manager.py +++ b/vibe/core/config/harness_files/_harness_manager.py @@ -6,6 +6,7 @@ from typing import Literal from vibe.core.config.harness_files._paths import ( GLOBAL_AGENTS_DIR, + GLOBAL_AGENTS_SKILLS_DIR, GLOBAL_PROMPTS_DIR, GLOBAL_SKILLS_DIR, GLOBAL_TOOLS_DIR, @@ -96,8 +97,11 @@ class HarnessFilesManager: def user_skills_dirs(self) -> list[Path]: if "user" not in self.sources: return [] - d = GLOBAL_SKILLS_DIR.path - return [d] if d.is_dir() else [] + return [ + d + for d in (GLOBAL_SKILLS_DIR.path, GLOBAL_AGENTS_SKILLS_DIR.path) + if d.is_dir() + ] @property def user_agents_dirs(self) -> list[Path]: diff --git a/vibe/core/config/harness_files/_paths.py b/vibe/core/config/harness_files/_paths.py index b3e06e5..e8b856b 100644 --- a/vibe/core/config/harness_files/_paths.py +++ b/vibe/core/config/harness_files/_paths.py @@ -1,8 +1,9 @@ from __future__ import annotations -from vibe.core.paths import VIBE_HOME, GlobalPath +from vibe.core.paths import AGENTS_HOME, VIBE_HOME, GlobalPath GLOBAL_TOOLS_DIR = GlobalPath(lambda: VIBE_HOME.path / "tools") GLOBAL_SKILLS_DIR = GlobalPath(lambda: VIBE_HOME.path / "skills") GLOBAL_AGENTS_DIR = GlobalPath(lambda: VIBE_HOME.path / "agents") GLOBAL_PROMPTS_DIR = GlobalPath(lambda: VIBE_HOME.path / "prompts") +GLOBAL_AGENTS_SKILLS_DIR = GlobalPath(lambda: AGENTS_HOME.path / "skills") diff --git a/vibe/core/config/patch.py b/vibe/core/config/patch.py index caea8db..172f65a 100644 --- a/vibe/core/config/patch.py +++ b/vibe/core/config/patch.py @@ -4,6 +4,37 @@ from dataclasses import dataclass from typing import Any +class ConfigPatch: + """Declarative, storage-agnostic description of a config delta.""" + + def __init__( + self, *operations: PatchOp, fingerprint: str, reason: str = "" + ) -> None: + self.operations = list(operations) + self.fingerprint = fingerprint + self.reason = reason + + def add(self, *operations: PatchOp) -> ConfigPatch: + """Append operations after construction.""" + self.operations.extend(operations) + return self + + def describe(self) -> list[str]: + """Human-readable summary of each operation.""" + lines: list[str] = [] + for op in self.operations: + match op: + case SetField(key=key, value=value): + lines.append(f"set {key!r} = {value!r}") + case AppendToList(key=key, items=items): + lines.append(f"append to {key!r}: {list(items)!r}") + case RemoveFromList(key=key, values=values): + lines.append(f"remove from {key!r}: {list(values)!r}") + case DeleteField(key=key): + lines.append(f"delete {key!r}") + return lines + + @dataclass(frozen=True, slots=True) class SetField: """Set a top-level or nested field to a value.""" diff --git a/vibe/core/feedback.py b/vibe/core/feedback.py new file mode 100644 index 0000000..d234de4 --- /dev/null +++ b/vibe/core/feedback.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import random +import time + +from vibe.cli.cache import read_cache, write_cache +from vibe.core.paths import CACHE_FILE + +FEEDBACK_PROBABILITY = 0.2 +FEEDBACK_COOLDOWN_SECONDS = 3600 +MIN_USER_MESSAGES_FOR_FEEDBACK = 3 +_CACHE_SECTION = "user_feedback" +_LAST_SHOWN_KEY = "last_shown_at" + + +def should_show_feedback( + *, telemetry_active: bool, is_mistral_model: bool, user_message_count: int +) -> bool: + if not telemetry_active: + return False + if not is_mistral_model: + return False + if user_message_count < MIN_USER_MESSAGES_FOR_FEEDBACK: + return False + + last_ts = ( + read_cache(CACHE_FILE.path).get(_CACHE_SECTION, {}).get(_LAST_SHOWN_KEY, 0) + ) + if not isinstance(last_ts, int): + return False + + return ( + time.time() - last_ts >= FEEDBACK_COOLDOWN_SECONDS + and random.random() <= FEEDBACK_PROBABILITY + ) + + +def record_feedback_asked() -> None: + write_cache(CACHE_FILE.path, _CACHE_SECTION, {_LAST_SHOWN_KEY: int(time.time())}) diff --git a/vibe/core/paths/__init__.py b/vibe/core/paths/__init__.py index 24c6150..4c80c63 100644 --- a/vibe/core/paths/__init__.py +++ b/vibe/core/paths/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +from vibe.core.paths._agents_home import AGENTS_HOME from vibe.core.paths._local_config_walk import ( WALK_MAX_DEPTH, ConfigWalkResult, @@ -22,6 +23,7 @@ from vibe.core.paths._vibe_home import ( from vibe.core.paths.conventions import AGENTS_MD_FILENAME __all__ = [ + "AGENTS_HOME", "AGENTS_MD_FILENAME", "CACHE_FILE", "DEFAULT_TOOL_DIR", diff --git a/vibe/core/paths/_agents_home.py b/vibe/core/paths/_agents_home.py new file mode 100644 index 0000000..48acb42 --- /dev/null +++ b/vibe/core/paths/_agents_home.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pathlib import Path + +from vibe.core.paths._vibe_home import GlobalPath + +_DEFAULT_AGENTS_HOME = Path.home() / ".agents" + + +def _get_agents_home() -> Path: + return _DEFAULT_AGENTS_HOME + + +AGENTS_HOME = GlobalPath(_get_agents_home) diff --git a/vibe/core/prompts/__init__.py b/vibe/core/prompts/__init__.py index e4d691f..732a924 100644 --- a/vibe/core/prompts/__init__.py +++ b/vibe/core/prompts/__init__.py @@ -24,6 +24,7 @@ class SystemPrompt(Prompt): EXPLORE = auto() TESTS = auto() LEAN = auto() + MINIMAL = auto() class UtilityPrompt(Prompt): diff --git a/vibe/core/prompts/cli.md b/vibe/core/prompts/cli.md index 61e9c35..e790525 100644 --- a/vibe/core/prompts/cli.md +++ b/vibe/core/prompts/cli.md @@ -1,4 +1,5 @@ You are Mistral Vibe, a CLI coding agent built by Mistral AI. You interact with a local codebase through tools. +Today's date is $current_date. CRITICAL: Users complain you are too verbose. Your responses must be minimal. Most tasks need <150 words. Code speaks for itself. Phase 1 - Orient diff --git a/vibe/core/prompts/cli_2026-05.md b/vibe/core/prompts/cli_2026-05.md index ecca823..0cd29cd 100644 --- a/vibe/core/prompts/cli_2026-05.md +++ b/vibe/core/prompts/cli_2026-05.md @@ -1,4 +1,5 @@ You are Mistral Vibe, a CLI coding agent built by Mistral AI. You work on a local codebase using tools. +Today's date is $current_date. ## Instruction hierarchy diff --git a/vibe/core/prompts/lean.md b/vibe/core/prompts/lean.md index 1bf68db..47aac19 100644 --- a/vibe/core/prompts/lean.md +++ b/vibe/core/prompts/lean.md @@ -1,4 +1,5 @@ You are Mistral Vibe, a CLI coding agent built by Mistral AI. You interact with a local codebase through tools. +Today's date is $current_date. Use markdown when appropriate. Communicate clearly to the user. diff --git a/vibe/core/prompts/minimal.md b/vibe/core/prompts/minimal.md new file mode 100644 index 0000000..ac11fba --- /dev/null +++ b/vibe/core/prompts/minimal.md @@ -0,0 +1,12 @@ +You are Mistral Vibe, a CLI coding agent built by Mistral AI. You work on a local codebase through tools. + +**Goal** +Finish the task. Prove it works. + +**How** +1. Read every file you will edit before touching it. +2. Make the minimal change needed. +3. Verify: run the relevant test. A passing edit is not done — only passing verification is done. + +**If stuck** +Same error twice: stop, re-read the file, change approach. diff --git a/vibe/core/skills/builtins/vibe.py b/vibe/core/skills/builtins/vibe.py index 823b922..11d65ab 100644 --- a/vibe/core/skills/builtins/vibe.py +++ b/vibe/core/skills/builtins/vibe.py @@ -35,6 +35,9 @@ agents, prompts, logs, and session data live here. vibe.log # Main log file session/ # Session log files plans/ # Session plans + +~/.agents/ + skills/ # Additional user-level skills directory ``` ### Project-Local Configuration @@ -413,6 +416,7 @@ Custom agents are TOML files in `~/.vibe/agents/NAME.toml`. - `/config` - Edit config settings - `/model` - Select active model - `/thinking` - Select thinking level +- `/theme` - Select Textual UI theme (persisted in config) - `/reload` - Reload configuration, agent instructions, and skills from disk - `/clear` - Clear conversation history - `/log` - Show path to current interaction log file @@ -466,6 +470,7 @@ Detailed instructions for the model... 2. `.vibe/skills/` in trusted project directory 3. `.agents/skills/` in trusted project directory 4. `~/.vibe/skills/` (user global) +5. `~/.agents/skills/` (user global, Agent Skills standard) ## Environment Variables diff --git a/vibe/core/system_prompt.py b/vibe/core/system_prompt.py index 55daf22..eaff67a 100644 --- a/vibe/core/system_prompt.py +++ b/vibe/core/system_prompt.py @@ -1,6 +1,7 @@ from __future__ import annotations from concurrent.futures import ThreadPoolExecutor +from datetime import date import html import os from pathlib import Path @@ -168,6 +169,11 @@ def _get_os_system_prompt() -> str: return prompt +def _format_current_date() -> str: + today = date.today() + return f"{today.isoformat()} ({today.strftime('%A')})" + + def _get_windows_system_prompt() -> str: return ( "### COMMAND COMPATIBILITY RULES (MUST FOLLOW):\n" @@ -284,6 +290,10 @@ def _resolve_system_prompt( return prompt +def _interpolate_prompt(prompt: str) -> str: + return Template(prompt).safe_substitute(current_date=_format_current_date()) + + def _get_headless_section() -> str: return ( "# Headless Mode\n\n" @@ -306,7 +316,7 @@ def get_universal_system_prompt( # noqa: PLR0912 headless: bool = False, experiment_manager: ExperimentManager | None = None, ) -> str: - sections = [_resolve_system_prompt(config, experiment_manager)] + sections = [_interpolate_prompt(_resolve_system_prompt(config, experiment_manager))] if headless: sections.append(_get_headless_section()) diff --git a/vibe/core/teleport/experimental_nuage.py b/vibe/core/teleport/experimental_nuage.py new file mode 100644 index 0000000..fc9d220 --- /dev/null +++ b/vibe/core/teleport/experimental_nuage.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +import types + +import httpx +from pydantic import BaseModel, ConfigDict, Field, ValidationError + +from vibe.core.teleport.errors import ServiceTeleportError +from vibe.core.utils.http import build_ssl_context + + +class ExperimentalNuageTextPart(BaseModel): + model_config = ConfigDict(extra="forbid") + + type: str = "text" + text: str + + +class ExperimentalNuageMessage(BaseModel): + model_config = ConfigDict(extra="forbid") + + role: str = "user" + parts: list[ExperimentalNuageTextPart] + + +class ExperimentalNuageRepository(BaseModel): + model_config = ConfigDict(extra="forbid") + + repo_url: str = Field(serialization_alias="repoUrl") + branch: str | None = None + + +class ExperimentalNuageContext(BaseModel): + model_config = ConfigDict(extra="forbid") + + repositories: list[ExperimentalNuageRepository] + + +class ExperimentalNuageRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + + project_name: str = Field(default="Vibe CLI", serialization_alias="project_name") + source: str = "vibe_code_cli" + idempotency_key: str = Field(serialization_alias="idempotencyKey") + message: ExperimentalNuageMessage + context: ExperimentalNuageContext + + +class ExperimentalNuageResponse(BaseModel): + model_config = ConfigDict(extra="ignore") + + nuage_session_id: str = Field(validation_alias="sessionId") + nuage_web_session_id: str = Field(validation_alias="webSessionId") + nuage_project_id: str = Field(validation_alias="projectId") + status: str + url: str + + +class ExperimentalNuageClient: + def __init__( + self, + base_url: str, + api_key: str, + *, + client: httpx.AsyncClient | None = None, + timeout: float = 60.0, + ) -> None: + self._base_url = base_url.rstrip("/") + self._api_key = api_key + self._client = client + self._owns_client = client is None + self._timeout = timeout + + async def __aenter__(self) -> ExperimentalNuageClient: + if self._client is None: + self._client = httpx.AsyncClient( + timeout=httpx.Timeout(self._timeout), verify=build_ssl_context() + ) + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> None: + if self._owns_client and self._client: + await self._client.aclose() + self._client = None + + @property + def _http_client(self) -> httpx.AsyncClient: + if self._client is None: + self._client = httpx.AsyncClient( + timeout=httpx.Timeout(self._timeout), verify=build_ssl_context() + ) + self._owns_client = True + return self._client + + def _headers(self) -> dict[str, str]: + return { + "Authorization": f"Bearer {self._api_key}", + "Content-Type": "application/json", + } + + async def start( + self, request: ExperimentalNuageRequest + ) -> ExperimentalNuageResponse: + response = await self._http_client.post( + f"{self._base_url}/api/v1/code/sessions", + headers=self._headers(), + json=request.model_dump(mode="json", by_alias=True, exclude_none=True), + ) + if not response.is_success: + raise ServiceTeleportError(f"Vibe Code Nuage start failed: {response.text}") + + try: + return ExperimentalNuageResponse.model_validate(response.json()) + except (ValueError, ValidationError) as e: + raise ServiceTeleportError("Vibe Code Nuage response was invalid") from e diff --git a/vibe/core/teleport/teleport.py b/vibe/core/teleport/teleport.py index d37759c..addb8a3 100644 --- a/vibe/core/teleport/teleport.py +++ b/vibe/core/teleport/teleport.py @@ -5,6 +5,7 @@ import base64 from collections.abc import AsyncGenerator from pathlib import Path import types +from uuid import uuid4 import httpx import zstandard @@ -12,6 +13,14 @@ import zstandard from vibe.core.config import VibeConfig from vibe.core.session.session_logger import SessionLogger from vibe.core.teleport.errors import ServiceTeleportError +from vibe.core.teleport.experimental_nuage import ( + ExperimentalNuageClient, + ExperimentalNuageContext, + ExperimentalNuageMessage, + ExperimentalNuageRepository, + ExperimentalNuageRequest, + ExperimentalNuageTextPart, +) from vibe.core.teleport.git import GitRepoInfo, GitRepository from vibe.core.teleport.nuage import ( ChatAssistantParams, @@ -65,25 +74,34 @@ class TeleportService: self._vibe_code_project_name = ( vibe_config.vibe_code_project_name if vibe_config else None ) + self._experimental_nuage_enabled = ( + vibe_config.vibe_code_experimental_nuage_enabled if vibe_config else False + ) self._vibe_config = vibe_config self._git = GitRepository(workdir) self._client = client self._owns_client = client is None self._timeout = timeout self._nuage_client_instance: NuageClient | None = None + self._experimental_nuage_client_instance: ExperimentalNuageClient | None = None async def __aenter__(self) -> TeleportService: if self._client is None: self._client = httpx.AsyncClient( timeout=httpx.Timeout(self._timeout), verify=build_ssl_context() ) - self._nuage_client_instance = NuageClient( - self._vibe_code_base_url, - self._vibe_code_api_key, - self._vibe_code_workflow_id, - task_queue=self._vibe_code_task_queue, - client=self._client, - ) + if self._experimental_nuage_enabled: + self._experimental_nuage_client_instance = ExperimentalNuageClient( + self._vibe_code_base_url, self._vibe_code_api_key, client=self._client + ) + else: + self._nuage_client_instance = NuageClient( + self._vibe_code_base_url, + self._vibe_code_api_key, + self._vibe_code_workflow_id, + task_queue=self._vibe_code_task_queue, + client=self._client, + ) await self._git.__aenter__() return self @@ -119,6 +137,16 @@ class TeleportService: ) return self._nuage_client_instance + @property + def _experimental_nuage_client(self) -> ExperimentalNuageClient: + if self._experimental_nuage_client_instance is None: + self._experimental_nuage_client_instance = ExperimentalNuageClient( + self._vibe_code_base_url, + self._vibe_code_api_key, + client=self._http_client, + ) + return self._experimental_nuage_client_instance + async def check_supported(self) -> None: await self._git.get_info() @@ -141,6 +169,8 @@ class TeleportService: self._validate_config() git_info = await self._git.get_info() + if self._experimental_nuage_enabled: + self._validate_experimental_nuage_git_info(git_info) yield TeleportCheckingGitEvent() await self._git.fetch() @@ -165,6 +195,15 @@ class TeleportService: yield TeleportStartingWorkflowEvent() + if self._experimental_nuage_enabled: + result = await self._experimental_nuage_client.start( + self._build_experimental_nuage_request( + lechat_user_message=lechat_user_message, git_info=git_info + ) + ) + yield TeleportCompleteEvent(url=result.url) + return + execution_id = await self._nuage_client.start_workflow( WorkflowParams( prompt=prompt, @@ -235,6 +274,36 @@ class TeleportService: teleported_diffs=self._compress_diff(git_info.diff or ""), ) + def _build_experimental_nuage_request( + self, *, lechat_user_message: str, git_info: GitRepoInfo + ) -> ExperimentalNuageRequest: + if git_info.branch is None: + raise ServiceTeleportError( + "Experimental Nuage teleport requires a checked-out branch." + ) + + return ExperimentalNuageRequest( + idempotency_key=str(uuid4()), + message=ExperimentalNuageMessage( + parts=[ExperimentalNuageTextPart(text=lechat_user_message)] + ), + context=ExperimentalNuageContext( + repositories=[ + ExperimentalNuageRepository( + repo_url=git_info.remote_url, branch=git_info.branch + ) + ] + ), + ) + + def _validate_experimental_nuage_git_info(self, git_info: GitRepoInfo) -> None: + if git_info.branch is not None: + return + + raise ServiceTeleportError( + "Experimental Nuage teleport requires a checked-out branch." + ) + def _compress_diff(self, diff: str, max_size: int = 1_000_000) -> bytes | None: if not diff: return None diff --git a/vibe/core/tools/builtins/grep.py b/vibe/core/tools/builtins/grep.py index a6c0316..5584940 100644 --- a/vibe/core/tools/builtins/grep.py +++ b/vibe/core/tools/builtins/grep.py @@ -239,6 +239,7 @@ class Grep( "rg", "--line-number", "--no-heading", + "--with-filename", "--smart-case", "--no-binary", # Request one extra to detect truncation @@ -261,7 +262,7 @@ class Grep( ) -> list[str]: max_matches = args.max_matches or self.config.default_max_matches - cmd = ["grep", "-r", "-n", "-I", "-E", f"--max-count={max_matches + 1}"] + cmd = ["grep", "-r", "-n", "-H", "-I", "-E", f"--max-count={max_matches + 1}"] if args.pattern.islower(): cmd.append("-i") diff --git a/vibe/core/tools/connectors/__init__.py b/vibe/core/tools/connectors/__init__.py index c856b0b..4b958b7 100644 --- a/vibe/core/tools/connectors/__init__.py +++ b/vibe/core/tools/connectors/__init__.py @@ -1,5 +1,8 @@ from __future__ import annotations -from vibe.core.tools.connectors.connector_registry import ConnectorRegistry +from vibe.core.tools.connectors.connector_registry import ( + ConnectorAuthAction, + ConnectorRegistry, +) -__all__ = ["ConnectorRegistry"] +__all__ = ["ConnectorAuthAction", "ConnectorRegistry"] diff --git a/vibe/core/tools/connectors/connector_registry.py b/vibe/core/tools/connectors/connector_registry.py index e34063a..44c117d 100644 --- a/vibe/core/tools/connectors/connector_registry.py +++ b/vibe/core/tools/connectors/connector_registry.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio from collections.abc import AsyncGenerator +from enum import StrEnum import re from typing import TYPE_CHECKING, Any, ClassVar @@ -34,6 +35,24 @@ if TYPE_CHECKING: _BOOTSTRAP_TIMEOUT = 30.0 +class ConnectorAuthAction(StrEnum): + NONE = "none" + OAUTH = "oauth" + CREDENTIALS_SETUP = "credentials_setup" + + @classmethod + def from_payload(cls, payload: dict[str, Any] | None) -> ConnectorAuthAction: + if not payload: + return cls.NONE + match payload.get("type"): + case "oauth": + return cls.OAUTH + case "credentials_setup": + return cls.CREDENTIALS_SETUP + case _: + return cls.NONE + + def _normalize_name(name: str) -> str: normalized = re.sub(r"[^a-zA-Z0-9_-]", "_", name) result = normalized.strip("_-")[:256] @@ -234,6 +253,7 @@ class ConnectorRegistry: self._cache: dict[str, dict[str, type[BaseTool]]] | None = None self._connector_names: list[str] = [] self._connector_connected: dict[str, bool] = {} + self._connector_auth_action: dict[str, ConnectorAuthAction] = {} self._alias_to_id: dict[str, str] = {} self._discover_lock = asyncio.Lock() @@ -255,10 +275,11 @@ class ConnectorRegistry: base_url = self._server_url or _DEFAULT_BASE_URL url = f"{base_url}/v1/connectors/bootstrap" headers = {"Authorization": f"Bearer {self._api_key}"} + params = {"include_auth_actionable_connectors": "true"} async with httpx.AsyncClient( timeout=_BOOTSTRAP_TIMEOUT, verify=build_ssl_context() ) as http: - response = await http.get(url, headers=headers) + response = await http.get(url, headers=headers, params=params) response.raise_for_status() return response.json() @@ -311,6 +332,7 @@ class ConnectorRegistry: self._cache = {} self._connector_names = [] self._connector_connected = {} + self._connector_auth_action = {} self._alias_to_id = {} return {} @@ -320,10 +342,15 @@ class ConnectorRegistry: all_tools: dict[str, type[BaseTool]] = {} connector_names: list[str] = [] connector_connected: dict[str, bool] = {} + connector_auth_action: dict[str, ConnectorAuthAction] = {} for connector_id, alias, connector in unique_connectors: connector_names.append(alias) name = connector.get("name") or connector_id + auth_action = ConnectorAuthAction.from_payload( + connector.get("auth_action") + ) + connector_auth_action[alias] = auth_action if bootstrap_errors := connector.get("bootstrap_errors"): logger.warning( @@ -349,6 +376,7 @@ class ConnectorRegistry: # lock will see the completed cache. self._connector_names = connector_names self._connector_connected = connector_connected + self._connector_auth_action = connector_auth_action self._alias_to_id = {alias: cid for cid, alias, _ in unique_connectors} self._cache = cache @@ -366,6 +394,9 @@ class ConnectorRegistry: def is_connected(self, name: str) -> bool: return self._connector_connected.get(name, False) + def get_auth_action(self, alias: str) -> ConnectorAuthAction: + return self._connector_auth_action.get(alias, ConnectorAuthAction.NONE) + def get_connector_id(self, alias: str) -> str | None: """Return the API connector ID for a given alias, or None.""" return self._alias_to_id.get(alias) @@ -382,13 +413,21 @@ class ConnectorRegistry: return {} tools_map: dict[str, type[BaseTool]] | None = None + fresh_auth_action: ConnectorAuthAction | None = None + found = False + fetch_ok = False try: data = await self._fetch_bootstrap() + fetch_ok = True for connector in data.get("connectors") or []: if str(connector.get("id")) != connector_id: continue + found = True name = connector.get("name") or connector_id + fresh_auth_action = ConnectorAuthAction.from_payload( + connector.get("auth_action") + ) status = connector.get("status") or {} if not status.get("is_ready", False): break @@ -406,6 +445,13 @@ class ConnectorRegistry: if self._cache is None: self._cache = {} + if fetch_ok and not found: + self._drop_connector(alias, connector_id) + return {} + + if fresh_auth_action is not None: + self._connector_auth_action[alias] = fresh_auth_action + if tools_map is None: self._cache.pop(connector_id, None) self._connector_connected[alias] = False @@ -415,6 +461,15 @@ class ConnectorRegistry: self._connector_connected[alias] = bool(tools_map) return tools_map + def _drop_connector(self, alias: str, connector_id: str) -> None: + if self._cache is not None: + self._cache.pop(connector_id, None) + self._connector_connected.pop(alias, None) + self._connector_auth_action.pop(alias, None) + self._alias_to_id.pop(alias, None) + if alias in self._connector_names: + self._connector_names.remove(alias) + async def get_auth_url(self, alias: str) -> str | None: """Return the OAuth authorization URL for a connector, or None. @@ -449,4 +504,5 @@ class ConnectorRegistry: self._cache = None self._connector_names = [] self._connector_connected = {} + self._connector_auth_action = {} self._alias_to_id = {} diff --git a/vibe/core/tools/manager.py b/vibe/core/tools/manager.py index c3314f7..32e2b90 100644 --- a/vibe/core/tools/manager.py +++ b/vibe/core/tools/manager.py @@ -254,6 +254,9 @@ class ToolManager: elif srv.disabled_tools: per_source_disabled[key] = set(srv.disabled_tools) + # Track connector names that have explicit config entries + configured_connectors = {cfg.name for cfg in self._config.connectors} + for cfg in self._config.connectors: key = (cfg.name, True) if cfg.disabled: @@ -261,6 +264,12 @@ class ToolManager: elif cfg.disabled_tools: per_source_disabled[key] = set(cfg.disabled_tools) + # Disable connectors discovered but not in config (new connectors) + if self._connector_registry is not None: + for name in self._connector_registry.get_connector_names(): + if name not in configured_connectors: + disabled_sources.add((name, True)) + return disabled_sources, per_source_disabled @staticmethod diff --git a/vibe/core/utils/io.py b/vibe/core/utils/io.py index 70b217b..d7e514f 100644 --- a/vibe/core/utils/io.py +++ b/vibe/core/utils/io.py @@ -68,6 +68,12 @@ def _get_candidate_encodings(raw: bytes) -> Iterator[str]: yield best +def normalize_newlines(text: str) -> tuple[str, str]: + r"""Return ``text`` with ``\n`` newlines and the detected original style.""" + newline = _detect_newline(text) + return text.replace("\r\n", "\n").replace("\r", "\n"), newline + + def decode_safe(raw: bytes, *, raise_on_error: bool = False) -> ReadSafeResult: """Decode ``raw`` like :func:`read_safe` after ``read_bytes``. @@ -85,8 +91,7 @@ def decode_safe(raw: bytes, *, raise_on_error: bool = False) -> ReadSafeResult: errors = "strict" if raise_on_error else "replace" encoding = "utf-8" text = raw.decode(encoding, errors=errors) - newline = _detect_newline(text) - text = text.replace("\r\n", "\n").replace("\r", "\n") + text, newline = normalize_newlines(text) return ReadSafeResult(text, encoding, newline) diff --git a/vibe/setup/onboarding/__init__.py b/vibe/setup/onboarding/__init__.py index ef0fac1..17caacb 100644 --- a/vibe/setup/onboarding/__init__.py +++ b/vibe/setup/onboarding/__init__.py @@ -16,8 +16,10 @@ from vibe.setup.onboarding.screens import ( ApiKeyScreen, AuthMethodScreen, BrowserSignInScreen, + ThemeSelectionScreen, WelcomeScreen, ) +from vibe.setup.onboarding.screens.browser_sign_in import SUCCESS_EXIT_DELAY_SECONDS class OnboardingApp(App[str | None]): @@ -29,6 +31,7 @@ class OnboardingApp(App[str | None]): browser_sign_in_service_factory: Callable[[], BrowserSignInService] | None = None, entrypoint_metadata: EntrypointMetadata | None = None, + browser_sign_in_success_delay: float = SUCCESS_EXIT_DELAY_SECONDS, **kwargs: Any, ) -> None: super().__init__(**kwargs) @@ -40,16 +43,20 @@ class OnboardingApp(App[str | None]): self._config = config self._provider = config.provider self._entrypoint_metadata = entrypoint_metadata + self._browser_sign_in_success_delay = browser_sign_in_success_delay self._browser_sign_in_service_factory = self._resolve_browser_sign_in_factory( browser_sign_in_service_factory ) def on_mount(self) -> None: - self.theme = "textual-ansi" + self.theme = "ansi-dark" - welcome_next = "auth_method" if self.supports_browser_sign_in else "api_key" - welcome_screen = WelcomeScreen(next_screen=welcome_next) + theme_next = "auth_method" if self.supports_browser_sign_in else "api_key" + welcome_screen = WelcomeScreen(next_screen="theme_selection") self.install_screen(welcome_screen, "welcome") + self.install_screen( + ThemeSelectionScreen(next_screen=theme_next), "theme_selection" + ) self.install_screen( ApiKeyScreen(self._provider, entrypoint_metadata=self._entrypoint_metadata), "api_key", @@ -61,6 +68,7 @@ class OnboardingApp(App[str | None]): self._provider, self._browser_sign_in_service_factory, entrypoint_metadata=self._entrypoint_metadata, + success_exit_delay=self._browser_sign_in_success_delay, ), "browser_sign_in", ) diff --git a/vibe/setup/onboarding/gradient_text.py b/vibe/setup/onboarding/gradient_text.py new file mode 100644 index 0000000..141e403 --- /dev/null +++ b/vibe/setup/onboarding/gradient_text.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from rich.text import Text + +GRADIENT_COLORS = [ + "#ff6b00", + "#ff7b00", + "#ff8c00", + "#ff9d00", + "#ffae00", + "#ffbf00", + "#ffae00", + "#ff9d00", + "#ff8c00", + "#ff7b00", +] + + +def gradient_markup(text: str, offset: int) -> str: + result = [] + for index, char in enumerate(text): + color = GRADIENT_COLORS[(index + offset) % len(GRADIENT_COLORS)] + result.append(f"[bold {color}]{char}[/]") + return "".join(result) + + +def append_gradient_text(content: Text, text: str, offset: int) -> None: + for index, char in enumerate(text): + color = GRADIENT_COLORS[(index + offset) % len(GRADIENT_COLORS)] + content.append(char, style=f"bold {color}") diff --git a/vibe/setup/onboarding/onboarding.tcss b/vibe/setup/onboarding/onboarding.tcss index f842daf..cf46f27 100644 --- a/vibe/setup/onboarding/onboarding.tcss +++ b/vibe/setup/onboarding/onboarding.tcss @@ -1,22 +1,22 @@ -/* ============================================================================= - Onboarding App Styles - ============================================================================= */ +/* Onboarding App Styles */ + +$mistral_orange: #FF8205; +$mistral_orange_title: #ff5a00; +$browser_sign_in_card_border: #303040; Screen, OnboardingScreen { align: center middle; } -/* ============================================================================= - Welcome Screen - ============================================================================= */ +/* Welcome Screen */ #welcome-container { align: center middle; } #welcome-text { - border: round #555555; + border: round $foreground-muted; padding: 1 3; margin-bottom: 2; text-align: center; @@ -24,7 +24,7 @@ OnboardingScreen { } WelcomeScreen #enter-hint { - color: ansi_bright_black; + color: $text; min-width: 16; text-align: center; } @@ -33,100 +33,301 @@ WelcomeScreen #enter-hint.hidden { visibility: hidden; } +/* Theme Selection Screen */ + +#theme-outer { + width: 100%; + height: 100%; + align: center middle; + overflow-y: auto; +} + +#theme-content { + width: auto; + height: auto; +} + +#theme-title { + text-align: center; + width: 100%; + margin-bottom: 2; +} + +#theme-row { + width: auto; + height: auto; +} + +#nav-hint { + color: $text; + width: 16; + height: 100%; + content-align: right middle; + padding-right: 2; +} + +#theme-list { + width: 30; + height: auto; +} + +.theme-item { + text-align: center; + width: 100%; + background: transparent; +} + +.theme-item.selected { + color: $text; + text-style: bold; + border: round $foreground-muted; + padding: 0 2; +} + +.theme-item.fade-1 { + text-opacity: 50%; +} + +.theme-item.fade-2 { + text-opacity: 25%; +} + +.theme-item.fade-3 { + text-opacity: 10%; +} + +ThemeSelectionScreen #enter-hint { + color: $text; + width: 16; + height: 100%; + content-align: left middle; + padding-left: 2; +} + +#preview-center { + width: 100%; + height: auto; + margin-top: 2; + align: center middle; +} + +#preview { + min-width: 50; + max-width: 70; + height: auto; + max-height: 100%; + overflow: auto; + border: round $foreground-muted; + border-title-align: center; +} + +#preview-inner { + width: 100%; + height: auto; + padding: 0 1 0 1; +} + /* ============================================================================= Auth Method Screen ============================================================================= */ -#auth-method-content, -#browser-sign-in-content { +.onboarding-content { width: 100%; height: auto; } -#auth-method-card, -#browser-sign-in-card { - border: round #555555; - padding: 1 2; - width: 72; - max-width: 90w; +.onboarding-panel { + width: 70; + max-width: 88w; height: auto; } -#auth-method-title, -#browser-sign-in-title { - margin-bottom: 1; - text-align: center; +.onboarding-chat { + width: auto; + height: auto; + margin-left: 2; + margin-bottom: 0; +} + +.onboarding-chat .petit-chat { + width: auto; + color: $text; +} + +.onboarding-heading { + width: 100%; + margin-left: 2; + margin-bottom: 0; text-style: bold; } -#auth-method-subtitle, -#browser-sign-in-subtitle { - color: ansi_bright_black; - text-align: center; - margin-bottom: 2; +#auth-method-title { + color: $mistral_orange_title; } -.auth-method-option { - border: round #444444; - padding: 1 2; - margin-top: 1; +#auth-method-subtitle { + width: 100%; + margin-left: 2; + margin-bottom: 2; + color: $text; + text-style: bold; +} + +#auth-method-options { width: 100%; height: auto; } -.auth-method-option.selected { - border: round ansi_bright_yellow; - background: #2b2400; +.onboarding-option-row { + width: 100%; + height: auto; + margin: 0 0; } -#auth-method-help, -#browser-sign-in-hint { - color: ansi_bright_black; - text-align: center; +.auth-method-option-marker { + width: 2; + height: 3; + content-align: center middle; + color: transparent; +} + +.auth-method-option-marker.selected { + color: $mistral_orange; +} + +.onboarding-card { + border: solid $browser_sign_in_card_border; + padding: 0 1; + width: 1fr; + height: auto; +} + +.auth-method-option { + color: $foreground-muted; + border-title-align: right; + border-title-color: $primary; + border-title-style: bold; +} + +.auth-method-option.selected { + border: solid $browser_sign_in_card_border; + border-left: solid $mistral_orange; + color: $text; +} + +.auth-method-separator { + width: 100%; + height: 1; + margin: 1 0 1 2; + color: $text-muted; + text-style: dim; + + &:ansi { + text-style: dim; + } +} + +.onboarding-hint-row { + width: 100%; + color: $foreground-muted; margin-top: 2; + padding-left: 2; } /* ============================================================================= Browser Sign-In Screen ============================================================================= */ -.browser-sign-in-step { - border-left: wide #444444; - padding-left: 1; +#browser-sign-in-subtitle { + width: 100%; + color: $foreground-muted; + margin-left: 2; margin-bottom: 1; - color: ansi_bright_black; +} + +#browser-sign-in-steps { + width: 100%; + height: auto; +} + +.browser-sign-in-step-marker { + width: 2; + height: 3; + margin-top: 0; + content-align: center middle; + color: $mistral_orange; +} + +.browser-sign-in-step-marker.idle, +.browser-sign-in-step-marker.done { + color: transparent; +} + +.browser-sign-in-step { + color: $foreground-muted; } .browser-sign-in-step.active { - border-left: wide ansi_bright_yellow; color: $text; } .browser-sign-in-step.done { - border-left: wide ansi_green; - color: ansi_green; + border-left: solid $success; + color: $text; } -#browser-sign-in-status { - border: round #444444; - padding: 1 2; +.browser-sign-in-step.idle { + color: $foreground-muted; +} + +.browser-sign-in-step-title, +.browser-sign-in-step-detail { width: 100%; height: auto; - margin-top: 2; + padding-left: 2; } -#browser-sign-in-status.pending { - border: round ansi_bright_yellow; +.browser-sign-in-step-title { + text-style: bold; } -#browser-sign-in-status.error { - border: round ansi_red; - color: ansi_red; +.browser-sign-in-step-title.idle { + color: $foreground-muted; + text-style: bold dim; } -#browser-sign-in-status.success { - border: round ansi_green; - color: ansi_green; +.browser-sign-in-step-detail.idle, +.browser-sign-in-step-detail.done { + color: $foreground-muted; + text-style: dim; +} + +.browser-sign-in-step-title.active, +.browser-sign-in-step-title.done { + color: $text; +} + +.browser-sign-in-step-detail.pending { + color: $mistral_orange; + text-style: bold; +} + +.browser-sign-in-step-detail.error { + color: $error; + text-style: bold; +} + +.browser-sign-in-step-detail.success { + color: $success; + text-style: bold; +} + +#browser-sign-in-hint { + width: 100%; + color: $foreground-muted; + text-align: center; + margin-top: 1; + padding-left: 2; } /* ============================================================================= @@ -134,94 +335,105 @@ WelcomeScreen #enter-hint.hidden { ============================================================================= */ #api-key-outer { - overflow-y: auto; -} - -.spacer { - height: 1fr; + width: 100%; + height: auto; } #api-key-title { - text-align: center; + color: $mistral_orange_title; +} + +#api-key-panel { + width: 76; + max-width: 100%; +} + +#api-key-subtitle { + width: 100%; + margin-left: 2; + margin-bottom: 1; + color: $text; + text-style: bold; +} + +#api-key-provider-link { + width: 100%; + height: auto; + margin-left: 2; margin-bottom: 2; -} + color: $primary; + text-style: underline; + pointer: pointer; -#api-key-content { - width: auto; - height: auto; -} - -#api-key-content Static { - text-align: center; -} - -.link-row { - width: auto; - height: auto; - margin-top: 1; -} - -.link-chevron { - width: auto; + &:hover { + color: $secondary; + text-style: bold underline; + } } #input-box { - border: round #555555; - padding: 0 1; - margin-top: 2; - width: auto; + border-title-align: left; + border-title-color: $text; + border-title-style: bold; + margin-left: 2; + margin-bottom: 1; + width: 100%; height: 3; } #input-box.valid { - border: round ansi_green; + border: solid $success; } #input-box.invalid { - border: round ansi_red; + border: solid $error; } #key { border: none; - width: 48; + width: 100%; height: 1; - padding: 0; -} - -#paste-hint { - margin-top: 3; + padding: 0 1; } #feedback { - text-align: center; + width: 100%; height: 1; - margin-top: 1; + color: $text-muted; + padding-left: 2; + + &:ansi { + text-style: dim; + } } #feedback.error { - color: ansi_red; + color: $error; } #feedback.success { - color: ansi_bright_black; + color: $text-muted; } -#config-docs-section { +#config-docs-label { width: 100%; - height: auto; - align: center top; - padding-bottom: 2; + height: 1; + margin-left: 2; + margin-top: 1; + color: $text-muted; + text-style: dim; } -#config-docs-group { - width: auto; - height: auto; -} +#config-docs-link { + width: 100%; + height: 1; + color: $text; + margin-left: 2; + text-style: dim; -#config-docs-group Static { - color: ansi_bright_black; -} - -#config-docs-group .link-row { - margin-top: 0; + pointer: pointer; + &:hover { + color: $primary; + text-style: bold underline; + } } diff --git a/vibe/setup/onboarding/screens/__init__.py b/vibe/setup/onboarding/screens/__init__.py index fd13e86..cd51b08 100644 --- a/vibe/setup/onboarding/screens/__init__.py +++ b/vibe/setup/onboarding/screens/__init__.py @@ -3,6 +3,13 @@ from __future__ import annotations from vibe.setup.onboarding.screens.api_key import ApiKeyScreen from vibe.setup.onboarding.screens.auth_method import AuthMethodScreen from vibe.setup.onboarding.screens.browser_sign_in import BrowserSignInScreen +from vibe.setup.onboarding.screens.theme_selection import ThemeSelectionScreen from vibe.setup.onboarding.screens.welcome import WelcomeScreen -__all__ = ["ApiKeyScreen", "AuthMethodScreen", "BrowserSignInScreen", "WelcomeScreen"] +__all__ = [ + "ApiKeyScreen", + "AuthMethodScreen", + "BrowserSignInScreen", + "ThemeSelectionScreen", + "WelcomeScreen", +] diff --git a/vibe/setup/onboarding/screens/api_key.py b/vibe/setup/onboarding/screens/api_key.py index 40b87c6..824bd91 100644 --- a/vibe/setup/onboarding/screens/api_key.py +++ b/vibe/setup/onboarding/screens/api_key.py @@ -4,12 +4,13 @@ from typing import ClassVar from textual.app import ComposeResult from textual.binding import Binding, BindingType -from textual.containers import Center, Horizontal, Vertical +from textual.containers import Center, Vertical from textual.events import MouseUp from textual.validation import Length -from textual.widgets import Input, Link, Static +from textual.widgets import Input, Link from vibe.cli.clipboard import copy_selection_to_clipboard +from vibe.cli.textual_ui.widgets.banner.petit_chat import PetitChat from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.core.config import ProviderConfig from vibe.core.telemetry.types import EntrypointMetadata @@ -19,9 +20,8 @@ from vibe.setup.auth.api_key_persistence import ( ) from vibe.setup.onboarding.base import OnboardingScreen -PROVIDER_HELP = { - "mistral": ("https://console.mistral.ai/codestral/cli", "Mistral AI Studio") -} +# providers with a dedicated key creation page get a direct onboarding link +PROVIDER_HELP = {"mistral": ("https://console.mistral.ai/codestral/cli", "AI Studio")} CONFIG_DOCS_URL = ( "https://github.com/mistralai/mistral-vibe?tab=readme-ov-file#configuration" ) @@ -45,54 +45,51 @@ class ApiKeyScreen(OnboardingScreen): self.provider = resolve_api_key_provider(provider) self._entrypoint_metadata = entrypoint_metadata - def _compose_provider_link(self, provider_name: str) -> ComposeResult: - if self.provider.name not in PROVIDER_HELP: + def _compose_provider_link(self) -> ComposeResult: + if (help_info := PROVIDER_HELP.get(self.provider.name)) is None: return - help_url, help_name = PROVIDER_HELP[self.provider.name] - yield NoMarkupStatic(f"Grab your {provider_name} API key from the {help_name}:") - yield Center( - Horizontal( - NoMarkupStatic("→ ", classes="link-chevron"), - Link(help_url, url=help_url), - classes="link-row", - ) - ) + help_url, _ = help_info + yield Link(help_url, url=help_url, id="api-key-provider-link") def _compose_config_docs(self) -> ComposeResult: - yield Static("[dim]Learn more about Vibe configuration:[/]") - yield Horizontal( - NoMarkupStatic("→ ", classes="link-chevron"), - Link(CONFIG_DOCS_URL, url=CONFIG_DOCS_URL), - classes="link-row", + yield NoMarkupStatic( + "Learn more about Vibe configurations", id="config-docs-label" ) + yield Link(CONFIG_DOCS_URL, url=CONFIG_DOCS_URL, id="config-docs-link") def compose(self) -> ComposeResult: provider_name = self.provider.name.capitalize() + help_info = PROVIDER_HELP.get(self.provider.name) + help_name = help_info[1] if help_info is not None else "your provider" self.input_widget = Input( password=True, id="key", - placeholder="Paste your API key here", validators=[Length(minimum=1, failure_description="No API key provided.")], ) + input_box = Vertical( + self.input_widget, id="input-box", classes="onboarding-card" + ) + input_box.border_title = "Paste API key" - with Vertical(id="api-key-outer"): - yield NoMarkupStatic("", classes="spacer") - yield Center(NoMarkupStatic("One last thing...", id="api-key-title")) + with Vertical(id="api-key-outer", classes="onboarding-content"): with Center(): - with Vertical(id="api-key-content"): - yield from self._compose_provider_link(provider_name) + with Vertical(id="api-key-panel", classes="onboarding-panel"): + yield PetitChat(id="api-key-chat", classes="onboarding-chat") yield NoMarkupStatic( - "...and paste it below to finish the setup:", id="paste-hint" + f"Get your {provider_name} API key", + id="api-key-title", + classes="onboarding-heading", ) - yield Center(Horizontal(self.input_widget, id="input-box")) + yield NoMarkupStatic( + f"Visit {help_name} to generate or copy your Vibe key", + id="api-key-subtitle", + ) + yield from self._compose_provider_link() + yield input_box yield NoMarkupStatic("", id="feedback") - yield NoMarkupStatic("", classes="spacer") - yield Vertical( - Vertical(*self._compose_config_docs(), id="config-docs-group"), - id="config-docs-section", - ) + yield from self._compose_config_docs() def on_mount(self) -> None: self.input_widget.focus() diff --git a/vibe/setup/onboarding/screens/auth_method.py b/vibe/setup/onboarding/screens/auth_method.py index 66e9298..3f69d5a 100644 --- a/vibe/setup/onboarding/screens/auth_method.py +++ b/vibe/setup/onboarding/screens/auth_method.py @@ -5,8 +5,9 @@ from typing import ClassVar from rich.text import Text from textual.app import ComposeResult from textual.binding import Binding, BindingType -from textual.containers import Center, Vertical +from textual.containers import Center, Horizontal, Vertical +from vibe.cli.textual_ui.widgets.banner.petit_chat import PetitChat from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.core.config import ProviderConfig from vibe.setup.onboarding.base import OnboardingScreen @@ -28,28 +29,46 @@ class AuthMethodScreen(OnboardingScreen): super().__init__() self.provider = provider self._selected_index = OPTION_BROWSER + self._option_markers: list[NoMarkupStatic] = [] self._option_widgets: list[NoMarkupStatic] = [] self._help_widget: NoMarkupStatic def compose(self) -> ComposeResult: - with Vertical(id="auth-method-content"): + with Vertical(id="auth-method-content", classes="onboarding-content"): with Center(): - with Vertical(id="auth-method-card"): + with Vertical(id="auth-method-panel", classes="onboarding-panel"): + yield PetitChat(id="auth-method-chat", classes="onboarding-chat") yield NoMarkupStatic( - "How would you like to sign in?", id="auth-method-title" + "Welcome to Mistral Vibe", + id="auth-method-title", + classes="onboarding-heading", ) yield NoMarkupStatic( - "Choose the setup that works best for you.", - id="auth-method-subtitle", + "Choose your sign in method", id="auth-method-subtitle" + ) + with Vertical(id="auth-method-options"): + yield from self._compose_option_rows() + self._help_widget = NoMarkupStatic( + "", id="auth-method-help", classes="onboarding-hint-row" ) - self._option_widgets = [ - NoMarkupStatic("", classes="auth-method-option"), - NoMarkupStatic("", classes="auth-method-option"), - ] - yield from self._option_widgets - self._help_widget = NoMarkupStatic("", id="auth-method-help") yield self._help_widget + def _compose_option_rows(self) -> ComposeResult: + self._option_markers = [] + self._option_widgets = [] + for index in range(2): + if index == OPTION_MANUAL: + yield NoMarkupStatic("or", classes="auth-method-separator") + with Horizontal(classes="auth-method-option-row onboarding-option-row"): + marker = NoMarkupStatic("", classes="auth-method-option-marker") + option = NoMarkupStatic( + "", classes="auth-method-option onboarding-card" + ) + self._option_markers.append(marker) + self._option_widgets.append(option) + yield marker + yield option + def on_mount(self) -> None: self._update_display() self.focus() @@ -75,27 +94,30 @@ class AuthMethodScreen(OnboardingScreen): self._update_display() def _update_display(self) -> None: - provider_name = self.provider.name.capitalize() options = [ ( - "Sign in with your browser", - f"Sign in to {provider_name} to finish setup automatically.", + "Launch browser", + "Recommended", + "Sign in to Mistral AI Studio and finish setup automatically.", ), - ("Use an API key", "Paste an existing API key to sign in manually."), + ("Use an API key", None, "Already have a key? Paste it manually instead."), ] - for index, (widget, (title, description)) in enumerate( - zip(self._option_widgets, options, strict=True) + for index, (marker, widget, (title, badge, description)) in enumerate( + zip(self._option_markers, self._option_widgets, options, strict=True) ): is_selected = index == self._selected_index - prefix = "›" if is_selected else " " content = Text() - content.append(f"{prefix} ") content.append(title, style="bold") - content.append(f"\n {description}") + content.append("\n") + content.append(description, style="dim") + marker.update(">" if is_selected else "") + marker.remove_class("selected") + widget.border_title = badge widget.update(content) widget.remove_class("selected") if is_selected: + marker.add_class("selected") widget.add_class("selected") - self._help_widget.update("↑↓ Choose · Enter to select · Esc Cancel") + self._help_widget.update("Use arrows to navigate - Enter Select - Esc Cancel") diff --git a/vibe/setup/onboarding/screens/browser_sign_in.py b/vibe/setup/onboarding/screens/browser_sign_in.py index e1a360c..4d511e3 100644 --- a/vibe/setup/onboarding/screens/browser_sign_in.py +++ b/vibe/setup/onboarding/screens/browser_sign_in.py @@ -6,12 +6,15 @@ from dataclasses import dataclass from enum import IntEnum from typing import ClassVar, Literal +from rich.text import Text from textual.app import ComposeResult from textual.binding import Binding, BindingType -from textual.containers import Center, Vertical +from textual.containers import Center, Horizontal, Vertical from textual.reactive import reactive +from textual.timer import Timer from textual.worker import Worker +from vibe.cli.textual_ui.widgets.banner.petit_chat import PetitChat from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.core.config import ProviderConfig from vibe.core.logger import logger @@ -27,10 +30,18 @@ from vibe.setup.auth.api_key_persistence import ( resolve_api_key_provider, ) from vibe.setup.onboarding.base import OnboardingScreen +from vibe.setup.onboarding.gradient_text import GRADIENT_COLORS, append_gradient_text -PENDING_HINT = "Press M to enter API key manually · Esc to cancel" -ERROR_HINT = "Press R to retry · Press M to enter API key manually · Esc to cancel" -STEP_DESCRIPTIONS = ["Open your browser", "Sign in and return here", "Finish setup"] +PENDING_HINT = "Press M to enter API key manually - Esc to cancel" +ERROR_HINT = "Press R to retry - Press M to enter API key manually - Esc to cancel" +SUCCESS_HINT = "Finishing setup..." +SUCCESS_EXIT_DELAY_SECONDS: float = 2.0 +WAITING_FOR_AUTHENTICATION_MESSAGE = "Waiting for authentication..." +STEP_DESCRIPTIONS = [ + ("Open browser", "Your browser should open automatically", "Browser opened"), + ("Complete sign-in", WAITING_FOR_AUTHENTICATION_MESSAGE, "Sign-in confirmed."), + ("Finished setup", "Vibe will start automatically", "Setup complete."), +] UNEXPECTED_ERROR_MESSAGE = ( "Something went wrong during browser sign-in. Please try again." ) @@ -55,6 +66,14 @@ class BrowserSignInViewState: running: bool +@dataclass(frozen=True) +class BrowserSignInStepWidgets: + marker: NoMarkupStatic + card: Vertical + title: NoMarkupStatic + detail: NoMarkupStatic + + class BrowserSignInScreen(OnboardingScreen): state = reactive( BrowserSignInViewState( @@ -80,14 +99,18 @@ class BrowserSignInScreen(OnboardingScreen): browser_sign_in_factory: Callable[[], BrowserSignInService], *, entrypoint_metadata: EntrypointMetadata | None = None, + success_exit_delay: float = SUCCESS_EXIT_DELAY_SECONDS, ) -> None: super().__init__() self.provider = provider self._browser_sign_in_factory = browser_sign_in_factory self._entrypoint_metadata = entrypoint_metadata + self._success_exit_delay = success_exit_delay self._attempt_number = 0 self._active_attempt_number: int | None = None self._worker: Worker[None] | None = None + self._gradient_offset = 0 + self._gradient_timer: Timer | None = None self._initial_state = BrowserSignInViewState( step=BrowserSignInStep.OPEN, message="Getting things ready...", @@ -95,45 +118,57 @@ class BrowserSignInScreen(OnboardingScreen): variant="pending", running=False, ) - self._step_widgets: list[NoMarkupStatic] = [] + self._step_widgets: list[BrowserSignInStepWidgets] = [] self._title_widget: NoMarkupStatic - self._subtitle_widget: NoMarkupStatic - self._status_widget: NoMarkupStatic self._hint_widget: NoMarkupStatic def compose(self) -> ComposeResult: - with Vertical(id="browser-sign-in-content"): + with Vertical(id="browser-sign-in-content", classes="onboarding-content"): with Center(): - with Vertical(id="browser-sign-in-card"): + with Vertical(id="browser-sign-in-panel", classes="onboarding-panel"): + yield PetitChat( + id="browser-sign-in-chat", classes="onboarding-chat" + ) self._title_widget = NoMarkupStatic( - "Sign in with your browser", id="browser-sign-in-title" + "Launch browser", + id="browser-sign-in-title", + classes="onboarding-heading", ) yield self._title_widget - self._subtitle_widget = NoMarkupStatic( - "", id="browser-sign-in-subtitle" + yield NoMarkupStatic( + "Your browser should open automatically", + id="browser-sign-in-subtitle", ) - yield self._subtitle_widget - self._step_widgets = [ - NoMarkupStatic("", classes="browser-sign-in-step"), - NoMarkupStatic("", classes="browser-sign-in-step"), - NoMarkupStatic("", classes="browser-sign-in-step"), - ] - yield from self._step_widgets - yield NoMarkupStatic("", id="browser-sign-in-status") + with Vertical(id="browser-sign-in-steps"): + yield from self._compose_step_rows() yield NoMarkupStatic("", id="browser-sign-in-hint") + def _compose_step_rows(self) -> ComposeResult: + self._step_widgets = [] + for _ in STEP_DESCRIPTIONS: + with Horizontal(classes="browser-sign-in-step-row onboarding-option-row"): + marker = NoMarkupStatic("", classes="browser-sign-in-step-marker") + yield marker + with Vertical(classes="browser-sign-in-step onboarding-card") as card: + title = NoMarkupStatic("", classes="browser-sign-in-step-title") + detail = NoMarkupStatic("", classes="browser-sign-in-step-detail") + self._step_widgets.append( + BrowserSignInStepWidgets(marker, card, title, detail) + ) + yield title + yield detail + def on_mount(self) -> None: - provider_name = self.provider.name.capitalize() - self._subtitle_widget.update( - f"Continue with {provider_name} to finish setup automatically." - ) self._hint_widget = self.query_one("#browser-sign-in-hint", NoMarkupStatic) - self._status_widget = self.query_one("#browser-sign-in-status", NoMarkupStatic) self.state = self._initial_state self.watch_state(self.state) + self._gradient_timer = self.set_interval(0.08, self._animate_gradient) self.call_after_refresh(self._start_browser_sign_in) def on_unmount(self) -> None: + if self._gradient_timer is not None: + self._gradient_timer.stop() + self._gradient_timer = None self._cancel_current_attempt() def action_retry(self) -> None: @@ -141,10 +176,14 @@ class BrowserSignInScreen(OnboardingScreen): self._start_browser_sign_in() def action_manual(self) -> None: + if self.state.variant == "success": + return self._cancel_current_attempt() self.app.switch_screen("api_key") def action_cancel(self) -> None: + if self.state.variant == "success": + return self._cancel_current_attempt() super().action_cancel() @@ -211,15 +250,29 @@ class BrowserSignInScreen(OnboardingScreen): if api_key is None: msg = "Browser sign-in finished without returning an API key." raise AssertionError(msg) + result = persist_api_key( + resolve_api_key_provider(self.provider), + api_key, + entrypoint_metadata=self._entrypoint_metadata, + ) + if result != "completed": + self._active_attempt_number = None + self._worker = None + self.app.exit(result) + return + + self.state = BrowserSignInViewState( + step=BrowserSignInStep.FINISH, + message="Sign-in complete", + hint=SUCCESS_HINT, + variant="success", + running=True, + ) + if self._success_exit_delay > 0: + await asyncio.sleep(self._success_exit_delay) self._active_attempt_number = None self._worker = None - self.app.exit( - persist_api_key( - resolve_api_key_provider(self.provider), - api_key, - entrypoint_metadata=self._entrypoint_metadata, - ) - ) + self.app.exit(result) def _on_status(self, attempt_number: int, status: BrowserSignInStatus) -> None: if not self._is_attempt_active(attempt_number): @@ -242,7 +295,7 @@ class BrowserSignInScreen(OnboardingScreen): variant="pending", running=True, ) - case BrowserSignInStatus.EXCHANGING: + case BrowserSignInStatus.EXCHANGING | BrowserSignInStatus.COMPLETED: state = BrowserSignInViewState( step=BrowserSignInStep.FINISH, message="Finishing setup...", @@ -250,14 +303,6 @@ class BrowserSignInScreen(OnboardingScreen): variant="pending", running=True, ) - case BrowserSignInStatus.COMPLETED: - state = BrowserSignInViewState( - step=BrowserSignInStep.FINISH, - message="You're signed in. Finishing setup...", - hint=PENDING_HINT, - variant="success", - running=True, - ) case _: return @@ -267,26 +312,64 @@ class BrowserSignInScreen(OnboardingScreen): if not self.is_mounted: return - self._status_widget.update(state.message) - self._status_widget.remove_class("pending", "error", "success") - self._status_widget.add_class(state.variant) self._hint_widget.update(state.hint) - for index, (widget, description) in enumerate( + for index, (widgets, (title, pending_detail, done_detail)) in enumerate( zip(self._step_widgets, STEP_DESCRIPTIONS, strict=True) ): if index < state.step: - prefix = "✓" + detail = done_detail widget_class = "done" elif index == state.step: - prefix = "›" + detail = pending_detail widget_class = "active" else: - prefix = "·" + detail = pending_detail widget_class = "idle" - widget.update(f"{prefix} {description}") - widget.remove_class("done", "active", "idle") - widget.add_class(widget_class) + + widgets.title.update(title) + widgets.title.remove_class("done", "active", "idle") + widgets.title.add_class(widget_class) + widgets.detail.remove_class("done", "active", "idle") + widgets.detail.remove_class("pending", "error", "success") + if widget_class == "active": + self._update_active_step_detail(widgets.detail, state) + else: + widgets.detail.update(detail) + widgets.detail.add_class(widget_class) + widgets.marker.update(">" if widget_class == "active" else "") + widgets.marker.remove_class("done", "active", "idle") + widgets.marker.add_class(widget_class) + widgets.card.remove_class("done", "active", "idle") + widgets.card.add_class(widget_class) + + def _update_active_step_detail( + self, detail: NoMarkupStatic, state: BrowserSignInViewState + ) -> None: + if state.variant == "pending" and state.step == BrowserSignInStep.CONFIRM: + content = Text() + append_gradient_text( + content, WAITING_FOR_AUTHENTICATION_MESSAGE, self._gradient_offset + ) + detail.update(content) + detail.add_class("pending") + return + + if state.variant == "error": + detail.update(state.message) + detail.add_class("error") + return + + detail.update(state.message) + detail.add_class(state.variant) + + def _animate_gradient(self) -> None: + self._gradient_offset = (self._gradient_offset + 1) % len(GRADIENT_COLORS) + if ( + self.state.variant == "pending" + and self.state.step == BrowserSignInStep.CONFIRM + ): + self.watch_state(self.state) async def _close_browser_sign_in( self, browser_sign_in: BrowserSignInService | None diff --git a/vibe/setup/onboarding/screens/theme_selection.py b/vibe/setup/onboarding/screens/theme_selection.py new file mode 100644 index 0000000..2de054c --- /dev/null +++ b/vibe/setup/onboarding/screens/theme_selection.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from typing import ClassVar + +from textual.app import ComposeResult +from textual.binding import Binding, BindingType +from textual.containers import Center, Container, Horizontal, Vertical +from textual.events import Resize +from textual.widgets import Markdown, Static + +from vibe.cli.textual_ui.widgets.theme_picker import sorted_theme_names +from vibe.core.config import MissingAPIKeyError, VibeConfig +from vibe.core.logger import logger +from vibe.setup.onboarding.base import OnboardingScreen + +THEMES = sorted_theme_names() + +VISIBLE_NEIGHBORS = 3 +FADE_CLASSES = ["fade-1", "fade-2", "fade-3"] + +PREVIEW_MARKDOWN = """\ +### Heading + +**Bold**, *italic*, and `inline code`. + +- Bullet point +- Another bullet point + +1. First item +2. Second item + +```python +def greet(name: str = "World") -> str: + return f"Hello, {name}!" +``` + +> Blockquote + +--- + +| Column 1 | Column 2 | +|----------|----------| +| Item 1 | Item 2 | +""" + + +class ThemeSelectionScreen(OnboardingScreen): + BINDINGS: ClassVar[list[BindingType]] = [ + Binding("enter", "next", "Next", show=False, priority=True), + Binding("up", "prev_theme", "Previous", show=False), + Binding("down", "next_theme", "Next Theme", show=False), + Binding("ctrl+c", "cancel", "Cancel", show=False), + Binding("escape", "cancel", "Cancel", show=False), + ] + + NEXT_SCREEN = "api_key" + + def __init__(self, next_screen: str = "api_key") -> None: + super().__init__() + self.NEXT_SCREEN = next_screen + self._theme_index = 0 + self._theme_widgets: list[Static] = [] + + def _compose_theme_list(self) -> ComposeResult: + for _ in range(VISIBLE_NEIGHBORS * 2 + 1): + widget = Static("", classes="theme-item") + self._theme_widgets.append(widget) + yield widget + + def compose(self) -> ComposeResult: + with Center(id="theme-outer"): + with Vertical(id="theme-content"): + yield Static("Select your preferred theme", id="theme-title") + yield Center( + Horizontal( + Static("Navigate ↑ ↓", id="nav-hint"), + Vertical(*self._compose_theme_list(), id="theme-list"), + Static("Press Enter ↵", id="enter-hint"), + id="theme-row", + ) + ) + with Container(id="preview-center"): + preview = Container(id="preview") + preview.border_title = "Preview" + with preview: + yield Container(Markdown(PREVIEW_MARKDOWN), id="preview-inner") + + def on_mount(self) -> None: + current_theme = self.app.theme + if current_theme in THEMES: + self._theme_index = THEMES.index(current_theme) + self._update_display() + self._update_preview_height() + self.focus() + + def on_resize(self, _: Resize) -> None: + self._update_preview_height() + + def _update_preview_height(self) -> None: + preview = self.query_one("#preview", Container) + header_height = 17 + available = self.app.size.height - header_height + preview.styles.max_height = max(7, available) + + def _get_theme_at_offset(self, offset: int) -> str: + index = (self._theme_index + offset) % len(THEMES) + return THEMES[index] + + def _update_display(self) -> None: + for i, widget in enumerate(self._theme_widgets): + offset = i - VISIBLE_NEIGHBORS + theme = self._get_theme_at_offset(offset) + + widget.remove_class("selected", *FADE_CLASSES) + + if offset == 0: + widget.update(f" {theme} ") + widget.add_class("selected") + else: + distance = min(abs(offset) - 1, len(FADE_CLASSES) - 1) + widget.update(theme) + widget.add_class(FADE_CLASSES[distance]) + + def _navigate(self, direction: int) -> None: + self._theme_index = (self._theme_index + direction) % len(THEMES) + self.app.theme = THEMES[self._theme_index] + self._update_display() + + def action_next_theme(self) -> None: + self._navigate(1) + + def action_prev_theme(self) -> None: + self._navigate(-1) + + def action_next(self) -> None: + theme = THEMES[self._theme_index] + try: + VibeConfig.save_updates({"theme": theme}) + except (OSError, MissingAPIKeyError) as e: + logger.warning("Failed to persist theme=%s: %s", theme, e) + super().action_next() diff --git a/vibe/setup/onboarding/screens/welcome.py b/vibe/setup/onboarding/screens/welcome.py index 0eebe01..a1dda46 100644 --- a/vibe/setup/onboarding/screens/welcome.py +++ b/vibe/setup/onboarding/screens/welcome.py @@ -10,6 +10,7 @@ from textual.widgets import Static from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.setup.onboarding.base import OnboardingScreen +from vibe.setup.onboarding.gradient_text import GRADIENT_COLORS, gradient_markup WELCOME_PREFIX = "Welcome to " WELCOME_HIGHLIGHT = "Mistral Vibe" @@ -21,27 +22,6 @@ HIGHLIGHT_END = HIGHLIGHT_START + len(WELCOME_HIGHLIGHT) BUTTON_TEXT = "Press Enter ↵" -GRADIENT_COLORS = [ - "#ff6b00", - "#ff7b00", - "#ff8c00", - "#ff9d00", - "#ffae00", - "#ffbf00", - "#ffae00", - "#ff9d00", - "#ff8c00", - "#ff7b00", -] - - -def _apply_gradient(text: str, offset: int) -> str: - result = [] - for i, char in enumerate(text): - color = GRADIENT_COLORS[(i + offset) % len(GRADIENT_COLORS)] - result.append(f"[bold {color}]{char}[/]") - return "".join(result) - class WelcomeScreen(OnboardingScreen): BINDINGS: ClassVar[list[BindingType]] = [ @@ -50,9 +30,9 @@ class WelcomeScreen(OnboardingScreen): Binding("escape", "cancel", "Cancel", show=False), ] - NEXT_SCREEN = "api_key" + NEXT_SCREEN = "theme_selection" - def __init__(self, next_screen: str = "api_key") -> None: + def __init__(self, next_screen: str = "theme_selection") -> None: super().__init__() self.NEXT_SCREEN = next_screen self._char_index = 0 @@ -86,7 +66,7 @@ class WelcomeScreen(OnboardingScreen): prefix = text[:HIGHLIGHT_START] highlight_len = min(length, HIGHLIGHT_END) - HIGHLIGHT_START - highlight = _apply_gradient( + highlight = gradient_markup( WELCOME_HIGHLIGHT[:highlight_len], self._gradient_offset ) diff --git a/vibe/setup/trusted_folders/trust_folder_dialog.py b/vibe/setup/trusted_folders/trust_folder_dialog.py index bafe5f6..bf6b51c 100644 --- a/vibe/setup/trusted_folders/trust_folder_dialog.py +++ b/vibe/setup/trusted_folders/trust_folder_dialog.py @@ -187,7 +187,7 @@ class TrustFolderApp(App): self._quit_without_saving = False def on_mount(self) -> None: - self.theme = "textual-ansi" + self.theme = "ansi-dark" def compose(self) -> ComposeResult: yield TrustFolderDialog(self.folder_path, self.detected_files) diff --git a/vibe/setup/trusted_folders/trust_folder_dialog.tcss b/vibe/setup/trusted_folders/trust_folder_dialog.tcss index 51e188a..23acc8e 100644 --- a/vibe/setup/trusted_folders/trust_folder_dialog.tcss +++ b/vibe/setup/trusted_folders/trust_folder_dialog.tcss @@ -7,7 +7,7 @@ Screen { #trust-dialog { max-width: 70; overflow-y: scroll; - border: round ansi_bright_black; + border: round $border-blurred; background: transparent; height:auto; max-height: 1fr; @@ -28,7 +28,7 @@ Screen { width: 100%; height: auto; text-style: bold; - color: ansi_yellow; + color: $warning; text-align: center; margin-bottom: 1; } @@ -36,7 +36,7 @@ Screen { #trust-dialog-path { width: 100%; height: auto; - color: ansi_blue; + color: $primary; text-align: center; text-wrap: wrap; margin-bottom: 1; @@ -45,7 +45,7 @@ Screen { .trust-dialog-section-header { width: 100%; height: auto; - color: ansi_default; + color: $foreground; text-align: center; text-wrap: wrap; padding: 0 2; @@ -61,7 +61,7 @@ Screen { width: auto; max-width: 100%; height: auto; - color: ansi_default; + color: $foreground; text-align: center; text-overflow: fold; } @@ -69,11 +69,11 @@ Screen { .trust-dialog-footer-warning { width: 100%; height: auto; - color: ansi_yellow; + color: $warning; text-align: center; text-style: bold; - border-top: solid ansi_bright_black; - border-bottom: solid ansi_bright_black; + border-top: solid $border-blurred; + border-bottom: solid $border-blurred; margin-bottom: 1; padding: 0 1; } @@ -88,33 +88,41 @@ Screen { .trust-option { height: auto; width: auto; - color: ansi_default; + color: $foreground; margin: 0 3; content-align: center middle; } .trust-cursor-selected { - color: ansi_default; + color: $foreground; text-style: bold; } .trust-option-selected { - color: ansi_default; + color: $foreground; } .trust-dialog-help { width: 100%; height: auto; - color: ansi_bright_black; + color: $text-muted; text-align: center; margin-bottom: 1; + + &:ansi { + text-style: dim; + } } .trust-dialog-save-info { width: 100%; height: auto; - color: ansi_bright_black; + color: $text-muted; text-align: center; text-style: italic; text-wrap: wrap; + + &:ansi { + text-style: dim; + } } diff --git a/vibe/whats_new.md b/vibe/whats_new.md index 0d2bac2..6af9474 100644 --- a/vibe/whats_new.md +++ b/vibe/whats_new.md @@ -1,4 +1,5 @@ -# What's new in v2.10.0 +# What's new in v2.11.0 -- **Multi-repo sessions**: Use `--add-dir` to pull additional repository roots into your session -- **Improved plan mode**: Plan is now displayed as live-updating markdown with Ctrl+G to edit in your default editor +- **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`