v2.2.0 (#395)
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:
parent
51fecc67d9
commit
ec7f3b25ea
107 changed files with 8002 additions and 535 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
127
vibe/cli/textual_ui/widgets/proxy_setup_app.py
Normal file
127
vibe/cli/textual_ui/widgets/proxy_setup_app.py
Normal 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))
|
||||
Loading…
Add table
Add a link
Reference in a new issue