v2.11.1 (#721)
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: Mert Unsal <mertunsal1905@gmail.com> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: maximevoisin-pm <maxime.voisin@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
adb1ca74ce
commit
cf3f4ca58f
143 changed files with 3457 additions and 1351 deletions
|
|
@ -11,13 +11,7 @@ import tomli_w
|
|||
from vibe import __version__
|
||||
from vibe.cli.textual_ui.app import StartupOptions, run_textual_ui
|
||||
from vibe.core.agent_loop import AgentLoop, TeleportError
|
||||
from vibe.core.agents.models import BuiltinAgentName
|
||||
from vibe.core.config import (
|
||||
MissingAPIKeyError,
|
||||
MissingPromptFileError,
|
||||
VibeConfig,
|
||||
load_dotenv_values,
|
||||
)
|
||||
from vibe.core.config import MissingAPIKeyError, VibeConfig, load_dotenv_values
|
||||
from vibe.core.config.harness_files import get_harness_files_manager
|
||||
from vibe.core.hooks.config import load_hooks_from_fs
|
||||
from vibe.core.logger import logger
|
||||
|
|
@ -44,8 +38,6 @@ def _build_cli_entrypoint_metadata() -> EntrypointMetadata:
|
|||
|
||||
|
||||
def get_initial_agent_name(args: argparse.Namespace, config: VibeConfig) -> str:
|
||||
if args.prompt is not None and args.agent is None:
|
||||
return BuiltinAgentName.AUTO_APPROVE
|
||||
return args.agent or config.default_agent
|
||||
|
||||
|
||||
|
|
@ -77,9 +69,6 @@ def load_config_or_exit(*, interactive: bool) -> VibeConfig:
|
|||
sys.exit(1)
|
||||
run_onboarding(entrypoint_metadata=_build_cli_entrypoint_metadata())
|
||||
return VibeConfig.load()
|
||||
except MissingPromptFileError as e:
|
||||
rprint(f"[yellow]Invalid system prompt id: {e}[/]")
|
||||
sys.exit(1)
|
||||
except ValueError as e:
|
||||
rprint(f"[yellow]{e}[/]")
|
||||
sys.exit(1)
|
||||
|
|
@ -238,6 +227,7 @@ def run_cli(args: argparse.Namespace) -> None:
|
|||
prompt=programmatic_prompt or "",
|
||||
max_turns=args.max_turns,
|
||||
max_price=args.max_price,
|
||||
max_session_tokens=args.max_tokens,
|
||||
output_format=output_format,
|
||||
previous_messages=loaded_session[0] if loaded_session else None,
|
||||
agent_name=initial_agent_name,
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ class CommandRegistry:
|
|||
),
|
||||
"teleport": Command(
|
||||
aliases=frozenset(["/teleport"]),
|
||||
description="Teleport session to Vibe Code",
|
||||
description="Teleport session to Vibe Code Web",
|
||||
handler="_teleport_command",
|
||||
is_available=CommandAvailabilityContext.is_teleport_available,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -44,8 +44,9 @@ def parse_arguments() -> argparse.Namespace:
|
|||
nargs="?",
|
||||
const="",
|
||||
metavar="TEXT",
|
||||
help="Run in programmatic mode: send prompt, auto-approve all tools, "
|
||||
"output response, and exit.",
|
||||
help="Run in programmatic mode: send prompt, output response, and exit. "
|
||||
"Tool approval follows the selected --agent (or 'default_agent' config); "
|
||||
"pass --agent auto-approve for the previous YOLO behavior.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-turns",
|
||||
|
|
@ -61,6 +62,14 @@ def parse_arguments() -> argparse.Namespace:
|
|||
help="Maximum cost in dollars (only applies in programmatic mode with -p). "
|
||||
"Session will be interrupted if cost exceeds this limit.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-tokens",
|
||||
type=int,
|
||||
metavar="N",
|
||||
help="Maximum total prompt + completion tokens across the session "
|
||||
"(only applies in programmatic mode with -p). "
|
||||
"Session will be interrupted if usage exceeds this limit.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enabled-tools",
|
||||
action="append",
|
||||
|
|
@ -84,10 +93,10 @@ def parse_arguments() -> argparse.Namespace:
|
|||
metavar="NAME",
|
||||
default=None,
|
||||
help="Agent to use (builtin: default, plan, accept-edits, auto-approve, "
|
||||
"or custom from ~/.vibe/agents/NAME.toml). In interactive mode, "
|
||||
"defaults to the 'default_agent' config setting. In programmatic "
|
||||
"mode (-p/--prompt), defaults to auto-approve and 'default_agent' "
|
||||
"is ignored.",
|
||||
"or custom from ~/.vibe/agents/NAME.toml). Defaults to the "
|
||||
"'default_agent' config setting in both interactive and programmatic "
|
||||
"(-p/--prompt) mode. Pass --agent auto-approve for non-interactive "
|
||||
"automation that needs tools to run without approval.",
|
||||
)
|
||||
parser.add_argument("--setup", action="store_true", help="Setup API key and exit")
|
||||
parser.add_argument(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ from vibe.cli.plan_offer.decide_plan_offer import (
|
|||
resolve_api_key_for_plan,
|
||||
)
|
||||
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIGateway, WhoAmIPlanType
|
||||
from vibe.cli.terminal_detect import Terminal, detect_terminal
|
||||
from vibe.cli.textual_ui.handlers.event_handler import EventHandler
|
||||
from vibe.cli.textual_ui.notifications import (
|
||||
NotificationContext,
|
||||
|
|
@ -73,13 +74,17 @@ from vibe.cli.textual_ui.widgets.loading import (
|
|||
)
|
||||
from vibe.cli.textual_ui.widgets.mcp_app import MCPApp, MCPSourceKind
|
||||
from vibe.cli.textual_ui.widgets.messages import (
|
||||
VSCODE_EXTENSION_PROMO_WHATS_NEW_SUFFIX,
|
||||
AssistantMessage,
|
||||
BashOutputMessage,
|
||||
ErrorMessage,
|
||||
InterruptMessage,
|
||||
SlashCommandMessage,
|
||||
StreamingMessageBase,
|
||||
TeleportUserMessage,
|
||||
UserCommandMessage,
|
||||
UserMessage,
|
||||
VscodeExtensionPromoMessage,
|
||||
WarningMessage,
|
||||
WhatsNewMessage,
|
||||
)
|
||||
|
|
@ -121,6 +126,12 @@ from vibe.cli.update_notifier import (
|
|||
from vibe.cli.update_notifier.update import do_update
|
||||
from vibe.cli.voice_manager import VoiceManager, VoiceManagerPort
|
||||
from vibe.cli.voice_manager.voice_manager_port import TranscribeState
|
||||
from vibe.cli.vscode_extension_promo import (
|
||||
FileSystemVscodeExtensionPromoRepository,
|
||||
VscodeExtensionPromo,
|
||||
VscodeExtensionPromoState,
|
||||
should_show_promo,
|
||||
)
|
||||
from vibe.core.agent_loop import AgentLoop, TeleportError
|
||||
from vibe.core.agents import AgentProfile
|
||||
from vibe.core.audio_player.audio_player import AudioPlayer
|
||||
|
|
@ -186,6 +197,12 @@ from vibe.core.utils import (
|
|||
is_dangerous_directory,
|
||||
)
|
||||
|
||||
_VSCODE_FAMILY_TERMINALS = {Terminal.VSCODE, Terminal.VSCODE_INSIDERS, Terminal.CURSOR}
|
||||
|
||||
|
||||
def _is_vscode_family_terminal() -> bool:
|
||||
return detect_terminal() in _VSCODE_FAMILY_TERMINALS
|
||||
|
||||
|
||||
def _compute_connector_counts(
|
||||
config: VibeConfig, connector_registry: ConnectorRegistry | None
|
||||
|
|
@ -362,6 +379,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
terminal_notifier: NotificationPort | None = None,
|
||||
voice_manager: VoiceManagerPort | None = None,
|
||||
narrator_manager: NarratorManagerPort | None = None,
|
||||
vscode_extension_promo: VscodeExtensionPromo | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
|
@ -404,6 +422,12 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self._update_cache_repository = update_cache_repository
|
||||
self._current_version = current_version
|
||||
self._plan_offer_gateway = plan_offer_gateway
|
||||
self._vscode_extension_promo = vscode_extension_promo
|
||||
self._show_vscode_extension_promo = (
|
||||
vscode_extension_promo is not None
|
||||
and _is_vscode_family_terminal()
|
||||
and should_show_promo(vscode_extension_promo.initial_state)
|
||||
)
|
||||
self._configure_startup_options(startup)
|
||||
self._last_escape_time: float | None = None
|
||||
self._quit_manager = QuitManager(self)
|
||||
|
|
@ -643,6 +667,9 @@ class VibeApp(App): # noqa: PLR0904
|
|||
if self._agent_running:
|
||||
await self._interrupt_agent_loop()
|
||||
|
||||
await self._dispatch_submitted_input(value)
|
||||
|
||||
async def _dispatch_submitted_input(self, value: str) -> None:
|
||||
if value.startswith("!"):
|
||||
self._bash_task = asyncio.create_task(self._handle_bash_command(value[1:]))
|
||||
return
|
||||
|
|
@ -956,7 +983,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self.agent_loop.telemetry_client.send_slash_command_used(
|
||||
cmd_name, "builtin"
|
||||
)
|
||||
await self._mount_and_scroll(UserMessage(user_input))
|
||||
await self._mount_and_scroll(SlashCommandMessage(user_input[1:]))
|
||||
handler = getattr(self, command.handler)
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
await handler(cmd_args=cmd_args)
|
||||
|
|
@ -1510,7 +1537,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
has_history = any(msg.role != Role.system for msg in self.agent_loop.messages)
|
||||
if not value:
|
||||
if show_message:
|
||||
await self._mount_and_scroll(UserMessage("/teleport"))
|
||||
await self._mount_and_scroll(SlashCommandMessage("teleport"))
|
||||
if not has_history:
|
||||
send_teleport_early_failure_telemetry(
|
||||
self.agent_loop.telemetry_client,
|
||||
|
|
@ -1526,7 +1553,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
)
|
||||
return
|
||||
elif show_message:
|
||||
await self._mount_and_scroll(UserMessage(value))
|
||||
await self._mount_and_scroll(TeleportUserMessage(value))
|
||||
self.run_worker(self._teleport(value), exclusive=False)
|
||||
|
||||
async def _teleport(self, prompt: str | None = None) -> None:
|
||||
|
|
@ -2106,7 +2133,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
messages_area = self._cached_messages_area or self.query_one("#messages")
|
||||
await messages_area.remove_children()
|
||||
|
||||
await messages_area.mount(UserMessage("/clear"))
|
||||
await messages_area.mount(SlashCommandMessage("clear"))
|
||||
await self._mount_and_scroll(
|
||||
UserCommandMessage("Conversation history cleared!")
|
||||
)
|
||||
|
|
@ -2986,23 +3013,45 @@ class VibeApp(App): # noqa: PLR0904
|
|||
)
|
||||
await self._mount_and_scroll(WarningMessage(warning, show_border=False))
|
||||
|
||||
async def _record_vscode_extension_promo_shown(self) -> None:
|
||||
if self._vscode_extension_promo is None:
|
||||
return
|
||||
previous_count = (
|
||||
self._vscode_extension_promo.initial_state.shown_count
|
||||
if self._vscode_extension_promo.initial_state is not None
|
||||
else 0
|
||||
)
|
||||
try:
|
||||
await self._vscode_extension_promo.repository.set(
|
||||
VscodeExtensionPromoState(shown_count=previous_count + 1)
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to persist VSCode extension promo shown count", exc_info=True
|
||||
)
|
||||
|
||||
async def _check_and_show_whats_new(self) -> None:
|
||||
if self._update_cache_repository is None:
|
||||
await self._maybe_show_vscode_extension_promo()
|
||||
return
|
||||
|
||||
if not await should_show_whats_new(
|
||||
self._current_version, self._update_cache_repository
|
||||
):
|
||||
await self._maybe_show_vscode_extension_promo()
|
||||
return
|
||||
|
||||
content = load_whats_new_content()
|
||||
if content is not None:
|
||||
whats_new_message = WhatsNewMessage(content)
|
||||
body = content
|
||||
plan_offer = plan_offer_cta(
|
||||
self._plan_info, console_base_url=self.config.console_base_url
|
||||
)
|
||||
if plan_offer is not None:
|
||||
whats_new_message = WhatsNewMessage(f"{content}\n\n{plan_offer}")
|
||||
body = f"{body}\n\n{plan_offer}"
|
||||
if self._show_vscode_extension_promo:
|
||||
body = f"{body}{VSCODE_EXTENSION_PROMO_WHATS_NEW_SUFFIX}"
|
||||
whats_new_message = WhatsNewMessage(body)
|
||||
if self._history_widget_indices:
|
||||
whats_new_message.add_class("after-history")
|
||||
messages_area = self._cached_messages_area or self.query_one("#messages")
|
||||
|
|
@ -3012,8 +3061,28 @@ class VibeApp(App): # noqa: PLR0904
|
|||
self._whats_new_message = whats_new_message
|
||||
if should_anchor:
|
||||
chat.anchor()
|
||||
if self._show_vscode_extension_promo:
|
||||
self.run_worker(
|
||||
self._record_vscode_extension_promo_shown(), exclusive=False
|
||||
)
|
||||
else:
|
||||
await self._maybe_show_vscode_extension_promo()
|
||||
await mark_version_as_seen(self._current_version, self._update_cache_repository)
|
||||
|
||||
async def _maybe_show_vscode_extension_promo(self) -> None:
|
||||
if not self._show_vscode_extension_promo:
|
||||
return
|
||||
promo_message = VscodeExtensionPromoMessage()
|
||||
if self._history_widget_indices:
|
||||
promo_message.add_class("after-history")
|
||||
messages_area = self._cached_messages_area or self.query_one("#messages")
|
||||
chat = self._cached_chat or self.query_one("#chat", ChatScroll)
|
||||
should_anchor = chat.is_at_bottom
|
||||
await chat.mount(promo_message, after=messages_area)
|
||||
if should_anchor:
|
||||
chat.anchor()
|
||||
self.run_worker(self._record_vscode_extension_promo_shown(), exclusive=False)
|
||||
|
||||
async def _resolve_plan(self) -> None:
|
||||
if self._plan_offer_gateway is None:
|
||||
self._plan_info = None
|
||||
|
|
@ -3192,6 +3261,11 @@ def run_textual_ui(
|
|||
update_notifier = PyPIUpdateGateway(project_name="mistral-vibe")
|
||||
update_cache_repository = FileSystemUpdateCacheRepository()
|
||||
plan_offer_gateway = HttpWhoAmIGateway(base_url=agent_loop.config.console_base_url)
|
||||
vscode_extension_promo_repository = FileSystemVscodeExtensionPromoRepository()
|
||||
vscode_extension_promo = VscodeExtensionPromo(
|
||||
repository=vscode_extension_promo_repository,
|
||||
initial_state=asyncio.run(vscode_extension_promo_repository.get()),
|
||||
)
|
||||
|
||||
with stderr_guard():
|
||||
app = VibeApp(
|
||||
|
|
@ -3200,6 +3274,7 @@ def run_textual_ui(
|
|||
update_notifier=update_notifier,
|
||||
update_cache_repository=update_cache_repository,
|
||||
plan_offer_gateway=plan_offer_gateway,
|
||||
vscode_extension_promo=vscode_extension_promo,
|
||||
)
|
||||
session_id = app.run()
|
||||
|
||||
|
|
|
|||
|
|
@ -154,33 +154,39 @@ OptionList, OptionList:focus {
|
|||
height: auto;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: solid $foreground-muted;
|
||||
border: none;
|
||||
border-top: solid $foreground-muted;
|
||||
border-bottom: solid $foreground-muted;
|
||||
border-title-align: right;
|
||||
border-title-color: $text-muted;
|
||||
padding: 0 1;
|
||||
|
||||
&.border-warning {
|
||||
border: solid $warning;
|
||||
border-top: solid $warning;
|
||||
border-bottom: solid $warning;
|
||||
border-title-color: $warning;
|
||||
}
|
||||
|
||||
&.border-safe {
|
||||
border: solid $success;
|
||||
border-top: solid $success;
|
||||
border-bottom: solid $success;
|
||||
border-title-color: $success;
|
||||
}
|
||||
|
||||
&.border-error {
|
||||
border: solid $error;
|
||||
border-top: solid $error;
|
||||
border-bottom: solid $error;
|
||||
border-title-color: $error;
|
||||
}
|
||||
|
||||
&.border-recording {
|
||||
border: solid $mistral_orange;
|
||||
border-top: solid $mistral_orange;
|
||||
border-bottom: solid $mistral_orange;
|
||||
border-title-color: $mistral_orange;
|
||||
}
|
||||
|
||||
&.border-remote {
|
||||
border: solid $mistral_orange;
|
||||
border-top: solid $mistral_orange;
|
||||
border-bottom: solid $mistral_orange;
|
||||
border-title-color: $mistral_orange;
|
||||
}
|
||||
}
|
||||
|
|
@ -275,6 +281,12 @@ Markdown {
|
|||
}
|
||||
}
|
||||
|
||||
.user-message-wrapper {
|
||||
margin-top: 1;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.user-message-container {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
|
@ -282,7 +294,8 @@ Markdown {
|
|||
|
||||
.user-message-prompt {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
height: auto;
|
||||
color: $mistral_orange;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
|
|
@ -290,8 +303,22 @@ Markdown {
|
|||
width: 1fr;
|
||||
height: auto;
|
||||
text-style: bold;
|
||||
padding-left: 1;
|
||||
border-left: heavy $mistral_orange;
|
||||
}
|
||||
|
||||
.slash-command-message {
|
||||
.user-message-content {
|
||||
color: $mistral_orange;
|
||||
}
|
||||
}
|
||||
|
||||
.user-message-separator {
|
||||
width: 100%;
|
||||
height: 1;
|
||||
color: $text-muted;
|
||||
|
||||
&:ansi {
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
.assistant-message {
|
||||
|
|
@ -1135,7 +1162,7 @@ NarratorStatus {
|
|||
|
||||
#banner-container {
|
||||
align: left middle;
|
||||
padding: 0 1 0 0;
|
||||
padding: 1 1 0 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
|
@ -1231,6 +1258,33 @@ NarratorStatus {
|
|||
margin-top: 1;
|
||||
}
|
||||
|
||||
.vscode-extension-promo-message {
|
||||
border-left: heavy $mistral_orange;
|
||||
margin-bottom: 1;
|
||||
|
||||
Markdown {
|
||||
padding: 0 0 0 1;
|
||||
margin: 0;
|
||||
|
||||
& > MarkdownBlock:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
& > MarkdownBlock:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
link-style: none;
|
||||
link-background-hover: transparent;
|
||||
link-style-hover: underline;
|
||||
link-color-hover: $mistral_orange;
|
||||
}
|
||||
|
||||
.vscode-extension-promo-message.after-history {
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
#sessionpicker-app {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
|
|
|||
|
|
@ -263,9 +263,8 @@ class ChatInputContainer(Vertical):
|
|||
return SAFETY_BORDER_CLASSES.get(self._safety, "")
|
||||
|
||||
def _get_border_title(self) -> str:
|
||||
if self._custom_border_label is not None:
|
||||
return self._custom_border_label
|
||||
return self._agent_name
|
||||
label = self._custom_border_label or self._agent_name
|
||||
return f" {label} " if label else ""
|
||||
|
||||
def _apply_input_box_chrome(self) -> None:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, cast
|
||||
|
||||
from vibe.core.hooks.models import HookMessageSeverity
|
||||
from vibe.core.logger import logger
|
||||
|
|
@ -47,7 +47,20 @@ class ExpandingBorder(NonSelectableStatic):
|
|||
self.refresh()
|
||||
|
||||
|
||||
# Mimic a border bottom with this component in order to have dimmed colors in ANSI themes
|
||||
# Move back to border when Textual supports dimmed borders or foreground-muted in ANSI themes
|
||||
class ExpandingSeparator(NonSelectableStatic):
|
||||
def render(self) -> str:
|
||||
return "─" * max(self.size.width, 1)
|
||||
|
||||
def on_resize(self) -> None:
|
||||
self.refresh()
|
||||
|
||||
|
||||
class UserMessage(Static):
|
||||
PROMPT_CHAR: ClassVar[str] = ">"
|
||||
SHOW_SEPARATOR: ClassVar[bool] = True
|
||||
|
||||
def __init__(
|
||||
self, content: str, pending: bool = False, message_index: int | None = None
|
||||
) -> None:
|
||||
|
|
@ -61,10 +74,16 @@ class UserMessage(Static):
|
|||
return self._content
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Horizontal(classes="user-message-container"):
|
||||
yield NoMarkupStatic(self._content, classes="user-message-content")
|
||||
if self._pending:
|
||||
self.add_class("pending")
|
||||
with Vertical(classes="user-message-wrapper"):
|
||||
with Horizontal(classes="user-message-container"):
|
||||
yield NonSelectableStatic(
|
||||
f"{self.PROMPT_CHAR} ", classes="user-message-prompt"
|
||||
)
|
||||
yield NoMarkupStatic(self._content, classes="user-message-content")
|
||||
if self.SHOW_SEPARATOR:
|
||||
yield ExpandingSeparator(classes="user-message-separator")
|
||||
if self._pending:
|
||||
self.add_class("pending")
|
||||
|
||||
async def set_pending(self, pending: bool) -> None:
|
||||
if pending == self._pending:
|
||||
|
|
@ -79,6 +98,19 @@ class UserMessage(Static):
|
|||
self.remove_class("pending")
|
||||
|
||||
|
||||
class SlashCommandMessage(UserMessage):
|
||||
PROMPT_CHAR = "/"
|
||||
SHOW_SEPARATOR = False
|
||||
|
||||
def __init__(self, content: str) -> None:
|
||||
super().__init__(content)
|
||||
self.add_class("slash-command-message")
|
||||
|
||||
|
||||
class TeleportUserMessage(UserMessage):
|
||||
PROMPT_CHAR = "&"
|
||||
|
||||
|
||||
class StreamingMessageBase(Static):
|
||||
def __init__(self, content: str) -> None:
|
||||
super().__init__()
|
||||
|
|
@ -247,6 +279,15 @@ class UserCommandMessage(Static):
|
|||
yield Markdown(self._content)
|
||||
|
||||
|
||||
VSCODE_EXTENSION_URI = "vscode:extension/mistralai.vibe-code"
|
||||
VSCODE_EXTENSION_LINK_LABEL = "VS Code extension"
|
||||
VSCODE_EXTENSION_PROMO_STANDALONE = f"We now have a [{VSCODE_EXTENSION_LINK_LABEL}]({VSCODE_EXTENSION_URI}) with a rich UI. Check it out!"
|
||||
VSCODE_EXTENSION_PROMO_WHATS_NEW_SUFFIX = (
|
||||
f"\n\n_Btw, we also have a new [{VSCODE_EXTENSION_LINK_LABEL}]"
|
||||
f"({VSCODE_EXTENSION_URI}). Check it out!_"
|
||||
)
|
||||
|
||||
|
||||
class WhatsNewMessage(Static):
|
||||
def __init__(self, content: str) -> None:
|
||||
super().__init__()
|
||||
|
|
@ -257,6 +298,16 @@ class WhatsNewMessage(Static):
|
|||
yield Markdown(self._content)
|
||||
|
||||
|
||||
class VscodeExtensionPromoMessage(Static):
|
||||
def __init__(self, content: str = VSCODE_EXTENSION_PROMO_STANDALONE) -> None:
|
||||
super().__init__()
|
||||
self.add_class("vscode-extension-promo-message")
|
||||
self._content = content
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Markdown(self._content)
|
||||
|
||||
|
||||
class InterruptMessage(Static):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class TeleportMessage(StatusMessage):
|
|||
if self._error:
|
||||
return f"Teleport failed: {self._error}"
|
||||
if self._final_url:
|
||||
return f"Teleported to Vibe Code: {self._final_url}"
|
||||
return f"Teleported to Vibe Code Web: {self._final_url}"
|
||||
return self._status
|
||||
|
||||
def set_status(self, status: str) -> None:
|
||||
|
|
|
|||
43
vibe/cli/vscode_extension_promo/__init__.py
Normal file
43
vibe/cli/vscode_extension_promo/__init__.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from vibe.cli.vscode_extension_promo._port import (
|
||||
VscodeExtensionPromoRepository,
|
||||
VscodeExtensionPromoState,
|
||||
)
|
||||
from vibe.cli.vscode_extension_promo.adapters.filesystem_repository import (
|
||||
FileSystemVscodeExtensionPromoRepository,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"MAX_SHOWN_COUNT",
|
||||
"PROMO_START",
|
||||
"FileSystemVscodeExtensionPromoRepository",
|
||||
"VscodeExtensionPromo",
|
||||
"VscodeExtensionPromoRepository",
|
||||
"VscodeExtensionPromoState",
|
||||
"should_show_promo",
|
||||
]
|
||||
|
||||
|
||||
MAX_SHOWN_COUNT = 10
|
||||
PROMO_START = datetime(2026, 5, 28, 16, 0, tzinfo=UTC)
|
||||
|
||||
|
||||
def should_show_promo(
|
||||
state: VscodeExtensionPromoState | None, now: datetime | None = None
|
||||
) -> bool:
|
||||
current = now or datetime.now(UTC)
|
||||
if current < PROMO_START:
|
||||
return False
|
||||
if state is None:
|
||||
return True
|
||||
return state.shown_count < MAX_SHOWN_COUNT
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VscodeExtensionPromo:
|
||||
repository: VscodeExtensionPromoRepository
|
||||
initial_state: VscodeExtensionPromoState | None
|
||||
14
vibe/cli/vscode_extension_promo/_port.py
Normal file
14
vibe/cli/vscode_extension_promo/_port.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VscodeExtensionPromoState:
|
||||
shown_count: int
|
||||
|
||||
|
||||
class VscodeExtensionPromoRepository(Protocol):
|
||||
async def get(self) -> VscodeExtensionPromoState | None: ...
|
||||
async def set(self, state: VscodeExtensionPromoState) -> None: ...
|
||||
0
vibe/cli/vscode_extension_promo/adapters/__init__.py
Normal file
0
vibe/cli/vscode_extension_promo/adapters/__init__.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from vibe.cli.cache import read_cache, write_cache
|
||||
from vibe.cli.vscode_extension_promo._port import (
|
||||
VscodeExtensionPromoRepository,
|
||||
VscodeExtensionPromoState,
|
||||
)
|
||||
from vibe.core.paths import VIBE_HOME
|
||||
|
||||
_CACHE_SECTION = "vscode_extension_promo"
|
||||
|
||||
|
||||
class FileSystemVscodeExtensionPromoRepository(VscodeExtensionPromoRepository):
|
||||
def __init__(self, base_path: Path | str | None = None) -> None:
|
||||
self._base_path = Path(base_path) if base_path is not None else VIBE_HOME.path
|
||||
self._cache_file = self._base_path / "cache.toml"
|
||||
|
||||
async def get(self) -> VscodeExtensionPromoState | None:
|
||||
data = await asyncio.to_thread(self._read_section)
|
||||
if data is None:
|
||||
return None
|
||||
shown_count = data.get("shown_count")
|
||||
if not isinstance(shown_count, int):
|
||||
return None
|
||||
return VscodeExtensionPromoState(shown_count=shown_count)
|
||||
|
||||
async def set(self, state: VscodeExtensionPromoState) -> None:
|
||||
await asyncio.to_thread(
|
||||
write_cache,
|
||||
self._cache_file,
|
||||
_CACHE_SECTION,
|
||||
{"shown_count": state.shown_count},
|
||||
)
|
||||
|
||||
def _read_section(self) -> dict | None:
|
||||
cache = read_cache(self._cache_file)
|
||||
section = cache.get(_CACHE_SECTION)
|
||||
if isinstance(section, dict):
|
||||
return section
|
||||
return None
|
||||
Loading…
Add table
Add a link
Reference in a new issue