Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-05-11 11:44:53 +02:00 committed by GitHub
parent b23f49e5f4
commit 626f905186
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 702 additions and 183 deletions

View file

@ -210,7 +210,11 @@ def run_cli(args: argparse.Namespace) -> None:
stdin_prompt = get_prompt_from_stdin()
if args.prompt is not None:
warn_if_workdir_trust_is_unset()
config.disabled_tools = [*config.disabled_tools, "ask_user_question"]
config.disabled_tools = [
*config.disabled_tools,
"ask_user_question",
"exit_plan_mode",
]
programmatic_prompt = args.prompt or stdin_prompt
if not programmatic_prompt:
print(

View file

@ -10,6 +10,7 @@ from vibe.cli.plan_offer.ports.whoami_gateway import (
WhoAmIGatewayUnauthorized,
WhoAmIResponse,
)
from vibe.core.utils.http import build_ssl_context
BASE_URL = "https://console.mistral.ai"
WHOAMI_PATH = "/api/vibe/whoami"
@ -23,7 +24,7 @@ class HttpWhoAmIGateway:
url = f"{self._base_url}{WHOAMI_PATH}"
headers = {"Authorization": f"Bearer {api_key}"}
try:
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(verify=build_ssl_context()) as client:
response = await client.get(url, headers=headers)
except httpx.RequestError as exc:
raise WhoAmIGatewayError() from exc

View file

@ -987,6 +987,8 @@ class VibeApp(App): # noqa: PLR0904
bash_msg = BashOutputMessage(command, str(Path.cwd()), pending=True)
await self._mount_and_scroll(bash_msg)
await self._ensure_loading_widget("Running command")
bash_loading_widget = self._loading_widget
proc: asyncio.subprocess.Process | None = None
stdout_parts: list[str] = []
@ -1071,6 +1073,9 @@ class VibeApp(App): # noqa: PLR0904
status=f"failed before completion: {e}",
)
)
finally:
if self._loading_widget is bash_loading_widget:
await self._remove_loading_widget()
def _get_bash_max_output_bytes(self) -> int:
from vibe.core.tools.builtins.bash import BashToolConfig
@ -2132,7 +2137,9 @@ class VibeApp(App): # noqa: PLR0904
self._emit_session_closed_for_active_session()
await self._loop_runner.stop()
self._log_reader.shutdown()
await self._voice_manager.close()
await self._narrator_manager.close()
await self.agent_loop.aclose()
try:
await self.agent_loop.telemetry_client.aclose()
except Exception as exc:
@ -2629,6 +2636,9 @@ class VibeApp(App): # noqa: PLR0904
return True
interrupted = False
if self._bash_task and not self._bash_task.done():
self._bash_task.cancel()
interrupted = True
if self._agent_running:
self._handle_agent_running_escape()
interrupted = True
@ -2815,6 +2825,7 @@ class VibeApp(App): # noqa: PLR0904
self._log_reader.shutdown()
self._narrator_manager.cancel()
await self.agent_loop.aclose()
try:
await self.agent_loop.telemetry_client.aclose()
except Exception as exc:

View file

@ -406,6 +406,10 @@ Markdown {
width: auto;
height: auto;
&.bash-pending {
color: $foreground;
}
&.bash-success {
color: ansi_green;
}

View file

@ -263,7 +263,9 @@ class InterruptMessage(Static):
)
class BashOutputMessage(Static):
class BashOutputMessage(SpinnerMixin, Static):
SPINNER_TYPE = SpinnerType.PULSE
def __init__(
self,
command: str,
@ -274,6 +276,7 @@ class BashOutputMessage(Static):
pending: bool = False,
) -> None:
super().__init__()
self.init_spinner()
self.add_class("bash-output-message")
self._command = command
self._cwd = cwd
@ -283,17 +286,29 @@ class BashOutputMessage(Static):
self._output_widget: NoMarkupStatic | None = None
self._output_container: Horizontal | None = None
self._prompt_widget: NonSelectableStatic | None = None
self._indicator_widget: Static | None = None
def _update_spinner_frame(self) -> None:
if not self._is_spinning or not self._prompt_widget:
return
self._prompt_widget.update(f"{self._spinner.next_frame()} ")
def on_mount(self) -> None:
if self._pending:
self.start_spinner_timer()
def compose(self) -> ComposeResult:
status_class = (
"bash-error"
if not self._pending and self._exit_code != 0
else "bash-success"
)
if self._pending:
status_class = "bash-pending"
elif self._exit_code != 0:
status_class = "bash-error"
else:
status_class = "bash-success"
self.add_class(status_class)
prompt_text = f"{self._spinner.current_frame()} " if self._pending else "$ "
with Horizontal(classes="bash-command-line"):
self._prompt_widget = NonSelectableStatic(
"$ ", classes=f"bash-prompt {status_class}"
prompt_text, classes=f"bash-prompt {status_class}"
)
yield self._prompt_widget
yield NoMarkupStatic(self._command, classes="bash-command")
@ -326,18 +341,20 @@ class BashOutputMessage(Static):
async def finish(self, exit_code: int, *, interrupted: bool = False) -> None:
self._exit_code = exit_code
self._pending = False
self.stop_spinning()
if self._prompt_widget:
self._prompt_widget.update("$ ")
if interrupted:
self.remove_class("bash-success")
self.add_class("bash-interrupted")
if self._prompt_widget:
self._prompt_widget.remove_class("bash-success")
self._prompt_widget.add_class("bash-interrupted")
new_class = "bash-interrupted"
elif exit_code != 0:
self.remove_class("bash-success")
self.add_class("bash-error")
if self._prompt_widget:
self._prompt_widget.remove_class("bash-success")
self._prompt_widget.add_class("bash-error")
new_class = "bash-error"
else:
new_class = "bash-success"
self.remove_class("bash-pending")
self.add_class(new_class)
if self._prompt_widget:
self._prompt_widget.remove_class("bash-pending")
self._prompt_widget.add_class(new_class)
if interrupted:
suffix = (
"\n(interrupted)"

View file

@ -8,6 +8,7 @@ from vibe.cli.update_notifier.ports.update_gateway import (
UpdateGatewayCause,
UpdateGatewayError,
)
from vibe.core.utils.http import build_ssl_context
class GitHubUpdateGateway(UpdateGateway):
@ -47,7 +48,9 @@ class GitHubUpdateGateway(UpdateGateway):
)
else:
async with httpx.AsyncClient(
base_url=self._base_url, timeout=self._timeout
base_url=self._base_url,
timeout=self._timeout,
verify=build_ssl_context(),
) as client:
response = await client.get(request_path, headers=headers)
except httpx.RequestError as exc:

View file

@ -10,6 +10,7 @@ from vibe.cli.update_notifier.ports.update_gateway import (
UpdateGatewayCause,
UpdateGatewayError,
)
from vibe.core.utils.http import build_ssl_context
_STATUS_CAUSES: dict[int, UpdateGatewayCause] = {
httpx.codes.NOT_FOUND: UpdateGatewayCause.NOT_FOUND,
@ -81,7 +82,9 @@ class PyPIUpdateGateway(UpdateGateway):
)
async with httpx.AsyncClient(
base_url=self._base_url, timeout=self._timeout
base_url=self._base_url,
timeout=self._timeout,
verify=build_ssl_context(),
) as client:
return await client.get(request_path, headers=headers)
except httpx.RequestError as exc:

View file

@ -1,6 +1,7 @@
from __future__ import annotations
from asyncio import CancelledError, create_task, wait_for
import contextlib
from typing import TYPE_CHECKING
from vibe.cli.voice_manager.telemetry import TranscriptionTrackingState
@ -154,6 +155,15 @@ class VoiceManager:
except ValueError:
pass
async def close(self) -> None:
transcribe_task = self._transcribe_task
self.cancel_recording()
if transcribe_task is not None:
with contextlib.suppress(CancelledError):
await transcribe_task
if self._transcribe_client is not None:
await self._transcribe_client.close()
async def _run_transcription(self) -> None:
if self._transcribe_client is None:
return

View file

@ -54,3 +54,5 @@ class VoiceManagerPort(Protocol):
def add_listener(self, listener: VoiceManagerListener) -> None: ...
def remove_listener(self, listener: VoiceManagerListener) -> None: ...
async def close(self) -> None: ...