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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ from pathlib import Path
import sys
from rich import print as rprint
import tomli_w
from vibe import __version__
from vibe.cli.textual_ui.app import run_textual_ui
@ -16,8 +17,9 @@ from vibe.core.config import (
VibeConfig,
load_dotenv_values,
)
from vibe.core.config.harness_files import get_harness_files_manager
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.session.session_loader import SessionLoader
from vibe.core.types import EntrypointMetadata, LLMMessage, OutputFormat, Role
@ -61,16 +63,21 @@ def load_config_or_exit() -> VibeConfig:
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:
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:
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:
HISTORY_FILE.path.parent.mkdir(parents=True, exist_ok=True)
HISTORY_FILE.path.write_text("Hello Vibe!\n", "utf-8")
history_file.parent.mkdir(parents=True, exist_ok=True)
history_file.write_text("Hello Vibe!\n", "utf-8")
except Exception as e:
rprint(f"[yellow]Could not create history file: {e}[/]")

View file

@ -9,7 +9,7 @@ from rich import print as rprint
from vibe import __version__
from vibe.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.setup.trusted_folders.trust_folder_dialog import (
TrustDialogQuitException,
@ -152,7 +152,7 @@ def main() -> None:
is_interactive = args.prompt is None
if is_interactive:
check_and_resolve_trusted_folder()
unlock_config_paths()
init_harness_files_manager("user", "project")
from vibe.cli.cli import run_cli

View file

@ -92,7 +92,7 @@ from vibe.core.agents import AgentProfile
from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt
from vibe.core.config import Backend, VibeConfig
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.teleport.types import (
TeleportAuthCompleteEvent,

View file

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

View file

@ -10,6 +10,7 @@ from textual.message import Message
from textual.widgets import Input, Static
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 (
SUPPORTED_PROXY_VARS,
get_current_proxy_settings,
@ -48,7 +49,7 @@ class ProxySetupApp(Container):
yield Static(f"[bold ansi_blue]{key}[/]", classes="proxy-label-line")
initial_value = self.initial_values.get(key) or ""
input_widget = Input(
input_widget = VscodeCompatInput(
value=initial_value,
placeholder=description,
id=f"proxy-input-{key}",

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ from pathlib import Path
import tomllib
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:
from vibe.core.config import VibeConfig

View file

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

View file

@ -20,12 +20,8 @@ from pydantic_settings import (
)
import tomli_w
from vibe.core.paths.config_paths import CONFIG_DIR, CONFIG_FILE, PROMPTS_DIR
from vibe.core.paths.global_paths import (
GLOBAL_ENV_FILE,
GLOBAL_PROMPTS_DIR,
SESSION_LOG_DIR,
)
from vibe.core.config.harness_files import get_harness_files_manager
from vibe.core.paths import GLOBAL_ENV_FILE, SESSION_LOG_DIR
from vibe.core.prompts import SystemPrompt
from vibe.core.tools.base import BaseToolConfig
@ -55,20 +51,14 @@ class MissingAPIKeyError(RuntimeError):
class MissingPromptFileError(RuntimeError):
def __init__(
self, system_prompt_id: str, prompt_dir: str, global_prompt_dir: str
) -> None:
extra_global_prompt_dir = (
f" or {global_prompt_dir}" if global_prompt_dir != prompt_dir else ""
)
def __init__(self, system_prompt_id: str, *prompt_dirs: str) -> None:
dirs_str = " or ".join(prompt_dirs) if prompt_dirs else "<no prompt dirs>"
super().__init__(
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"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.prompt_dir = prompt_dir
class TomlFileSettingsSource(PydanticBaseSettingsSource):
@ -77,7 +67,9 @@ class TomlFileSettingsSource(PydanticBaseSettingsSource):
self.toml_data = self._load_toml()
def _load_toml(self) -> dict[str, Any]:
file = CONFIG_FILE.path
file = get_harness_files_manager().config_file
if file is None:
return {}
try:
with file.open("rb") as f:
return tomllib.load(f)
@ -422,7 +414,9 @@ class VibeConfig(BaseSettings):
except KeyError:
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(
".md"
)
@ -430,7 +424,7 @@ class VibeConfig(BaseSettings):
return custom_sp_path.read_text()
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:
@ -533,7 +527,8 @@ class VibeConfig(BaseSettings):
@classmethod
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
def deep_merge(target: dict, source: dict) -> None:
@ -563,7 +558,12 @@ class VibeConfig(BaseSettings):
@classmethod
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)
@classmethod
@ -577,11 +577,7 @@ class VibeConfig(BaseSettings):
@classmethod
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)
from vibe.core.tools.manager import ToolManager

View file

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

View file

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

View file

@ -0,0 +1,8 @@
from __future__ import annotations
from vibe.core.paths import VIBE_HOME, GlobalPath
GLOBAL_TOOLS_DIR = GlobalPath(lambda: VIBE_HOME.path / "tools")
GLOBAL_SKILLS_DIR = GlobalPath(lambda: VIBE_HOME.path / "skills")
GLOBAL_AGENTS_DIR = GlobalPath(lambda: VIBE_HOME.path / "agents")
GLOBAL_PROMPTS_DIR = GlobalPath(lambda: VIBE_HOME.path / "prompts")

View file

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

View file

@ -5,7 +5,7 @@ import logging
from logging.handlers import RotatingFileHandler
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)

View file

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

View file

@ -26,16 +26,12 @@ def _get_vibe_home() -> Path:
VIBE_HOME = GlobalPath(_get_vibe_home)
GLOBAL_CONFIG_FILE = GlobalPath(lambda: VIBE_HOME.path / "config.toml")
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")
TRUSTED_FOLDERS_FILE = GlobalPath(lambda: VIBE_HOME.path / "trusted_folders.toml")
LOG_DIR = GlobalPath(lambda: VIBE_HOME.path / "logs")
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")
DEFAULT_TOOL_DIR = GlobalPath(lambda: VIBE_ROOT / "core" / "tools" / "builtins")

View file

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

View file

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

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from pathlib import Path
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

View file

@ -2,7 +2,7 @@ from __future__ import annotations
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] = {
"HTTP_PROXY": "Proxy URL for HTTP requests",

View file

@ -4,9 +4,8 @@ from collections.abc import Callable
from pathlib import Path
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.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.parser import SkillParseError, parse_frontmatter
from vibe.core.utils import name_matches
@ -56,10 +55,9 @@ class SkillManager:
if path.is_dir():
paths.append(path)
paths.extend(discover_local_skills_dirs(Path.cwd()))
if GLOBAL_SKILLS_DIR.path.is_dir():
paths.append(GLOBAL_SKILLS_DIR.path)
mgr = get_harness_files_manager()
paths.extend(mgr.project_skills_dirs)
paths.extend(mgr.user_skills_dirs)
unique: list[Path] = []
for p in paths:

View file

@ -7,8 +7,8 @@ import subprocess
import sys
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.trusted_folders import AGENTS_MD_FILENAMES, trusted_folders_manager
from vibe.core.utils import is_dangerous_directory, is_windows
if TYPE_CHECKING:
@ -20,18 +20,6 @@ if TYPE_CHECKING:
_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:
def __init__(
self, config: ProjectContextConfig, root_path: str | Path = "."
@ -297,8 +285,8 @@ def get_universal_system_prompt(
sections.append(context)
project_doc = _load_project_doc(
Path.cwd(), config.project_context.max_doc_bytes
project_doc = get_harness_files_manager().load_project_doc(
config.project_context.max_doc_bytes
)
if project_doc.strip():
sections.append(project_doc)

View file

@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, ClassVar, final
import mistralai
from pydantic import BaseModel, Field
from vibe.core.config import Backend
from vibe.core.tools.base import (
BaseTool,
BaseToolConfig,
@ -17,6 +18,7 @@ from vibe.core.tools.base import (
)
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData
from vibe.core.types import ToolStreamEvent
from vibe.core.utils import get_server_url_from_api_base
if TYPE_CHECKING:
from vibe.core.types import ToolCallEvent, ToolResultEvent
@ -66,7 +68,9 @@ class WebSearch(
raise ToolError("MISTRAL_API_KEY environment variable not set.")
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:
@ -84,6 +88,14 @@ class WebSearch(
except mistralai.SDKError as 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(
self, response: mistralai.ConversationResponse
) -> WebSearchResult:

View file

@ -9,9 +9,9 @@ import re
import sys
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.paths.config_paths import discover_local_tools_dirs
from vibe.core.paths.global_paths import DEFAULT_TOOL_DIR, GLOBAL_TOOLS_DIR
from vibe.core.paths import DEFAULT_TOOL_DIR
from vibe.core.tools.base import BaseTool, BaseToolConfig
from vibe.core.tools.mcp import MCPRegistry
from vibe.core.utils import name_matches
@ -89,8 +89,10 @@ class ToolManager:
paths: list[Path] = [DEFAULT_TOOL_DIR.path]
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] = []
seen: set[Path] = set()

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import sys
from rich import print as rprint
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

View file

@ -14,7 +14,7 @@ from textual.widgets import Input, Link, Static
from vibe.cli.clipboard import copy_selection_to_clipboard
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
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.setup.onboarding.base import OnboardingScreen

View file

@ -11,7 +11,7 @@ from textual.message import Message
from textual.widgets import Static
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):

View file

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