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>
This commit is contained in:
Mathias Gesbert 2026-01-30 14:08:38 +01:00 committed by Mathias Gesbert
parent bd3497b1c0
commit 9809cfc831
32 changed files with 622 additions and 305 deletions

View file

@ -12,7 +12,7 @@ from vibe.core.config import (
MissingAPIKeyError,
MissingPromptFileError,
VibeConfig,
load_api_keys_from_env,
load_dotenv_values,
)
from vibe.core.paths.config_paths import CONFIG_FILE, HISTORY_FILE
from vibe.core.programmatic import run_programmatic
@ -122,7 +122,7 @@ def _load_messages_from_previous_session(
def run_cli(args: argparse.Namespace) -> None:
load_api_keys_from_env()
load_dotenv_values()
bootstrap_config_files()
if args.setup:

View file

@ -29,28 +29,36 @@ ACTION_TO_URL: dict[PlanOfferAction, str] = {
}
class PlanType(StrEnum):
FREE = "free"
PRO = "pro"
UNKNOWN = "unknown"
async def decide_plan_offer(
api_key: str | None, gateway: WhoAmIGateway
) -> PlanOfferAction:
) -> tuple[PlanOfferAction, PlanType]:
if not api_key:
return PlanOfferAction.UPGRADE
return PlanOfferAction.UPGRADE, PlanType.FREE
try:
response = await gateway.whoami(api_key)
except WhoAmIGatewayUnauthorized:
return PlanOfferAction.UPGRADE
return PlanOfferAction.UPGRADE, PlanType.FREE
except WhoAmIGatewayError:
logger.warning("Failed to fetch plan status.", exc_info=True)
return PlanOfferAction.NONE
return _action_from_response(response)
return PlanOfferAction.NONE, PlanType.UNKNOWN
return _action_and_plan_from_response(response)
def _action_from_response(response: WhoAmIResponse) -> PlanOfferAction:
def _action_and_plan_from_response(
response: WhoAmIResponse,
) -> tuple[PlanOfferAction, PlanType]:
match response:
case WhoAmIResponse(is_pro_plan=True):
return PlanOfferAction.NONE
return PlanOfferAction.NONE, PlanType.PRO
case WhoAmIResponse(prompt_switching_to_pro_plan=True):
return PlanOfferAction.SWITCH_TO_PRO_KEY
return PlanOfferAction.SWITCH_TO_PRO_KEY, PlanType.PRO
case WhoAmIResponse(advertise_pro_plan=True):
return PlanOfferAction.UPGRADE
return PlanOfferAction.UPGRADE, PlanType.FREE
case _:
return PlanOfferAction.NONE
return PlanOfferAction.NONE, PlanType.UNKNOWN

View file

@ -23,6 +23,7 @@ from vibe.cli.plan_offer.adapters.http_whoami_gateway import HttpWhoAmIGateway
from vibe.cli.plan_offer.decide_plan_offer import (
ACTION_TO_URL,
PlanOfferAction,
PlanType,
decide_plan_offer,
)
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIGateway
@ -74,12 +75,19 @@ from vibe.core.agents import AgentProfile
from vibe.core.autocompletion.path_prompt_adapter import render_path_prompt
from vibe.core.config import VibeConfig
from vibe.core.paths.config_paths import HISTORY_FILE
from vibe.core.session.session_loader import SessionLoader
from vibe.core.tools.base import ToolPermission
from vibe.core.tools.builtins.ask_user_question import (
AskUserQuestionArgs,
AskUserQuestionResult,
)
from vibe.core.types import AgentStats, ApprovalResponse, LLMMessage, Role
from vibe.core.types import (
AgentStats,
ApprovalResponse,
LLMMessage,
RateLimitError,
Role,
)
from vibe.core.utils import (
CancellationReason,
get_user_cancellation_message,
@ -594,8 +602,16 @@ class VibeApp(App): # noqa: PLR0904
await self._loading_widget.remove()
if self.event_handler:
self.event_handler.stop_current_tool_call()
message = str(e)
if isinstance(e, RateLimitError):
if self.plan_type == PlanType.FREE:
message = "Rate limits exceeded. Please wait a moment before trying again, or upgrade to Pro for higher rate limits and uninterrupted access."
else:
message = "Rate limits exceeded. Please wait a moment before trying again."
await self._mount_and_scroll(
ErrorMessage(str(e), collapsed=self._tools_collapsed)
ErrorMessage(message, collapsed=self._tools_collapsed)
)
finally:
self._agent_running = False
@ -773,6 +789,12 @@ class VibeApp(App): # noqa: PLR0904
return None
if not self.agent_loop.session_logger.session_id:
return None
session_config = self.agent_loop.session_logger.session_config
session_path = SessionLoader.does_session_exist(
self.agent_loop.session_logger.session_id, session_config
)
if session_path is None:
return None
return self.agent_loop.session_logger.session_id[:8]
async def _exit_app(self) -> None:
@ -1023,7 +1045,8 @@ class VibeApp(App): # noqa: PLR0904
async def _maybe_show_plan_offer(self) -> None:
if self._plan_offer_shown:
return
action = await self._resolve_plan_offer_action()
action, plan_type = await self._resolve_plan_offer_action()
self.plan_type = plan_type
if action is PlanOfferAction.NONE:
return
url = ACTION_TO_URL[action]
@ -1035,12 +1058,12 @@ class VibeApp(App): # noqa: PLR0904
await self._mount_and_scroll(PlanOfferMessage(text))
self._plan_offer_shown = True
async def _resolve_plan_offer_action(self) -> PlanOfferAction:
async def _resolve_plan_offer_action(self) -> tuple[PlanOfferAction, PlanType]:
try:
active_model = self.config.get_active_model()
provider = self.config.get_provider_for_model(active_model)
except ValueError:
return PlanOfferAction.NONE
return PlanOfferAction.NONE, PlanType.UNKNOWN
api_key_env = provider.api_key_env_var
api_key = getenv(api_key_env) if api_key_env else None
@ -1051,7 +1074,7 @@ class VibeApp(App): # noqa: PLR0904
logger.warning(
"Plan-offer check failed (%s).", type(exc).__name__, exc_info=True
)
return PlanOfferAction.NONE
return PlanOfferAction.NONE, PlanType.UNKNOWN
async def _finalize_current_streaming_message(self) -> None:
if self._current_streaming_reasoning is not None:

View file

@ -949,21 +949,24 @@ WelcomeBanner {
.whats-new-message {
background: $surface;
border-left: solid $warning;
margin-bottom: 1;
}
.plan-offer-message {
background: $surface;
border-left: solid $warning;
background: transparent;
border: none;
height: auto;
text-align: center;
content-align: center middle;
padding: 1 2;
margin-top: 1;
padding: 0;
margin-top: 0;
color: $foreground-muted;
text-style: dim;
Markdown > *:last-child {
margin-bottom: 0;
link-style: bold underline;
link-style: none;
link-color: $foreground-muted;
link-background-hover: transparent;
link-style-hover: underline;
link-color-hover: $warning;
}
}