Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai>
Co-authored-by: Clément Siriex <clement.sirieix@mistral.ai>
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-authored-by: Michel Thomazo <michel.thomazo@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
This commit is contained in:
Mathias Gesbert 2026-02-17 16:23:28 +01:00 committed by GitHub
parent 51fecc67d9
commit ec7f3b25ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
107 changed files with 8002 additions and 535 deletions

View file

@ -26,7 +26,7 @@ def _shorten_preview(texts: list[str]) -> str:
return dense_text
def copy_selection_to_clipboard(app: App, show_toast: bool = True) -> None:
def copy_selection_to_clipboard(app: App, show_toast: bool = True) -> str | None:
selected_texts = []
for widget in app.query("*"):
@ -48,7 +48,7 @@ def copy_selection_to_clipboard(app: App, show_toast: bool = True) -> None:
selected_texts.append(selected_text)
if not selected_texts:
return
return None
combined_text = "\n".join(selected_texts)
@ -61,7 +61,9 @@ def copy_selection_to_clipboard(app: App, show_toast: bool = True) -> None:
timeout=2,
markup=False,
)
return combined_text
except Exception:
app.notify(
"Failed to copy - clipboard not available", severity="warning", timeout=3
)
return None

View file

@ -67,6 +67,11 @@ class CommandRegistry:
description="Teleport session to Vibe Nuage",
handler="_teleport_command",
),
"proxy-setup": Command(
aliases=frozenset(["/proxy-setup"]),
description="Configure proxy and SSL certificate settings",
handler="_show_proxy_setup",
),
}
for command in excluded_commands:
@ -78,9 +83,12 @@ class CommandRegistry:
self._alias_map[alias] = cmd_name
def find_command(self, user_input: str) -> Command | None:
cmd_name = self._alias_map.get(user_input.lower().strip())
cmd_name = self.get_command_name(user_input)
return self.commands.get(cmd_name) if cmd_name else None
def get_command_name(self, user_input: str) -> str | None:
return self._alias_map.get(user_input.lower().strip())
def get_help_text(self) -> str:
lines: list[str] = [
"### Keyboard Shortcuts",

View file

@ -51,6 +51,7 @@ from vibe.cli.textual_ui.widgets.messages import (
)
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
from vibe.cli.textual_ui.widgets.path_display import PathDisplay
from vibe.cli.textual_ui.widgets.proxy_setup_app import ProxySetupApp
from vibe.cli.textual_ui.widgets.question_app import QuestionApp
from vibe.cli.textual_ui.widgets.teleport_message import TeleportMessage
from vibe.cli.textual_ui.widgets.tools import ToolCallMessage, ToolResultMessage
@ -127,6 +128,7 @@ class BottomApp(StrEnum):
Approval = auto()
Config = auto()
Input = auto()
ProxySetup = auto()
Question = auto()
@ -308,6 +310,7 @@ class VibeApp(App): # noqa: PLR0904
await self._resume_history_from_messages()
await self._check_and_show_whats_new()
self._schedule_update_notification()
self.agent_loop.emit_new_session_telemetry("cli")
if self._initial_prompt or self._teleport_on_start:
self.call_after_refresh(self._process_initial_prompt)
@ -422,6 +425,24 @@ class VibeApp(App): # noqa: PLR0904
await self._switch_to_input_app()
async def on_proxy_setup_app_proxy_setup_closed(
self, message: ProxySetupApp.ProxySetupClosed
) -> None:
if message.error:
await self._mount_and_scroll(
ErrorMessage(f"Failed to save proxy settings: {message.error}")
)
elif message.saved:
await self._mount_and_scroll(
UserCommandMessage(
"Proxy settings saved. Restart the CLI for changes to take effect."
)
)
else:
await self._mount_and_scroll(UserCommandMessage("Proxy setup cancelled."))
await self._switch_to_input_app()
async def on_compact_message_completed(
self, message: CompactMessage.Completed
) -> None:
@ -449,6 +470,10 @@ class VibeApp(App): # noqa: PLR0904
async def _handle_command(self, user_input: str) -> bool:
if command := self.commands.find_command(user_input):
if cmd_name := self.commands.get_command_name(user_input):
self.agent_loop.telemetry_client.send_slash_command_used(
cmd_name, "builtin"
)
await self._mount_and_scroll(UserMessage(user_input))
handler = getattr(self, command.handler)
if asyncio.iscoroutinefunction(handler):
@ -479,6 +504,8 @@ class VibeApp(App): # noqa: PLR0904
if not skill_info:
return False
self.agent_loop.telemetry_client.send_slash_command_used(skill_name, "skill")
try:
skill_content = skill_info.skill_path.read_text(encoding="utf-8")
except OSError as e:
@ -823,6 +850,11 @@ class VibeApp(App): # noqa: PLR0904
return
await self._switch_to_config_app()
async def _show_proxy_setup(self) -> None:
if self._current_bottom_app == BottomApp.ProxySetup:
return
await self._switch_to_proxy_setup_app()
async def _reload_config(self) -> None:
try:
self._windowing.reset()
@ -996,6 +1028,13 @@ class VibeApp(App): # noqa: PLR0904
await self._mount_and_scroll(UserCommandMessage("Configuration opened..."))
await self._switch_from_input(ConfigApp(self.config))
async def _switch_to_proxy_setup_app(self) -> None:
if self._current_bottom_app == BottomApp.ProxySetup:
return
await self._mount_and_scroll(UserCommandMessage("Proxy setup opened..."))
await self._switch_from_input(ProxySetupApp())
async def _switch_to_approval_app(
self, tool_name: str, tool_args: BaseModel
) -> None:
@ -1020,6 +1059,7 @@ class VibeApp(App): # noqa: PLR0904
self._chat_input_container.display = True
self._current_bottom_app = BottomApp.Input
self.call_after_refresh(self._chat_input_container.focus_input)
self.call_after_refresh(self._scroll_to_bottom)
def _focus_current_bottom_app(self) -> None:
try:
@ -1028,6 +1068,8 @@ class VibeApp(App): # noqa: PLR0904
self.query_one(ChatInputContainer).focus_input()
case BottomApp.Config:
self.query_one(ConfigApp).focus()
case BottomApp.ProxySetup:
self.query_one(ProxySetupApp).focus()
case BottomApp.Approval:
self.query_one(ApprovalApp).focus()
case BottomApp.Question:
@ -1037,34 +1079,66 @@ class VibeApp(App): # noqa: PLR0904
except Exception:
pass
def _handle_config_app_escape(self) -> None:
try:
config_app = self.query_one(ConfigApp)
config_app.action_close()
except Exception:
pass
self._last_escape_time = None
def _handle_approval_app_escape(self) -> None:
try:
approval_app = self.query_one(ApprovalApp)
approval_app.action_reject()
except Exception:
pass
self.agent_loop.telemetry_client.send_user_cancelled_action("reject_approval")
self._last_escape_time = None
def _handle_question_app_escape(self) -> None:
try:
question_app = self.query_one(QuestionApp)
question_app.action_cancel()
except Exception:
pass
self.agent_loop.telemetry_client.send_user_cancelled_action("cancel_question")
self._last_escape_time = None
def _handle_input_app_escape(self) -> None:
try:
input_widget = self.query_one(ChatInputContainer)
input_widget.value = ""
except Exception:
pass
self._last_escape_time = None
def _handle_agent_running_escape(self) -> None:
self.agent_loop.telemetry_client.send_user_cancelled_action("interrupt_agent")
self.run_worker(self._interrupt_agent_loop(), exclusive=False)
def action_interrupt(self) -> None:
current_time = time.monotonic()
if self._current_bottom_app == BottomApp.Config:
self._handle_config_app_escape()
return
if self._current_bottom_app == BottomApp.ProxySetup:
try:
config_app = self.query_one(ConfigApp)
config_app.action_close()
proxy_setup_app = self.query_one(ProxySetupApp)
proxy_setup_app.action_close()
except Exception:
pass
self._last_escape_time = None
return
if self._current_bottom_app == BottomApp.Approval:
try:
approval_app = self.query_one(ApprovalApp)
approval_app.action_reject()
except Exception:
pass
self._last_escape_time = None
self._handle_approval_app_escape()
return
if self._current_bottom_app == BottomApp.Question:
try:
question_app = self.query_one(QuestionApp)
question_app.action_cancel()
except Exception:
pass
self._last_escape_time = None
self._handle_question_app_escape()
return
if (
@ -1072,17 +1146,11 @@ class VibeApp(App): # noqa: PLR0904
and self._last_escape_time is not None
and (current_time - self._last_escape_time) < 0.2 # noqa: PLR2004
):
try:
input_widget = self.query_one(ChatInputContainer)
if input_widget.value:
input_widget.value = ""
self._last_escape_time = None
return
except Exception:
pass
self._handle_input_app_escape()
return
if self._agent_running:
self.run_worker(self._interrupt_agent_loop(), exclusive=False)
self._handle_agent_running_escape()
self._last_escape_time = current_time
self._scroll_to_bottom()
@ -1430,11 +1498,15 @@ class VibeApp(App): # noqa: PLR0904
)
def action_copy_selection(self) -> None:
copy_selection_to_clipboard(self, show_toast=False)
copied_text = copy_selection_to_clipboard(self, show_toast=False)
if copied_text is not None:
self.agent_loop.telemetry_client.send_user_copied_text(copied_text)
def on_mouse_up(self, event: MouseUp) -> None:
if self.config.autocopy_to_clipboard:
copy_selection_to_clipboard(self, show_toast=True)
copied_text = copy_selection_to_clipboard(self, show_toast=True)
if copied_text is not None:
self.agent_loop.telemetry_client.send_user_copied_text(copied_text)
def on_app_blur(self, event: AppBlur) -> None:
if self._chat_input_container and self._chat_input_container.input_widget:

View file

@ -703,6 +703,44 @@ StatusMessage {
color: ansi_bright_black;
}
#proxysetup-app {
width: 100%;
height: auto;
background: transparent;
border: solid ansi_bright_black;
padding: 0 1;
margin: 0;
}
#proxysetup-content {
width: 100%;
height: auto;
}
.proxy-label {
height: auto;
color: ansi_blue;
text-style: bold;
}
.proxy-description {
height: auto;
color: ansi_bright_black;
}
.proxy-label-line {
height: auto;
}
.proxy-input {
width: 100%;
height: auto;
border: none;
border-left: wide ansi_bright_black;
margin-top: 1;
padding: 0 0 0 1;
}
#approval-app {
width: 100%;
height: auto;

View file

@ -0,0 +1,127 @@
from __future__ import annotations
from typing import ClassVar
from textual import events
from textual.app import ComposeResult
from textual.binding import Binding, BindingType
from textual.containers import Container, Vertical
from textual.message import Message
from textual.widgets import Input, Static
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
from vibe.core.proxy_setup import (
SUPPORTED_PROXY_VARS,
get_current_proxy_settings,
set_proxy_var,
unset_proxy_var,
)
class ProxySetupApp(Container):
can_focus = True
can_focus_children = True
BINDINGS: ClassVar[list[BindingType]] = [
Binding("up", "focus_previous", "Up", show=False),
Binding("down", "focus_next", "Down", show=False),
]
class ProxySetupClosed(Message):
def __init__(self, saved: bool, error: str | None = None) -> None:
super().__init__()
self.saved = saved
self.error = error
def __init__(self) -> None:
super().__init__(id="proxysetup-app")
self.inputs: dict[str, Input] = {}
self.initial_values: dict[str, str | None] = {}
def compose(self) -> ComposeResult:
self.initial_values = get_current_proxy_settings()
with Vertical(id="proxysetup-content"):
yield NoMarkupStatic("Proxy Configuration", classes="settings-title")
yield NoMarkupStatic("")
for key, description in SUPPORTED_PROXY_VARS.items():
yield Static(
f"[bold ansi_blue]{key}[/] [dim]{description}[/dim]",
classes="proxy-label-line",
)
initial_value = self.initial_values.get(key) or ""
input_widget = Input(
value=initial_value,
placeholder="NOT SET",
id=f"proxy-input-{key}",
classes="proxy-input",
)
self.inputs[key] = input_widget
yield input_widget
yield NoMarkupStatic("")
yield NoMarkupStatic(
"↑↓ navigate Enter save & exit ESC cancel", classes="settings-help"
)
def focus(self, scroll_visible: bool = True) -> ProxySetupApp:
"""Override focus to focus the first input widget."""
if self.inputs:
first_input = list(self.inputs.values())[0]
first_input.focus(scroll_visible=scroll_visible)
else:
super().focus(scroll_visible=scroll_visible)
return self
def action_focus_next(self) -> None:
inputs = list(self.inputs.values())
focused = self.screen.focused
if focused is not None and isinstance(focused, Input) and focused in inputs:
idx = inputs.index(focused)
next_idx = (idx + 1) % len(inputs)
inputs[next_idx].focus()
def action_focus_previous(self) -> None:
inputs = list(self.inputs.values())
focused = self.screen.focused
if focused is not None and isinstance(focused, Input) and focused in inputs:
idx = inputs.index(focused)
prev_idx = (idx - 1) % len(inputs)
inputs[prev_idx].focus()
def on_input_submitted(self, event: Input.Submitted) -> None:
self._save_and_close()
def on_blur(self, _event: events.Blur) -> None:
self.call_after_refresh(self._refocus_if_needed)
def on_input_blurred(self, _event: Input.Blurred) -> None:
self.call_after_refresh(self._refocus_if_needed)
def _refocus_if_needed(self) -> None:
if self.has_focus or any(inp.has_focus for inp in self.inputs.values()):
return
self.focus()
def _save_and_close(self) -> None:
try:
for key, input_widget in self.inputs.items():
new_value = input_widget.value.strip()
old_value = self.initial_values.get(key) or ""
if new_value != old_value:
if new_value:
set_proxy_var(key, new_value)
else:
unset_proxy_var(key)
except Exception as e:
self.post_message(self.ProxySetupClosed(saved=False, error=str(e)))
return
self.post_message(self.ProxySetupClosed(saved=True))
def action_close(self) -> None:
self.post_message(self.ProxySetupClosed(saved=False))