"""
Interactive shell for AiCippy.

Provides a rich terminal interface with:
- Live status sidebar with agent visualization
- Task checklist footer
- Keyboard shortcuts
- Beautiful progress indicators
"""

from __future__ import annotations

import asyncio
import contextlib
import re
import signal
import time
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any, Final

from prompt_toolkit import PromptSession
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.history import FileHistory
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.keys import Keys
from prompt_toolkit.styles import Style
from rich.align import Align
from rich.box import ROUNDED
from rich.console import Console, Group
from rich.live import Live
from rich.panel import Panel
from rich.table import Table
from rich.text import Text

from aicippy import __version__
from aicippy.cli.ai_command_executor import AICommandExecutor
from aicippy.cli.mascot import (
    MASCOT_PRIMARY,
    MascotAnimator,
    MascotMood,
)
from aicippy.cli.themes import (
    DEFAULT_THEME_NAME,
    get_available_themes,
    get_theme,
    prompt_theme_selection,
)
from aicippy.cli.ui_components import (
    BRAND_ACCENT,
    BRAND_ERROR,
    BRAND_INFO,
    BRAND_PRIMARY,
    BRAND_SUCCESS,
    BRAND_WARNING,
    STATUS_ICONS,
    AgentWidget,
    InteractiveLayout,
    TaskItem,
    create_error_panel,
    create_input_section,
    create_success_panel,
)
from aicippy.cli.upload_manager import UploadManager, get_working_directory
from aicippy.config import FOUNDER_CANNED_RESPONSE, get_settings, is_founder
from aicippy.memory import (
    ConversationHistory,
    PlatformBackedHistory,
    PlatformBackedTenantMemory,
    TenantMemory,
)
from aicippy.platform.tenant_context import TenantContext
from aicippy.utils.correlation import generate_correlation_id
from aicippy.utils.logging import get_logger

if TYPE_CHECKING:
    from collections.abc import Callable

logger = get_logger(__name__)
console = Console()


# Internal query detection disabled — unrestricted agent mode


def _is_internal_query(text: str) -> bool:
    """Internal query detection disabled for unrestricted operation."""
    return False


# ============================================================================
# Data Classes
# ============================================================================


@dataclass(slots=True)
class AgentStatus:
    """Status of a single agent."""

    id: str
    type: str
    status: str  # running, thinking, error, idle, complete
    progress: int = 0
    message: str = ""
    started_at: datetime | None = None
    tokens: int = 0

    def to_widget(self) -> AgentWidget:
        """Convert to AgentWidget for UI."""
        return AgentWidget(
            id=self.id,
            name=self.type,
            status=self.status,
            progress=self.progress,
            message=self.message,
            tokens=self.tokens,
        )


@dataclass(slots=True)
class SessionState:
    """Current interactive session state."""

    session_id: str
    model: str = "opus"
    mode: str = "agent"
    agents: list[AgentStatus] = field(default_factory=list)
    tokens_used: int = 0
    connected: bool = False
    conversation: list[dict[str, str]] = field(default_factory=list)
    tasks: list[TaskItem] = field(default_factory=list)
    current_task_idx: int = 0
    mascot_mood: str = "idle"  # idle, thinking, working, happy
    mascot_frame: int = 0
    # New fields for status bar
    session_start: datetime = field(default_factory=datetime.now)
    username: str | None = None
    background_tasks: int = 0
    # WebSocket connection fields
    ws_latency_ms: float = 0.0
    ws_reconnecting: bool = False
    ws_reconnect_attempts: int = 0
    # Streaming state
    is_streaming: bool = False
    stream_tokens: int = 0
    stream_content: str = ""
    # Persistent memory
    conversation_history: ConversationHistory | None = None
    tenant_memory: TenantMemory | None = None
    user_id: str | None = None
    # Tenant isolation context
    tenant_ctx: TenantContext | None = None
    # Pending image uploads for next message
    pending_image_paths: list[str] = field(default_factory=list)
    # Active color theme
    active_theme: str = DEFAULT_THEME_NAME
    # Billing / plan state
    plan_info: Any = None  # PlanInfo from billing module
    credit_manager: Any = None  # CreditManager instance
    plan_validator: Any = None  # PlanValidator instance
    is_admin: bool = False
    credits_remaining: int = -1  # -1 = unlimited (admin)
    user_email: str | None = None
    _next_agent_id: int = 1
    _last_layout_key: tuple[str, ...] | None = None

    def add_task(self, content: str) -> None:
        """Add a task to the checklist."""
        self.tasks.append(TaskItem(content=content, status="pending"))

    def start_task(self, idx: int | None = None) -> None:
        """Start a task."""
        idx = idx if idx is not None else self.current_task_idx
        if 0 <= idx < len(self.tasks):
            self.tasks[idx].status = "in_progress"
            self.current_task_idx = idx

    def complete_task(self, idx: int | None = None) -> None:
        """Complete a task."""
        idx = idx if idx is not None else self.current_task_idx
        if 0 <= idx < len(self.tasks):
            self.tasks[idx].status = "completed"
            self.tasks[idx].progress = 100
            if idx == self.current_task_idx and idx + 1 < len(self.tasks):
                self.current_task_idx = idx + 1

    def update_task_progress(self, progress: int, idx: int | None = None) -> None:
        """Update task progress."""
        idx = idx if idx is not None else self.current_task_idx
        if 0 <= idx < len(self.tasks):
            self.tasks[idx].progress = min(100, max(0, progress))

    def get_session_duration(self) -> str:
        """Get session duration as HH:MM:SS string."""
        elapsed = datetime.now() - self.session_start
        total_seconds = int(elapsed.total_seconds())
        hours = total_seconds // 3600
        minutes = (total_seconds % 3600) // 60
        seconds = total_seconds % 60
        return f"{hours:02d}:{minutes:02d}:{seconds:02d}"


# ============================================================================
# Commands
# ============================================================================

# Slash command completions
SLASH_COMMANDS = [
    "/help",
    "/model",
    "/mode",
    "/theme",
    "/agents",
    "/audit",
    "/kb",
    "/tools",
    "/usage",
    "/history",
    "/sessions",
    "/memory",
    "/clear",
    "/export",
    "/shortcuts",
    "/upload",
    "/uploads",
    "/workspace",
    "/plan",
    "/shell",
    "/exec",
    "/quit",
]

# Natural language exit phrases (lowercase) - kept intentionally narrow
# to avoid intercepting legitimate user messages like "stop", "done", etc.
EXIT_PHRASES: set[str] = {
    "quit",
    "exit",
    "q",
    "/quit",
    "/exit",
    "bye",
    "goodbye",
    "logout",
    "/logout",
}

# Maximum allowed user input length (characters)
MAX_INPUT_LENGTH: int = 32000

MODEL_OPTIONS = ["opus", "sonnet", "haiku", "llama"]
MODE_OPTIONS = ["agent", "edit", "research", "code"]


# ============================================================================
# Slash Command Handler
# ============================================================================


class SlashCommandHandler:
    """Handler for slash commands in interactive mode."""

    def __init__(self, state: SessionState, console: Console) -> None:
        self.state = state
        self.console = console
        self._upload_manager: UploadManager | None = None
        self._handlers: dict[str, Callable[[list[str]], bool]] = {
            "/help": self._handle_help,
            "/model": self._handle_model,
            "/mode": self._handle_mode,
            "/agents": self._handle_agents,
            "/audit": self._handle_audit,
            "/kb": self._handle_kb,
            "/tools": self._handle_tools,
            "/usage": self._handle_usage,
            "/history": self._handle_history,
            "/sessions": self._handle_sessions,
            "/memory": self._handle_memory,
            "/clear": self._handle_clear,
            "/export": self._handle_export,
            "/shortcuts": self._handle_shortcuts,
            "/upload": self._handle_upload,
            "/uploads": self._handle_uploads,
            "/workspace": self._handle_workspace,
            "/theme": self._handle_theme,
            "/plan": self._handle_plan,
            "/shell": self._handle_shell,
            "/exec": self._handle_shell,  # Alias for /shell
            "/quit": self._handle_quit,
        }
        self._shell_executor = None
        # Coroutine to be awaited by the main loop (set by async-aware handlers)
        self._pending_coro: Any = None

    @property
    def upload_manager(self) -> UploadManager:
        """Get or create upload manager."""
        if self._upload_manager is None:
            self._upload_manager = UploadManager(console=self.console)
        return self._upload_manager

    def handle(self, command: str) -> bool:
        """Handle a slash command. Returns False to exit."""
        parts = command.strip().split()
        if not parts:
            return True

        cmd = parts[0].lower()
        args = parts[1:]

        handler = self._handlers.get(cmd)
        if handler:
            return handler(args)

        self.console.print(
            create_error_panel(
                f"Unknown command: {cmd}", suggestion="Type /help for available commands"
            )
        )
        return True

    def _handle_help(self, _args: list[str]) -> bool:
        """Show help information."""
        help_table = Table(
            title="[bold]Available Commands[/bold]",
            border_style=BRAND_PRIMARY,
            title_style=f"bold {BRAND_INFO}",
            header_style=f"bold {BRAND_ACCENT}",
        )
        help_table.add_column("Command", style=BRAND_INFO)
        help_table.add_column("Description", style="white")

        commands = [
            ("/help", "Show this help message"),
            ("/model <opus|sonnet|haiku|llama>", "Switch AI model"),
            ("/mode <agent|edit|research|code>", "Change operation mode"),
            ("/theme [name|number]", "Switch color theme"),
            ("/audit [scope]", "Run AI-powered security audit (via aivedha.ai)"),
            ("/agents spawn <1-10>", "Spawn parallel agents"),
            ("/agents list", "List active agents"),
            ("/agents stop [id|all]", "Stop agent(s)"),
            ("/agents status", "List agents (alias for /agents list)"),
            ("/sessions", "List previous conversation sessions"),
            ("/memory [set|get|clear]", "Manage persistent tenant memory"),
            ("/upload", "Upload files for multimodal AI processing"),
            ("/uploads", "List all uploaded files"),
            ("/workspace", "Show current working directory info"),
            ("/shell <cmd>", "Execute shell command with input passthrough"),
            ("/exec <cmd>", "Alias for /shell"),
            ("/kb sync", "Push local files to Knowledge Base"),
            ("/kb status", "Knowledge Base status"),
            ("/kb search <query>", "Search Knowledge Base"),
            ("/tools list", "List available tools"),
            ("/tools enable <name>", "Enable a tool"),
            ("/tools disable <name>", "Disable a tool"),
            ("/plan", "Show subscription & credits info"),
            ("/usage", "Token usage summary"),
            ("/history", "Session history"),
            ("/clear", "Clear conversation"),
            ("/export <json|md>", "Export session"),
            ("/shortcuts", "Show keyboard shortcuts"),
            ("/quit", "Exit AiCippy"),
        ]

        for cmd, desc in commands:
            help_table.add_row(cmd, desc)

        self.console.print(help_table)
        return True

    def _handle_shortcuts(self, _args: list[str]) -> bool:
        """Show keyboard shortcuts."""
        from aicippy.cli.ui_components import KEYBOARD_SHORTCUTS

        shortcuts_table = Table(
            title="[bold]Keyboard Shortcuts[/bold]",
            border_style=BRAND_ACCENT,
            title_style=f"bold {BRAND_ACCENT}",
        )
        shortcuts_table.add_column("Key", style=BRAND_ACCENT)
        shortcuts_table.add_column("Action", style="white")

        for key, desc in KEYBOARD_SHORTCUTS:
            shortcuts_table.add_row(key, desc)

        # Add more shortcuts
        extra_shortcuts = [
            ("Ctrl+L", "Clear screen"),
            ("Ctrl+R", "Reverse search history"),
            ("Alt+.", "Insert last argument"),
            ("Esc", "Cancel current input"),
        ]
        for key, desc in extra_shortcuts:
            shortcuts_table.add_row(key, desc)

        self.console.print(shortcuts_table)
        return True

    def _handle_model(self, args: list[str]) -> bool:
        """Switch AI model."""
        if not args:
            current = Text()
            current.append(f"\n {STATUS_ICONS['complete']} ", style=BRAND_SUCCESS)
            current.append("Current model: ", style="dim")
            current.append(self.state.model.upper(), style=f"bold {BRAND_PRIMARY}")
            current.append("\n\n Available: ", style="dim")
            for i, m in enumerate(MODEL_OPTIONS):
                if i > 0:
                    current.append(" │ ", style="dim")
                style = f"bold {BRAND_SUCCESS}" if m == self.state.model else BRAND_INFO
                current.append(m, style=style)
            current.append("\n")
            self.console.print(Panel(current, border_style="dim"))
            return True

        model = args[0].lower()
        if model not in MODEL_OPTIONS:
            self.console.print(
                create_error_panel(
                    f"Invalid model: {model}", suggestion=f"Choose from: {', '.join(MODEL_OPTIONS)}"
                )
            )
            return True

        self.state.model = model
        # Persist model preference
        if self.state.tenant_memory:
            self.state.tenant_memory.set_preference("last_model", model)
        self.console.print(
            create_success_panel(
                f"Switched to model: {model.upper()}", details="Changes take effect on next query"
            )
        )
        return True

    def _handle_mode(self, args: list[str]) -> bool:
        """Change operation mode."""
        if not args:
            mode_colors = {
                "agent": BRAND_INFO,
                "edit": BRAND_WARNING,
                "research": BRAND_ACCENT,
                "code": BRAND_SUCCESS,
            }
            current = Text()
            current.append(f"\n {STATUS_ICONS['complete']} ", style=BRAND_SUCCESS)
            current.append("Current mode: ", style="dim")
            current.append(
                self.state.mode.upper(), style=f"bold {mode_colors.get(self.state.mode, 'white')}"
            )
            current.append("\n\n Available:\n", style="dim")

            mode_descriptions = {
                "agent": "Multi-agent task orchestration",
                "edit": "Code editing and refactoring",
                "research": "Information gathering and analysis",
                "code": "Code generation and completion",
            }

            for mode, desc in mode_descriptions.items():
                icon = (
                    STATUS_ICONS["complete"] if mode == self.state.mode else STATUS_ICONS["pending"]
                )
                color = mode_colors.get(mode, "white")
                current.append(f"   {icon} ", style=color)
                current.append(f"{mode:<10}", style=f"bold {color}")
                current.append(f" {desc}\n", style="dim")

            self.console.print(Panel(current, border_style="dim"))
            return True

        mode = args[0].lower()
        if mode not in MODE_OPTIONS:
            self.console.print(
                create_error_panel(
                    f"Invalid mode: {mode}", suggestion=f"Choose from: {', '.join(MODE_OPTIONS)}"
                )
            )
            return True

        self.state.mode = mode
        self.console.print(
            create_success_panel(
                f"Switched to mode: {mode.upper()}",
                details="Mode affects prompt styling. AI behavior customization coming soon.",
            )
        )
        return True

    def _handle_theme(self, args: list[str]) -> bool:
        """Switch the active color theme.

        Usage:
            /theme              - Show theme selector panel
            /theme <name>       - Switch to named theme directly
            /theme <number>     - Switch by number (1-5)
        """
        available = get_available_themes()

        if not args:
            # Interactive theme selection
            try:
                selected = prompt_theme_selection(self.console, self.state.active_theme)
                self.state.active_theme = selected
                if self.state.tenant_memory:
                    self.state.tenant_memory.set_preference("theme", selected)
            except Exception:
                logger.warning("theme_selection_prompt_failed", exc_info=True)
                self.console.print(
                    create_error_panel(
                        "Theme selection failed",
                        suggestion="Use /theme <name> to set a theme directly. "
                        f"Available: {', '.join(available)}",
                    )
                )
            return True

        # Direct selection by name or number
        choice = " ".join(args)

        # Try numeric
        try:
            idx = int(choice)
            if 1 <= idx <= len(available):
                selected = available[idx - 1]
                self.state.active_theme = selected
                theme = get_theme(selected)
                if self.state.tenant_memory:
                    self.state.tenant_memory.set_preference("theme", selected)
                self.console.print(
                    create_success_panel(
                        f"Theme set to: {selected}",
                        details=theme.description,
                    )
                )
                return True
        except ValueError:
            logger.debug("theme_index_parse_failed", exc_info=True)

        # Try name match (case-insensitive partial)
        choice_lower = choice.lower()
        for name in available:
            if choice_lower in name.lower():
                self.state.active_theme = name
                theme = get_theme(name)
                if self.state.tenant_memory:
                    self.state.tenant_memory.set_preference("theme", name)
                self.console.print(
                    create_success_panel(
                        f"Theme set to: {name}",
                        details=theme.description,
                    )
                )
                return True

        # No match
        self.console.print(
            create_error_panel(
                f"Unknown theme: {choice}", suggestion=f"Available: {', '.join(available)}"
            )
        )
        return True

    def _handle_agents(self, args: list[str]) -> bool:
        """Handle agent commands."""
        if not args:
            count = len(self.state.agents)
            self.console.print(
                Panel(Text(f" ⚡ Active agents: {count}/10", style=BRAND_INFO), border_style="dim")
            )
            return True

        subcommand = args[0].lower()

        if subcommand == "spawn":
            try:
                count = int(args[1]) if len(args) > 1 else 3
            except ValueError:
                self.console.print(
                    create_error_panel(
                        f"Invalid agent count: {args[1]}",
                        suggestion="Provide a number between 1 and 10",
                    )
                )
                return True
            count = max(1, min(10, count))

            # Enforce maximum of 10 agents total
            available_slots = 10 - len(self.state.agents)
            if available_slots <= 0:
                self.console.print(
                    create_error_panel(
                        "Maximum of 10 agents already reached",
                        suggestion="Use /agents stop to remove agents first",
                    )
                )
                return True
            count = min(count, available_slots)

            # Create placeholder agents with monotonic IDs
            for i in range(count):
                agent_id = f"agent-{self.state._next_agent_id}"
                self.state._next_agent_id += 1
                self.state.agents.append(
                    AgentStatus(
                        id=agent_id,
                        type=f"specialist-{i + 1}",
                        status="idle",
                        progress=0,
                    )
                )

            self.console.print(
                create_success_panel(
                    f"Spawned {count} agents",
                    details="Agent pool configured. Parallelism per plan limits.",
                )
            )
            return True

        elif subcommand == "list":
            if not self.state.agents:
                self.console.print(
                    Panel(Text(" No active agents", style="dim italic"), border_style="dim")
                )
                return True

            table = Table(
                title="[bold]Active Agents[/bold]",
                border_style=BRAND_SUCCESS,
                title_style=f"bold {BRAND_INFO}",
            )
            table.add_column("ID", style=BRAND_INFO)
            table.add_column("Type", style=BRAND_ACCENT)
            table.add_column("Status")
            table.add_column("Progress")
            table.add_column("Tokens", justify="right")

            for agent in self.state.agents:
                status_color = {
                    "running": BRAND_INFO,
                    "thinking": BRAND_WARNING,
                    "error": BRAND_ERROR,
                    "idle": "dim",
                    "complete": BRAND_SUCCESS,
                }.get(agent.status, "white")

                icon = STATUS_ICONS.get(agent.status, "○")

                # Progress bar
                bar_width = 10
                completed = int(agent.progress / 100 * bar_width)
                bar = "█" * completed + "░" * (bar_width - completed)

                table.add_row(
                    agent.id,
                    agent.type,
                    f"[{status_color}]{icon} {agent.status}[/]",
                    f"[{status_color}]{bar}[/] {agent.progress}%",
                    f"{agent.tokens:,}",
                )

            self.console.print(table)
            return True

        elif subcommand == "stop":
            target = args[1] if len(args) > 1 else "all"
            if target == "all":
                count = len(self.state.agents)
                self.state.agents.clear()
                self.console.print(create_success_panel(f"Stopped all {count} agents"))
            else:
                before = len(self.state.agents)
                self.state.agents = [a for a in self.state.agents if a.id != target]
                if len(self.state.agents) < before:
                    self.console.print(create_success_panel(f"Stopped agent: {target}"))
                else:
                    self.console.print(
                        create_error_panel(
                            f"Agent not found: {target}",
                            suggestion="Use /agents list to see agent IDs",
                        )
                    )
            return True

        elif subcommand == "status":
            return self._handle_agents(["list"])

        self.console.print(
            create_error_panel(
                f"Unknown agent subcommand: {subcommand}",
                suggestion="Use: spawn, list, stop, or status",
            )
        )
        return True

    def _handle_audit(self, args: list[str]) -> bool:
        """Run AI-powered security audit via api.aicippy.com + aivedha.ai.

        Usage:
            /audit              - Run full security audit
            /audit quick        - Run quick security scan
            /audit targeted     - Run targeted scan on changed files

        This is a cross-service feature that does NOT require CHAKRA plan
        validation. Uses the same auth.aivibe.cloud JWT token.
        """
        import asyncio as _audit_asyncio
        import time as _audit_time

        from aicippy.security.audit_client import SecurityAuditClient
        from aicippy.security.formatter import SecurityAuditFormatter
        from aicippy.security.models import AuditFinding, AuditStatus

        # 1. Check for auth token in session state
        auth_token: str | None = None
        try:
            from aicippy.auth.cognito import CognitoAuth

            auth = CognitoAuth()
            tokens = auth.get_current_tokens()
            if tokens and not tokens.is_expired():
                auth_token = tokens.access_token
        except Exception:
            logger.warning("auth_token_retrieval_failed", exc_info=True)

        if not auth_token:
            self.console.print(
                create_error_panel(
                    "Authentication required for security audit",
                    suggestion="Please log in first with 'aicippy login'",
                )
            )
            return True

        # Determine scan scope from args (NO plan check - special case)
        scan_scope = "full"
        if args:
            scope_arg = args[0].lower()
            if scope_arg in ("full", "quick", "targeted"):
                scan_scope = scope_arg
            else:
                self.console.print(
                    create_error_panel(
                        f"Invalid audit scope: {scope_arg}",
                        suggestion="Valid scopes: full, quick, targeted",
                    )
                )
                return True

        # 2. Create SecurityAuditClient with token (NO plan check)
        formatter = SecurityAuditFormatter(self.console)

        async def _run_audit() -> None:
            async with SecurityAuditClient(
                auth_token=auth_token,
                base_url=get_settings().app_api_url,
                ws_url=get_settings().websocket_url,
            ) as client:
                # 3. Show spinner "Starting security audit..."
                self.console.print(
                    Panel(
                        Text(
                            f" Starting {scan_scope} security audit...",
                            style=f"bold {BRAND_INFO}",
                        ),
                        border_style=BRAND_INFO,
                    )
                )

                start_time = _audit_time.monotonic()

                try:
                    audit_id = await client.start_audit(scan_scope=scan_scope)
                except Exception as e:
                    self.console.print(
                        create_error_panel(
                            f"Failed to start security audit: {e}",
                            suggestion="Check your connection and try again",
                        )
                    )
                    return

                self.console.print(
                    Text(
                        f"  Audit ID: {audit_id}",
                        style="dim",
                    )
                )

                # 4. Stream results via WebSocket with progress display
                findings_count = 0

                def on_finding(finding: AuditFinding) -> None:
                    nonlocal findings_count
                    findings_count += 1
                    formatter.render_streaming_finding(finding)

                try:
                    result = await client.stream_audit_results(
                        audit_id=audit_id,
                        callback=on_finding,
                    )
                except (ConnectionError, TimeoutError):
                    # Fallback to polling if WebSocket fails
                    self.console.print(
                        Text(
                            "  WebSocket unavailable, polling for results...",
                            style="dim italic",
                        )
                    )

                    result = None
                    for _poll_attempt in range(60):
                        await _audit_asyncio.sleep(2.0)
                        try:
                            result = await client.get_audit_status(audit_id)
                            elapsed = _audit_time.monotonic() - start_time
                            self.console.print(
                                formatter.build_progress_panel(
                                    status=result.status.value,
                                    findings_count=len(result.findings),
                                    elapsed_seconds=elapsed,
                                )
                            )
                            if result.status in (
                                AuditStatus.COMPLETE,
                                AuditStatus.ERROR,
                            ):
                                break
                        except Exception as poll_err:
                            logger.warning(
                                "security_audit_poll_error",
                                error=str(poll_err),
                            )
                            continue

                    if result is None:
                        self.console.print(
                            create_error_panel(
                                "Security audit timed out",
                                suggestion="Try again with /audit quick for a faster scan",
                            )
                        )
                        return

                # 5. Format final results using the formatter
                formatter.render_full_report(result)

                # 6. Show fix recommendations for critical/high findings
                critical_high = [
                    f for f in result.findings if f.severity.value in ("critical", "high")
                ]
                if critical_high:
                    self.console.print(
                        Text(
                            f"\n  {len(critical_high)} critical/high finding(s) require attention.",
                            style=f"bold {BRAND_ERROR}",
                        )
                    )
                    self.console.print(
                        Text(
                            "  Use the API to get detailed fix recommendations for each finding.",
                            style="dim",
                        )
                    )

        # Store coroutine for the main loop to await (avoids run_sync deadlock
        # when an event loop is already running in the same thread).
        self._pending_coro = _run_audit()
        return True

    def _handle_kb(self, args: list[str]) -> bool:
        """Handle Knowledge Base commands."""
        if not args:
            self.console.print(
                Panel(
                    Text(
                        " Knowledge Base is not yet configured.\n"
                        " Configure via your AiCippy dashboard at https://aicippy.com",
                        style=f"{BRAND_WARNING}",
                    ),
                    title="[bold]Knowledge Base[/bold]",
                    border_style=BRAND_WARNING,
                )
            )
            return True

        subcommand = args[0].lower()

        if subcommand == "sync":
            self.console.print(
                Panel(
                    Text(
                        " Knowledge Base sync not available - configure via dashboard\n"
                        " https://aicippy.com",
                        style=f"{BRAND_WARNING}",
                    ),
                    border_style=BRAND_WARNING,
                )
            )
            return True

        elif subcommand == "status":
            self.console.print(
                Panel(
                    Text(
                        " Knowledge Base is not yet configured.\n"
                        " Configure via your AiCippy dashboard at https://aicippy.com",
                        style=f"{BRAND_WARNING}",
                    ),
                    title="[bold]Knowledge Base Status[/bold]",
                    border_style=BRAND_WARNING,
                )
            )
            return True

        elif subcommand == "search":
            self.console.print(
                Panel(
                    Text(
                        " Knowledge Base search not available - configure via dashboard\n"
                        " https://aicippy.com",
                        style=f"{BRAND_WARNING}",
                    ),
                    border_style=BRAND_WARNING,
                )
            )
            return True

        self.console.print(
            create_error_panel(
                f"Unknown KB subcommand: {subcommand}", suggestion="Use: sync, status, or search"
            )
        )
        return True

    def _handle_tools(self, args: list[str]) -> bool:
        """Handle tools commands."""
        if not args:
            self.console.print(
                Panel(
                    Text(
                        " No tools configured.\n"
                        " Use the AiCippy dashboard to enable tool integrations.\n"
                        " https://aicippy.com",
                        style=f"{BRAND_WARNING}",
                    ),
                    title="[bold]Tool Integrations[/bold]",
                    border_style=BRAND_WARNING,
                )
            )
            return True

        subcommand = args[0].lower()

        if subcommand == "list":
            self.console.print(
                Panel(
                    Text(
                        " No tools configured.\n"
                        " Use the AiCippy dashboard to enable tool integrations.\n"
                        " https://aicippy.com",
                        style=f"{BRAND_WARNING}",
                    ),
                    title="[bold]Tool Integrations[/bold]",
                    border_style=BRAND_WARNING,
                )
            )
            return True

        elif subcommand in ("enable", "disable"):
            self.console.print(
                Panel(
                    Text(
                        " Tool management available via dashboard at https://aicippy.com",
                        style=f"{BRAND_WARNING}",
                    ),
                    border_style=BRAND_WARNING,
                )
            )
            return True

        self.console.print(
            create_error_panel(
                f"Unknown tools subcommand: {subcommand}",
                suggestion="Use: list, enable, or disable",
            )
        )
        return True

    def _handle_usage(self, _args: list[str]) -> bool:
        """Show token usage and credits."""
        table = Table(
            title="[bold]Token Usage[/bold]",
            border_style=BRAND_WARNING,
        )
        table.add_column("Metric", style=BRAND_INFO)
        table.add_column("Value", style=BRAND_WARNING, justify="right")

        table.add_row("Session Tokens", f"{self.state.tokens_used:,}")
        table.add_row("Stream Tokens (last)", f"{self.state.stream_tokens:,}")
        table.add_row("Model", self.state.model.upper())
        table.add_row("Active Agents", str(len(self.state.agents)))
        table.add_row("Messages", str(len(self.state.conversation)))
        table.add_row("Session Duration", self.state.get_session_duration())

        # Credits info
        if self.state.is_admin:
            table.add_row("Credits", f"[{BRAND_SUCCESS}]Unlimited (Admin)[/{BRAND_SUCCESS}]")
        elif self.state.plan_info is not None:
            credits_total = getattr(self.state.plan_info, "credits_total", 0)
            credits_str = f"{self.state.credits_remaining}/{credits_total}"
            table.add_row("Credits Remaining", credits_str)
            if self.state.credit_manager:
                table.add_row(
                    "Credits Used (session)",
                    str(self.state.credit_manager.session_credits_used),
                )

        self.console.print(table)
        return True

    def _handle_history(self, _args: list[str]) -> bool:
        """Show conversation history (persistent if logged in)."""
        # Use persistent history if available
        messages = []
        if self.state.conversation_history:
            messages = self.state.conversation_history.get_recent(15)
        elif self.state.conversation:
            messages = self.state.conversation[-15:]

        if not messages:
            self.console.print(
                Panel(Text(" No conversation history", style="dim italic"), border_style="dim")
            )
            return True

        persistent_label = " (persistent)" if self.state.conversation_history else ""
        self.console.print(Text(f"\n History{persistent_label}:\n", style=f"bold {BRAND_INFO}"))

        for msg in messages:
            role = msg.get("role", "unknown")
            content = msg.get("content", "")[:120]
            role_color = BRAND_INFO if role == "user" else BRAND_SUCCESS
            icon = ">" if role == "user" else "*"
            timestamp = msg.get("timestamp", "")[:19]

            line = Text()
            line.append(f" {icon} ", style=role_color)
            if timestamp:
                line.append(f"[{timestamp}] ", style="dim")
            line.append(f"{role}: ", style=f"bold {role_color}")
            line.append(content, style="white")
            if len(msg.get("content", "")) > 120:
                line.append("...", style="dim")
            self.console.print(line)

        self.console.print()
        return True

    def _handle_clear(self, _args: list[str]) -> bool:
        """Clear conversation (in-memory and persistent for current session)."""
        self.state.conversation.clear()
        self.state.tasks.clear()
        self.state.current_task_idx = 0
        # NOTE: tokens_used is a session metric - intentionally NOT reset on /clear
        self.state.pending_image_paths.clear()
        self.state.is_streaming = False
        self.state.stream_tokens = 0
        self.state.stream_content = ""
        if self.state.conversation_history:
            self.state.conversation_history.clear()
        self.console.clear()
        self.console.print(create_success_panel("Conversation and tasks cleared"))
        return True

    def _handle_export(self, args: list[str]) -> bool:
        """Export session."""
        import json

        format_type = args[0] if args else "json"

        if format_type not in ("json", "md"):
            self.console.print(
                create_error_panel("Invalid format", suggestion="Use 'json' or 'md'")
            )
            return True

        filename = f"aicippy_session_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{format_type}"

        # Use persistent history if available, else in-memory conversation
        export_messages = self.state.conversation
        if self.state.conversation_history:
            all_msgs = self.state.conversation_history.get_all_messages()
            if all_msgs:
                export_messages = all_msgs

        try:
            if format_type == "json":
                with Path(filename).open("w", encoding="utf-8") as f:
                    json.dump(
                        {
                            "session_id": self.state.session_id,
                            "model": self.state.model,
                            "mode": self.state.mode,
                            "tokens_used": self.state.tokens_used,
                            "conversation": export_messages,
                        },
                        f,
                        indent=2,
                        default=str,
                    )
            else:
                with Path(filename).open("w", encoding="utf-8") as f:
                    f.write("# AiCippy Session Export\n\n")
                    f.write(f"**Session:** {self.state.session_id}\n")
                    f.write(f"**Model:** {self.state.model}\n")
                    f.write(f"**Tokens:** {self.state.tokens_used:,}\n\n")
                    f.write("## Conversation\n\n")
                    for msg in export_messages:
                        role = msg.get("role", "unknown")
                        msg_content = msg.get("content", "")
                        timestamp = msg.get("timestamp", "")
                        ts_header = f" ({timestamp})" if timestamp else ""
                        f.write(f"### {role.title()}{ts_header}\n\n{msg_content}\n\n")

            self.console.print(
                create_success_panel("Session exported", details=f"Saved to: {filename}")
            )
        except PermissionError:
            self.console.print(
                create_error_panel(
                    f"Permission denied: cannot write to {filename}",
                    suggestion="Check file permissions or try exporting to a different directory",
                )
            )
        except OSError as e:
            self.console.print(
                create_error_panel(
                    f"Export failed: {e}",
                    suggestion="Check disk space and file path, then try again",
                )
            )
        return True

    def _handle_upload(self, args: list[str]) -> bool:
        """
        Handle /upload command - opens OS file browser dialog.

        Usage:
            /upload             - Open file browser for current uploads folder
            /upload <path>      - Upload to specific folder
        """
        from pathlib import Path

        target_dir = None
        if args:
            target_path = Path(args[0]).expanduser()
            if target_path.is_dir():
                target_dir = target_path
            else:
                # Create if doesn't exist
                target_dir = target_path
                self.console.print(f"[dim]Creating directory: {target_dir}[/dim]")
                target_dir.mkdir(parents=True, exist_ok=True)

        try:
            results = self.upload_manager.prompt_upload(target_dir=target_dir)

            if results:
                successful = [r for r in results if r.success]
                if successful:
                    # Track image files for multimodal AI processing
                    image_exts = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
                    for r in successful:
                        is_image = any(r.file_name.lower().endswith(ext) for ext in image_exts)
                        if r.file_path and is_image:
                            self.state.pending_image_paths.append(
                                str(r.file_path),
                            )

                    image_count = len(
                        [
                            r
                            for r in successful
                            if any(r.file_name.lower().endswith(ext) for ext in image_exts)
                        ]
                    )

                    tip_msg = "Reference uploaded files in your next prompt."
                    if image_count:
                        tip_msg = (
                            f"{image_count} image(s) queued"
                            " for AI analysis. Your next"
                            " message will include them."
                        )

                    self.console.print(
                        Panel(
                            Text(
                                f"Tip: {tip_msg}\n"
                                '   Example: "Analyze the'
                                ' uploaded image" or "Use '
                                f"{successful[0].file_name}"
                                ' to..."',
                                style="dim italic",
                            ),
                            border_style="dim",
                        )
                    )

        except Exception as e:
            self.console.print(
                create_error_panel(
                    f"Upload failed: {e}", suggestion="Check file permissions and try again"
                )
            )

        return True

    def _handle_uploads(self, args: list[str]) -> bool:
        """
        Handle /uploads command - list uploaded files.

        Usage:
            /uploads            - List all uploaded files
            /uploads clear      - Clear all uploads
        """
        if args and args[0].lower() == "clear":
            if self.upload_manager.clear_uploads():
                self.console.print(create_success_panel("All uploads cleared"))
            else:
                self.console.print(
                    create_error_panel(
                        "Failed to clear uploads", suggestion="Check file permissions and try again"
                    )
                )
            return True

        self.upload_manager.show_uploads_list()
        return True

    def _handle_workspace(self, _args: list[str]) -> bool:
        """
        Handle /workspace command - show working directory info.

        Usage:
            /workspace          - Show current workspace info
        """
        self.upload_manager.show_working_directory()
        return True

    def _handle_shell(self, args: list[str]) -> bool:
        """
        Handle /shell command - execute shell command with input passthrough.

        Usage:
            /shell <command>    - Execute command with interactive input support
            /exec <command>     - Alias for /shell

        Examples:
            /shell git status
            /shell npm install
            /exec pip install requests
        """
        if not args:
            self.console.print(
                Panel(
                    Text(
                        "Usage: /shell <command>\n\n"
                        "Execute shell commands with interactive input passthrough.\n"
                        "Any prompts from the command will be displayed for you to respond.\n\n"
                        "Examples:\n"
                        "  /shell git status\n"
                        "  /shell npm install\n"
                        "  /shell pip install requests\n"
                        "  /exec docker login",
                        style=BRAND_INFO,
                    ),
                    title=f"[bold {BRAND_ACCENT}]Shell Command[/bold {BRAND_ACCENT}]",
                    border_style=BRAND_ACCENT,
                )
            )
            return True

        command = " ".join(args)

        try:
            from aicippy.cli.shell_executor import ShellExecutor, is_interactive_command

            if self._shell_executor is None:
                self._shell_executor = ShellExecutor(
                    console=self.console,
                    working_dir=get_working_directory(),
                )

            # Check if command is interactive
            if is_interactive_command(command):
                # Run interactive command with input callback via _pending_coro
                # to avoid run_sync deadlock when event loop is already running
                shell_exec = self._shell_executor

                def get_input(prompt: str) -> str:
                    return shell_exec.prompt_user_input(prompt)

                async def _run_shell() -> None:
                    res = await shell_exec.execute_interactive(
                        command,
                        input_callback=get_input,
                    )
                    if res.stdout:
                        self.state.conversation.append(
                            {
                                "role": "system",
                                "content": (
                                    f"[Shell Command Output]\n$ {command}\n{res.stdout[:2000]}"
                                ),
                            }
                        )
                    shell_exec.display_result(res)

                self._pending_coro = _run_shell()
                return True

            # Simple (non-interactive) command execution
            result = self._shell_executor.execute_simple(command)

            # Store output in conversation for AI context
            if result.stdout:
                self.state.conversation.append(
                    {
                        "role": "system",
                        "content": f"[Shell Command Output]\n$ {command}\n{result.stdout[:2000]}",
                    }
                )

            # Display result
            self._shell_executor.display_result(result)

        except Exception as e:
            self.console.print(
                create_error_panel(
                    f"Shell execution failed: {e}",
                    suggestion="Check the command syntax and try again",
                )
            )

        return True

    def _handle_sessions(self, _args: list[str]) -> bool:
        """List previous conversation sessions."""
        if not self.state.conversation_history:
            self.console.print(
                Panel(
                    Text(" No persistent history available (not logged in)", style="dim italic"),
                    border_style="dim",
                )
            )
            return True

        sessions = self.state.conversation_history.list_sessions()
        if not sessions:
            self.console.print(
                Panel(Text(" No previous sessions found", style="dim italic"), border_style="dim")
            )
            return True

        table = Table(
            title="[bold]Previous Sessions[/bold]",
            border_style=BRAND_PRIMARY,
            title_style=f"bold {BRAND_INFO}",
        )
        table.add_column("Session ID", style=BRAND_INFO)
        table.add_column("Last Updated", style="white")
        table.add_column("Messages", style=BRAND_ACCENT, justify="right")

        for s in sessions[:15]:
            table.add_row(
                s["session_id"],
                s.get("updated_at", "")[:19],
                str(s.get("message_count", 0)),
            )

        self.console.print(table)
        return True

    def _handle_memory(self, args: list[str]) -> bool:
        """Show or manage tenant memory."""
        if not self.state.tenant_memory:
            self.console.print(
                Panel(
                    Text(" No tenant memory available (not logged in)", style="dim italic"),
                    border_style="dim",
                )
            )
            return True

        if args and args[0].lower() == "clear":
            self.state.tenant_memory.clear()
            self.console.print(create_success_panel("Tenant memory cleared"))
            return True

        if args and args[0].lower() == "set":
            if len(args) < 3:
                self.console.print(
                    create_error_panel(
                        "Missing arguments for /memory set",
                        suggestion="Usage: /memory set <key> <value>",
                    )
                )
                return True
            key = args[1]
            value = " ".join(args[2:])
            self.state.tenant_memory.set(key, value)
            self.console.print(create_success_panel(f"Memory set: {key}"))
            return True

        if args and args[0].lower() == "get":
            if len(args) < 2:
                self.console.print(
                    create_error_panel(
                        "Missing key for /memory get",
                        suggestion="Usage: /memory get <key>",
                    )
                )
                return True
            key = args[1]
            value = self.state.tenant_memory.get(key)
            if value is not None:
                self.console.print(
                    Panel(Text(f" {key} = {value}", style=BRAND_INFO), border_style="dim")
                )
            else:
                self.console.print(
                    Panel(Text(f" Key '{key}' not found", style="dim italic"), border_style="dim")
                )
            return True

        # Default: show summary
        summary = self.state.tenant_memory.get_summary()
        keys = self.state.tenant_memory.list_keys()

        if not summary and not keys:
            self.console.print(
                Panel(
                    Text(
                        " Tenant memory is empty\n\n"
                        " Usage:\n"
                        "   /memory set <key> <value>\n"
                        "   /memory get <key>\n"
                        "   /memory clear",
                        style="dim",
                    ),
                    border_style="dim",
                )
            )
            return True

        table = Table(
            title="[bold]Tenant Memory[/bold]",
            border_style=BRAND_ACCENT,
            title_style=f"bold {BRAND_ACCENT}",
        )
        table.add_column("Key", style=BRAND_INFO)
        table.add_column("Value", style="white", max_width=60)

        for key in keys[:20]:
            value = self.state.tenant_memory.get(key, "")
            display_val = str(value)[:60]
            if len(str(value)) > 60:
                display_val += "..."
            table.add_row(key, display_val)

        self.console.print(table)
        return True

    def _handle_plan(self, _args: list[str]) -> bool:
        """Show subscription and credits information."""
        plan_info = self.state.plan_info
        if plan_info is None:
            self.console.print(
                Panel(
                    Text(" Plan info unavailable (billing not initialized)", style="dim italic"),
                    border_style="dim",
                )
            )
            return True

        table = Table(
            title="[bold]AiVibe Subscription[/bold]",
            border_style=BRAND_PRIMARY,
            title_style=f"bold {BRAND_INFO}",
        )
        table.add_column("Property", style=BRAND_INFO)
        table.add_column("Value", style="white")

        try:
            is_admin = getattr(plan_info, "is_admin", False)
            if is_admin:
                table.add_row("Role", f"[bold {BRAND_SUCCESS}]Admin[/bold {BRAND_SUCCESS}]")
                table.add_row("Credits", f"[{BRAND_SUCCESS}]Unlimited[/{BRAND_SUCCESS}]")
                table.add_row(
                    "Plan Restrictions",
                    f"[{BRAND_SUCCESS}]None (admin bypass)[/{BRAND_SUCCESS}]",
                )
            else:
                plan_name = getattr(plan_info, "plan", "unknown")
                plan_display = plan_name.upper() if isinstance(plan_name, str) else str(plan_name)
                status = getattr(plan_info, "status", None)
                status_val = status.value if status and hasattr(status, "value") else "unknown"
                status_color = BRAND_SUCCESS if status_val == "active" else BRAND_ERROR
                table.add_row("Plan", f"[bold]{plan_display}[/bold]")
                table.add_row(
                    "Status",
                    f"[{status_color}]{status_val.upper()}[/{status_color}]",
                )
                table.add_row("Tenant ID", getattr(plan_info, "tenant_id", "N/A"))

                # Credits display
                credits_total = getattr(plan_info, "credits_total", 0)
                credits_remaining = getattr(plan_info, "credits_remaining", 0)
                if credits_total > 0:
                    used = credits_total - credits_remaining
                    pct = max(0, min(100, int(credits_remaining / credits_total * 100)))
                    bar_w = 20
                    filled = max(0, min(bar_w, int(pct / 100 * bar_w)))
                    bar = "\u2588" * filled + "\u2591" * (bar_w - filled)
                    credits_display = (
                        f"{credits_remaining}/{credits_total} ({used} used) [{bar}] {pct}%"
                    )
                else:
                    credits_display = str(credits_remaining)
                table.add_row("Credits Remaining", credits_display)

                # Session credits
                if self.state.credit_manager:
                    table.add_row(
                        "Session Credits Used",
                        str(getattr(self.state.credit_manager, "session_credits_used", 0)),
                    )

                # Expiry
                expiry = getattr(plan_info, "expiry", None)
                if expiry:
                    expiry_str = expiry.strftime("%Y-%m-%d %H:%M UTC")
                    table.add_row("Period Ends", expiry_str)
        except AttributeError as attr_err:
            logger.warning("plan_info_attribute_error", error=str(attr_err))
            table.add_row(
                "Error",
                f"[{BRAND_ERROR}]Plan data incomplete: {attr_err}[/{BRAND_ERROR}]",
            )

        self.console.print(table)
        return True

    def _handle_quit(self, _args: list[str]) -> bool:
        """Exit the session - save state before returning."""
        saved = _save_session_state(self.state)
        if saved == 0 and self.state.tenant_memory and self.state.tenant_memory.list_keys():
            self.console.print(
                Panel(
                    Text(
                        " Warning: session state may not have saved completely.",
                        style=f"bold {BRAND_WARNING}",
                    ),
                    border_style=BRAND_WARNING,
                )
            )
        return False


# ============================================================================
# Session State Helpers
# ============================================================================


def _save_session_state(state: SessionState) -> int:
    """
    Save all persistent session state to disk.

    Saves conversation history, tenant memory preferences, and session context.
    Called on any exit path (/quit, Ctrl+C, Ctrl+D, SIGTERM, natural exit).

    Returns:
        Number of memory entries saved.
    """
    memory_entries_saved = 0

    try:
        if state.tenant_memory:
            state.tenant_memory.set_preference("last_model", state.model)
            state.tenant_memory.set_preference("theme", state.active_theme)
            state.tenant_memory.set_context(
                "last_session",
                {
                    "session_id": state.session_id,
                    "tokens_used": state.tokens_used,
                    "message_count": len(state.conversation),
                    "duration": state.get_session_duration(),
                },
            )
            memory_entries_saved = len(state.tenant_memory.list_keys())
    except Exception:
        logger.warning("tenant_memory_save_failed", exc_info=True)

    try:
        if state.conversation_history:
            state.conversation_history.cleanup_old_sessions()
    except Exception:
        logger.warning("conversation_history_cleanup_failed", exc_info=True)

    logger.info(
        "session_state_saved",
        session_id=state.session_id,
        tokens_used=state.tokens_used,
        messages=len(state.conversation),
        memory_entries=memory_entries_saved,
    )

    return memory_entries_saved


# Prefix used to identify tasks created by the interactive session
_TASK_NAME_PREFIX = "aicippy_interactive_"


async def _cancel_pending_tasks() -> None:
    """Cancel pending async tasks created by the interactive session.

    Only cancels tasks whose name starts with the session prefix to avoid
    interfering with tasks owned by other parts of the application (e.g.
    background updater, event loop internals).
    """
    current_task = asyncio.current_task()
    session_tasks = [
        t
        for t in asyncio.all_tasks()
        if t is not current_task
        and not t.done()
        and (t.get_name() or "").startswith(_TASK_NAME_PREFIX)
    ]
    for task in session_tasks:
        task.cancel()
    if session_tasks:
        await asyncio.gather(*session_tasks, return_exceptions=True)


async def _close_orchestrator_if_exists(orchestrator: object) -> None:
    """Close an orchestrator instance if it has a close method."""
    try:
        if orchestrator is not None and hasattr(orchestrator, "close"):
            await orchestrator.close()
    except Exception:
        logger.warning("orchestrator_close_failed", exc_info=True)


# ============================================================================
# Main Interactive Session
# ============================================================================


async def start_interactive_session() -> None:
    """Start the interactive AiCippy session with beautiful UI."""
    settings = get_settings()

    # Start background auto-updater (24-hour cycle)
    bg_updater = None
    try:
        from aicippy.installer.auto_updater import get_auto_updater

        bg_updater = get_auto_updater()
        if bg_updater.needs_check():
            bg_updater.start_background()
    except Exception:
        logger.debug("auto_updater_init_failed", exc_info=True)

    # Generate session ID
    session_id = generate_correlation_id()

    # Initialize state
    state = SessionState(
        session_id=session_id,
        model=settings.default_model,
    )

    # Add initial demo tasks
    state.add_task("Initialize session")
    state.start_task(0)

    # Create command handler
    cmd_handler = SlashCommandHandler(state, console)

    # Create the new interactive layout
    layout = InteractiveLayout(console, __version__)

    # Create prompt session with history
    history_file = settings.local_config_dir / "history"
    history_file.parent.mkdir(parents=True, exist_ok=True)

    # Custom prompt style - use bright white for input text (visible on both dark/light terminals)
    prompt_style = Style.from_dict(
        {
            "prompt": f"{BRAND_PRIMARY} bold",
            "": "#e0e0e0 bold",  # Bright white for better contrast on all terminals
        }
    )

    # Key bindings
    kb = KeyBindings()

    @kb.add(Keys.ControlL)
    def clear_screen(_event: Any) -> None:
        """Clear the screen and preview."""
        console.clear()
        layout.clear_preview()

    completer = WordCompleter(
        SLASH_COMMANDS + MODEL_OPTIONS + MODE_OPTIONS + ["quit", "exit", "q", "bye"],
        ignore_case=True,
    )

    session: PromptSession[str] = PromptSession(
        history=FileHistory(str(history_file)),
        auto_suggest=AutoSuggestFromHistory(),
        completer=completer,
        style=prompt_style,
        key_bindings=kb,
    )

    # Skip welcome animation - shown during login
    # Just complete initialization silently
    state.complete_task(0)

    # Check authentication and get username (silently)
    try:
        from aicippy.auth.cognito import CognitoAuth
        from aicippy.installer.session_manager import get_current_user

        auth = CognitoAuth()
        if auth.is_authenticated():
            state.connected = True
            # Get username from session
            username, email = get_current_user()
            state.username = username or email or "authenticated"
            state.user_email = email or username or ""

            # Get user_id for memory isolation
            try:
                from aicippy.cli.main import get_session_manager

                sm = get_session_manager()
                validation = sm.validate()
                if validation.session:
                    state.user_id = validation.session.user_id
            except Exception:
                logger.warning("session_user_id_retrieval_failed", exc_info=True)

            # Fall back to email-based user_id if session didn't provide one
            if not state.user_id:
                state.user_id = email or username or "anonymous"

            # Initialize persistent conversation history and tenant memory
            state.conversation_history = ConversationHistory(
                session_id=session_id,
                tenant_ctx=state.tenant_ctx,
                user_id=state.user_id,
            )
            state.tenant_memory = TenantMemory(
                tenant_ctx=state.tenant_ctx,
                user_id=state.user_id,
            )

            # Store last model preference
            last_model = state.tenant_memory.get_preference("last_model")
            if last_model and last_model in ("opus", "sonnet", "haiku", "llama"):
                state.model = last_model

            # Theme restoration is handled after platform memory upgrade (below)

            logger.info(
                "memory_initialized",
                user_id=state.user_id,
                history_messages=state.conversation_history.message_count,
                memory_keys=len(state.tenant_memory.list_keys()),
            )

            # Show brief status if reconnecting to existing session
            if state.conversation_history.message_count > 0:
                resuming_text = Text()
                resuming_text.append("Resuming session...", style="dim italic")
                console.print(resuming_text)
    except Exception as auth_check_err:
        logger.warning("auth_check_during_session_init_failed", error=str(auth_check_err))

    # Initialize billing (plan validation + credit manager) via platform API
    platform_client = None
    app_client = None
    try:
        from aicippy.billing.credit_manager import CreditManager
        from aicippy.billing.plan_validator import PlanValidator

        # Determine email for billing lookup (reuse from auth check above if available)
        billing_email = state.user_email if state.user_email else None
        if not billing_email:
            try:
                from aicippy.installer.session_manager import get_current_user as _get_user

                _u, _e = _get_user()
                billing_email = _e or _u
            except Exception:
                logger.warning("billing_email_retrieval_failed", exc_info=True)

        if billing_email:
            state.user_email = billing_email

            # Reuse validated plan_info and platform_client from login flow if available
            cached_plan_info = None
            try:
                from aicippy.cli.main import get_login_context

                cached_plan_info, cached_platform_client = get_login_context()
                if cached_platform_client is not None:
                    platform_client = cached_platform_client
            except Exception:
                logger.warning("login_context_cache_retrieval_failed", exc_info=True)

            if platform_client is None:
                # Try to create PlatformClient with stored auth token
                try:
                    from aicippy.auth.cognito import CognitoAuth
                    from aicippy.platform.app_client import AppClient
                    from aicippy.platform.client import PlatformClient

                    auth_inst = CognitoAuth()
                    tokens = auth_inst.get_current_tokens()
                    if tokens and not tokens.is_expired():
                        platform_client = PlatformClient(
                            base_url=settings.platform_api_url,
                            auth_token=tokens.access_token,
                        )
                except Exception:
                    logger.warning(
                        "platform_client_creation_failed_fallback_to_dynamodb",
                        exc_info=True,
                    )

            # Always create validator (needed for CreditManager)
            validator = PlanValidator(platform_client=platform_client)

            # Reuse cached plan_info if still fresh, otherwise re-validate
            if cached_plan_info is not None and not cached_plan_info.is_cache_stale:
                plan_info = cached_plan_info
                logger.debug("plan_info_reused_from_login_cache")
            else:
                try:
                    plan_info = validator.validate(billing_email)
                except Exception as plan_err:
                    logger.warning(
                        "plan_validation_failed_proceeding_degraded",
                        error=str(plan_err),
                    )
                    console.print(
                        f"[{BRAND_WARNING}]  Plan verification unavailable."
                        f" Running in limited mode.[/{BRAND_WARNING}]"
                    )
                    plan_info = None

            state.plan_validator = validator
            if plan_info is not None:
                state.plan_info = plan_info
                state.is_admin = plan_info.is_admin
                state.credits_remaining = plan_info.credits_remaining

                # Build TenantContext for strict tenant isolation
                if plan_info.tenant_id and state.user_id:
                    try:
                        state.tenant_ctx = TenantContext(
                            tenant_id=plan_info.tenant_id,
                            user_id=state.user_id,
                            email=state.user_email or "",
                            org_tenant_id=getattr(plan_info, "org_tenant_id", ""),
                        )
                        logger.info(
                            "tenant_context_created",
                            tenant_id=state.tenant_ctx.tenant_id,
                            user_id=state.tenant_ctx.user_id,
                        )
                    except ValueError as tc_err:
                        logger.warning(
                            "tenant_context_creation_failed",
                            error=str(tc_err),
                        )
            else:
                state.is_admin = False
                state.credits_remaining = 0

            credit_mgr = CreditManager(validator, platform_client=platform_client)
            state.credit_manager = credit_mgr

            # Create AppClient for app-specific operations (memory, logs)
            if platform_client is not None and plan_info is not None and plan_info.tenant_id:
                try:
                    from aicippy.auth.cognito import CognitoAuth as _CognitoAuth
                    from aicippy.platform.app_client import AppClient

                    _auth = _CognitoAuth()
                    tokens = _auth.get_current_tokens()
                    if tokens:
                        app_client = AppClient(
                            base_url=settings.app_api_url,
                            auth_token=tokens.access_token,
                            tenant_id=plan_info.tenant_id,
                        )
                except Exception:
                    logger.warning("app_client_creation_failed", exc_info=True)

            if plan_info is not None and not plan_info.is_valid_for_aicippy:
                # Log but don't block — allow degraded access for founder
                logger.warning(
                    "plan_not_valid_proceeding_degraded",
                    reason=plan_info.denial_reason,
                )
                console.print(
                    f"[{BRAND_WARNING}]  Plan not fully validated."
                    f" Proceeding with available access.[/{BRAND_WARNING}]"
                )

            logger.info(
                "billing_initialized",
                email=billing_email,
                plan=plan_info.plan if plan_info else "unverified",
                credits=plan_info.credits_remaining if plan_info else 0,
                is_admin=plan_info.is_admin if plan_info else False,
            )
    except Exception as billing_err:
        # Fail-open for founder/admin: allow access when billing backend is unreachable
        logger.warning("billing_init_failed_proceeding", error=str(billing_err))
        console.print(
            f"[{BRAND_WARNING}]  Billing service unavailable."
            f" Running with full access.[/{BRAND_WARNING}]"
        )
        state.is_admin = True
        state.credits_remaining = -1

    # Upgrade to platform-backed memory if AppClient is available
    if app_client is not None and state.user_id:
        try:
            state.conversation_history = PlatformBackedHistory(
                session_id=session_id,
                app_client=app_client,
                tenant_ctx=state.tenant_ctx,
                user_id=state.user_id,
            )
            state.tenant_memory = PlatformBackedTenantMemory(
                app_client=app_client,
                tenant_ctx=state.tenant_ctx,
                user_id=state.user_id,
            )
            logger.info("platform_backed_memory_enabled", user_id=state.user_id)
        except Exception as mem_err:
            logger.warning("platform_memory_upgrade_failed", error=str(mem_err))
            # Keep using local memory (already initialized)

    # Restore saved theme silently — no interactive prompt on startup
    if state.tenant_memory:
        saved_theme = state.tenant_memory.get_preference("theme")
        if saved_theme and saved_theme in get_available_themes():
            state.active_theme = saved_theme

    # Setup working directory silently
    working_dir = get_working_directory()

    # Create uploads folder silently
    try:
        uploads_dir = working_dir / "uploads"
        if not uploads_dir.exists():
            uploads_dir.mkdir(parents=True, exist_ok=True)
    except Exception:
        logger.warning("uploads_dir_creation_failed", exc_info=True)

    # Show compact workspace status line
    try:
        status_parts = []
        if state.user_email:
            status_parts.append(state.user_email)
        elif state.username:
            status_parts.append(state.username)
        if state.plan_info:
            plan_label = state.plan_info.plan.upper() if state.plan_info.plan else "PLAN"
            if state.is_admin:
                status_parts.append(f"{plan_label} unlimited credits")
            elif state.credits_remaining >= 0:
                status_parts.append(
                    f"{plan_label} "
                    f"{state.credits_remaining}"
                    f"/{state.plan_info.credits_total}"
                    " credits"
                )
        if state.active_theme and state.active_theme != DEFAULT_THEME_NAME:
            status_parts.append(f"{state.active_theme} theme")
        home_dir = str(working_dir.home())
        wd_str = str(working_dir)
        workspace_display = (
            wd_str.replace(home_dir, "~", 1) if wd_str.startswith(home_dir) else wd_str
        )
        status_parts.append(f"workspace: {workspace_display}")
        status_line = Text()
        status_line.append("\u2713 ", style="green")
        status_line.append(" | ".join(status_parts), style="dim")
        console.print(status_line)
    except Exception:
        logger.warning("workspace_status_display_failed", exc_info=True)

    # Main loop
    running = True
    _sigint_count = 0
    _sigint_pending_message = False  # Flag checked by main loop (signal-safe)
    _last_orchestrator: Any = None

    def signal_handler(_sig: int, _frame: Any) -> None:
        nonlocal running, _sigint_count, _sigint_pending_message
        _sigint_count += 1
        if _sigint_count >= 2:
            # Second Ctrl+C - force quit after saving state
            _save_session_state(state)
            running = False
            raise KeyboardInterrupt
        # Set flag instead of modifying layout directly (avoids reentrancy)
        _sigint_pending_message = True

    def sigterm_handler(_sig: int, _frame: Any) -> None:
        nonlocal running
        _save_session_state(state)
        running = False

    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, sigterm_handler)

    while running:
        try:
            # Reset sigint counter at each prompt iteration
            _sigint_count = 0

            # Process deferred signal handler message (avoids reentrancy)
            if _sigint_pending_message:
                _sigint_pending_message = False
                layout.add_preview_line(
                    "Press Ctrl+C again to force quit, or type /quit",
                    style=BRAND_WARNING,
                )

            # Update and display the layout only when state changes
            _layout_key = (
                state.model,
                state.mode,
                state.connected,
                state.credits_remaining,
                len(state.agents),
                len(state.tasks),
                state.tokens_used,
                state.username,
                state.mascot_mood,
                state.background_tasks,
            )
            if _layout_key != state._last_layout_key:
                state._last_layout_key = _layout_key
                updated_layout = layout.update(
                    agents=[a.to_widget() for a in state.agents],
                    tasks=state.tasks,
                    current_task_idx=state.current_task_idx,
                    session_model=state.model,
                    tokens_used=state.tokens_used,
                    username=state.username,
                    session_duration=state.get_session_duration(),
                    background_tasks=state.background_tasks,
                    connected=state.connected,
                    mascot_mood=state.mascot_mood,
                )
                console.print(updated_layout)

            # Create input prompt
            prompt_text = create_input_section(mode=state.mode, model=state.model)
            console.print(prompt_text, end="")

            # Get input
            user_input = await asyncio.get_running_loop().run_in_executor(
                None,
                lambda: session.prompt(""),
            )

            if not user_input.strip():
                continue

            # Reject excessively long input to prevent API errors and memory issues
            if len(user_input) > MAX_INPUT_LENGTH:
                console.print(
                    create_error_panel(
                        f"Input too long ({len(user_input):,}"
                        f" chars). Maximum is"
                        f" {MAX_INPUT_LENGTH:,} characters.",
                        suggestion="Please shorten your message and try again.",
                    )
                )
                continue

            # Handle quit/exit commands via natural language
            user_input_lower = user_input.strip().lower()
            if user_input_lower in EXIT_PHRASES:
                _save_session_state(state)
                running = False
                continue

            # Handle slash commands
            if user_input.startswith("/"):
                running = cmd_handler.handle(user_input)
                # Await any async coroutine set by the handler (e.g. /audit)
                if cmd_handler._pending_coro is not None:
                    pending = cmd_handler._pending_coro
                    cmd_handler._pending_coro = None
                    try:
                        await pending
                    except Exception as pending_err:
                        logger.warning("pending_coro_error", error=str(pending_err))
                        console.print(
                            create_error_panel(
                                f"Command failed: {pending_err}",
                                suggestion="Check your connection and try again",
                            )
                        )
                continue

            # Internal query restrictions removed — unrestricted agent mode

            # Log user input to preview
            layout.add_preview_line(f"> {user_input}", style=BRAND_INFO)

            # Regular chat message
            state.conversation.append({"role": "user", "content": user_input})

            # Persist to conversation history
            if state.conversation_history:
                state.conversation_history.add_user_message(user_input)

            # Add task for this query
            task_label = user_input[:30] + ("..." if len(user_input) > 30 else "")
            state.add_task(f"Process: {task_label}")
            state.start_task(len(state.tasks) - 1)

            # Collect pending image paths for this message
            current_image_paths = (
                list(state.pending_image_paths) if state.pending_image_paths else None
            )
            state.pending_image_paths.clear()

            # Get conversation history for context continuity
            context_history = None
            if state.conversation_history:
                context_history = state.conversation_history.get_context_messages()

            # Inject tenant memory summary into system context
            memory_context = ""
            if state.tenant_memory:
                memory_context = state.tenant_memory.get_summary()

            # PRE-INVOCATION: Check credits before calling Bedrock
            if state.credit_manager and state.user_email and not state.is_admin:
                has_credits, _remaining = state.credit_manager.check_credits(state.user_email)
                if not has_credits:
                    console.print(
                        create_error_panel(
                            "Monthly credits exhausted",
                            suggestion="Credits reset at the start of your next billing cycle. "
                            "Use /plan to check your subscription details.",
                        )
                    )
                    # Remove the task we just added
                    if state.tasks:
                        state.tasks.pop()
                    continue

            # Process with orchestrator
            try:
                from aicippy.agents.orchestrator import AgentOrchestrator

                orchestrator = AgentOrchestrator(
                    model_id=settings.get_model_id(state.model),
                    max_agents=len(state.agents) if state.agents else 3,
                    tenant_ctx=state.tenant_ctx,
                )
                _last_orchestrator = orchestrator

                # Update agents status and mascot mood
                state.mascot_mood = "thinking"
                state.is_streaming = True
                state.stream_tokens = 0
                state.stream_content = ""

                for agent in state.agents:
                    agent.status = "thinking"
                    agent.progress = 0
                    layout.add_agent_update(agent.id, "thinking", "Processing...")

                # Show processing indicator
                layout.add_preview_line(
                    "Processing...",
                    style=BRAND_WARNING,
                )

                # Show animated processing spinner with live progress
                start_time = time.monotonic()
                animation_running = True
                frame_idx = 0

                async def process_with_streaming(
                    start_time: float = start_time,
                    user_input: str = user_input,
                    memory_context: str = memory_context,
                    orchestrator: AgentOrchestrator = orchestrator,
                    current_image_paths: list[str] | None = current_image_paths,
                    context_history: list[dict[str, str]] | None = context_history,
                ) -> Any:
                    """Process the chat request with live streaming display."""
                    from rich.markdown import Markdown as _StreamMd

                    state.mascot_mood = "working"
                    active_theme = get_theme(state.active_theme)
                    streamed_content = ""
                    token_count = 0

                    def _build_stream_panel(content_so_far: str, elapsed: float) -> Panel:
                        """Build the live streaming response panel."""
                        header = Text()
                        header.append(
                            " \u25c8 ",
                            style=f"bold {active_theme.accent}",
                        )
                        header.append(
                            "Cippy",
                            style=f"bold {active_theme.primary}",
                        )
                        header.append("  \u2502  ", style="dim")
                        header.append(
                            f"{elapsed:.1f}s",
                            style=f"bold {active_theme.info}",
                        )
                        header.append("  \u2502  ", style="dim")
                        header.append(
                            f"{token_count:,} tokens",
                            style=f"{active_theme.warning}",
                        )
                        header.append("  \u2502  ", style="dim")
                        header.append(
                            state.model.upper(),
                            style=f"bold {active_theme.success}",
                        )

                        if content_so_far.strip():
                            try:
                                body = _StreamMd(content_so_far)
                            except Exception:
                                body = Text(content_so_far)
                        else:
                            body = Text("\u2588", style=f"bold {active_theme.accent}")

                        return Panel(
                            body,
                            title=header,
                            title_align="left",
                            border_style=active_theme.primary,
                            box=ROUNDED,
                            padding=(1, 2),
                        )

                    # Build enriched message with memory context
                    enriched_message = user_input
                    if memory_context:
                        enriched_message = (
                            f"[User Context]\n"
                            f"{memory_context}\n\n"
                            f"[User Message]\n"
                            f"{user_input}"
                        )

                    with Live(
                        _build_stream_panel("", 0.0),
                        console=console,
                        transient=True,
                        refresh_per_second=12,
                    ) as live:

                        def on_token(text: str) -> None:
                            nonlocal streamed_content, token_count
                            streamed_content += text
                            token_count += 1
                            state.stream_tokens = token_count
                            state.stream_content = streamed_content
                            elapsed = time.monotonic() - start_time
                            live.update(_build_stream_panel(streamed_content, elapsed))

                        try:
                            result = await orchestrator.chat_stream(
                                enriched_message,
                                image_paths=current_image_paths,
                                conversation_history=context_history,
                                on_token=on_token,
                            )
                            return result
                        except Exception as stream_err:
                            logger.warning(
                                "streaming_failed_fallback",
                                error=str(stream_err),
                            )
                            # Fallback to non-streaming
                            result = await orchestrator.chat(
                                enriched_message,
                                image_paths=current_image_paths,
                                conversation_history=context_history,
                            )
                            return result

                response = await process_with_streaming()

                # Close the orchestrator after each request to free resources
                await _close_orchestrator_if_exists(orchestrator)
                _last_orchestrator = None

                # Mark processing complete
                state.is_streaming = False
                execution_time = time.monotonic() - start_time

                # Mark agents complete
                for agent in state.agents:
                    agent.status = "complete"
                    agent.progress = 100
                    layout.add_agent_update(agent.id, "complete", "Done")

                # Display final response with beautiful themed panel
                active_theme = get_theme(state.active_theme)
                resp_tokens = (
                    (response.usage.input_tokens + response.usage.output_tokens)
                    if response.usage
                    else 0
                )
                resp_header_parts: list[str] = []
                if response.agent_type:
                    resp_header_parts.append(
                        f"[{active_theme.info}]\u25c8 {response.agent_type.value.upper()}[/]"
                    )
                if resp_tokens:
                    resp_header_parts.append(f"[{active_theme.warning}]{resp_tokens:,} tokens[/]")
                if execution_time:
                    resp_header_parts.append(f"[dim]{execution_time:.1f}s[/dim]")
                resp_title = " \u2502 ".join(resp_header_parts) if resp_header_parts else "Response"

                from rich.markdown import Markdown as _RespMd

                console.print(
                    Panel(
                        _RespMd(response.content),
                        title=resp_title,
                        title_align="left",
                        border_style=active_theme.success,
                        box=ROUNDED,
                        padding=(1, 2),
                    )
                )

                # Update state
                state.conversation.append({"role": "assistant", "content": response.content})
                if response.usage:
                    state.tokens_used += response.usage.input_tokens + response.usage.output_tokens

                # Persist assistant response to conversation history
                if state.conversation_history:
                    state.conversation_history.add_assistant_message(response.content)

                # POST-INVOCATION: Deduct credit after successful response
                if state.credit_manager and state.user_email:
                    txn = state.credit_manager.deduct_credit(state.user_email, "aicippy_chat")
                    if txn.success:
                        state.credits_remaining = txn.credits_remaining
                    else:
                        logger.warning(
                            "credit_deduction_failed_post_response",
                            error=txn.error,
                            email=state.user_email,
                        )

                # AUTO-EXECUTE: Detect and offer to execute commands from AI response
                try:
                    ai_cmd_executor = AICommandExecutor(
                        console=console,
                        working_dir=get_working_directory(),
                        auto_confirm=False,  # Always ask for confirmation
                    )

                    detected_commands = ai_cmd_executor.detect_commands(response.content)

                    if detected_commands:
                        layout.add_preview_line(
                            f"Detected {len(detected_commands)} command(s)",
                            style=BRAND_INFO,
                        )

                        # Execute detected commands with passthrough
                        await ai_cmd_executor.execute_detected_commands(
                            response.content,
                            execute_all=False,
                        )

                        # Add execution results to conversation for AI context
                        execution_summary = ai_cmd_executor.get_execution_summary()
                        if execution_summary:
                            state.conversation.append(
                                {
                                    "role": "system",
                                    "content": f"[Command Execution Results]\n{execution_summary}",
                                }
                            )
                            layout.add_success("Command results added to context")

                except Exception as cmd_err:
                    logger.warning("ai_command_execution_warning", error=str(cmd_err))
                    # Don't fail the conversation if command detection/execution fails

                # Complete task
                state.complete_task()

                # Set mascot to happy after success
                state.mascot_mood = "happy"

                # Reset agents to idle
                for agent in state.agents:
                    agent.status = "idle"

                # Reset mascot to idle after a short happy moment
                await asyncio.sleep(0.5)
                state.mascot_mood = "idle"

            except Exception as e:
                logger.exception("chat_error", error=str(e))
                layout.add_error(str(e))
                console.print(
                    create_error_panel(str(e), suggestion="Check your connection and try again")
                )

                # Reset streaming and mascot state on error
                state.is_streaming = False
                state.mascot_mood = "idle"

                # Close orchestrator on error to free resources
                await _close_orchestrator_if_exists(_last_orchestrator)
                _last_orchestrator = None

                # Mark task as failed
                if state.tasks and state.current_task_idx < len(state.tasks):
                    state.tasks[state.current_task_idx].status = "pending"

        except EOFError:
            # Ctrl+D: save state before exiting
            _save_session_state(state)
            running = False
        except KeyboardInterrupt:
            # Second Ctrl+C arrives here from the signal handler
            _save_session_state(state)
            running = False

    # Stop background auto-updater
    if bg_updater is not None:
        try:
            bg_updater.stop()
        except Exception:
            logger.warning("auto_updater_stop_failed", exc_info=True)

    # Ensure any remaining orchestrator is closed
    await _close_orchestrator_if_exists(_last_orchestrator)

    # Cancel any pending async tasks to prevent zombie coroutines
    await _cancel_pending_tasks()

    # Save session state (unified helper - idempotent if already saved)
    memory_entries_saved = _save_session_state(state)

    logger.info(
        "session_ended",
        session_id=session_id,
        tokens_used=state.tokens_used,
        messages=len(state.conversation),
        duration=state.get_session_duration(),
        memory_entries=memory_entries_saved,
    )

    # Farewell message with waving mascot and accurate session stats
    console.print()

    # Create farewell with mascot waving goodbye
    animator = MascotAnimator(mood=MascotMood.WAVING, use_mini=True)
    frames = animator.get_frames()
    farewell_mascot = animator.render_frame(frames[0])

    farewell_text = Text()
    farewell_text.append("\n")
    farewell_text.append(f" {STATUS_ICONS['complete']} ", style=BRAND_SUCCESS)
    farewell_text.append("Session ended. ", style=f"bold {BRAND_PRIMARY}")
    farewell_text.append("Goodbye!", style=f"bold {MASCOT_PRIMARY}")
    farewell_text.append(f"\n\n   Tokens used:      {state.tokens_used:,}", style="dim")
    farewell_text.append(f"\n   Messages sent:    {len(state.conversation)}", style="dim")
    farewell_text.append(f"\n   Session duration: {state.get_session_duration()}", style="dim")
    farewell_text.append(f"\n   Memory entries:   {memory_entries_saved}", style="dim")
    # Show credits used this session
    if state.credit_manager:
        credits_used = state.credit_manager.session_credits_used
        if state.is_admin:
            farewell_text.append(
                f"\n   Credits used:     {credits_used} (admin - unlimited)",
                style="dim",
            )
        else:
            farewell_text.append(f"\n   Credits used:     {credits_used}", style="dim")
            farewell_text.append(f"\n   Credits left:     {state.credits_remaining}", style="dim")
    farewell_text.append("\n\n   See you next time!", style=f"italic {BRAND_ACCENT}")

    console.print(
        Panel(
            Group(
                Align.center(farewell_mascot),
                farewell_text,
            ),
            border_style=BRAND_PRIMARY,
            title="[bold]Bye![/bold]",
            title_align="center",
        )
    )
