Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai>
Co-authored-by: Clement Sirieix <clem.sirieix@gmail.com>
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-03-10 16:07:18 +01:00 committed by GitHub
parent dd372ce494
commit e9428bce23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 877 additions and 362 deletions

2
.vscode/launch.json vendored
View file

@ -1,5 +1,5 @@
{ {
"version": "2.4.0", "version": "2.4.1",
"configurations": [ "configurations": [
{ {
"name": "ACP Server", "name": "ACP Server",

View file

@ -5,6 +5,23 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.4.1] - 2026-03-10
### Added
- `HarnessFilesManager` for selective loading of harness files, enabling SDK usage without accessing the file system.
### Changed
- Web search tool infers server URL from provider config instead of hardcoded production API
- `ask_user_questions` tool disabled in prompt mode
### Fixed
- Space key fix extended to all `Input` widgets (question prompts, proxy setup) in VS Code terminal
- Ruff isort/formatter config conflict resolved (`split-on-trailing-comma` set to `false`)
## [2.4.0] - 2026-03-09 ## [2.4.0] - 2026-03-09
### Added ### Added

View file

@ -1,7 +1,7 @@
id = "mistral-vibe" id = "mistral-vibe"
name = "Mistral Vibe" name = "Mistral Vibe"
description = "Mistral's open-source coding assistant" description = "Mistral's open-source coding assistant"
version = "2.4.0" version = "2.4.1"
schema_version = 1 schema_version = 1
authors = ["Mistral AI"] authors = ["Mistral AI"]
repository = "https://github.com/mistralai/mistral-vibe" repository = "https://github.com/mistralai/mistral-vibe"
@ -11,25 +11,25 @@ name = "Mistral Vibe"
icon = "./icons/mistral_vibe.svg" icon = "./icons/mistral_vibe.svg"
[agent_servers.mistral-vibe.targets.darwin-aarch64] [agent_servers.mistral-vibe.targets.darwin-aarch64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.0/vibe-acp-darwin-aarch64-2.4.0.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.1/vibe-acp-darwin-aarch64-2.4.1.zip"
cmd = "./vibe-acp" cmd = "./vibe-acp"
[agent_servers.mistral-vibe.targets.darwin-x86_64] [agent_servers.mistral-vibe.targets.darwin-x86_64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.0/vibe-acp-darwin-x86_64-2.4.0.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.1/vibe-acp-darwin-x86_64-2.4.1.zip"
cmd = "./vibe-acp" cmd = "./vibe-acp"
[agent_servers.mistral-vibe.targets.linux-aarch64] [agent_servers.mistral-vibe.targets.linux-aarch64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.0/vibe-acp-linux-aarch64-2.4.0.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.1/vibe-acp-linux-aarch64-2.4.1.zip"
cmd = "./vibe-acp" cmd = "./vibe-acp"
[agent_servers.mistral-vibe.targets.linux-x86_64] [agent_servers.mistral-vibe.targets.linux-x86_64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.0/vibe-acp-linux-x86_64-2.4.0.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.1/vibe-acp-linux-x86_64-2.4.1.zip"
cmd = "./vibe-acp" cmd = "./vibe-acp"
[agent_servers.mistral-vibe.targets.windows-aarch64] [agent_servers.mistral-vibe.targets.windows-aarch64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.0/vibe-acp-windows-aarch64-2.4.0.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.1/vibe-acp-windows-aarch64-2.4.1.zip"
cmd = "./vibe-acp.exe" cmd = "./vibe-acp.exe"
[agent_servers.mistral-vibe.targets.windows-x86_64] [agent_servers.mistral-vibe.targets.windows-x86_64]
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.0/vibe-acp-windows-x86_64-2.4.0.zip" archive = "https://github.com/mistralai/mistral-vibe/releases/download/v2.4.1/vibe-acp-windows-x86_64-2.4.1.zip"
cmd = "./vibe-acp.exe" cmd = "./vibe-acp.exe"

View file

@ -1,6 +1,6 @@
[project] [project]
name = "mistral-vibe" name = "mistral-vibe"
version = "2.4.0" version = "2.4.1"
description = "Minimal CLI coding agent by Mistral" description = "Minimal CLI coding agent by Mistral"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
@ -151,7 +151,7 @@ ban-relative-imports = "all"
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
known-first-party = ["vibe"] known-first-party = ["vibe"]
force-sort-within-sections = true force-sort-within-sections = true
split-on-trailing-comma = true split-on-trailing-comma = false
combine-as-imports = true combine-as-imports = true
force-wrap-aliases = false force-wrap-aliases = false
order-by-type = true order-by-type = true

View file

@ -28,7 +28,7 @@ class TestACPInitialize:
session_capabilities=SessionCapabilities(list=SessionListCapabilities()), session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
) )
assert response.agent_info == Implementation( assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.4.0" name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.4.1"
) )
assert response.auth_methods == [] assert response.auth_methods == []
@ -52,7 +52,7 @@ class TestACPInitialize:
session_capabilities=SessionCapabilities(list=SessionListCapabilities()), session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
) )
assert response.agent_info == Implementation( assert response.agent_info == Implementation(
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.4.0" name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.4.1"
) )
assert response.auth_methods is not None assert response.auth_methods is not None

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from vibe.acp.utils import get_proxy_help_text from vibe.acp.utils import get_proxy_help_text
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE from vibe.core.paths import GLOBAL_ENV_FILE
from vibe.core.proxy_setup import SUPPORTED_PROXY_VARS from vibe.core.proxy_setup import SUPPORTED_PROXY_VARS

View file

@ -23,9 +23,11 @@ from vibe.core.config import (
SessionLoggingConfig, SessionLoggingConfig,
VibeConfig, VibeConfig,
) )
from vibe.core.config.harness_files import (
init_harness_files_manager,
reset_harness_files_manager,
)
from vibe.core.llm.types import BackendLike from vibe.core.llm.types import BackendLike
from vibe.core.paths import global_paths
from vibe.core.paths.config_paths import unlock_config_paths
def get_base_config() -> dict[str, Any]: def get_base_config() -> dict[str, Any]:
@ -69,13 +71,16 @@ def config_dir(
config_file = config_dir / "config.toml" config_file = config_dir / "config.toml"
config_file.write_text(tomli_w.dumps(get_base_config()), encoding="utf-8") config_file.write_text(tomli_w.dumps(get_base_config()), encoding="utf-8")
monkeypatch.setattr(global_paths, "_DEFAULT_VIBE_HOME", config_dir) monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", config_dir)
return config_dir return config_dir
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _unlock_config_paths(): def _init_harness_files_manager():
unlock_config_paths() reset_harness_files_manager()
init_harness_files_manager("user", "project")
yield
reset_harness_files_manager()
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)

View file

@ -1,184 +1,334 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from unittest.mock import patch
from vibe.core.paths.config_paths import ( import pytest
discover_local_agents_dirs,
discover_local_skills_dirs, from vibe.core.config.harness_files import HarnessFilesManager
discover_local_tools_dirs, from vibe.core.trusted_folders import trusted_folders_manager
)
class TestDiscoverLocalSkillsDirs: class TestTrustedWorkdir:
def test_returns_empty_list_when_dir_not_trusted(self, tmp_path: Path) -> None: def test_returns_none_when_project_not_in_sources(
(tmp_path / ".vibe" / "skills").mkdir(parents=True) self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = False
assert discover_local_skills_dirs(tmp_path) == []
def test_returns_empty_list_when_trusted_but_no_skills_dirs(
self, tmp_path: Path
) -> None: ) -> None:
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm: monkeypatch.chdir(tmp_path)
mock_tfm.is_trusted.return_value = True mgr = HarnessFilesManager(sources=("user",))
assert discover_local_skills_dirs(tmp_path) == [] assert mgr.trusted_workdir is None
def test_returns_vibe_skills_only_when_only_it_exists(self, tmp_path: Path) -> None: def test_returns_none_when_not_trusted(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: False)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.trusted_workdir is None
def test_returns_cwd_when_project_in_sources_and_trusted(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.trusted_workdir == tmp_path
class TestProjectToolsDirs:
def test_returns_empty_when_project_not_in_sources(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
mgr = HarnessFilesManager(sources=("user",))
assert mgr.project_tools_dirs == []
def test_returns_empty_when_not_trusted(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: False)
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_tools_dirs == []
def test_returns_empty_when_tools_dir_does_not_exist(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_tools_dirs == []
def test_returns_path_when_tools_dir_exists_and_trusted(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
tools_dir = tmp_path / ".vibe" / "tools"
tools_dir.mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_tools_dirs == [tools_dir]
def test_ignores_tools_when_file_not_dir(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
(tmp_path / ".vibe").mkdir()
(tmp_path / ".vibe" / "tools").write_text("", encoding="utf-8")
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_tools_dirs == []
def test_finds_tools_dirs_recursively(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
(tmp_path / "sub" / ".vibe" / "tools").mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_tools_dirs == [
tmp_path / ".vibe" / "tools",
tmp_path / "sub" / ".vibe" / "tools",
]
def test_does_not_descend_into_ignored_dirs(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
(tmp_path / ".git" / ".vibe" / "tools").mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_tools_dirs == [tmp_path / ".vibe" / "tools"]
class TestProjectAgentsDirs:
def test_returns_empty_when_project_not_in_sources(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
mgr = HarnessFilesManager(sources=("user",))
assert mgr.project_agents_dirs == []
def test_returns_empty_when_not_trusted(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: False)
(tmp_path / ".vibe" / "agents").mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_agents_dirs == []
def test_returns_empty_when_agents_dir_does_not_exist(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_agents_dirs == []
def test_returns_path_when_agents_dir_exists_and_trusted(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
agents_dir = tmp_path / ".vibe" / "agents"
agents_dir.mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_agents_dirs == [agents_dir]
def test_ignores_agents_when_file_not_dir(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
(tmp_path / ".vibe").mkdir()
(tmp_path / ".vibe" / "agents").write_text("", encoding="utf-8")
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_agents_dirs == []
def test_finds_agents_dirs_recursively(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
(tmp_path / ".vibe" / "agents").mkdir(parents=True)
(tmp_path / "sub" / "deep" / ".vibe" / "agents").mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_agents_dirs == [
tmp_path / ".vibe" / "agents",
tmp_path / "sub" / "deep" / ".vibe" / "agents",
]
def test_does_not_descend_into_ignored_dirs(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
(tmp_path / ".vibe" / "agents").mkdir(parents=True)
(tmp_path / "__pycache__" / ".vibe" / "agents").mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_agents_dirs == [tmp_path / ".vibe" / "agents"]
class TestUserToolsDirs:
def test_returns_empty_when_user_not_in_sources(self) -> None:
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)
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
) -> None:
monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path)
tools_dir = tmp_path / "tools"
tools_dir.mkdir()
mgr = HarnessFilesManager(sources=("user",))
assert mgr.user_tools_dirs == [tools_dir]
class TestUserSkillsDirs:
def test_returns_empty_when_user_not_in_sources(self) -> None:
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)
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
) -> None:
monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path)
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
mgr = HarnessFilesManager(sources=("user",))
assert mgr.user_skills_dirs == [skills_dir]
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)
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
) -> None:
monkeypatch.setattr("vibe.core.paths._vibe_home._DEFAULT_VIBE_HOME", tmp_path)
agents_dir = tmp_path / "agents"
agents_dir.mkdir()
mgr = HarnessFilesManager(sources=("user",))
assert mgr.user_agents_dirs == [agents_dir]
class TestProjectSkillsDirs:
def test_returns_empty_list_when_no_skills_dirs(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_skills_dirs == []
def test_returns_vibe_skills_only_when_only_it_exists(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
vibe_skills = tmp_path / ".vibe" / "skills" vibe_skills = tmp_path / ".vibe" / "skills"
vibe_skills.mkdir(parents=True) vibe_skills.mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm: mgr = HarnessFilesManager(sources=("user", "project"))
mock_tfm.is_trusted.return_value = True assert mgr.project_skills_dirs == [vibe_skills]
result = discover_local_skills_dirs(tmp_path)
assert result == [vibe_skills]
def test_returns_agents_skills_only_when_only_it_exists( def test_returns_agents_skills_only_when_only_it_exists(
self, tmp_path: Path self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
agents_skills = tmp_path / ".agents" / "skills" agents_skills = tmp_path / ".agents" / "skills"
agents_skills.mkdir(parents=True) agents_skills.mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm: mgr = HarnessFilesManager(sources=("user", "project"))
mock_tfm.is_trusted.return_value = True assert mgr.project_skills_dirs == [agents_skills]
result = discover_local_skills_dirs(tmp_path)
assert result == [agents_skills]
def test_returns_both_in_order_when_both_exist(self, tmp_path: Path) -> None: def test_returns_both_in_order_when_both_exist(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
vibe_skills = tmp_path / ".vibe" / "skills" vibe_skills = tmp_path / ".vibe" / "skills"
agents_skills = tmp_path / ".agents" / "skills" agents_skills = tmp_path / ".agents" / "skills"
vibe_skills.mkdir(parents=True) vibe_skills.mkdir(parents=True)
agents_skills.mkdir(parents=True) agents_skills.mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm: mgr = HarnessFilesManager(sources=("user", "project"))
mock_tfm.is_trusted.return_value = True assert mgr.project_skills_dirs == [vibe_skills, agents_skills]
result = discover_local_skills_dirs(tmp_path)
assert result == [vibe_skills, agents_skills]
def test_ignores_vibe_skills_when_file_not_dir(self, tmp_path: Path) -> None: def test_ignores_vibe_skills_when_file_not_dir(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
(tmp_path / ".vibe").mkdir() (tmp_path / ".vibe").mkdir()
(tmp_path / ".vibe" / "skills").write_text("", encoding="utf-8") (tmp_path / ".vibe" / "skills").write_text("", encoding="utf-8")
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm: mgr = HarnessFilesManager(sources=("user", "project"))
mock_tfm.is_trusted.return_value = True assert mgr.project_skills_dirs == []
result = discover_local_skills_dirs(tmp_path)
assert result == [] def test_returns_empty_when_project_not_in_sources(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
vibe_skills = tmp_path / ".vibe" / "skills"
vibe_skills.mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user",))
assert mgr.project_skills_dirs == []
def test_returns_empty_when_not_trusted(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: False)
vibe_skills = tmp_path / ".vibe" / "skills"
vibe_skills.mkdir(parents=True)
mgr = HarnessFilesManager(sources=("user", "project"))
assert mgr.project_skills_dirs == []
def test_finds_skills_dirs_recursively_in_trusted_folder( def test_finds_skills_dirs_recursively_in_trusted_folder(
self, tmp_path: Path self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
(tmp_path / ".vibe" / "skills").mkdir(parents=True) (tmp_path / ".vibe" / "skills").mkdir(parents=True)
(tmp_path / "sub" / ".agents" / "skills").mkdir(parents=True) (tmp_path / "sub" / ".agents" / "skills").mkdir(parents=True)
(tmp_path / "sub" / "deep" / ".vibe" / "skills").mkdir(parents=True) (tmp_path / "sub" / "deep" / ".vibe" / "skills").mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm: mgr = HarnessFilesManager(sources=("user", "project"))
mock_tfm.is_trusted.return_value = True assert mgr.project_skills_dirs == [
result = discover_local_skills_dirs(tmp_path)
assert result == [
tmp_path / ".vibe" / "skills", tmp_path / ".vibe" / "skills",
tmp_path / "sub" / ".agents" / "skills", tmp_path / "sub" / ".agents" / "skills",
tmp_path / "sub" / "deep" / ".vibe" / "skills", tmp_path / "sub" / "deep" / ".vibe" / "skills",
] ]
def test_does_not_descend_into_ignored_dirs(self, tmp_path: Path) -> None: def test_does_not_descend_into_ignored_dirs(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
(tmp_path / ".vibe" / "skills").mkdir(parents=True) (tmp_path / ".vibe" / "skills").mkdir(parents=True)
(tmp_path / "node_modules" / ".vibe" / "skills").mkdir(parents=True) (tmp_path / "node_modules" / ".vibe" / "skills").mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm: mgr = HarnessFilesManager(sources=("user", "project"))
mock_tfm.is_trusted.return_value = True assert mgr.project_skills_dirs == [tmp_path / ".vibe" / "skills"]
result = discover_local_skills_dirs(tmp_path)
assert result == [tmp_path / ".vibe" / "skills"]
class TestDiscoverLocalToolsDirs:
def test_returns_empty_list_when_dir_not_trusted(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = False
assert discover_local_tools_dirs(tmp_path) == []
def test_returns_empty_list_when_trusted_but_no_tools_dir(
self, tmp_path: Path
) -> None:
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
assert discover_local_tools_dirs(tmp_path) == []
def test_returns_tools_dir_when_exists(self, tmp_path: Path) -> None:
vibe_tools = tmp_path / ".vibe" / "tools"
vibe_tools.mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
result = discover_local_tools_dirs(tmp_path)
assert result == [vibe_tools]
def test_ignores_tools_when_file_not_dir(self, tmp_path: Path) -> None:
(tmp_path / ".vibe").mkdir()
(tmp_path / ".vibe" / "tools").write_text("", encoding="utf-8")
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
result = discover_local_tools_dirs(tmp_path)
assert result == []
def test_finds_tools_dirs_recursively(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
(tmp_path / "sub" / ".vibe" / "tools").mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
result = discover_local_tools_dirs(tmp_path)
assert result == [
tmp_path / ".vibe" / "tools",
tmp_path / "sub" / ".vibe" / "tools",
]
def test_does_not_descend_into_ignored_dirs(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "tools").mkdir(parents=True)
(tmp_path / ".git" / ".vibe" / "tools").mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
result = discover_local_tools_dirs(tmp_path)
assert result == [tmp_path / ".vibe" / "tools"]
class TestDiscoverLocalAgentsDirs:
def test_returns_empty_list_when_dir_not_trusted(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "agents").mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = False
assert discover_local_agents_dirs(tmp_path) == []
def test_returns_empty_list_when_trusted_but_no_agents_dir(
self, tmp_path: Path
) -> None:
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
assert discover_local_agents_dirs(tmp_path) == []
def test_returns_agents_dir_when_exists(self, tmp_path: Path) -> None:
vibe_agents = tmp_path / ".vibe" / "agents"
vibe_agents.mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
result = discover_local_agents_dirs(tmp_path)
assert result == [vibe_agents]
def test_ignores_agents_when_file_not_dir(self, tmp_path: Path) -> None:
(tmp_path / ".vibe").mkdir()
(tmp_path / ".vibe" / "agents").write_text("", encoding="utf-8")
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
result = discover_local_agents_dirs(tmp_path)
assert result == []
def test_finds_agents_dirs_recursively(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "agents").mkdir(parents=True)
(tmp_path / "sub" / "deep" / ".vibe" / "agents").mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
result = discover_local_agents_dirs(tmp_path)
assert result == [
tmp_path / ".vibe" / "agents",
tmp_path / "sub" / "deep" / ".vibe" / "agents",
]
def test_does_not_descend_into_ignored_dirs(self, tmp_path: Path) -> None:
(tmp_path / ".vibe" / "agents").mkdir(parents=True)
(tmp_path / "__pycache__" / ".vibe" / "agents").mkdir(parents=True)
with patch("vibe.core.paths.config_paths.trusted_folders_manager") as mock_tfm:
mock_tfm.is_trusted.return_value = True
result = discover_local_agents_dirs(tmp_path)
assert result == [tmp_path / ".vibe" / "agents"]

View file

@ -4,8 +4,12 @@ from pathlib import Path
import pytest import pytest
from vibe.core.paths.config_paths import CONFIG_FILE from vibe.core.config.harness_files import (
from vibe.core.paths.global_paths import GLOBAL_CONFIG_FILE, VIBE_HOME HarnessFilesManager,
init_harness_files_manager,
reset_harness_files_manager,
)
from vibe.core.paths import VIBE_HOME
from vibe.core.trusted_folders import trusted_folders_manager from vibe.core.trusted_folders import trusted_folders_manager
@ -21,11 +25,18 @@ class TestResolveConfigFile:
monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True) monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)
assert CONFIG_FILE.path == local_config reset_harness_files_manager()
assert CONFIG_FILE.path.is_file() init_harness_files_manager("user", "project")
assert CONFIG_FILE.path.read_text(encoding="utf-8") == 'active_model = "test"' from vibe.core.config.harness_files import get_harness_files_manager
def test_resolves_local_config_when_exists_and_folder_is_not_trusted( mgr = get_harness_files_manager()
resolved = mgr.config_file
assert resolved is not None
assert resolved == local_config
assert resolved.is_file()
assert resolved.read_text(encoding="utf-8") == 'active_model = "test"'
def test_resolves_global_config_when_folder_is_not_trusted(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
@ -34,7 +45,12 @@ class TestResolveConfigFile:
local_config = local_config_dir / "config.toml" local_config = local_config_dir / "config.toml"
local_config.write_text('active_model = "test"', encoding="utf-8") local_config.write_text('active_model = "test"', encoding="utf-8")
assert CONFIG_FILE.path == GLOBAL_CONFIG_FILE.path reset_harness_files_manager()
init_harness_files_manager("user", "project")
from vibe.core.config.harness_files import get_harness_files_manager
mgr = get_harness_files_manager()
assert mgr.config_file == VIBE_HOME.path / "config.toml"
def test_falls_back_to_global_config_when_local_missing( def test_falls_back_to_global_config_when_local_missing(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
@ -43,7 +59,12 @@ class TestResolveConfigFile:
# Ensure no local config exists # Ensure no local config exists
assert not (tmp_path / ".vibe" / "config.toml").exists() assert not (tmp_path / ".vibe" / "config.toml").exists()
assert CONFIG_FILE.path == GLOBAL_CONFIG_FILE.path reset_harness_files_manager()
init_harness_files_manager("user", "project")
from vibe.core.config.harness_files import get_harness_files_manager
mgr = get_harness_files_manager()
assert mgr.config_file == VIBE_HOME.path / "config.toml"
def test_respects_vibe_home_env_var( def test_respects_vibe_home_env_var(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
@ -51,3 +72,11 @@ class TestResolveConfigFile:
assert VIBE_HOME.path != tmp_path assert VIBE_HOME.path != tmp_path
monkeypatch.setenv("VIBE_HOME", str(tmp_path)) monkeypatch.setenv("VIBE_HOME", str(tmp_path))
assert VIBE_HOME.path == tmp_path assert VIBE_HOME.path == tmp_path
def test_returns_none_when_no_sources(self) -> None:
mgr = HarnessFilesManager(sources=())
assert mgr.config_file is None
def test_user_only_returns_global_config(self) -> None:
mgr = HarnessFilesManager(sources=("user",))
assert mgr.config_file == VIBE_HOME.path / "config.toml"

View file

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from vibe.core.paths.global_paths import PLANS_DIR from vibe.core.paths import PLANS_DIR
from vibe.core.plan_session import PlanSession from vibe.core.plan_session import PlanSession

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import pytest import pytest
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE from vibe.core.paths import GLOBAL_ENV_FILE
from vibe.core.proxy_setup import ( from vibe.core.proxy_setup import (
SUPPORTED_PROXY_VARS, SUPPORTED_PROXY_VARS,
ProxySetupError, ProxySetupError,

View file

@ -7,9 +7,8 @@ from unittest.mock import patch
import pytest import pytest
import tomli_w import tomli_w
from vibe.core.paths.global_paths import TRUSTED_FOLDERS_FILE from vibe.core.paths import AGENTS_MD_FILENAMES, TRUSTED_FOLDERS_FILE
from vibe.core.trusted_folders import ( from vibe.core.trusted_folders import (
AGENTS_MD_FILENAMES,
TrustedFoldersManager, TrustedFoldersManager,
has_agents_md_file, has_agents_md_file,
has_trustable_content, has_trustable_content,

19
tests/core/test_utils.py Normal file
View file

@ -0,0 +1,19 @@
from __future__ import annotations
import pytest
from vibe.core.utils import get_server_url_from_api_base
@pytest.mark.parametrize(
("api_base", "expected"),
[
("https://api.mistral.ai/v1", "https://api.mistral.ai"),
("https://on-prem.example.com/v1", "https://on-prem.example.com"),
("http://localhost:8080/v2", "http://localhost:8080"),
("not-a-url", None),
("ftp://example.com/v1", None),
],
)
def test_get_server_url_from_api_base(api_base, expected):
assert get_server_url_from_api_base(api_base) == expected

View file

@ -14,10 +14,10 @@ from unittest.mock import patch
from pydantic import ValidationError from pydantic import ValidationError
from vibe.core.paths.config_paths import unlock_config_paths from vibe.core.config.harness_files import init_harness_files_manager
if __name__ == "__main__": if __name__ == "__main__":
unlock_config_paths() init_harness_files_manager("user", "project")
from tests import TESTS_ROOT from tests import TESTS_ROOT
from tests.mock.utils import MOCK_DATA_ENV_VAR from tests.mock.utils import MOCK_DATA_ENV_VAR

View file

@ -6,7 +6,7 @@ import pytest
from textual.pilot import Pilot from textual.pilot import Pilot
from textual.widgets import Input from textual.widgets import Input
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE from vibe.core.paths import GLOBAL_ENV_FILE
from vibe.setup.onboarding import OnboardingApp from vibe.setup.onboarding import OnboardingApp
from vibe.setup.onboarding.screens.api_key import ApiKeyScreen from vibe.setup.onboarding.screens.api_key import ApiKeyScreen

View file

@ -16,8 +16,7 @@ from vibe.core.agents.models import (
_deep_merge, _deep_merge,
) )
from vibe.core.config import VibeConfig from vibe.core.config import VibeConfig
from vibe.core.paths.config_paths import ConfigPath from vibe.core.config.harness_files import HarnessFilesManager
from vibe.core.paths.global_paths import GlobalPath
from vibe.core.tools.base import ToolPermission from vibe.core.tools.base import ToolPermission
from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role from vibe.core.types import LLMChunk, LLMMessage, LLMUsage, Role
@ -176,11 +175,18 @@ class TestAgentApplyToConfig:
global_prompts.mkdir(parents=True) global_prompts.mkdir(parents=True)
(global_prompts / "cc.md").write_text("Global custom prompt") (global_prompts / "cc.md").write_text("Global custom prompt")
class _MockManager(HarnessFilesManager):
@property
def project_prompts_dirs(self) -> list[Path]:
return [project_prompts]
@property
def user_prompts_dirs(self) -> list[Path]:
return [global_prompts]
mock_manager = _MockManager(sources=("user",))
monkeypatch.setattr( monkeypatch.setattr(
"vibe.core.config.PROMPTS_DIR", ConfigPath(lambda: project_prompts) "vibe.core.config._settings.get_harness_files_manager", lambda: mock_manager
)
monkeypatch.setattr(
"vibe.core.config.GLOBAL_PROMPTS_DIR", GlobalPath(lambda: global_prompts)
) )
base = VibeConfig(include_project_context=False, include_prompt_detail=False) base = VibeConfig(include_project_context=False, include_prompt_detail=False)
@ -555,11 +561,18 @@ class TestAgentLoopInitialization:
custom_prompt_content = "CUSTOM_AGENT_PROMPT_MARKER" custom_prompt_content = "CUSTOM_AGENT_PROMPT_MARKER"
(global_prompts / "custom_agent.md").write_text(custom_prompt_content) (global_prompts / "custom_agent.md").write_text(custom_prompt_content)
class _MockManager(HarnessFilesManager):
@property
def project_prompts_dirs(self) -> list[Path]:
return [project_prompts]
@property
def user_prompts_dirs(self) -> list[Path]:
return [global_prompts]
mock_manager = _MockManager(sources=("user",))
monkeypatch.setattr( monkeypatch.setattr(
"vibe.core.config.PROMPTS_DIR", ConfigPath(lambda: project_prompts) "vibe.core.config._settings.get_harness_files_manager", lambda: mock_manager
)
monkeypatch.setattr(
"vibe.core.config.GLOBAL_PROMPTS_DIR", GlobalPath(lambda: global_prompts)
) )
custom_agent = AgentProfile( custom_agent = AgentProfile(

View file

@ -1,12 +1,13 @@
from __future__ import annotations from __future__ import annotations
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import mistralai import mistralai
import pytest import pytest
from tests.mock.utils import collect_result from tests.mock.utils import collect_result
from vibe.core.tools.base import BaseToolState, ToolError from vibe.core.config import Backend, ProviderConfig
from vibe.core.tools.base import BaseToolState, InvokeContext, ToolError
from vibe.core.tools.builtins.websearch import WebSearch, WebSearchArgs, WebSearchConfig from vibe.core.tools.builtins.websearch import WebSearch, WebSearchArgs, WebSearchConfig
@ -151,6 +152,61 @@ async def test_run_sdk_error_wrapped(websearch):
await collect_result(websearch.run(WebSearchArgs(query="test"))) await collect_result(websearch.run(WebSearchArgs(query="test")))
def test_resolve_server_url_no_ctx(websearch):
assert websearch._resolve_server_url(None) is None
def test_resolve_server_url_no_agent_manager(websearch):
ctx = InvokeContext(tool_call_id="t1", agent_manager=None)
assert websearch._resolve_server_url(ctx) is None
def test_resolve_server_url_with_mistral_provider(websearch):
config = MagicMock()
config.providers = [
ProviderConfig(
name="mistral",
api_base="https://on-prem.example.com/v1",
api_key_env_var="MISTRAL_API_KEY",
backend=Backend.MISTRAL,
)
]
agent_manager = MagicMock()
agent_manager.config = config
ctx = InvokeContext(tool_call_id="t1", agent_manager=agent_manager)
assert websearch._resolve_server_url(ctx) == "https://on-prem.example.com"
def test_resolve_server_url_with_default_provider(websearch):
config = MagicMock()
config.providers = [
ProviderConfig(
name="mistral",
api_base="https://api.mistral.ai/v1",
api_key_env_var="MISTRAL_API_KEY",
backend=Backend.MISTRAL,
)
]
agent_manager = MagicMock()
agent_manager.config = config
ctx = InvokeContext(tool_call_id="t1", agent_manager=agent_manager)
assert websearch._resolve_server_url(ctx) == "https://api.mistral.ai"
def test_resolve_server_url_no_mistral_provider(websearch):
config = MagicMock()
config.providers = [
ProviderConfig(name="llamacpp", api_base="http://127.0.0.1:8080/v1")
]
agent_manager = MagicMock()
agent_manager.config = config
ctx = InvokeContext(tool_call_id="t1", agent_manager=agent_manager)
assert websearch._resolve_server_url(ctx) is None
def test_is_available_with_key(monkeypatch): def test_is_available_with_key(monkeypatch):
monkeypatch.setenv("MISTRAL_API_KEY", "key") monkeypatch.setenv("MISTRAL_API_KEY", "key")
assert WebSearch.is_available() is True assert WebSearch.is_available() is True

2
uv.lock generated
View file

@ -779,7 +779,7 @@ wheels = [
[[package]] [[package]]
name = "mistral-vibe" name = "mistral-vibe"
version = "2.4.0" version = "2.4.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "agent-client-protocol" }, { name = "agent-client-protocol" },

View file

@ -3,4 +3,4 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
VIBE_ROOT = Path(__file__).parent VIBE_ROOT = Path(__file__).parent
__version__ = "2.4.0" __version__ = "2.4.1"

View file

@ -5,10 +5,16 @@ from dataclasses import dataclass
import os import os
import sys import sys
import tomli_w
from vibe import __version__ from vibe import __version__
from vibe.core.config import VibeConfig from vibe.core.config import VibeConfig
from vibe.core.config.harness_files import (
get_harness_files_manager,
init_harness_files_manager,
)
from vibe.core.logger import logger from vibe.core.logger import logger
from vibe.core.paths.config_paths import CONFIG_FILE, HISTORY_FILE, unlock_config_paths from vibe.core.paths import HISTORY_FILE
# Configure line buffering for subprocess communication # Configure line buffering for subprocess communication
sys.stdout.reconfigure(line_buffering=True) # pyright: ignore[reportAttributeAccessIssue] sys.stdout.reconfigure(line_buffering=True) # pyright: ignore[reportAttributeAccessIssue]
@ -32,17 +38,22 @@ def parse_arguments() -> Arguments:
def bootstrap_config_files() -> None: def bootstrap_config_files() -> None:
if not CONFIG_FILE.path.exists(): mgr = get_harness_files_manager()
config_file = mgr.user_config_file
if not config_file.exists():
try: try:
VibeConfig.save_updates(VibeConfig.create_default()) config_file.parent.mkdir(parents=True, exist_ok=True)
with config_file.open("wb") as f:
tomli_w.dump(VibeConfig.create_default(), f)
except Exception as e: except Exception as e:
logger.error(f"Could not create default config file: {e}") logger.error(f"Could not create default config file: {e}")
raise raise
if not HISTORY_FILE.path.exists(): history_file = HISTORY_FILE.path
if not history_file.exists():
try: try:
HISTORY_FILE.path.parent.mkdir(parents=True, exist_ok=True) history_file.parent.mkdir(parents=True, exist_ok=True)
HISTORY_FILE.path.write_text("Hello Vibe!\n", "utf-8") history_file.write_text("Hello Vibe!\n", "utf-8")
except Exception as e: except Exception as e:
logger.error(f"Could not create history file: {e}") logger.error(f"Could not create history file: {e}")
raise raise
@ -64,7 +75,7 @@ def handle_debug_mode() -> None:
def main() -> None: def main() -> None:
handle_debug_mode() handle_debug_mode()
unlock_config_paths() init_harness_files_manager("user", "project")
from vibe.acp.acp_agent_loop import run_acp_server from vibe.acp.acp_agent_loop import run_acp_server
from vibe.setup.onboarding import run_onboarding from vibe.setup.onboarding import run_onboarding

View file

@ -5,6 +5,7 @@ from pathlib import Path
import sys import sys
from rich import print as rprint from rich import print as rprint
import tomli_w
from vibe import __version__ from vibe import __version__
from vibe.cli.textual_ui.app import run_textual_ui from vibe.cli.textual_ui.app import run_textual_ui
@ -16,8 +17,9 @@ from vibe.core.config import (
VibeConfig, VibeConfig,
load_dotenv_values, load_dotenv_values,
) )
from vibe.core.config.harness_files import get_harness_files_manager
from vibe.core.logger import logger from vibe.core.logger import logger
from vibe.core.paths.config_paths import CONFIG_FILE, HISTORY_FILE from vibe.core.paths import HISTORY_FILE
from vibe.core.programmatic import run_programmatic from vibe.core.programmatic import run_programmatic
from vibe.core.session.session_loader import SessionLoader from vibe.core.session.session_loader import SessionLoader
from vibe.core.types import EntrypointMetadata, LLMMessage, OutputFormat, Role from vibe.core.types import EntrypointMetadata, LLMMessage, OutputFormat, Role
@ -61,16 +63,21 @@ def load_config_or_exit() -> VibeConfig:
def bootstrap_config_files() -> None: def bootstrap_config_files() -> None:
if not CONFIG_FILE.path.exists(): mgr = get_harness_files_manager()
config_file = mgr.user_config_file
if not config_file.exists():
try: try:
VibeConfig.save_updates(VibeConfig.create_default()) config_file.parent.mkdir(parents=True, exist_ok=True)
with config_file.open("wb") as f:
tomli_w.dump(VibeConfig.create_default(), f)
except Exception as e: except Exception as e:
rprint(f"[yellow]Could not create default config file: {e}[/]") rprint(f"[yellow]Could not create default config file: {e}[/]")
if not HISTORY_FILE.path.exists(): history_file = HISTORY_FILE.path
if not history_file.exists():
try: try:
HISTORY_FILE.path.parent.mkdir(parents=True, exist_ok=True) history_file.parent.mkdir(parents=True, exist_ok=True)
HISTORY_FILE.path.write_text("Hello Vibe!\n", "utf-8") history_file.write_text("Hello Vibe!\n", "utf-8")
except Exception as e: except Exception as e:
rprint(f"[yellow]Could not create history file: {e}[/]") rprint(f"[yellow]Could not create history file: {e}[/]")

View file

@ -9,7 +9,7 @@ from rich import print as rprint
from vibe import __version__ from vibe import __version__
from vibe.core.agents.models import BuiltinAgentName from vibe.core.agents.models import BuiltinAgentName
from vibe.core.paths.config_paths import unlock_config_paths from vibe.core.config.harness_files import init_harness_files_manager
from vibe.core.trusted_folders import has_trustable_content, trusted_folders_manager from vibe.core.trusted_folders import has_trustable_content, trusted_folders_manager
from vibe.setup.trusted_folders.trust_folder_dialog import ( from vibe.setup.trusted_folders.trust_folder_dialog import (
TrustDialogQuitException, TrustDialogQuitException,
@ -152,7 +152,7 @@ def main() -> None:
is_interactive = args.prompt is None is_interactive = args.prompt is None
if is_interactive: if is_interactive:
check_and_resolve_trusted_folder() check_and_resolve_trusted_folder()
unlock_config_paths() init_harness_files_manager("user", "project")
from vibe.cli.cli import run_cli from vibe.cli.cli import run_cli

View file

@ -92,7 +92,7 @@ from vibe.core.agents import AgentProfile
from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt
from vibe.core.config import Backend, VibeConfig from vibe.core.config import Backend, VibeConfig
from vibe.core.logger import logger from vibe.core.logger import logger
from vibe.core.paths.config_paths import HISTORY_FILE from vibe.core.paths import HISTORY_FILE
from vibe.core.session.session_loader import SessionLoader from vibe.core.session.session_loader import SessionLoader
from vibe.core.teleport.types import ( from vibe.core.teleport.types import (
TeleportAuthCompleteEvent, TeleportAuthCompleteEvent,

View file

@ -12,6 +12,7 @@ from vibe.cli.textual_ui.external_editor import ExternalEditor
from vibe.cli.textual_ui.widgets.chat_input.completion_manager import ( from vibe.cli.textual_ui.widgets.chat_input.completion_manager import (
MultiCompletionManager, MultiCompletionManager,
) )
from vibe.cli.textual_ui.widgets.vscode_compat import patch_vscode_space
InputMode = Literal["!", "/", ">", "&"] InputMode = Literal["!", "/", ">", "&"]
@ -267,10 +268,7 @@ class ChatTextArea(TextArea):
event.stop() event.stop()
return return
# Work around VS Code 1.110+ sending space as CSI u (\x1b[32u), patch_vscode_space(event)
# which Textual parses as Key("space", character=None, is_printable=False).
if event.key == "space" and event.character is None:
event.character = " "
await super()._on_key(event) await super()._on_key(event)
self._mark_cursor_moved_if_needed() self._mark_cursor_moved_if_needed()

View file

@ -10,6 +10,7 @@ from textual.message import Message
from textual.widgets import Input, Static from textual.widgets import Input, Static
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
from vibe.cli.textual_ui.widgets.vscode_compat import VscodeCompatInput
from vibe.core.proxy_setup import ( from vibe.core.proxy_setup import (
SUPPORTED_PROXY_VARS, SUPPORTED_PROXY_VARS,
get_current_proxy_settings, get_current_proxy_settings,
@ -48,7 +49,7 @@ class ProxySetupApp(Container):
yield Static(f"[bold ansi_blue]{key}[/]", classes="proxy-label-line") yield Static(f"[bold ansi_blue]{key}[/]", classes="proxy-label-line")
initial_value = self.initial_values.get(key) or "" initial_value = self.initial_values.get(key) or ""
input_widget = Input( input_widget = VscodeCompatInput(
value=initial_value, value=initial_value,
placeholder=description, placeholder=description,
id=f"proxy-input-{key}", id=f"proxy-input-{key}",

View file

@ -13,6 +13,7 @@ from textual.widgets import Input
from vibe.cli.textual_ui.ansi_markdown import AnsiMarkdown from vibe.cli.textual_ui.ansi_markdown import AnsiMarkdown
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
from vibe.cli.textual_ui.widgets.vscode_compat import VscodeCompatInput
if TYPE_CHECKING: if TYPE_CHECKING:
from vibe.core.tools.builtins.ask_user_question import ( from vibe.core.tools.builtins.ask_user_question import (
@ -134,7 +135,7 @@ class QuestionApp(Container):
with Horizontal(classes="question-other-row"): with Horizontal(classes="question-other-row"):
self.other_prefix = NoMarkupStatic("", classes="question-other-prefix") self.other_prefix = NoMarkupStatic("", classes="question-other-prefix")
yield self.other_prefix yield self.other_prefix
self.other_input = Input( self.other_input = VscodeCompatInput(
placeholder="Type your answer...", classes="question-other-input" placeholder="Type your answer...", classes="question-other-input"
) )
yield self.other_input yield self.other_input

View file

@ -0,0 +1,26 @@
"""Workarounds for VS Code terminal quirks affecting Textual widgets."""
from __future__ import annotations
from textual import events
from textual.widgets import Input
def patch_vscode_space(event: events.Key) -> None:
"""Patch space key events sent as CSI u by VS Code 1.110+.
VS Code encodes space as ``\\x1b[32u`` (CSI u), which Textual parses as
``Key("space", character=None, is_printable=False)``. Input widgets then
silently drop the keystroke because there is no printable character.
Assigning ``event.character = " "`` restores normal behaviour.
"""
if event.key == "space" and event.character is None:
event.character = " "
class VscodeCompatInput(Input):
"""``Input`` subclass that handles the VS Code CSI-u space quirk."""
async def _on_key(self, event: events.Key) -> None:
patch_vscode_space(event)
await super()._on_key(event)

View file

@ -8,7 +8,7 @@ from vibe.cli.update_notifier.ports.update_cache_repository import (
UpdateCache, UpdateCache,
UpdateCacheRepository, UpdateCacheRepository,
) )
from vibe.core.paths.global_paths import VIBE_HOME from vibe.core.paths import VIBE_HOME
class FileSystemUpdateCacheRepository(UpdateCacheRepository): class FileSystemUpdateCacheRepository(UpdateCacheRepository):

View file

@ -10,9 +10,8 @@ from vibe.core.agents.models import (
AgentType, AgentType,
BuiltinAgentName, BuiltinAgentName,
) )
from vibe.core.config.harness_files import get_harness_files_manager
from vibe.core.logger import logger from vibe.core.logger import logger
from vibe.core.paths.config_paths import discover_local_agents_dirs
from vibe.core.paths.global_paths import GLOBAL_AGENTS_DIR
from vibe.core.utils import name_matches from vibe.core.utils import name_matches
if TYPE_CHECKING: if TYPE_CHECKING:
@ -88,9 +87,9 @@ class AgentManager:
for path in config.agent_paths: for path in config.agent_paths:
if path.is_dir(): if path.is_dir():
paths.append(path) paths.append(path)
paths.extend(discover_local_agents_dirs(Path.cwd())) mgr = get_harness_files_manager()
if GLOBAL_AGENTS_DIR.path.is_dir(): paths.extend(mgr.project_agents_dirs)
paths.append(GLOBAL_AGENTS_DIR.path) paths.extend(mgr.user_agents_dirs)
unique: list[Path] = [] unique: list[Path] = []
for p in paths: for p in paths:
rp = p.resolve() rp = p.resolve()

View file

@ -6,7 +6,7 @@ from pathlib import Path
import tomllib import tomllib
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from vibe.core.paths.global_paths import PLANS_DIR from vibe.core.paths import PLANS_DIR
if TYPE_CHECKING: if TYPE_CHECKING:
from vibe.core.config import VibeConfig from vibe.core.config import VibeConfig

View file

@ -0,0 +1,41 @@
from __future__ import annotations
from vibe.core.config._settings import (
DEFAULT_MISTRAL_API_ENV_KEY,
DEFAULT_MODELS,
DEFAULT_PROVIDERS,
Backend,
MCPHttp,
MCPServer,
MCPStdio,
MCPStreamableHttp,
MissingAPIKeyError,
MissingPromptFileError,
ModelConfig,
ProjectContextConfig,
ProviderConfig,
SessionLoggingConfig,
TomlFileSettingsSource,
VibeConfig,
load_dotenv_values,
)
__all__ = [
"DEFAULT_MISTRAL_API_ENV_KEY",
"DEFAULT_MODELS",
"DEFAULT_PROVIDERS",
"Backend",
"MCPHttp",
"MCPServer",
"MCPStdio",
"MCPStreamableHttp",
"MissingAPIKeyError",
"MissingPromptFileError",
"ModelConfig",
"ProjectContextConfig",
"ProviderConfig",
"SessionLoggingConfig",
"TomlFileSettingsSource",
"VibeConfig",
"load_dotenv_values",
]

View file

@ -20,12 +20,8 @@ from pydantic_settings import (
) )
import tomli_w import tomli_w
from vibe.core.paths.config_paths import CONFIG_DIR, CONFIG_FILE, PROMPTS_DIR from vibe.core.config.harness_files import get_harness_files_manager
from vibe.core.paths.global_paths import ( from vibe.core.paths import GLOBAL_ENV_FILE, SESSION_LOG_DIR
GLOBAL_ENV_FILE,
GLOBAL_PROMPTS_DIR,
SESSION_LOG_DIR,
)
from vibe.core.prompts import SystemPrompt from vibe.core.prompts import SystemPrompt
from vibe.core.tools.base import BaseToolConfig from vibe.core.tools.base import BaseToolConfig
@ -55,20 +51,14 @@ class MissingAPIKeyError(RuntimeError):
class MissingPromptFileError(RuntimeError): class MissingPromptFileError(RuntimeError):
def __init__( def __init__(self, system_prompt_id: str, *prompt_dirs: str) -> None:
self, system_prompt_id: str, prompt_dir: str, global_prompt_dir: str dirs_str = " or ".join(prompt_dirs) if prompt_dirs else "<no prompt dirs>"
) -> None:
extra_global_prompt_dir = (
f" or {global_prompt_dir}" if global_prompt_dir != prompt_dir else ""
)
super().__init__( super().__init__(
f"Invalid system_prompt_id value: '{system_prompt_id}'. " f"Invalid system_prompt_id value: '{system_prompt_id}'. "
f"Must be one of the available prompts ({', '.join(f'{p.name.lower()}' for p in SystemPrompt)}), " f"Must be one of the available prompts ({', '.join(f'{p.name.lower()}' for p in SystemPrompt)}), "
f"or correspond to a .md file in {prompt_dir}{extra_global_prompt_dir}" f"or correspond to a .md file in {dirs_str}"
) )
self.system_prompt_id = system_prompt_id self.system_prompt_id = system_prompt_id
self.prompt_dir = prompt_dir
class TomlFileSettingsSource(PydanticBaseSettingsSource): class TomlFileSettingsSource(PydanticBaseSettingsSource):
@ -77,7 +67,9 @@ class TomlFileSettingsSource(PydanticBaseSettingsSource):
self.toml_data = self._load_toml() self.toml_data = self._load_toml()
def _load_toml(self) -> dict[str, Any]: def _load_toml(self) -> dict[str, Any]:
file = CONFIG_FILE.path file = get_harness_files_manager().config_file
if file is None:
return {}
try: try:
with file.open("rb") as f: with file.open("rb") as f:
return tomllib.load(f) return tomllib.load(f)
@ -422,7 +414,9 @@ class VibeConfig(BaseSettings):
except KeyError: except KeyError:
pass pass
for current_prompt_dir in [PROMPTS_DIR.path, GLOBAL_PROMPTS_DIR.path]: mgr = get_harness_files_manager()
prompt_dirs = mgr.project_prompts_dirs + mgr.user_prompts_dirs
for current_prompt_dir in prompt_dirs:
custom_sp_path = (current_prompt_dir / self.system_prompt_id).with_suffix( custom_sp_path = (current_prompt_dir / self.system_prompt_id).with_suffix(
".md" ".md"
) )
@ -430,7 +424,7 @@ class VibeConfig(BaseSettings):
return custom_sp_path.read_text() return custom_sp_path.read_text()
raise MissingPromptFileError( raise MissingPromptFileError(
self.system_prompt_id, str(PROMPTS_DIR.path), str(GLOBAL_PROMPTS_DIR.path) self.system_prompt_id, *(str(d) for d in prompt_dirs)
) )
def get_active_model(self) -> ModelConfig: def get_active_model(self) -> ModelConfig:
@ -533,7 +527,8 @@ class VibeConfig(BaseSettings):
@classmethod @classmethod
def save_updates(cls, updates: dict[str, Any]) -> None: def save_updates(cls, updates: dict[str, Any]) -> None:
CONFIG_DIR.path.mkdir(parents=True, exist_ok=True) if not get_harness_files_manager().persist_allowed:
return
current_config = TomlFileSettingsSource(cls).toml_data current_config = TomlFileSettingsSource(cls).toml_data
def deep_merge(target: dict, source: dict) -> None: def deep_merge(target: dict, source: dict) -> None:
@ -563,7 +558,12 @@ class VibeConfig(BaseSettings):
@classmethod @classmethod
def dump_config(cls, config: dict[str, Any]) -> None: def dump_config(cls, config: dict[str, Any]) -> None:
with CONFIG_FILE.path.open("wb") as f: mgr = get_harness_files_manager()
if not mgr.persist_allowed:
return
target = mgr.config_file or mgr.user_config_file
target.parent.mkdir(parents=True, exist_ok=True)
with target.open("wb") as f:
tomli_w.dump(config, f) tomli_w.dump(config, f)
@classmethod @classmethod
@ -577,11 +577,7 @@ class VibeConfig(BaseSettings):
@classmethod @classmethod
def create_default(cls) -> dict[str, Any]: def create_default(cls) -> dict[str, Any]:
try: config = cls.model_construct()
config = cls()
except MissingAPIKeyError:
config = cls.model_construct()
config_dict = config.model_dump(mode="json", exclude_none=True) config_dict = config.model_dump(mode="json", exclude_none=True)
from vibe.core.tools.manager import ToolManager from vibe.core.tools.manager import ToolManager

View file

@ -0,0 +1,17 @@
from __future__ import annotations
from vibe.core.config.harness_files._harness_manager import (
FileSource,
HarnessFilesManager,
get_harness_files_manager,
init_harness_files_manager,
reset_harness_files_manager,
)
__all__ = [
"FileSource",
"HarnessFilesManager",
"get_harness_files_manager",
"init_harness_files_manager",
"reset_harness_files_manager",
]

View file

@ -0,0 +1,151 @@
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Literal
from vibe.core.config.harness_files._paths import (
GLOBAL_AGENTS_DIR,
GLOBAL_PROMPTS_DIR,
GLOBAL_SKILLS_DIR,
GLOBAL_TOOLS_DIR,
)
from vibe.core.paths import AGENTS_MD_FILENAMES, VIBE_HOME, walk_local_config_dirs_all
from vibe.core.trusted_folders import trusted_folders_manager
FileSource = Literal["user", "project"]
@dataclass(frozen=True)
class HarnessFilesManager:
sources: tuple[FileSource, ...] = ("user",)
cwd: Path | None = field(default=None)
@property
def _effective_cwd(self) -> Path:
return self.cwd or Path.cwd()
@property
def trusted_workdir(self) -> Path | None:
"""Return cwd if project source is enabled and trusted, else None."""
if "project" not in self.sources:
return None
cwd = self._effective_cwd
if trusted_folders_manager.is_trusted(cwd) is not True:
return None
return cwd
@property
def config_file(self) -> Path | None:
workdir = self.trusted_workdir
if workdir is not None:
candidate = workdir / ".vibe" / "config.toml"
if candidate.is_file():
return candidate
if "user" in self.sources:
return VIBE_HOME.path / "config.toml"
return None
@property
def persist_allowed(self) -> bool:
return "user" in self.sources
@property
def user_tools_dirs(self) -> list[Path]:
if "user" not in self.sources:
return []
d = GLOBAL_TOOLS_DIR.path
return [d] if d.is_dir() else []
@property
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 []
@property
def user_agents_dirs(self) -> list[Path]:
if "user" not in self.sources:
return []
d = GLOBAL_AGENTS_DIR.path
return [d] if d.is_dir() else []
def _walk_project_dirs(
self,
) -> tuple[tuple[Path, ...], tuple[Path, ...], tuple[Path, ...]]:
workdir = self.trusted_workdir
if workdir is None:
return ((), (), ())
return walk_local_config_dirs_all(workdir)
@property
def project_tools_dirs(self) -> list[Path]:
return list(self._walk_project_dirs()[0])
@property
def project_skills_dirs(self) -> list[Path]:
return list(self._walk_project_dirs()[1])
@property
def project_agents_dirs(self) -> list[Path]:
return list(self._walk_project_dirs()[2])
@property
def user_config_file(self) -> Path:
return VIBE_HOME.path / "config.toml"
@property
def project_prompts_dirs(self) -> list[Path]:
workdir = self.trusted_workdir
if workdir is None:
return []
candidate = workdir / ".vibe" / "prompts"
return [candidate] if candidate.is_dir() else []
@property
def user_prompts_dirs(self) -> list[Path]:
if "user" not in self.sources:
return []
d = GLOBAL_PROMPTS_DIR.path
return [d] if d.is_dir() else []
def load_project_doc(self, max_bytes: int) -> str:
workdir = self.trusted_workdir
if workdir is None:
return ""
for name in AGENTS_MD_FILENAMES:
path = workdir / name
try:
return path.read_text("utf-8", errors="ignore")[:max_bytes]
except (FileNotFoundError, OSError):
continue
return ""
_manager: HarnessFilesManager | None = None
def init_harness_files_manager(*sources: FileSource) -> None:
global _manager
if _manager is not None:
if _manager.sources == sources:
return
raise RuntimeError(
"HarnessFilesManager already initialized with different sources"
)
_manager = HarnessFilesManager(sources=sources)
def get_harness_files_manager() -> HarnessFilesManager:
if _manager is None:
raise RuntimeError(
"HarnessFilesManager not initialized — call init_harness_files_manager() first"
)
return _manager
def reset_harness_files_manager() -> None:
"""Reset the singleton. Only intended for use in tests."""
global _manager
_manager = None

View file

@ -0,0 +1,8 @@
from __future__ import annotations
from vibe.core.paths import 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")

View file

@ -3,7 +3,6 @@ from __future__ import annotations
from collections.abc import AsyncGenerator, Sequence from collections.abc import AsyncGenerator, Sequence
import json import json
import os import os
import re
import types import types
from typing import TYPE_CHECKING, NamedTuple, cast from typing import TYPE_CHECKING, NamedTuple, cast
@ -24,6 +23,7 @@ from vibe.core.types import (
StrToolChoice, StrToolChoice,
ToolCall, ToolCall,
) )
from vibe.core.utils import get_server_url_from_api_base
if TYPE_CHECKING: if TYPE_CHECKING:
from vibe.core.config import ModelConfig, ProviderConfig from vibe.core.config import ModelConfig, ProviderConfig
@ -170,14 +170,13 @@ class MistralBackend:
) )
# Mistral SDK takes server URL without api version as input # Mistral SDK takes server URL without api version as input
url_pattern = r"(https?://[^/]+)(/v.*)" server_url = get_server_url_from_api_base(self._provider.api_base)
match = re.match(url_pattern, self._provider.api_base) if not server_url:
if not match:
raise ValueError( raise ValueError(
f"Invalid API base URL: {self._provider.api_base}. " f"Invalid API base URL: {self._provider.api_base}. "
"Expected format: <server_url>/v<api_version>" "Expected format: <server_url>/v<api_version>"
) )
self._server_url = match.group(1) self._server_url = server_url
self._timeout = timeout self._timeout = timeout
self._retry_config = self._build_retry_config() self._retry_config = self._build_retry_config()

View file

@ -5,7 +5,7 @@ import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
import os import os
from vibe.core.paths.global_paths import LOG_DIR, LOG_FILE from vibe.core.paths import LOG_DIR, LOG_FILE
LOG_DIR.path.mkdir(parents=True, exist_ok=True) LOG_DIR.path.mkdir(parents=True, exist_ok=True)

View file

@ -0,0 +1,31 @@
from __future__ import annotations
from vibe.core.paths._local_config_walk import walk_local_config_dirs_all
from vibe.core.paths._vibe_home import (
DEFAULT_TOOL_DIR,
GLOBAL_ENV_FILE,
HISTORY_FILE,
LOG_DIR,
LOG_FILE,
PLANS_DIR,
SESSION_LOG_DIR,
TRUSTED_FOLDERS_FILE,
VIBE_HOME,
GlobalPath,
)
from vibe.core.paths.conventions import AGENTS_MD_FILENAMES
__all__ = [
"AGENTS_MD_FILENAMES",
"DEFAULT_TOOL_DIR",
"GLOBAL_ENV_FILE",
"HISTORY_FILE",
"LOG_DIR",
"LOG_FILE",
"PLANS_DIR",
"SESSION_LOG_DIR",
"TRUSTED_FOLDERS_FILE",
"VIBE_HOME",
"GlobalPath",
"walk_local_config_dirs_all",
]

View file

@ -26,16 +26,12 @@ def _get_vibe_home() -> Path:
VIBE_HOME = GlobalPath(_get_vibe_home) VIBE_HOME = GlobalPath(_get_vibe_home)
GLOBAL_CONFIG_FILE = GlobalPath(lambda: VIBE_HOME.path / "config.toml")
GLOBAL_ENV_FILE = GlobalPath(lambda: VIBE_HOME.path / ".env") GLOBAL_ENV_FILE = GlobalPath(lambda: VIBE_HOME.path / ".env")
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")
SESSION_LOG_DIR = GlobalPath(lambda: VIBE_HOME.path / "logs" / "session") SESSION_LOG_DIR = GlobalPath(lambda: VIBE_HOME.path / "logs" / "session")
TRUSTED_FOLDERS_FILE = GlobalPath(lambda: VIBE_HOME.path / "trusted_folders.toml") TRUSTED_FOLDERS_FILE = GlobalPath(lambda: VIBE_HOME.path / "trusted_folders.toml")
LOG_DIR = GlobalPath(lambda: VIBE_HOME.path / "logs") LOG_DIR = GlobalPath(lambda: VIBE_HOME.path / "logs")
LOG_FILE = GlobalPath(lambda: VIBE_HOME.path / "logs" / "vibe.log") LOG_FILE = GlobalPath(lambda: VIBE_HOME.path / "logs" / "vibe.log")
HISTORY_FILE = GlobalPath(lambda: VIBE_HOME.path / "vibehistory")
PLANS_DIR = GlobalPath(lambda: VIBE_HOME.path / "plans") PLANS_DIR = GlobalPath(lambda: VIBE_HOME.path / "plans")
DEFAULT_TOOL_DIR = GlobalPath(lambda: VIBE_ROOT / "core" / "tools" / "builtins") DEFAULT_TOOL_DIR = GlobalPath(lambda: VIBE_ROOT / "core" / "tools" / "builtins")

View file

@ -1,63 +0,0 @@
from __future__ import annotations
from pathlib import Path
from typing import Literal
from vibe.core.paths.global_paths import VIBE_HOME, GlobalPath
from vibe.core.paths.local_config_walk import walk_local_config_dirs_all
from vibe.core.trusted_folders import trusted_folders_manager
_config_paths_locked: bool = True
class ConfigPath(GlobalPath):
@property
def path(self) -> Path:
if _config_paths_locked:
raise RuntimeError("Config path is locked")
return super().path
def _resolve_config_path(basename: str, type: Literal["file", "dir"]) -> Path:
cwd = Path.cwd()
is_folder_trusted = trusted_folders_manager.is_trusted(cwd)
if not is_folder_trusted:
return VIBE_HOME.path / basename
if type == "file":
if (candidate := cwd / ".vibe" / basename).is_file():
return candidate
elif type == "dir":
if (candidate := cwd / ".vibe" / basename).is_dir():
return candidate
return VIBE_HOME.path / basename
def _discover_local_config_dirs_all(
root: Path,
) -> tuple[tuple[Path, ...], tuple[Path, ...], tuple[Path, ...]]:
if not trusted_folders_manager.is_trusted(root):
return ((), (), ())
return walk_local_config_dirs_all(root)
def discover_local_tools_dirs(root: Path) -> list[Path]:
return list(_discover_local_config_dirs_all(root)[0])
def discover_local_skills_dirs(root: Path) -> list[Path]:
return list(_discover_local_config_dirs_all(root)[1])
def discover_local_agents_dirs(root: Path) -> list[Path]:
return list(_discover_local_config_dirs_all(root)[2])
def unlock_config_paths() -> None:
global _config_paths_locked
_config_paths_locked = False
CONFIG_FILE = ConfigPath(lambda: _resolve_config_path("config.toml", "file"))
CONFIG_DIR = ConfigPath(lambda: CONFIG_FILE.path.parent)
PROMPTS_DIR = ConfigPath(lambda: _resolve_config_path("prompts", "dir"))
HISTORY_FILE = ConfigPath(lambda: _resolve_config_path("vibehistory", "file"))

View file

@ -0,0 +1,3 @@
from __future__ import annotations
AGENTS_MD_FILENAMES = ["AGENTS.md", "VIBE.md", ".vibe.md"]

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
import time import time
from vibe.core.paths.global_paths import PLANS_DIR from vibe.core.paths import PLANS_DIR
from vibe.core.slug import create_slug from vibe.core.slug import create_slug

View file

@ -2,7 +2,7 @@ from __future__ import annotations
from dotenv import dotenv_values, set_key, unset_key from dotenv import dotenv_values, set_key, unset_key
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE from vibe.core.paths import GLOBAL_ENV_FILE
SUPPORTED_PROXY_VARS: dict[str, str] = { SUPPORTED_PROXY_VARS: dict[str, str] = {
"HTTP_PROXY": "Proxy URL for HTTP requests", "HTTP_PROXY": "Proxy URL for HTTP requests",

View file

@ -4,9 +4,8 @@ from collections.abc import Callable
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from vibe.core.config.harness_files import get_harness_files_manager
from vibe.core.logger import logger from vibe.core.logger import logger
from vibe.core.paths.config_paths import discover_local_skills_dirs
from vibe.core.paths.global_paths import GLOBAL_SKILLS_DIR
from vibe.core.skills.models import SkillInfo, SkillMetadata from vibe.core.skills.models import SkillInfo, SkillMetadata
from vibe.core.skills.parser import SkillParseError, parse_frontmatter from vibe.core.skills.parser import SkillParseError, parse_frontmatter
from vibe.core.utils import name_matches from vibe.core.utils import name_matches
@ -56,10 +55,9 @@ class SkillManager:
if path.is_dir(): if path.is_dir():
paths.append(path) paths.append(path)
paths.extend(discover_local_skills_dirs(Path.cwd())) mgr = get_harness_files_manager()
paths.extend(mgr.project_skills_dirs)
if GLOBAL_SKILLS_DIR.path.is_dir(): paths.extend(mgr.user_skills_dirs)
paths.append(GLOBAL_SKILLS_DIR.path)
unique: list[Path] = [] unique: list[Path] = []
for p in paths: for p in paths:

View file

@ -7,8 +7,8 @@ import subprocess
import sys import sys
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from vibe.core.config.harness_files import get_harness_files_manager
from vibe.core.prompts import UtilityPrompt from vibe.core.prompts import UtilityPrompt
from vibe.core.trusted_folders import AGENTS_MD_FILENAMES, trusted_folders_manager
from vibe.core.utils import is_dangerous_directory, is_windows from vibe.core.utils import is_dangerous_directory, is_windows
if TYPE_CHECKING: if TYPE_CHECKING:
@ -20,18 +20,6 @@ if TYPE_CHECKING:
_git_status_cache: dict[Path, str] = {} _git_status_cache: dict[Path, str] = {}
def _load_project_doc(workdir: Path, max_bytes: int) -> str:
if not trusted_folders_manager.is_trusted(workdir):
return ""
for name in AGENTS_MD_FILENAMES:
path = workdir / name
try:
return path.read_text("utf-8", errors="ignore")[:max_bytes]
except (FileNotFoundError, OSError):
continue
return ""
class ProjectContextProvider: class ProjectContextProvider:
def __init__( def __init__(
self, config: ProjectContextConfig, root_path: str | Path = "." self, config: ProjectContextConfig, root_path: str | Path = "."
@ -297,8 +285,8 @@ def get_universal_system_prompt(
sections.append(context) sections.append(context)
project_doc = _load_project_doc( project_doc = get_harness_files_manager().load_project_doc(
Path.cwd(), config.project_context.max_doc_bytes config.project_context.max_doc_bytes
) )
if project_doc.strip(): if project_doc.strip():
sections.append(project_doc) sections.append(project_doc)

View file

@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, ClassVar, final
import mistralai import mistralai
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from vibe.core.config import Backend
from vibe.core.tools.base import ( from vibe.core.tools.base import (
BaseTool, BaseTool,
BaseToolConfig, BaseToolConfig,
@ -17,6 +18,7 @@ from vibe.core.tools.base import (
) )
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData
from vibe.core.types import ToolStreamEvent from vibe.core.types import ToolStreamEvent
from vibe.core.utils import get_server_url_from_api_base
if TYPE_CHECKING: if TYPE_CHECKING:
from vibe.core.types import ToolCallEvent, ToolResultEvent from vibe.core.types import ToolCallEvent, ToolResultEvent
@ -66,7 +68,9 @@ class WebSearch(
raise ToolError("MISTRAL_API_KEY environment variable not set.") raise ToolError("MISTRAL_API_KEY environment variable not set.")
client = mistralai.Mistral( client = mistralai.Mistral(
api_key=api_key, timeout_ms=self.config.timeout * 1000 api_key=api_key,
server_url=self._resolve_server_url(ctx),
timeout_ms=self.config.timeout * 1000,
) )
try: try:
@ -84,6 +88,14 @@ class WebSearch(
except mistralai.SDKError as exc: except mistralai.SDKError as exc:
raise ToolError(f"Mistral API error: {exc}") from exc raise ToolError(f"Mistral API error: {exc}") from exc
def _resolve_server_url(self, ctx: InvokeContext | None) -> str | None:
if not ctx or not ctx.agent_manager:
return None
for provider in ctx.agent_manager.config.providers:
if provider.backend == Backend.MISTRAL:
return get_server_url_from_api_base(provider.api_base)
return None
def _parse_response( def _parse_response(
self, response: mistralai.ConversationResponse self, response: mistralai.ConversationResponse
) -> WebSearchResult: ) -> WebSearchResult:

View file

@ -9,9 +9,9 @@ import re
import sys import sys
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from vibe.core.config.harness_files import get_harness_files_manager
from vibe.core.logger import logger from vibe.core.logger import logger
from vibe.core.paths.config_paths import discover_local_tools_dirs from vibe.core.paths import DEFAULT_TOOL_DIR
from vibe.core.paths.global_paths import DEFAULT_TOOL_DIR, GLOBAL_TOOLS_DIR
from vibe.core.tools.base import BaseTool, BaseToolConfig from vibe.core.tools.base import BaseTool, BaseToolConfig
from vibe.core.tools.mcp import MCPRegistry from vibe.core.tools.mcp import MCPRegistry
from vibe.core.utils import name_matches from vibe.core.utils import name_matches
@ -89,8 +89,10 @@ class ToolManager:
paths: list[Path] = [DEFAULT_TOOL_DIR.path] paths: list[Path] = [DEFAULT_TOOL_DIR.path]
paths.extend(config.tool_paths) paths.extend(config.tool_paths)
paths.extend(discover_local_tools_dirs(Path.cwd()))
paths.append(GLOBAL_TOOLS_DIR.path) mgr = get_harness_files_manager()
paths.extend(mgr.project_tools_dirs)
paths.extend(mgr.user_tools_dirs)
unique: list[Path] = [] unique: list[Path] = []
seen: set[Path] = set() seen: set[Path] = set()

View file

@ -5,10 +5,11 @@ import tomllib
import tomli_w import tomli_w
from vibe.core.paths.global_paths import TRUSTED_FOLDERS_FILE from vibe.core.paths import (
from vibe.core.paths.local_config_walk import walk_local_config_dirs_all AGENTS_MD_FILENAMES,
TRUSTED_FOLDERS_FILE,
AGENTS_MD_FILENAMES = ["AGENTS.md", "VIBE.md", ".vibe.md"] walk_local_config_dirs_all,
)
def has_agents_md_file(path: Path) -> bool: def has_agents_md_file(path: Path) -> bool:

View file

@ -337,5 +337,10 @@ def compact_reduction_display(old_tokens: int | None, new_tokens: int | None) ->
) )
def get_server_url_from_api_base(api_base: str) -> str | None:
match = re.match(r"(https?://[^/]+)(/v.*)", api_base)
return match.group(1) if match else None
def utc_now() -> datetime: def utc_now() -> datetime:
return datetime.now(UTC) return datetime.now(UTC)

View file

@ -5,7 +5,7 @@ import sys
from rich import print as rprint from rich import print as rprint
from textual.app import App from textual.app import App
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE from vibe.core.paths import GLOBAL_ENV_FILE
from vibe.setup.onboarding.screens import ApiKeyScreen, WelcomeScreen from vibe.setup.onboarding.screens import ApiKeyScreen, WelcomeScreen

View file

@ -14,7 +14,7 @@ from textual.widgets import Input, Link, Static
from vibe.cli.clipboard import copy_selection_to_clipboard from vibe.cli.clipboard import copy_selection_to_clipboard
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
from vibe.core.config import Backend, VibeConfig from vibe.core.config import Backend, VibeConfig
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE from vibe.core.paths import GLOBAL_ENV_FILE
from vibe.core.telemetry.send import TelemetryClient from vibe.core.telemetry.send import TelemetryClient
from vibe.setup.onboarding.base import OnboardingScreen from vibe.setup.onboarding.base import OnboardingScreen

View file

@ -11,7 +11,7 @@ from textual.message import Message
from textual.widgets import Static from textual.widgets import Static
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
from vibe.core.paths.global_paths import TRUSTED_FOLDERS_FILE from vibe.core.paths import TRUSTED_FOLDERS_FILE
class TrustDialogQuitException(Exception): class TrustDialogQuitException(Exception):

View file

@ -1,4 +1,3 @@
# What's new in v2.4.0 # What's new in v2.4.1
- **Plan mode**: Plan can be reviewed before switching to accept mode to apply edits. - **Prompt mode**: Disabled interactive questions in prompt mode for smoother non-interactive usage
- **Pricing plan**: Your current plan appears in the CLI banner - **VS Code terminal fix**: Space key now works correctly in all input widgets (question prompts, proxy setup)
- **Per-model auto-compact**: Auto-compact threshold is configurable per model