vibe/vibe/setup/onboarding/screens/api_key.py
Clément Drouin e1a25caa52
v2.7.5 (#589)
Co-authored-by: Bastien <bastien.baret@gmail.com>
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Julien Legrand <72564015+JulienLGRD@users.noreply.github.com>
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com>
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-04-14 10:33:15 +02:00

165 lines
5.8 KiB
Python

from __future__ import annotations
import os
from typing import ClassVar
from dotenv import set_key
from textual.app import ComposeResult
from textual.binding import Binding, BindingType
from textual.containers import Center, Horizontal, Vertical
from textual.events import MouseUp
from textual.validation import Length
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 DEFAULT_PROVIDERS, ProviderConfig, VibeConfig
from vibe.core.paths import GLOBAL_ENV_FILE
from vibe.core.telemetry.send import TelemetryClient
from vibe.core.types import Backend
from vibe.setup.onboarding.base import OnboardingScreen
from vibe.setup.onboarding.context import OnboardingContext
PROVIDER_HELP = {
"mistral": ("https://console.mistral.ai/codestral/cli", "Mistral AI Studio")
}
CONFIG_DOCS_URL = (
"https://github.com/mistralai/mistral-vibe?tab=readme-ov-file#configuration"
)
def _save_api_key_to_env_file(env_key: str, api_key: str) -> None:
GLOBAL_ENV_FILE.path.parent.mkdir(parents=True, exist_ok=True)
set_key(GLOBAL_ENV_FILE.path, env_key, api_key)
def persist_api_key(provider: ProviderConfig, api_key: str) -> str:
env_key = provider.api_key_env_var
if not env_key:
return "env_var_error:<empty>"
try:
os.environ[env_key] = api_key
except ValueError:
return f"env_var_error:{env_key}"
try:
_save_api_key_to_env_file(env_key, api_key)
except (OSError, ValueError) as err:
return f"save_error:{err}"
if provider.backend == Backend.MISTRAL:
try:
telemetry = TelemetryClient(config_getter=VibeConfig)
telemetry.send_onboarding_api_key_added()
except Exception:
pass
return "completed"
def _get_mistral_provider() -> ProviderConfig:
return next(
provider for provider in DEFAULT_PROVIDERS if provider.name == "mistral"
)
def _resolve_onboarding_provider(
provider: ProviderConfig | None = None,
) -> ProviderConfig:
resolved_provider = provider or OnboardingContext.load().provider
if resolved_provider.api_key_env_var:
return resolved_provider
return _get_mistral_provider()
class ApiKeyScreen(OnboardingScreen):
BINDINGS: ClassVar[list[BindingType]] = [
Binding("ctrl+c", "cancel", "Cancel", show=False),
Binding("escape", "cancel", "Cancel", show=False),
]
NEXT_SCREEN = None
def __init__(self, provider: ProviderConfig | None = None) -> None:
super().__init__()
self.provider = _resolve_onboarding_provider(provider)
def _compose_provider_link(self, provider_name: str) -> ComposeResult:
if self.provider.name not in PROVIDER_HELP:
return
help_url, help_name = PROVIDER_HELP[self.provider.name]
yield NoMarkupStatic(f"Grab your {provider_name} API key from the {help_name}:")
yield Center(
Horizontal(
NoMarkupStatic("", classes="link-chevron"),
Link(help_url, url=help_url),
classes="link-row",
)
)
def _compose_config_docs(self) -> ComposeResult:
yield Static("[dim]Learn more about Vibe configuration:[/]")
yield Horizontal(
NoMarkupStatic("", classes="link-chevron"),
Link(CONFIG_DOCS_URL, url=CONFIG_DOCS_URL),
classes="link-row",
)
def compose(self) -> ComposeResult:
provider_name = self.provider.name.capitalize()
self.input_widget = Input(
password=True,
id="key",
placeholder="Paste your API key here",
validators=[Length(minimum=1, failure_description="No API key provided.")],
)
with Vertical(id="api-key-outer"):
yield NoMarkupStatic("", classes="spacer")
yield Center(NoMarkupStatic("One last thing...", id="api-key-title"))
with Center():
with Vertical(id="api-key-content"):
yield from self._compose_provider_link(provider_name)
yield NoMarkupStatic(
"...and paste it below to finish the setup:", id="paste-hint"
)
yield Center(Horizontal(self.input_widget, id="input-box"))
yield NoMarkupStatic("", id="feedback")
yield NoMarkupStatic("", classes="spacer")
yield Vertical(
Vertical(*self._compose_config_docs(), id="config-docs-group"),
id="config-docs-section",
)
def on_mount(self) -> None:
self.input_widget.focus()
def on_input_changed(self, event: Input.Changed) -> None:
feedback = self.query_one("#feedback", NoMarkupStatic)
input_box = self.query_one("#input-box")
if event.validation_result is None:
return
input_box.remove_class("valid", "invalid")
feedback.remove_class("error", "success")
if event.validation_result.is_valid:
feedback.update("Press Enter to submit ↵")
feedback.add_class("success")
input_box.add_class("valid")
return
descriptions = event.validation_result.failure_descriptions
feedback.update(descriptions[0])
feedback.add_class("error")
input_box.add_class("invalid")
def on_input_submitted(self, event: Input.Submitted) -> None:
if event.validation_result and event.validation_result.is_valid:
self._save_and_finish(event.value)
def _save_and_finish(self, api_key: str) -> None:
self.app.exit(persist_api_key(self.provider, api_key))
def on_mouse_up(self, event: MouseUp) -> None:
copy_selection_to_clipboard(self.app)