v2.4.1 (#475)
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:
parent
dd372ce494
commit
e9428bce23
54 changed files with 877 additions and 362 deletions
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.4.0",
|
"version": "2.4.1",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "ACP Server",
|
"name": "ACP Server",
|
||||||
|
|
|
||||||
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
19
tests/core/test_utils.py
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
2
uv.lock
generated
|
|
@ -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" },
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}[/]")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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}",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
26
vibe/cli/textual_ui/widgets/vscode_compat.py
Normal file
26
vibe/cli/textual_ui/widgets/vscode_compat.py
Normal 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)
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
41
vibe/core/config/__init__.py
Normal file
41
vibe/core/config/__init__.py
Normal 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",
|
||||||
|
]
|
||||||
|
|
@ -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()
|
|
||||||
except MissingAPIKeyError:
|
|
||||||
config = cls.model_construct()
|
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
|
||||||
17
vibe/core/config/harness_files/__init__.py
Normal file
17
vibe/core/config/harness_files/__init__.py
Normal 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",
|
||||||
|
]
|
||||||
151
vibe/core/config/harness_files/_harness_manager.py
Normal file
151
vibe/core/config/harness_files/_harness_manager.py
Normal 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
|
||||||
8
vibe/core/config/harness_files/_paths.py
Normal file
8
vibe/core/config/harness_files/_paths.py
Normal 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")
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
]
|
||||||
|
|
@ -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")
|
||||||
|
|
@ -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"))
|
|
||||||
3
vibe/core/paths/conventions.py
Normal file
3
vibe/core/paths/conventions.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
AGENTS_MD_FILENAMES = ["AGENTS.md", "VIBE.md", ".vibe.md"]
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue