v2.11.0 (#717)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@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: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
f71bfd3b8c
commit
adb1ca74ce
202 changed files with 5828 additions and 1968 deletions
|
|
@ -174,6 +174,11 @@ class CommandRegistry:
|
|||
description="Show data retention information",
|
||||
handler="_show_data_retention",
|
||||
),
|
||||
"theme": Command(
|
||||
aliases=frozenset(["/theme"]),
|
||||
description="Select theme",
|
||||
handler="_show_theme",
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pygments.token import Token
|
||||
from textual.content import Content
|
||||
from textual.highlight import HighlightTheme, highlight
|
||||
from textual.widgets import Markdown
|
||||
from textual.widgets._markdown import MarkdownFence
|
||||
|
||||
|
||||
class AnsiHighlightTheme(HighlightTheme):
|
||||
STYLES = {
|
||||
Token.Comment: "ansi_bright_black italic",
|
||||
Token.Error: "ansi_red",
|
||||
Token.Generic.Strong: "bold",
|
||||
Token.Generic.Emph: "italic",
|
||||
Token.Generic.Error: "ansi_red",
|
||||
Token.Generic.Heading: "ansi_blue underline",
|
||||
Token.Generic.Subheading: "ansi_blue",
|
||||
Token.Keyword: "ansi_magenta",
|
||||
Token.Keyword.Constant: "ansi_cyan",
|
||||
Token.Keyword.Namespace: "ansi_magenta",
|
||||
Token.Keyword.Type: "ansi_cyan",
|
||||
Token.Literal.Number: "ansi_yellow",
|
||||
Token.Literal.String.Backtick: "ansi_bright_black",
|
||||
Token.Literal.String: "ansi_green",
|
||||
Token.Literal.String.Doc: "ansi_green italic",
|
||||
Token.Literal.String.Double: "ansi_green",
|
||||
Token.Name: "ansi_default",
|
||||
Token.Name.Attribute: "ansi_yellow",
|
||||
Token.Name.Builtin: "ansi_cyan",
|
||||
Token.Name.Builtin.Pseudo: "italic",
|
||||
Token.Name.Class: "ansi_yellow",
|
||||
Token.Name.Constant: "ansi_red",
|
||||
Token.Name.Decorator: "ansi_blue",
|
||||
Token.Name.Function: "ansi_blue",
|
||||
Token.Name.Function.Magic: "ansi_blue",
|
||||
Token.Name.Tag: "ansi_blue",
|
||||
Token.Name.Variable: "ansi_default",
|
||||
Token.Number: "ansi_yellow",
|
||||
Token.Operator: "ansi_default",
|
||||
Token.Operator.Word: "ansi_magenta",
|
||||
Token.String: "ansi_green",
|
||||
Token.Whitespace: "",
|
||||
}
|
||||
|
||||
|
||||
class AnsiMarkdownFence(MarkdownFence):
|
||||
@classmethod
|
||||
def highlight(cls, code: str, language: str) -> Content:
|
||||
return highlight(code, language=language or None, theme=AnsiHighlightTheme)
|
||||
|
||||
|
||||
class AnsiMarkdown(Markdown):
|
||||
BLOCKS = {
|
||||
**Markdown.BLOCKS,
|
||||
"fence": AnsiMarkdownFence,
|
||||
"code_block": AnsiMarkdownFence,
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ from textual.binding import Binding, BindingType
|
|||
from textual.containers import Horizontal, VerticalGroup, VerticalScroll
|
||||
from textual.driver import Driver
|
||||
from textual.events import AppBlur, AppFocus, MouseUp
|
||||
from textual.theme import BUILTIN_THEMES
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static
|
||||
|
||||
|
|
@ -91,6 +92,7 @@ from vibe.cli.textual_ui.widgets.question_app import QuestionApp
|
|||
from vibe.cli.textual_ui.widgets.rewind_app import RewindApp
|
||||
from vibe.cli.textual_ui.widgets.session_picker import SessionPickerApp
|
||||
from vibe.cli.textual_ui.widgets.teleport_message import TeleportMessage
|
||||
from vibe.cli.textual_ui.widgets.theme_picker import ThemePickerApp, sorted_theme_names
|
||||
from vibe.cli.textual_ui.widgets.thinking_picker import ThinkingPickerApp
|
||||
from vibe.cli.textual_ui.widgets.tools import ToolResultMessage
|
||||
from vibe.cli.textual_ui.widgets.voice_app import VoiceApp
|
||||
|
|
@ -128,7 +130,7 @@ from vibe.core.autocompletion.path_prompt import (
|
|||
build_title_segments,
|
||||
)
|
||||
from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.config import DEFAULT_THEME, VibeConfig
|
||||
from vibe.core.data_retention import DATA_RETENTION_MESSAGE
|
||||
from vibe.core.hooks.models import HookStartEvent
|
||||
from vibe.core.log_reader import LogReader
|
||||
|
|
@ -185,17 +187,18 @@ from vibe.core.utils import (
|
|||
)
|
||||
|
||||
|
||||
def _compute_connectors_count(
|
||||
def _compute_connector_counts(
|
||||
config: VibeConfig, connector_registry: ConnectorRegistry | None
|
||||
) -> int:
|
||||
) -> tuple[int, int]:
|
||||
total = connector_registry.connector_count if connector_registry else 0
|
||||
if total == 0:
|
||||
return 0
|
||||
return (0, 0)
|
||||
disabled_names = {c.name for c in config.connectors if c.disabled}
|
||||
known_names = set(
|
||||
connector_registry.get_connector_names() if connector_registry else []
|
||||
)
|
||||
return total - len(disabled_names & known_names)
|
||||
enabled = total - len(disabled_names & known_names)
|
||||
return (enabled, total)
|
||||
|
||||
|
||||
class BottomApp(StrEnum):
|
||||
|
|
@ -214,6 +217,7 @@ class BottomApp(StrEnum):
|
|||
ModelPicker = auto()
|
||||
ProxySetup = auto()
|
||||
Question = auto()
|
||||
ThemePicker = auto()
|
||||
ThinkingPicker = auto()
|
||||
Rewind = auto()
|
||||
SessionPicker = auto()
|
||||
|
|
@ -464,13 +468,14 @@ class VibeApp(App): # noqa: PLR0904
|
|||
|
||||
def compose(self) -> ComposeResult:
|
||||
with ChatScroll(id="chat"):
|
||||
connectors_enabled, connectors_total = _compute_connector_counts(
|
||||
self.config, self.agent_loop.connector_registry
|
||||
)
|
||||
self._banner = Banner(
|
||||
config=self.config,
|
||||
skill_manager=self.agent_loop.skill_manager,
|
||||
mcp_registry=self.agent_loop.mcp_registry,
|
||||
connectors_count=_compute_connectors_count(
|
||||
self.config, self.agent_loop.connector_registry
|
||||
),
|
||||
connectors_enabled=connectors_enabled,
|
||||
connectors_total=connectors_total,
|
||||
)
|
||||
yield self._banner
|
||||
yield VerticalGroup(id="messages")
|
||||
|
|
@ -498,7 +503,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
yield ContextProgress()
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
self.theme = "textual-ansi"
|
||||
self._apply_theme(self.config.theme)
|
||||
self._terminal_notifier.restore()
|
||||
|
||||
self._cached_messages_area = self.query_one("#messages")
|
||||
|
|
@ -853,6 +858,25 @@ class VibeApp(App): # noqa: PLR0904
|
|||
) -> None:
|
||||
await self._switch_to_input_app()
|
||||
|
||||
async def on_theme_picker_app_theme_previewed(
|
||||
self, message: ThemePickerApp.ThemePreviewed
|
||||
) -> None:
|
||||
self._apply_theme(message.theme)
|
||||
|
||||
async def on_theme_picker_app_theme_selected(
|
||||
self, message: ThemePickerApp.ThemeSelected
|
||||
) -> None:
|
||||
self._apply_theme(message.theme)
|
||||
self.config.theme = message.theme
|
||||
VibeConfig.save_updates({"theme": message.theme})
|
||||
await self._switch_to_input_app()
|
||||
|
||||
async def on_theme_picker_app_cancelled(
|
||||
self, message: ThemePickerApp.Cancelled
|
||||
) -> None:
|
||||
self._apply_theme(message.original_theme)
|
||||
await self._switch_to_input_app()
|
||||
|
||||
async def on_mcpapp_mcpclosed(self, _message: MCPApp.MCPClosed) -> None:
|
||||
await self._mount_and_scroll(UserCommandMessage("MCP servers closed."))
|
||||
await self._switch_to_input_app()
|
||||
|
|
@ -1751,6 +1775,11 @@ class VibeApp(App): # noqa: PLR0904
|
|||
return
|
||||
await self._switch_to_thinking_picker_app()
|
||||
|
||||
async def _show_theme(self, **kwargs: Any) -> None:
|
||||
if self._current_bottom_app == BottomApp.ThemePicker:
|
||||
return
|
||||
await self._switch_to_theme_picker_app()
|
||||
|
||||
async def _show_proxy_setup(self, **kwargs: Any) -> None:
|
||||
if self._current_bottom_app == BottomApp.ProxySetup:
|
||||
return
|
||||
|
|
@ -2017,13 +2046,14 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self._narrator_manager.sync()
|
||||
|
||||
if self._banner:
|
||||
ce, ct = _compute_connector_counts(
|
||||
base_config, self.agent_loop.connector_registry
|
||||
)
|
||||
self._banner.set_state(
|
||||
base_config,
|
||||
self.agent_loop.skill_manager,
|
||||
self.agent_loop.mcp_registry,
|
||||
connectors_count=_compute_connectors_count(
|
||||
base_config, self.agent_loop.connector_registry
|
||||
),
|
||||
connectors_enabled=ce,
|
||||
connectors_total=ct,
|
||||
plan_description=plan_title(self._plan_info),
|
||||
)
|
||||
await self._mount_and_scroll(
|
||||
|
|
@ -2287,6 +2317,23 @@ class VibeApp(App): # noqa: PLR0904
|
|||
)
|
||||
)
|
||||
|
||||
async def _switch_to_theme_picker_app(self) -> None:
|
||||
if self._current_bottom_app == BottomApp.ThemePicker:
|
||||
return
|
||||
|
||||
await self._switch_from_input(
|
||||
ThemePickerApp(
|
||||
theme_names=sorted_theme_names(), current_theme=self.config.theme
|
||||
)
|
||||
)
|
||||
|
||||
def _apply_theme(self, theme: str) -> None:
|
||||
if theme not in BUILTIN_THEMES:
|
||||
logger.warning("Unknown theme=%s; falling back to %s", theme, DEFAULT_THEME)
|
||||
self.theme = DEFAULT_THEME
|
||||
return
|
||||
self.theme = theme
|
||||
|
||||
async def _switch_to_proxy_setup_app(self) -> None:
|
||||
if self._current_bottom_app == BottomApp.ProxySetup:
|
||||
return
|
||||
|
|
@ -2340,6 +2387,8 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self.query_one(ConfigApp).focus()
|
||||
case BottomApp.ModelPicker:
|
||||
self.query_one(ModelPickerApp).focus()
|
||||
case BottomApp.ThemePicker:
|
||||
self.query_one(ThemePickerApp).focus()
|
||||
case BottomApp.ThinkingPicker:
|
||||
self.query_one(ThinkingPickerApp).focus()
|
||||
case BottomApp.ProxySetup:
|
||||
|
|
@ -2411,6 +2460,16 @@ class VibeApp(App): # noqa: PLR0904
|
|||
pass
|
||||
self._last_escape_time = None
|
||||
|
||||
def _handle_theme_picker_app_escape(self) -> None:
|
||||
try:
|
||||
theme_picker = self.query_one(ThemePickerApp)
|
||||
theme_picker.post_message(
|
||||
ThemePickerApp.Cancelled(original_theme=self.config.theme)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
self._last_escape_time = None
|
||||
|
||||
def _handle_thinking_picker_app_escape(self) -> None:
|
||||
try:
|
||||
thinking_picker = self.query_one(ThinkingPickerApp)
|
||||
|
|
@ -2661,6 +2720,8 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self._handle_question_app_escape()
|
||||
elif self._current_bottom_app == BottomApp.ModelPicker:
|
||||
self._handle_model_picker_app_escape()
|
||||
elif self._current_bottom_app == BottomApp.ThemePicker:
|
||||
self._handle_theme_picker_app_escape()
|
||||
elif self._current_bottom_app == BottomApp.ThinkingPicker:
|
||||
self._handle_thinking_picker_app_escape()
|
||||
elif self._current_bottom_app == BottomApp.SessionPicker:
|
||||
|
|
@ -2781,13 +2842,14 @@ class VibeApp(App): # noqa: PLR0904
|
|||
|
||||
def _refresh_banner(self) -> None:
|
||||
if self._banner:
|
||||
ce, ct = _compute_connector_counts(
|
||||
self.config, self.agent_loop.connector_registry
|
||||
)
|
||||
self._banner.set_state(
|
||||
self.config,
|
||||
self.agent_loop.skill_manager,
|
||||
self.agent_loop.mcp_registry,
|
||||
connectors_count=_compute_connectors_count(
|
||||
self.config, self.agent_loop.connector_registry
|
||||
),
|
||||
connectors_enabled=ce,
|
||||
connectors_total=ct,
|
||||
plan_description=plan_title(self._plan_info),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
$mistral_orange: #FF8205;
|
||||
|
||||
* {
|
||||
scrollbar-color: ansi_black;
|
||||
scrollbar-color-hover: ansi_bright_black;
|
||||
scrollbar-color-active: ansi_bright_black;
|
||||
scrollbar-background: transparent;
|
||||
scrollbar-background-hover: transparent;
|
||||
scrollbar-background-active: transparent;
|
||||
scrollbar-size: 1 1;
|
||||
}
|
||||
|
||||
|
|
@ -20,7 +14,7 @@ Horizontal {
|
|||
}
|
||||
|
||||
TextArea > .text-area--cursor {
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
}
|
||||
|
||||
#chat {
|
||||
|
|
@ -54,6 +48,55 @@ TextArea > .text-area--cursor {
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
#queued-messages {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
padding: 0 0 1 0;
|
||||
}
|
||||
|
||||
#queued-messages-box {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
border: round $surface-lighten-2;
|
||||
border-title-color: $text-muted;
|
||||
border-title-style: bold;
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
#queued-messages-list {
|
||||
height: auto;
|
||||
max-height: 8;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#queued-messages-hint {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: $text-muted;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.queued-message-item {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
color: $text-muted;
|
||||
text-style: italic;
|
||||
}
|
||||
|
||||
.queued-message-bash {
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
.queued-message-content {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#bottom-bar {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
|
|
@ -89,14 +132,19 @@ TextArea > .text-area--cursor {
|
|||
display: none;
|
||||
width: auto;
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
background: $surface;
|
||||
border: solid ansi_bright_black;
|
||||
color: $foreground;
|
||||
background: $background;
|
||||
border: solid $foreground-muted;
|
||||
overflow-y: auto;
|
||||
scrollbar-size-vertical: 1;
|
||||
/* max-height, max-width and padding set in completion_popup.py */
|
||||
}
|
||||
|
||||
OptionList, OptionList:focus {
|
||||
background: $background;
|
||||
background-tint: transparent;
|
||||
}
|
||||
|
||||
#completion-popup _CompletionItem {
|
||||
height: auto;
|
||||
width: 1fr;
|
||||
|
|
@ -106,24 +154,24 @@ TextArea > .text-area--cursor {
|
|||
height: auto;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: solid ansi_bright_black;
|
||||
border: solid $foreground-muted;
|
||||
border-title-align: right;
|
||||
border-title-color: ansi_bright_black;
|
||||
border-title-color: $text-muted;
|
||||
padding: 0 1;
|
||||
|
||||
&.border-warning {
|
||||
border: solid ansi_yellow;
|
||||
border-title-color: ansi_yellow;
|
||||
border: solid $warning;
|
||||
border-title-color: $warning;
|
||||
}
|
||||
|
||||
&.border-safe {
|
||||
border: solid ansi_green;
|
||||
border-title-color: ansi_green;
|
||||
border: solid $success;
|
||||
border-title-color: $success;
|
||||
}
|
||||
|
||||
&.border-error {
|
||||
border: solid ansi_red;
|
||||
border-title-color: ansi_red;
|
||||
border: solid $error;
|
||||
border-title-color: $error;
|
||||
}
|
||||
|
||||
&.border-recording {
|
||||
|
|
@ -157,13 +205,17 @@ TextArea > .text-area--cursor {
|
|||
min-height: 3;
|
||||
max-height: 50vh;
|
||||
background: transparent;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
border: none;
|
||||
padding: 0;
|
||||
scrollbar-visibility: hidden;
|
||||
|
||||
&.recording {
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,10 +226,10 @@ ToastRack {
|
|||
}
|
||||
|
||||
Markdown {
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
|
||||
.code_inline {
|
||||
color: ansi_green;
|
||||
color: $success;
|
||||
background: transparent;
|
||||
text-style: bold;
|
||||
}
|
||||
|
|
@ -199,13 +251,13 @@ Markdown {
|
|||
|
||||
MarkdownBlockQuote {
|
||||
background: transparent;
|
||||
border-left: outer ansi_bright_black;
|
||||
border-left: outer $foreground-muted;
|
||||
}
|
||||
|
||||
MarkdownBullet,
|
||||
MarkdownBulletList,
|
||||
MarkdownOrderedList {
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -240,7 +292,6 @@ Markdown {
|
|||
text-style: bold;
|
||||
padding-left: 1;
|
||||
border-left: heavy $mistral_orange;
|
||||
color: $mistral_orange;
|
||||
}
|
||||
|
||||
.assistant-message {
|
||||
|
|
@ -277,11 +328,15 @@ Markdown {
|
|||
.reasoning-indicator {
|
||||
width: auto;
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
margin-right: 1;
|
||||
|
||||
&.success {
|
||||
color: ansi_green;
|
||||
color: $success;
|
||||
}
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -289,8 +344,12 @@ Markdown {
|
|||
.reasoning-triangle {
|
||||
width: auto;
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
text-style: italic;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.reasoning-triangle {
|
||||
|
|
@ -303,12 +362,20 @@ Markdown {
|
|||
height: auto;
|
||||
padding: 0 0 0 2;
|
||||
margin: 0;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
text-style: italic;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
|
||||
* {
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
text-style: italic;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
& > MarkdownBlock:last-child {
|
||||
|
|
@ -335,7 +402,7 @@ Markdown {
|
|||
.plan-file-wrapper {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
border: solid ansi_bright_black;
|
||||
border: solid $foreground-muted;
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
|
|
@ -372,21 +439,20 @@ Markdown {
|
|||
width: auto;
|
||||
height: 100%;
|
||||
padding: 0 1 0 2;
|
||||
color: ansi_bright_black;
|
||||
}
|
||||
|
||||
.interrupt-content {
|
||||
width: 1fr;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
color: ansi_yellow;
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
width: 1fr;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
color: ansi_red;
|
||||
color: $error;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
|
|
@ -394,7 +460,7 @@ Markdown {
|
|||
width: 1fr;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
color: ansi_yellow;
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
.user-command-content {
|
||||
|
|
@ -441,15 +507,15 @@ Markdown {
|
|||
}
|
||||
|
||||
&.bash-success {
|
||||
color: ansi_green;
|
||||
color: $success;
|
||||
}
|
||||
|
||||
&.bash-error {
|
||||
color: ansi_red;
|
||||
color: $error;
|
||||
}
|
||||
|
||||
&.bash-interrupted {
|
||||
color: ansi_yellow;
|
||||
color: $warning;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -468,7 +534,11 @@ Markdown {
|
|||
width: auto;
|
||||
height: 100%;
|
||||
padding: 0 1 0 2;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.bash-output {
|
||||
|
|
@ -478,7 +548,11 @@ Markdown {
|
|||
|
||||
.unknown-event {
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
StatusMessage {
|
||||
|
|
@ -494,22 +568,22 @@ StatusMessage {
|
|||
.status-indicator-icon {
|
||||
width: auto;
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
margin-right: 1;
|
||||
|
||||
&.success {
|
||||
color: ansi_green;
|
||||
color: $success;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: ansi_red;
|
||||
color: $error;
|
||||
}
|
||||
}
|
||||
|
||||
.status-indicator-text {
|
||||
width: 1fr;
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
}
|
||||
|
||||
.compact-message,
|
||||
|
|
@ -531,8 +605,12 @@ StatusMessage {
|
|||
.tool-stream-message {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
padding-left: 2;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-result {
|
||||
|
|
@ -542,14 +620,14 @@ StatusMessage {
|
|||
margin-left: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
|
||||
&.error-text {
|
||||
color: ansi_red;
|
||||
color: $error;
|
||||
}
|
||||
|
||||
&.warning-text {
|
||||
color: ansi_yellow;
|
||||
color: $warning;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -564,7 +642,11 @@ StatusMessage {
|
|||
width: auto;
|
||||
height: 100%;
|
||||
padding: 0 1 0 2;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-result-content {
|
||||
|
|
@ -577,7 +659,7 @@ StatusMessage {
|
|||
.tool-result-widget {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
}
|
||||
|
||||
.tool-result-widget Static {
|
||||
|
|
@ -605,7 +687,11 @@ StatusMessage {
|
|||
.tool-call-detail,
|
||||
.tool-result-detail {
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-result-detail {
|
||||
|
|
@ -613,64 +699,76 @@ StatusMessage {
|
|||
}
|
||||
|
||||
.tool-result-error {
|
||||
color: ansi_red;
|
||||
color: $error;
|
||||
}
|
||||
|
||||
.tool-result-warning {
|
||||
color: ansi_yellow;
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
.diff-header {
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
text-style: bold;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.diff-removed {
|
||||
height: auto;
|
||||
color: ansi_red;
|
||||
color: $error;
|
||||
}
|
||||
|
||||
.diff-added {
|
||||
height: auto;
|
||||
color: ansi_green;
|
||||
color: $success;
|
||||
}
|
||||
|
||||
.diff-range {
|
||||
height: auto;
|
||||
color: ansi_blue;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.diff-context {
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-empty,
|
||||
.todo-cancelled {
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-pending {
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
}
|
||||
|
||||
.todo-in_progress {
|
||||
height: auto;
|
||||
color: ansi_yellow;
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
.todo-completed {
|
||||
height: auto;
|
||||
color: ansi_green;
|
||||
color: $success;
|
||||
}
|
||||
|
||||
.loading-widget {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
|
|
@ -698,7 +796,11 @@ StatusMessage {
|
|||
.loading-hint {
|
||||
width: auto;
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-debounce {
|
||||
|
|
@ -724,7 +826,7 @@ StatusMessage {
|
|||
.history-load-more-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ansi_yellow;
|
||||
color: $warning;
|
||||
text-style: bold;
|
||||
pointer: pointer;
|
||||
padding: 1;
|
||||
|
|
@ -738,7 +840,7 @@ StatusMessage {
|
|||
width: 100%;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
border: solid ansi_bright_black;
|
||||
border: solid $foreground-muted;
|
||||
padding: 0 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -751,7 +853,7 @@ StatusMessage {
|
|||
.settings-title {
|
||||
height: auto;
|
||||
text-style: bold;
|
||||
color: ansi_blue;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
#config-options, #mcp-options, #connectorauth-options {
|
||||
|
|
@ -770,14 +872,18 @@ StatusMessage {
|
|||
|
||||
.settings-help {
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
#proxysetup-app {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
border: solid ansi_bright_black;
|
||||
border: solid $foreground-muted;
|
||||
padding: 0 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -795,7 +901,7 @@ StatusMessage {
|
|||
width: 100%;
|
||||
height: auto;
|
||||
border: none;
|
||||
border-left: wide ansi_bright_black;
|
||||
border-left: wide $foreground-muted;
|
||||
padding: 0 0 0 1;
|
||||
}
|
||||
|
||||
|
|
@ -804,7 +910,7 @@ StatusMessage {
|
|||
height: auto;
|
||||
max-height: 70vh;
|
||||
background: transparent;
|
||||
border: solid ansi_bright_black;
|
||||
border: solid $foreground-muted;
|
||||
padding: 0 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -829,7 +935,7 @@ StatusMessage {
|
|||
.approval-title {
|
||||
height: auto;
|
||||
text-style: bold;
|
||||
color: ansi_bright_yellow;
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
.approval-tool-info-container {
|
||||
|
|
@ -850,7 +956,7 @@ StatusMessage {
|
|||
|
||||
.approval-option {
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
margin-bottom: 1;
|
||||
|
||||
&:last-of-type {
|
||||
|
|
@ -860,39 +966,47 @@ StatusMessage {
|
|||
|
||||
.approval-cursor-selected {
|
||||
&.approval-option-yes {
|
||||
color: ansi_green;
|
||||
color: $success;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
&.approval-option-no {
|
||||
color: ansi_bright_red;
|
||||
color: $error;
|
||||
text-style: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.approval-option-selected {
|
||||
&.approval-option-yes {
|
||||
color: ansi_white;
|
||||
color: $foreground;
|
||||
}
|
||||
|
||||
&.approval-option-no {
|
||||
color: ansi_red;
|
||||
color: $error;
|
||||
}
|
||||
}
|
||||
|
||||
.approval-help {
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.approval-description {
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.code-block {
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
background: transparent;
|
||||
padding: 1;
|
||||
}
|
||||
|
|
@ -902,7 +1016,7 @@ StatusMessage {
|
|||
height: auto;
|
||||
max-height: 70vh;
|
||||
background: transparent;
|
||||
border: solid ansi_bright_black;
|
||||
border: solid $foreground-muted;
|
||||
padding: 0 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -914,24 +1028,24 @@ StatusMessage {
|
|||
|
||||
.question-tabs {
|
||||
height: auto;
|
||||
color: ansi_blue;
|
||||
color: $primary;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
.question-title {
|
||||
height: auto;
|
||||
text-style: bold;
|
||||
color: ansi_blue;
|
||||
color: $primary;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
.question-option {
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
}
|
||||
|
||||
.question-option-selected {
|
||||
color: ansi_blue;
|
||||
color: $primary;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
|
|
@ -943,7 +1057,7 @@ StatusMessage {
|
|||
.question-other-prefix {
|
||||
width: auto;
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
}
|
||||
|
||||
.question-other-input {
|
||||
|
|
@ -957,26 +1071,38 @@ StatusMessage {
|
|||
.question-other-static {
|
||||
width: auto;
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.question-submit {
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
.question-footer-note {
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
text-style: italic;
|
||||
margin-top: 1;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.question-help {
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
margin-top: 1;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
ExpandingBorder {
|
||||
|
|
@ -992,7 +1118,11 @@ ContextProgress {
|
|||
background: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
NarratorStatus {
|
||||
|
|
@ -1037,12 +1167,12 @@ NarratorStatus {
|
|||
}
|
||||
|
||||
.banner-meta {
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#banner-model {
|
||||
color: ansi_cyan;
|
||||
color: $secondary;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
|
@ -1055,12 +1185,12 @@ NarratorStatus {
|
|||
}
|
||||
|
||||
.banner-cmd {
|
||||
color: ansi_cyan;
|
||||
color: $secondary;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.petit-chat {
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
width: 12;
|
||||
}
|
||||
|
||||
|
|
@ -1105,7 +1235,7 @@ NarratorStatus {
|
|||
width: 100%;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
border: solid ansi_bright_black;
|
||||
border: solid $foreground-muted;
|
||||
padding: 0 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -1130,8 +1260,12 @@ NarratorStatus {
|
|||
.sessionpicker-help {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
margin-top: 1;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
/* Debug Console */
|
||||
|
|
@ -1143,14 +1277,14 @@ NarratorStatus {
|
|||
min-width: 40;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
border-left: solid ansi_bright_black;
|
||||
border-left: solid $foreground-muted;
|
||||
padding: 0 1;
|
||||
}
|
||||
|
||||
#debug-console-header {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
padding: 0 0 1 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
|
@ -1167,7 +1301,7 @@ NarratorStatus {
|
|||
width: 100%;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
border: solid ansi_bright_black;
|
||||
border: solid $foreground-muted;
|
||||
padding: 0 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -1180,7 +1314,7 @@ NarratorStatus {
|
|||
.modelpicker-title {
|
||||
height: auto;
|
||||
text-style: bold;
|
||||
color: ansi_blue;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
#modelpicker-options {
|
||||
|
|
@ -1198,15 +1332,19 @@ NarratorStatus {
|
|||
.modelpicker-help {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
margin-top: 1;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
#thinkingpicker-app {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
border: solid ansi_bright_black;
|
||||
border: solid $foreground-muted;
|
||||
padding: 0 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -1219,7 +1357,7 @@ NarratorStatus {
|
|||
.thinkingpicker-title {
|
||||
height: auto;
|
||||
text-style: bold;
|
||||
color: ansi_blue;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
#thinkingpicker-options {
|
||||
|
|
@ -1237,8 +1375,55 @@ NarratorStatus {
|
|||
.thinkingpicker-help {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
margin-top: 1;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
#themepicker-app {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
border: solid $foreground-muted;
|
||||
padding: 0 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#themepicker-content {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.themepicker-title {
|
||||
height: auto;
|
||||
text-style: bold;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
#themepicker-options {
|
||||
width: 100%;
|
||||
max-height: 50vh;
|
||||
text-wrap: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#themepicker-options:focus {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.themepicker-help {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: $text-muted;
|
||||
margin-top: 1;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
FeedbackBar {
|
||||
|
|
@ -1257,7 +1442,7 @@ FeedbackBar {
|
|||
width: 100%;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
border: solid ansi_bright_black;
|
||||
border: solid $foreground-muted;
|
||||
padding: 0 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -1270,32 +1455,36 @@ FeedbackBar {
|
|||
.rewind-title {
|
||||
height: auto;
|
||||
text-style: bold;
|
||||
color: ansi_blue;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.rewind-option {
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
}
|
||||
|
||||
.rewind-cursor-selected {
|
||||
color: ansi_blue;
|
||||
color: $primary;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
.rewind-option-unselected {
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
}
|
||||
|
||||
.rewind-help {
|
||||
height: auto;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
#feedback-text {
|
||||
width: auto;
|
||||
height: auto;
|
||||
color: ansi_default;
|
||||
color: $foreground;
|
||||
}
|
||||
|
||||
.hook-system-message {
|
||||
|
|
@ -1333,17 +1522,21 @@ FeedbackBar {
|
|||
height: auto;
|
||||
max-height: 3;
|
||||
overflow: hidden;
|
||||
color: ansi_bright_black;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.hook-severity-ok .hook-system-icon {
|
||||
color: ansi_green;
|
||||
color: $success;
|
||||
}
|
||||
|
||||
.hook-severity-warning .hook-system-icon {
|
||||
color: ansi_yellow;
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
.hook-severity-error .hook-system-icon {
|
||||
color: ansi_red;
|
||||
color: $error;
|
||||
}
|
||||
|
|
|
|||
85
vibe/cli/textual_ui/message_queue.py
Normal file
85
vibe/cli/textual_ui/message_queue.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from enum import StrEnum, auto
|
||||
|
||||
|
||||
class QueuedItemKind(StrEnum):
|
||||
PROMPT = auto()
|
||||
BASH = auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class QueuedItem:
|
||||
kind: QueuedItemKind
|
||||
content: str
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class MessageQueue:
|
||||
_items: list[QueuedItem] = field(default_factory=list)
|
||||
_paused: bool = False
|
||||
_on_change: Callable[[], None] | None = None
|
||||
|
||||
def set_change_listener(self, listener: Callable[[], None] | None) -> None:
|
||||
self._on_change = listener
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._items)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self._items)
|
||||
|
||||
@property
|
||||
def items(self) -> list[QueuedItem]:
|
||||
return list(self._items)
|
||||
|
||||
@property
|
||||
def paused(self) -> bool:
|
||||
return self._paused
|
||||
|
||||
def append_prompt(self, content: str) -> None:
|
||||
self._items.append(QueuedItem(QueuedItemKind.PROMPT, content))
|
||||
self._notify()
|
||||
|
||||
def append_bash(self, content: str) -> None:
|
||||
self._items.append(QueuedItem(QueuedItemKind.BASH, content))
|
||||
self._notify()
|
||||
|
||||
def pop_last(self) -> QueuedItem | None:
|
||||
if not self._items:
|
||||
return None
|
||||
item = self._items.pop()
|
||||
self._notify()
|
||||
return item
|
||||
|
||||
def pop_first(self) -> QueuedItem | None:
|
||||
if not self._items:
|
||||
return None
|
||||
item = self._items.pop(0)
|
||||
self._notify()
|
||||
return item
|
||||
|
||||
def pause(self) -> None:
|
||||
if self._paused:
|
||||
return
|
||||
self._paused = True
|
||||
self._notify()
|
||||
|
||||
def resume(self) -> None:
|
||||
if not self._paused:
|
||||
return
|
||||
self._paused = False
|
||||
self._notify()
|
||||
|
||||
def clear(self) -> None:
|
||||
if not self._items and not self._paused:
|
||||
return
|
||||
self._items.clear()
|
||||
self._paused = False
|
||||
self._notify()
|
||||
|
||||
def _notify(self) -> None:
|
||||
if self._on_change is not None:
|
||||
self._on_change()
|
||||
|
|
@ -31,15 +31,18 @@ class QuitManager:
|
|||
and (time.monotonic() - self._confirm_time) < QUIT_CONFIRM_DELAY
|
||||
)
|
||||
|
||||
def request_confirmation(self, key: QuitConfirmKey) -> None:
|
||||
def request_confirmation(self, key: QuitConfirmKey, extra: str = "") -> None:
|
||||
if self._confirm_timer is not None:
|
||||
self._confirm_timer.stop()
|
||||
self._confirm_timer = None
|
||||
self._confirm_time = time.monotonic()
|
||||
self._confirm_key = key
|
||||
prompt = f"Press {key} again to quit"
|
||||
if extra:
|
||||
prompt = f"{prompt} ({extra})"
|
||||
try:
|
||||
path_display = self._app.query_one(PathDisplay)
|
||||
path_display.update(f"Press {key} again to quit")
|
||||
path_display.update(prompt)
|
||||
except Exception:
|
||||
pass
|
||||
self._confirm_timer = self._app.set_timer(
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from vibe.cli.textual_ui.widgets.banner.petit_chat import PetitChat
|
|||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.skills.manager import SkillManager
|
||||
from vibe.core.tools.mcp.registry import MCPRegistry
|
||||
|
||||
|
||||
def _pluralize(count: int, singular: str) -> str:
|
||||
|
|
@ -24,8 +23,10 @@ def _pluralize(count: int, singular: str) -> str:
|
|||
class BannerState:
|
||||
active_model: str = ""
|
||||
models_count: int = 0
|
||||
mcp_servers_count: int = 0
|
||||
connectors_count: int = 0
|
||||
mcp_servers_enabled: int = 0
|
||||
mcp_servers_total: int = 0
|
||||
connectors_enabled: int = 0
|
||||
connectors_total: int = 0
|
||||
skills_count: int = 0
|
||||
plan_description: str | None = None
|
||||
|
||||
|
|
@ -37,8 +38,8 @@ class Banner(Static):
|
|||
self,
|
||||
config: VibeConfig,
|
||||
skill_manager: SkillManager,
|
||||
mcp_registry: MCPRegistry,
|
||||
connectors_count: int = 0,
|
||||
connectors_enabled: int = 0,
|
||||
connectors_total: int = 0,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
|
@ -46,8 +47,8 @@ class Banner(Static):
|
|||
self._initial_state = self._build_state(
|
||||
config=config,
|
||||
skill_manager=skill_manager,
|
||||
mcp_registry=mcp_registry,
|
||||
connectors_count=connectors_count,
|
||||
connectors_enabled=connectors_enabled,
|
||||
connectors_total=connectors_total,
|
||||
plan_description=None,
|
||||
)
|
||||
self._animated = not config.disable_welcome_banner_animation
|
||||
|
|
@ -90,40 +91,62 @@ class Banner(Static):
|
|||
self,
|
||||
config: VibeConfig,
|
||||
skill_manager: SkillManager,
|
||||
mcp_registry: MCPRegistry,
|
||||
connectors_count: int = 0,
|
||||
connectors_enabled: int = 0,
|
||||
connectors_total: int = 0,
|
||||
plan_description: str | None = None,
|
||||
) -> None:
|
||||
self.state = self._build_state(
|
||||
config, skill_manager, mcp_registry, connectors_count, plan_description
|
||||
config,
|
||||
skill_manager,
|
||||
connectors_enabled,
|
||||
connectors_total,
|
||||
plan_description,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_state(
|
||||
config: VibeConfig,
|
||||
skill_manager: SkillManager,
|
||||
mcp_registry: MCPRegistry,
|
||||
connectors_count: int = 0,
|
||||
connectors_enabled: int = 0,
|
||||
connectors_total: int = 0,
|
||||
plan_description: str | None = None,
|
||||
) -> BannerState:
|
||||
enabled_servers = [s for s in config.mcp_servers if not s.disabled]
|
||||
mcp_count = mcp_registry.count_loaded(enabled_servers)
|
||||
all_servers = config.mcp_servers
|
||||
enabled_servers = [s for s in all_servers if not s.disabled]
|
||||
|
||||
active_model = config.get_active_model()
|
||||
return BannerState(
|
||||
active_model=f"{active_model.alias}[{active_model.thinking}]",
|
||||
models_count=len(config.models),
|
||||
mcp_servers_count=mcp_count,
|
||||
connectors_count=connectors_count,
|
||||
mcp_servers_enabled=len(enabled_servers),
|
||||
mcp_servers_total=len(all_servers),
|
||||
connectors_enabled=connectors_enabled,
|
||||
connectors_total=connectors_total,
|
||||
skills_count=skill_manager.custom_skills_count,
|
||||
plan_description=plan_description,
|
||||
)
|
||||
|
||||
def _format_meta_counts(self) -> str:
|
||||
parts = [_pluralize(self.state.models_count, "model")]
|
||||
if self.state.connectors_count > 0:
|
||||
parts.append(_pluralize(self.state.connectors_count, "connector"))
|
||||
parts.append(_pluralize(self.state.mcp_servers_count, "MCP server"))
|
||||
# Format as x/y for MCP servers and connectors (only when enabled != total)
|
||||
if self.state.connectors_total > 0:
|
||||
if self.state.connectors_enabled != self.state.connectors_total:
|
||||
connector_str = (
|
||||
f"{self.state.connectors_enabled}/{self.state.connectors_total} connector"
|
||||
+ ("s" if self.state.connectors_total != 1 else "")
|
||||
)
|
||||
else:
|
||||
connector_str = _pluralize(self.state.connectors_enabled, "connector")
|
||||
parts.append(connector_str)
|
||||
# Always show MCP servers count (even if 0/0)
|
||||
if self.state.mcp_servers_enabled != self.state.mcp_servers_total:
|
||||
mcp_str = (
|
||||
f"{self.state.mcp_servers_enabled}/{self.state.mcp_servers_total} MCP server"
|
||||
+ ("s" if self.state.mcp_servers_total != 1 else "")
|
||||
)
|
||||
else:
|
||||
mcp_str = _pluralize(self.state.mcp_servers_enabled, "MCP server")
|
||||
parts.append(mcp_str)
|
||||
parts.append(_pluralize(self.state.skills_count, "skill"))
|
||||
return " · ".join(parts)
|
||||
|
||||
|
|
|
|||
|
|
@ -163,7 +163,9 @@ TRANSITIONS = [
|
|||
|
||||
class PetitChat(Static):
|
||||
def __init__(self, animate: bool = True, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs, classes="banner-chat")
|
||||
classes = kwargs.pop("classes", None)
|
||||
merged_classes = "banner-chat" if classes is None else f"banner-chat {classes}"
|
||||
super().__init__(**kwargs, classes=merged_classes)
|
||||
self._dots = {1j * y + x for y, row in enumerate(STARTING_DOTS) for x in row}
|
||||
self._transition_index = 0
|
||||
self._do_animate = animate
|
||||
|
|
|
|||
|
|
@ -1,49 +1,23 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import time
|
||||
|
||||
from vibe.cli.cache import read_cache, write_cache
|
||||
from vibe.core.agent_loop import AgentLoop
|
||||
from vibe.core.paths import CACHE_FILE
|
||||
from vibe.core.feedback import record_feedback_asked, should_show_feedback
|
||||
from vibe.core.types import Role
|
||||
|
||||
FEEDBACK_PROBABILITY = 0.2
|
||||
FEEDBACK_COOLDOWN_SECONDS = 3600
|
||||
_CACHE_SECTION = "user_feedback"
|
||||
_LAST_SHOWN_KEY = "last_shown_at"
|
||||
MIN_USER_MESSAGES_FOR_FEEDBACK = 3
|
||||
|
||||
|
||||
class FeedbackBarManager:
|
||||
"""Decides whether to show the feedback bar and records when feedback is given."""
|
||||
|
||||
def should_show(self, agent_loop: AgentLoop) -> bool:
|
||||
if not agent_loop.telemetry_client.is_active():
|
||||
return False
|
||||
|
||||
if not agent_loop.config.is_active_model_mistral():
|
||||
return False
|
||||
|
||||
if (
|
||||
user_message_count = (
|
||||
sum(m.role == Role.user and not m.injected for m in agent_loop.messages)
|
||||
+ 1 # +1 for the message the user just sent
|
||||
< MIN_USER_MESSAGES_FOR_FEEDBACK
|
||||
):
|
||||
return False
|
||||
|
||||
last_ts = (
|
||||
read_cache(CACHE_FILE.path).get(_CACHE_SECTION, {}).get(_LAST_SHOWN_KEY, 0)
|
||||
)
|
||||
if not isinstance(last_ts, int):
|
||||
return False
|
||||
|
||||
return (
|
||||
time.time() - last_ts >= FEEDBACK_COOLDOWN_SECONDS
|
||||
and random.random() <= FEEDBACK_PROBABILITY
|
||||
return should_show_feedback(
|
||||
telemetry_active=agent_loop.telemetry_client.is_active(),
|
||||
is_mistral_model=agent_loop.config.is_active_model_mistral(),
|
||||
user_message_count=user_message_count,
|
||||
)
|
||||
|
||||
def record_feedback_asked(self) -> None:
|
||||
write_cache(
|
||||
CACHE_FILE.path, _CACHE_SECTION, {_LAST_SHOWN_KEY: int(time.time())}
|
||||
)
|
||||
record_feedback_asked()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from textual.worker import Worker
|
|||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.core.config import ConnectorConfig
|
||||
from vibe.core.tools.connectors import ConnectorRegistry
|
||||
from vibe.core.tools.connectors import ConnectorAuthAction, ConnectorRegistry
|
||||
from vibe.core.tools.mcp.tools import MCPTool
|
||||
from vibe.core.tools.mcp_settings import updated_tool_list
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ _LIST_VIEW_HELP_TOOLS = (
|
|||
"↑↓ Navigate Enter Show tools D Disable E Enable R Refresh Esc Close"
|
||||
)
|
||||
_LIST_VIEW_HELP_AUTH = (
|
||||
"↑↓ Navigate Enter Authenticate D Disable E Enable R Refresh Esc Close"
|
||||
"↑↓ Navigate Enter Connect D Disable E Enable R Refresh Esc Close"
|
||||
)
|
||||
_DETAIL_VIEW_HELP = (
|
||||
"↑↓ Navigate D Disable E Enable Backspace Back R Refresh Esc Close"
|
||||
|
|
@ -246,7 +246,11 @@ class MCPApp(Container):
|
|||
option_id = option.id or ""
|
||||
if option_id.startswith("connector:") and self._connector_registry:
|
||||
name = option_id.removeprefix("connector:")
|
||||
if not self._connector_registry.is_connected(name):
|
||||
if (
|
||||
not self._connector_registry.is_connected(name)
|
||||
and self._connector_registry.get_auth_action(name)
|
||||
== ConnectorAuthAction.OAUTH
|
||||
):
|
||||
return _LIST_VIEW_HELP_AUTH
|
||||
return _LIST_VIEW_HELP_TOOLS
|
||||
|
||||
|
|
@ -461,9 +465,7 @@ class MCPApp(Container):
|
|||
label.append(f" {type_tag:<{max_type}}", style="dim")
|
||||
label.append(f" {_tool_count_text(enabled, total)}", style="dim")
|
||||
if srv.disabled:
|
||||
label.append(" ")
|
||||
label.append("○", style="dim")
|
||||
label.append(" disabled", style="dim")
|
||||
_append_status(label, "○", "dim", "disabled")
|
||||
option_list.add_option(Option(label, id=f"server:{srv.name}"))
|
||||
|
||||
def _list_connectors(self, option_list: OptionList, index: MCPToolIndex) -> None:
|
||||
|
|
@ -485,7 +487,7 @@ class MCPApp(Container):
|
|||
)
|
||||
for cname in ordered_connector_names:
|
||||
cfg = self._find_connector_config(cname)
|
||||
is_disabled = cfg.disabled if cfg else False
|
||||
is_disabled = cfg.disabled if cfg else True
|
||||
connected = (
|
||||
self._connector_registry.is_connected(cname)
|
||||
if self._connector_registry
|
||||
|
|
@ -495,18 +497,24 @@ class MCPApp(Container):
|
|||
label.append(f" {cname:<{max_name}}")
|
||||
label.append(f" {type_tag:<{type_width}}", style="dim")
|
||||
label.append(f" {tool_texts[cname]:<{max_tools}}", style="dim")
|
||||
auth_action = (
|
||||
self._connector_registry.get_auth_action(cname)
|
||||
if self._connector_registry
|
||||
else ConnectorAuthAction.NONE
|
||||
)
|
||||
if is_disabled:
|
||||
label.append(" ")
|
||||
label.append("○", style="dim")
|
||||
label.append(" disabled", style="dim")
|
||||
_append_status(label, "○", "dim", "disabled")
|
||||
elif connected:
|
||||
label.append(" ")
|
||||
label.append("●", style="green")
|
||||
label.append(" connected", style="dim")
|
||||
_append_status(label, "●", "green", "connected")
|
||||
else:
|
||||
label.append(" ")
|
||||
label.append("○", style="dim")
|
||||
label.append(" not connected", style="dim")
|
||||
match auth_action:
|
||||
case ConnectorAuthAction.OAUTH:
|
||||
text = "needs auth"
|
||||
case ConnectorAuthAction.CREDENTIALS_SETUP:
|
||||
text = "needs setup"
|
||||
case _:
|
||||
text = "error - try refreshing"
|
||||
_append_status(label, "○", "dim", text)
|
||||
option_list.add_option(Option(label, id=f"connector:{cname}"))
|
||||
|
||||
# ── detail view ──────────────────────────────────────────────────
|
||||
|
|
@ -537,13 +545,31 @@ class MCPApp(Container):
|
|||
and self._connector_registry
|
||||
and not self._connector_registry.is_connected(server_name)
|
||||
):
|
||||
self.post_message(
|
||||
self.ConnectorAuthRequested(
|
||||
connector_name=server_name,
|
||||
connector_registry=self._connector_registry,
|
||||
tool_manager=self._tool_manager,
|
||||
)
|
||||
)
|
||||
auth_action = self._connector_registry.get_auth_action(server_name)
|
||||
match auth_action:
|
||||
case ConnectorAuthAction.CREDENTIALS_SETUP:
|
||||
option_list.add_option(
|
||||
Option(
|
||||
"Set up credentials in the Mistral dashboard, "
|
||||
"then press R to refresh.",
|
||||
disabled=True,
|
||||
)
|
||||
)
|
||||
case ConnectorAuthAction.OAUTH:
|
||||
self.post_message(
|
||||
self.ConnectorAuthRequested(
|
||||
connector_name=server_name,
|
||||
connector_registry=self._connector_registry,
|
||||
tool_manager=self._tool_manager,
|
||||
)
|
||||
)
|
||||
case _:
|
||||
option_list.add_option(
|
||||
Option(
|
||||
"Connector unavailable; press R to refresh.",
|
||||
disabled=True,
|
||||
)
|
||||
)
|
||||
else:
|
||||
option_list.add_option(
|
||||
Option("No tools discovered for this server", disabled=True)
|
||||
|
|
@ -569,6 +595,12 @@ class MCPApp(Container):
|
|||
option_list.highlighted = 0
|
||||
|
||||
|
||||
def _append_status(label: Text, symbol: str, symbol_style: str, text: str) -> None:
|
||||
label.append(" ")
|
||||
label.append(symbol, style=symbol_style)
|
||||
label.append(f" {text}", style="dim")
|
||||
|
||||
|
||||
def _tool_count_text(enabled: int, total: int | None = None) -> str:
|
||||
if total is not None and enabled < total:
|
||||
noun = "tool" if total == 1 else "tools"
|
||||
|
|
|
|||
|
|
@ -17,11 +17,10 @@ from textual.containers import Horizontal, Vertical
|
|||
from textual.css.query import NoMatches
|
||||
from textual.reactive import reactive
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static
|
||||
from textual.widgets import Markdown, Static
|
||||
from textual.widgets._markdown import MarkdownStream
|
||||
from watchfiles import awatch
|
||||
|
||||
from vibe.cli.textual_ui.ansi_markdown import AnsiMarkdown as Markdown
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.spinner import SpinnerMixin, SpinnerType
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class ProxySetupApp(Container):
|
|||
yield NoMarkupStatic("Proxy Configuration", classes="settings-title")
|
||||
|
||||
for key, description in SUPPORTED_PROXY_VARS.items():
|
||||
yield Static(f"[bold ansi_blue]{key}[/]", classes="proxy-label-line")
|
||||
yield Static(f"[bold $primary]{key}[/]", classes="proxy-label-line")
|
||||
|
||||
initial_value = self.initial_values.get(key) or ""
|
||||
input_widget = VscodeCompatInput(
|
||||
|
|
|
|||
97
vibe/cli/textual_ui/widgets/queued_messages.py
Normal file
97
vibe/cli/textual_ui/widgets/queued_messages.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Vertical, VerticalScroll
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.message_queue import MessageQueue, QueuedItem, QueuedItemKind
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
|
||||
|
||||
class QueuedMessageItem(Static):
|
||||
def __init__(self, item: QueuedItem) -> None:
|
||||
super().__init__()
|
||||
self.add_class("queued-message-item")
|
||||
if item.kind == QueuedItemKind.BASH:
|
||||
self.add_class("queued-message-bash")
|
||||
self._item = item
|
||||
|
||||
@property
|
||||
def item(self) -> QueuedItem:
|
||||
return self._item
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
prefix = "$ " if self._item.kind == QueuedItemKind.BASH else ""
|
||||
yield NoMarkupStatic(
|
||||
f"{prefix}{self._item.content}", classes="queued-message-content"
|
||||
)
|
||||
|
||||
|
||||
class QueuedMessages(Widget):
|
||||
DEFAULT_CSS = ""
|
||||
ID = "queued-messages"
|
||||
ID_LIST = "queued-messages-list"
|
||||
ID_HINT = "queued-messages-hint"
|
||||
ID_BOX = "queued-messages-box"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(id=self.ID)
|
||||
self._queue: MessageQueue | None = None
|
||||
self._is_job_running: bool = False
|
||||
|
||||
def bind(self, queue: MessageQueue) -> None:
|
||||
self._queue = queue
|
||||
self._queue.set_change_listener(self._on_queue_changed)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id=self.ID_BOX) as box:
|
||||
box.border_title = "Queue"
|
||||
yield VerticalScroll(id=self.ID_LIST)
|
||||
yield NoMarkupStatic("", id=self.ID_HINT)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._refresh()
|
||||
|
||||
def set_job_running(self, running: bool) -> None:
|
||||
if self._is_job_running == running:
|
||||
return
|
||||
self._is_job_running = running
|
||||
self._refresh()
|
||||
|
||||
def _on_queue_changed(self) -> None:
|
||||
if self.is_mounted:
|
||||
self._refresh()
|
||||
|
||||
def _refresh(self) -> None:
|
||||
queue = self._queue
|
||||
if queue is None or len(queue) == 0:
|
||||
self.display = False
|
||||
return
|
||||
|
||||
self.display = True
|
||||
|
||||
try:
|
||||
box = self.query_one(f"#{self.ID_BOX}", Vertical)
|
||||
list_widget = self.query_one(f"#{self.ID_LIST}", VerticalScroll)
|
||||
hint = self.query_one(f"#{self.ID_HINT}", NoMarkupStatic)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
title = f"Queue ({len(queue)})"
|
||||
if queue.paused:
|
||||
title = f"{title} — paused"
|
||||
box.border_title = title
|
||||
|
||||
existing = list(list_widget.query(QueuedMessageItem))
|
||||
for widget in existing:
|
||||
widget.remove()
|
||||
for item in queue.items:
|
||||
list_widget.mount(QueuedMessageItem(item))
|
||||
|
||||
if queue.paused:
|
||||
hint.update("Enter send queue • Ctrl+C drop last queued")
|
||||
elif self._is_job_running:
|
||||
hint.update("Esc cancel job + pause • Ctrl+C drop last queued")
|
||||
else:
|
||||
hint.update("")
|
||||
114
vibe/cli/textual_ui/widgets/theme_picker.py
Normal file
114
vibe/cli/textual_ui/widgets/theme_picker.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from rich.text import Text
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding, BindingType
|
||||
from textual.containers import Container, Vertical
|
||||
from textual.message import Message
|
||||
from textual.theme import BUILTIN_THEMES
|
||||
from textual.timer import Timer
|
||||
from textual.widgets import OptionList
|
||||
from textual.widgets.option_list import Option
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
|
||||
PREVIEW_DEBOUNCE_SECONDS = 0.1
|
||||
|
||||
|
||||
def sorted_theme_names() -> list[str]:
|
||||
light = sorted(name for name, t in BUILTIN_THEMES.items() if not t.dark)
|
||||
dark = sorted(name for name, t in BUILTIN_THEMES.items() if t.dark)
|
||||
return light + dark
|
||||
|
||||
|
||||
def _build_option_text(theme: str, is_current: bool) -> Text:
|
||||
text = Text(no_wrap=True)
|
||||
marker = "› " if is_current else " "
|
||||
text.append(marker, style="green" if is_current else "")
|
||||
text.append(theme, style="bold" if is_current else "")
|
||||
return text
|
||||
|
||||
|
||||
class ThemePickerApp(Container):
|
||||
can_focus_children = True
|
||||
|
||||
BINDINGS: ClassVar[list[BindingType]] = [
|
||||
Binding("escape", "cancel", "Cancel", show=False)
|
||||
]
|
||||
|
||||
class ThemeSelected(Message):
|
||||
def __init__(self, theme: str) -> None:
|
||||
self.theme = theme
|
||||
super().__init__()
|
||||
|
||||
class ThemePreviewed(Message):
|
||||
def __init__(self, theme: str) -> None:
|
||||
self.theme = theme
|
||||
super().__init__()
|
||||
|
||||
class Cancelled(Message):
|
||||
def __init__(self, original_theme: str) -> None:
|
||||
self.original_theme = original_theme
|
||||
super().__init__()
|
||||
|
||||
def __init__(
|
||||
self, theme_names: list[str], current_theme: str, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(id="themepicker-app", **kwargs)
|
||||
self._theme_names = theme_names
|
||||
self._current_theme = current_theme
|
||||
self._preview_timer: Timer | None = None
|
||||
self._pending_preview: str | None = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
options = [
|
||||
Option(_build_option_text(name, name == self._current_theme), id=name)
|
||||
for name in self._theme_names
|
||||
]
|
||||
with Vertical(id="themepicker-content"):
|
||||
yield NoMarkupStatic("Select Theme", classes="themepicker-title")
|
||||
yield OptionList(*options, id="themepicker-options")
|
||||
yield NoMarkupStatic(
|
||||
"↑↓ Preview Enter Select Esc Cancel", classes="themepicker-help"
|
||||
)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
option_list = self.query_one(OptionList)
|
||||
for i, name in enumerate(self._theme_names):
|
||||
if name == self._current_theme:
|
||||
option_list.highlighted = i
|
||||
break
|
||||
option_list.focus()
|
||||
|
||||
def on_option_list_option_highlighted(
|
||||
self, event: OptionList.OptionHighlighted
|
||||
) -> None:
|
||||
if not event.option.id:
|
||||
return
|
||||
self._pending_preview = event.option.id
|
||||
if self._preview_timer is not None:
|
||||
self._preview_timer.stop()
|
||||
self._preview_timer = self.set_timer(
|
||||
PREVIEW_DEBOUNCE_SECONDS, self._flush_preview
|
||||
)
|
||||
|
||||
def _flush_preview(self) -> None:
|
||||
if self._pending_preview is None:
|
||||
return
|
||||
self.post_message(self.ThemePreviewed(self._pending_preview))
|
||||
self._pending_preview = None
|
||||
|
||||
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
|
||||
if self._preview_timer is not None:
|
||||
self._preview_timer.stop()
|
||||
self._pending_preview = None
|
||||
if event.option.id:
|
||||
self.post_message(self.ThemeSelected(event.option.id))
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
if self._preview_timer is not None:
|
||||
self._preview_timer.stop()
|
||||
self._pending_preview = None
|
||||
self.post_message(self.Cancelled(self._current_theme))
|
||||
|
|
@ -6,9 +6,8 @@ from pathlib import Path
|
|||
from pydantic import BaseModel
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Vertical
|
||||
from textual.widgets import Static
|
||||
from textual.widgets import Markdown, Static
|
||||
|
||||
from vibe.cli.textual_ui.ansi_markdown import AnsiMarkdown as Markdown
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.core.tools.builtins.ask_user_question import AskUserQuestionResult
|
||||
from vibe.core.tools.builtins.bash import BashArgs, BashResult
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue