Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai>
Co-authored-by: Michel Thomazo <michel.thomazo@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Vincent Guilloux <vincent.guilloux@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: Nicolas Karolak <nicolas@karolak.fr>
This commit is contained in:
Mathias Gesbert 2026-02-11 18:17:30 +01:00 committed by GitHub
parent 9809cfc831
commit 51fecc67d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
176 changed files with 8652 additions and 4451 deletions

View file

@ -4,34 +4,18 @@ import sys
from rich import print as rprint
from textual.app import App
from textual.theme import Theme
from vibe.cli.textual_ui.terminal_theme import (
TERMINAL_THEME_NAME,
capture_terminal_theme,
)
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE
from vibe.setup.onboarding.screens import (
ApiKeyScreen,
ThemeSelectionScreen,
WelcomeScreen,
)
from vibe.setup.onboarding.screens import ApiKeyScreen, WelcomeScreen
class OnboardingApp(App[str | None]):
CSS_PATH = "onboarding.tcss"
def __init__(self) -> None:
super().__init__()
self._terminal_theme: Theme | None = capture_terminal_theme()
def on_mount(self) -> None:
if self._terminal_theme:
self.register_theme(self._terminal_theme)
self.theme = TERMINAL_THEME_NAME
self.theme = "textual-ansi"
self.install_screen(WelcomeScreen(), "welcome")
self.install_screen(ThemeSelectionScreen(), "theme_selection")
self.install_screen(ApiKeyScreen(), "api_key")
self.push_screen("welcome")

View file

@ -27,7 +27,7 @@ OnboardingScreen {
}
WelcomeScreen #enter-hint {
color: $text-muted;
color: ansi_bright_black;
min-width: 16;
text-align: center;
}
@ -36,102 +36,6 @@ WelcomeScreen #enter-hint.hidden {
visibility: hidden;
}
/* =============================================================================
Theme Selection Screen
============================================================================= */
#theme-outer {
width: 100%;
height: 100%;
align: center middle;
overflow-y: auto;
}
#theme-content {
width: auto;
height: auto;
padding: 1 0;
}
#theme-title {
text-align: center;
width: 100%;
margin-bottom: 2;
}
#theme-row {
width: auto;
height: auto;
}
#nav-hint {
color: $text-muted;
width: 16;
height: 100%;
content-align: right middle;
padding-right: 2;
}
#theme-list {
width: 24;
height: auto;
}
.theme-item {
text-align: center;
width: 100%;
}
.theme-item.selected {
color: $text;
text-style: bold;
border: round #555555;
padding: 0 2;
}
.theme-item.fade-1 {
text-opacity: 50%;
}
.theme-item.fade-2 {
text-opacity: 25%;
}
.theme-item.fade-3 {
text-opacity: 10%;
}
ThemeSelectionScreen #enter-hint {
color: $text-muted;
width: 16;
height: 100%;
content-align: left middle;
padding-left: 2;
}
#preview-center {
width: 100%;
height: auto;
margin-top: 2;
align: center middle;
}
#preview {
min-width: 50;
max-width: 70;
height: auto;
max-height: 100%;
overflow: auto;
border: round #555555;
border-title-align: center;
}
#preview-inner {
width: 100%;
height: auto;
padding: 0 1 0 1;
}
/* =============================================================================
API Key Screen
============================================================================= */
@ -177,11 +81,11 @@ ThemeSelectionScreen #enter-hint {
}
#input-box.valid {
border: round $success;
border: round ansi_green;
}
#input-box.invalid {
border: round $error;
border: round ansi_red;
}
#key {
@ -202,11 +106,11 @@ ThemeSelectionScreen #enter-hint {
}
#feedback.error {
color: $error;
color: ansi_red;
}
#feedback.success {
color: $text-muted;
color: ansi_bright_black;
}
#config-docs-section {
@ -222,7 +126,7 @@ ThemeSelectionScreen #enter-hint {
}
#config-docs-group Static {
color: $text-muted;
color: ansi_bright_black;
}
#config-docs-group .link-row {

View file

@ -1,7 +1,6 @@
from __future__ import annotations
from vibe.setup.onboarding.screens.api_key import ApiKeyScreen
from vibe.setup.onboarding.screens.theme_selection import ThemeSelectionScreen
from vibe.setup.onboarding.screens.welcome import WelcomeScreen
__all__ = ["ApiKeyScreen", "ThemeSelectionScreen", "WelcomeScreen"]
__all__ = ["ApiKeyScreen", "WelcomeScreen"]

View file

@ -1,164 +0,0 @@
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar
from textual.app import ComposeResult
from textual.binding import Binding, BindingType
from textual.containers import Center, Container, Horizontal, Vertical
from textual.events import Resize
from textual.theme import BUILTIN_THEMES
from textual.widgets import Markdown, Static
from vibe.cli.textual_ui.terminal_theme import TERMINAL_THEME_NAME
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
from vibe.core.config import VibeConfig
from vibe.setup.onboarding.base import OnboardingScreen
if TYPE_CHECKING:
from vibe.setup.onboarding import OnboardingApp
THEMES = [TERMINAL_THEME_NAME] + sorted(
k for k in BUILTIN_THEMES if k != "textual-ansi"
)
VISIBLE_NEIGHBORS = 3
FADE_CLASSES = ["fade-1", "fade-2", "fade-3"]
PREVIEW_MARKDOWN = """
### Heading
**Bold**, *italic*, and `inline code`.
- Bullet point
- Another bullet point
1. First item
2. Second item
```python
def greet(name: str = "World") -> str:
return f"Hello, {name}!"
```
> Blockquote
---
| Column 1 | Column 2 |
|----------|----------|
| Item 1 | Item 2 |
"""
class ThemeSelectionScreen(OnboardingScreen):
BINDINGS: ClassVar[list[BindingType]] = [
Binding("enter", "next", "Next", show=False, priority=True),
Binding("up", "prev_theme", "Previous", show=False),
Binding("down", "next_theme", "Next Theme", show=False),
Binding("ctrl+c", "cancel", "Cancel", show=False),
Binding("escape", "cancel", "Cancel", show=False),
]
NEXT_SCREEN = "api_key"
def __init__(self) -> None:
super().__init__()
self._theme_index = 0
self._theme_widgets: list[Static] = []
def _compose_theme_list(self) -> ComposeResult:
for _ in range(VISIBLE_NEIGHBORS * 2 + 1):
widget = NoMarkupStatic("", classes="theme-item")
self._theme_widgets.append(widget)
yield widget
def compose(self) -> ComposeResult:
with Center(id="theme-outer"):
with Vertical(id="theme-content"):
yield NoMarkupStatic("Select your preferred theme", id="theme-title")
yield Center(
Horizontal(
NoMarkupStatic("Navigate ↑ ↓", id="nav-hint"),
Vertical(*self._compose_theme_list(), id="theme-list"),
NoMarkupStatic("Press Enter \u21b5", id="enter-hint"),
id="theme-row",
)
)
with Container(id="preview-center"):
preview = Container(id="preview")
preview.border_title = "Preview"
with preview:
yield Container(Markdown(PREVIEW_MARKDOWN), id="preview-inner")
@property
def _has_terminal_theme(self) -> bool:
app: OnboardingApp = self.app # type: ignore[assignment]
return app._terminal_theme is not None
@property
def _available_themes(self) -> list[str]:
if self._has_terminal_theme:
return THEMES
return [t for t in THEMES if t != TERMINAL_THEME_NAME]
def on_mount(self) -> None:
current_theme = self.app.theme
themes = self._available_themes
if current_theme == TERMINAL_THEME_NAME:
self._theme_index = 0
elif current_theme in themes:
self._theme_index = themes.index(current_theme)
self._update_display()
self._update_preview_height()
self.focus()
def on_resize(self, _: Resize) -> None:
self._update_preview_height()
def _update_preview_height(self) -> None:
# Height is dynamically set because css won't allow filling available space and page scroll on overflow.
preview = self.query_one("#preview", Container)
header_height = 17 # title + margins + theme row + padding + buffer
available = self.app.size.height - header_height
preview.styles.max_height = max(10, available)
def _get_theme_at_offset(self, offset: int) -> str:
themes = self._available_themes
index = (self._theme_index + offset) % len(themes)
return themes[index]
def _update_display(self) -> None:
for i, widget in enumerate(self._theme_widgets):
offset = i - VISIBLE_NEIGHBORS
theme = self._get_theme_at_offset(offset)
widget.remove_class("selected", *FADE_CLASSES)
if offset == 0:
widget.update(f" {theme} ")
widget.add_class("selected")
else:
distance = min(abs(offset) - 1, len(FADE_CLASSES) - 1)
widget.update(theme)
widget.add_class(FADE_CLASSES[distance])
def _navigate(self, direction: int) -> None:
themes = self._available_themes
self._theme_index = (self._theme_index + direction) % len(themes)
theme = themes[self._theme_index]
self.app.theme = theme
self._update_display()
def action_next_theme(self) -> None:
self._navigate(1)
def action_prev_theme(self) -> None:
self._navigate(-1)
def action_next(self) -> None:
theme = self._available_themes[self._theme_index]
try:
VibeConfig.save_updates({"textual_theme": theme})
except OSError:
pass
super().action_next()

View file

@ -50,7 +50,7 @@ class WelcomeScreen(OnboardingScreen):
Binding("escape", "cancel", "Cancel", show=False),
]
NEXT_SCREEN = "theme_selection"
NEXT_SCREEN = "api_key"
def __init__(self) -> None:
super().__init__()

View file

@ -1,7 +1,6 @@
from __future__ import annotations
from pathlib import Path
import tomllib
from typing import Any, ClassVar
from textual import events
@ -11,12 +10,8 @@ from textual.containers import CenterMiddle, Horizontal
from textual.message import Message
from textual.widgets import Static
from vibe.cli.textual_ui.terminal_theme import (
TERMINAL_THEME_NAME,
capture_terminal_theme,
)
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
from vibe.core.paths.global_paths import GLOBAL_CONFIG_FILE, TRUSTED_FOLDERS_FILE
from vibe.core.paths.global_paths import TRUSTED_FOLDERS_FILE
class TrustDialogQuitException(Exception):
@ -154,32 +149,9 @@ class TrustFolderApp(App):
self.folder_path = folder_path
self._result: bool | None = None
self._quit_without_saving = False
self._terminal_theme = capture_terminal_theme()
self._load_theme()
def _load_theme(self) -> None:
if self._terminal_theme:
self.register_theme(self._terminal_theme)
config_file = GLOBAL_CONFIG_FILE.path
if not config_file.is_file():
return
try:
with config_file.open("rb") as f:
config_data = tomllib.load(f)
except (OSError, tomllib.TOMLDecodeError):
return
textual_theme = config_data.get("textual_theme")
if not textual_theme:
return
if textual_theme == TERMINAL_THEME_NAME:
if self._terminal_theme:
self.theme = TERMINAL_THEME_NAME
else:
self.theme = textual_theme
def on_mount(self) -> None:
self.theme = "textual-ansi"
def compose(self) -> ComposeResult:
yield TrustFolderDialog(self.folder_path)

View file

@ -1,6 +1,6 @@
Screen {
align: center middle;
background: $background 80%;
background: transparent 80%;
}
#trust-dialog {
@ -8,8 +8,8 @@ Screen {
max-width: 90%;
min-width: 50;
height: auto;
border: round $foreground-muted;
background: $background;
border: round ansi_bright_black;
background: transparent;
padding: 1;
}
@ -17,7 +17,7 @@ Screen {
width: 100%;
height: auto;
text-style: bold;
color: $warning;
color: ansi_yellow;
text-align: center;
margin-bottom: 1;
}
@ -25,7 +25,7 @@ Screen {
#trust-dialog-path {
width: 100%;
height: auto;
color: $primary;
color: ansi_blue;
text-align: center;
text-wrap: wrap;
margin-bottom: 1;
@ -34,7 +34,7 @@ Screen {
#trust-dialog-message {
width: 100%;
height: auto;
color: $foreground;
color: ansi_default;
text-align: center;
text-wrap: wrap;
margin-bottom: 1;
@ -50,24 +50,24 @@ Screen {
.trust-option {
height: auto;
width: auto;
color: $foreground;
color: ansi_default;
margin: 0 3;
content-align: center middle;
}
.trust-cursor-selected {
color: $foreground;
color: ansi_default;
text-style: bold;
}
.trust-option-selected {
color: $foreground;
color: ansi_default;
}
.trust-dialog-help {
width: 100%;
height: auto;
color: $text-muted;
color: ansi_bright_black;
text-align: center;
margin-bottom: 1;
}
@ -75,7 +75,7 @@ Screen {
.trust-dialog-save-info {
width: 100%;
height: auto;
color: $text-muted;
color: ansi_bright_black;
text-align: center;
text-style: italic;
text-wrap: wrap;