=== 目录结构 ===
comate_cli/main.py
comate_cli/__pycache__/mcp_cli.cpython-311.pyc
comate_cli/__pycache__/__init__.cpython-312.pyc
comate_cli/__pycache__/main.cpython-312.pyc
comate_cli/__pycache__/mcp_cli.cpython-313.pyc
comate_cli/__pycache__/main.cpython-311.pyc
comate_cli/__pycache__/__init__.cpython-313.pyc
comate_cli/__pycache__/main.cpython-313.pyc
comate_cli/__pycache__/__init__.cpython-311.pyc
comate_cli/__main__.py
comate_cli/terminal_agent/fragment_utils.py
comate_cli/terminal_agent/error_display.py
comate_cli/terminal_agent/plugins/__pycache__/plugin_picker.cpython-312.pyc
comate_cli/terminal_agent/plugins/__pycache__/plugin_picker.cpython-311.pyc
comate_cli/terminal_agent/plugins/__pycache__/__init__.cpython-312.pyc
comate_cli/terminal_agent/plugins/__pycache__/plugin_picker.cpython-313.pyc
comate_cli/terminal_agent/plugins/__pycache__/marketplace_install_view.cpython-313.pyc
comate_cli/terminal_agent/plugins/__pycache__/marketplace_install_view.cpython-311.pyc
comate_cli/terminal_agent/plugins/__pycache__/__init__.cpython-313.pyc
comate_cli/terminal_agent/plugins/__pycache__/__init__.cpython-311.pyc
comate_cli/terminal_agent/plugins/components/search_box.py
comate_cli/terminal_agent/plugins/components/detail_view.py
comate_cli/terminal_agent/plugins/components/__init__.py
comate_cli/terminal_agent/plugins/components/tab_bar.py
comate_cli/terminal_agent/plugins/components/plugin_list.py
comate_cli/terminal_agent/plugins/plugin_picker.py
comate_cli/terminal_agent/plugins/tabs/__pycache__/marketplaces_tab.cpython-311.pyc
comate_cli/terminal_agent/plugins/tabs/__pycache__/discover_tab.cpython-312.pyc
comate_cli/terminal_agent/plugins/tabs/__pycache__/__init__.cpython-312.pyc
comate_cli/terminal_agent/plugins/tabs/__pycache__/errors_tab.cpython-313.pyc

=== 文件统计 ===
Python 文件数: 61
总行数: 18715

=== 各文件行数 ===
   210 comate_cli/main.py
     5 comate_cli/__main__.py
    25 comate_cli/terminal_agent/fragment_utils.py
    83 comate_cli/terminal_agent/error_display.py
    50 comate_cli/terminal_agent/plugins/components/search_box.py
   123 comate_cli/terminal_agent/plugins/components/detail_view.py
     0 comate_cli/terminal_agent/plugins/components/__init__.py
    84 comate_cli/terminal_agent/plugins/components/tab_bar.py
   145 comate_cli/terminal_agent/plugins/components/plugin_list.py
   430 comate_cli/terminal_agent/plugins/plugin_picker.py
   482 comate_cli/terminal_agent/plugins/tabs/installed_tab.py
   477 comate_cli/terminal_agent/plugins/tabs/discover_tab.py
     0 comate_cli/terminal_agent/plugins/tabs/__init__.py
   434 comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py
   320 comate_cli/terminal_agent/plugins/tabs/errors_tab.py
     0 comate_cli/terminal_agent/plugins/__init__.py
   283 comate_cli/terminal_agent/plugins/marketplace_install_view.py
   494 comate_cli/terminal_agent/custom_slash_commands.py
    33 comate_cli/terminal_agent/message_style.py
    90 comate_cli/terminal_agent/path_context_hint.py
   566 comate_cli/terminal_agent/mention_completer.py
    90 comate_cli/terminal_agent/logo.py
    99 comate_cli/terminal_agent/session_view.py
    15 comate_cli/terminal_agent/tips.py
   455 comate_cli/terminal_agent/rpc_stdio.py
  1075 comate_cli/terminal_agent/preflight.py
   284 comate_cli/terminal_agent/resume_selector.py
    92 comate_cli/terminal_agent/input_geometry.py
   188 comate_cli/terminal_agent/layout_coordinator.py
   295 comate_cli/terminal_agent/status_bar.py
    90 comate_cli/terminal_agent/models.py
   477 comate_cli/terminal_agent/animations.py
    24 comate_cli/terminal_agent/markdown_render.py
    12 comate_cli/terminal_agent/tui_parts/ui_mode.py
  1489 comate_cli/terminal_agent/tui_parts/commands.py
   137 comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py
   356 comate_cli/terminal_agent/tui_parts/history_sync.py
    69 comate_cli/terminal_agent/tui_parts/slash_command_registry.py
   390 comate_cli/terminal_agent/tui_parts/input_behavior.py
    17 comate_cli/terminal_agent/tui_parts/__init__.py
   502 comate_cli/terminal_agent/tui_parts/key_bindings.py
   708 comate_cli/terminal_agent/tui_parts/render_panels.py
   260 comate_cli/terminal_agent/slash_commands.py
   750 comate_cli/terminal_agent/tool_view.py
     2 comate_cli/terminal_agent/__init__.py
   658 comate_cli/terminal_agent/question_view.py
    15 comate_cli/terminal_agent/codenames.py
   742 comate_cli/terminal_agent/rewind_store.py
   473 comate_cli/terminal_agent/selection_menu.py
    37 comate_cli/terminal_agent/env_utils.py
  1262 comate_cli/terminal_agent/event_renderer.py
   226 comate_cli/terminal_agent/logging_adapter.py
   433 comate_cli/terminal_agent/app.py
   180 comate_cli/terminal_agent/history_printer.py
    30 comate_cli/terminal_agent/text_effects.py
   110 comate_cli/terminal_agent/rpc_protocol.py
    77 comate_cli/terminal_agent/startup.py
   243 comate_cli/terminal_agent/assistant_render.py
  1615 comate_cli/terminal_agent/tui.py
     5 comate_cli/__init__.py
   399 comate_cli/mcp_cli.py
 18715 总计

=== 代码概览 ===
\n--- comate_cli/__init__.py ---
"""Comate CLI package."""

from comate_cli.main import main

__all__ = ["main"]
\n--- comate_cli/__main__.py ---
from comate_cli.main import main


if __name__ == "__main__":
    main()
\n--- comate_cli/main.py ---
from __future__ import annotations

import atexit
import asyncio
import logging
import signal
import subprocess
import sys
try:
    import termios
except ImportError:
    termios = None  # Windows 不支持
import threading

logger = logging.getLogger(__name__)


class _ArgumentError(ValueError):
    """Raised when CLI arguments are invalid."""


class _TerminalStateGuard:
    """Best-effort tty restore guard for abnormal shutdown paths."""

    def __init__(self) -> None:
        self._fd: int | None = None
        self._attrs: list[int | list[int | bytes]] | None = None
        self._enabled = False

        if termios is None:
\n--- comate_cli/mcp_cli.py ---
from __future__ import annotations

import argparse
import asyncio
import sys
from typing import Any

from comate_agent_sdk.mcp import (
    McpConfigError,
    collect_mcp_server_health,
    load_effective_servers,
    load_scope_servers,
    scope_mcp_path,
    write_mcp_servers_to_path,
)
from comate_agent_sdk.mcp.types import McpServerConfig
from comate_agent_sdk.utils.paths import PathInput


class McpCliError(ValueError):
    def __init__(self, message: str, *, exit_code: int = 2) -> None:
        super().__init__(message)
        self.exit_code = int(exit_code)


class _McpArgumentParser(argparse.ArgumentParser):
    def error(self, message: str) -> None:
        raise McpCliError(message)


\n--- comate_cli/terminal_agent/animations.py ---
from __future__ import annotations

import asyncio
import random
import time
from collections.abc import Sequence
from enum import Enum

from rich.console import RenderableType
from rich.text import Text

from comate_agent_sdk.agent.events import StopEvent, TextEvent, ToolCallEvent, ToolResultEvent, UserQuestionEvent

DEFAULT_STATUS_PHRASES: tuple[str, ...] = (
    "Embellishing",
    "Vibing",
    "Thinking",
    "Reasoning",
    "Planning next move",
    "Reading context",
    "Connecting dots",
    "Synthesizing signal",
    "Spotting edge cases",
    "Checking assumptions",
    "Tracing dependencies",
    "Drafting response",
    "Polishing details",
    "Validating flow",
    "Cross-checking facts",
    "Refining intent",
\n--- comate_cli/terminal_agent/app.py ---
from __future__ import annotations

import asyncio
import importlib.metadata
import locale
import logging
import os
import random
import signal
import sys
import threading
import time
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path

from rich.console import Console

from comate_agent_sdk.agent import Agent, AgentConfig, ChatSession, TextEvent
from comate_agent_sdk.context import EnvOptions
from comate_agent_sdk.tools import tool

from comate_cli.terminal_agent.event_renderer import EventRenderer
from comate_cli.terminal_agent.logo import print_logo
from comate_cli.terminal_agent.tips import TIPS
from comate_cli.terminal_agent.preflight import run_preflight_if_needed
from comate_cli.terminal_agent.resume_selector import select_resume_session_id
from comate_cli.terminal_agent.rpc_stdio import StdioRPCBridge
from comate_cli.terminal_agent.status_bar import StatusBar
from comate_cli.terminal_agent.tui import TerminalAgentTUI
\n--- comate_cli/terminal_agent/assistant_render.py ---
from __future__ import annotations

import re
import unicodedata
from typing import Literal

from rich.console import Group, RenderableType
from rich.markdown import Markdown
from rich.text import Text

from comate_cli.terminal_agent.message_style import (
    ASSISTANT_PREFIX,
    ASSISTANT_PREFIX_STYLE,
    USER_PREFIX,
    USER_PREFIX_STYLE,
)

_LEADING_NOISE_CHARS = frozenset(
    {
        "\ufeff",  # BOM
        "\u200b",  # zero width space
        "\u200c",  # zero width non-joiner
        "\u200d",  # zero width joiner
        "\u2060",  # word joiner
        "\u00ad",  # soft hyphen
    }
)
_BLOCK_MARKDOWN_PREFIX_RE = re.compile(
    r"^\s*(#{1,6}\s+|[-*+]\s+|\d+\.\s+|>\s+|```|~~~)",
    re.MULTILINE,
\n--- comate_cli/terminal_agent/codenames.py ---
"""Version codenames — one entry per memorable release."""

from __future__ import annotations

CODENAMES: dict[str, str] = {
    "0.3.4": "",
    "0.3.3": "Update for My Brothers",
    "0.3.2": "We are with Ukraine",
}


def get_codename(ver: str) -> str | None:
    """Return codename for a version string like '0.3.2' or 'v0.3.2'."""
    stripped = ver.lstrip("v")
    return CODENAMES.get(stripped)
\n--- comate_cli/terminal_agent/custom_slash_commands.py ---
from __future__ import annotations

import asyncio
import logging
import re
import shlex
from dataclasses import dataclass
from pathlib import Path
from typing import Literal

import yaml

from comate_agent_sdk.utils.paths import PathInput, normalize_path_input

logger = logging.getLogger(__name__)

DEFAULT_ITEM_MAX_BYTES = 32 * 1024
DEFAULT_TOTAL_MAX_BYTES = 128 * 1024
DEFAULT_BASH_TIMEOUT_SECONDS = 10.0

_COMMAND_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")
_ARG_PLACEHOLDER_PATTERN = re.compile(r"\$ARGUMENTS\[(\d+)\]|\$(\d+)|\$ARGUMENTS")
_BASH_PATTERN = re.compile(r"!\`([^`\n]+)\`")
FILE_REF_PATTERN = re.compile(r"(?<!\S)@([^\s]+)")
_MARKER_PREFIX = "<<__COMATE_CUSTOM_BLOCK_"
_MARKER_SUFFIX = "__>>"


class CustomSlashExpandError(RuntimeError):
    pass
\n--- comate_cli/terminal_agent/env_utils.py ---
from __future__ import annotations

import logging
import os

logger = logging.getLogger(__name__)


def read_env_int(name: str, default: int) -> int:
    raw = os.getenv(name)
    if raw is None or not raw.strip():
        return default
    try:
        value = int(raw.strip())
    except ValueError:
        logger.warning(f"Invalid env var {name}={raw!r}; using default {default}.")
        return default
    if value <= 0:
        logger.warning(f"Invalid env var {name}={raw!r}; using default {default}.")
        return default
    return value


def read_env_float(name: str, default: float) -> float:
    raw = os.getenv(name)
    if raw is None or not raw.strip():
        return default
    try:
        value = float(raw.strip())
    except ValueError:
\n--- comate_cli/terminal_agent/error_display.py ---
"""Unified error formatter for terminal agent."""
from __future__ import annotations

import re


def format_error(exc: Exception) -> tuple[str, str, str]:
    """Convert exception to user-friendly (message, transient_summary, severity).

    - message: full error text for scrollback (plain English, no icons)
    - transient_summary: short text for status bar transient notification
    - severity: "error" or "warning"
    """
    exc_type = type(exc).__name__
    exc_msg = str(exc)

    # LLM Provider errors
    if exc_type == "ModelRateLimitError":
        return "Rate limit exceeded", "Rate limited", "warning"

    if exc_type == "ModelProviderError":
        code = getattr(exc, "status_code", None)

        # Context size exceeded (400 with specific body pattern)
        if code == 400:
            ctx = _parse_context_size_error(exc_msg)
            if ctx:
                return (
                    f"Request ({ctx[0]} tokens) exceeds context limit ({ctx[1]})",
                    "Context limit exceeded",
\n--- comate_cli/terminal_agent/event_renderer.py ---
from __future__ import annotations

import logging
import time
from collections import deque
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Literal

from comate_agent_sdk.agent.events import (
    CompactionResultEvent,
    PlanApprovalRequiredEvent,
    SessionInitEvent,
    StepCompleteEvent,
    StopEvent,
    SubagentProgressEvent,
    SubagentStartEvent,
    SubagentStopEvent,
    SubagentToolCallEvent,
    SubagentToolResultEvent,
    TeamMessageEvent,
    TextEvent,
    ThinkingEvent,
    TaskUpdatedEvent,
    ToolCallEvent,
    ToolResultEvent,
    UsageDeltaEvent,
    UserQuestionEvent,
)

\n--- comate_cli/terminal_agent/fragment_utils.py ---
from __future__ import annotations

from typing import Iterable


def clip_fragments(
    fragments: list[tuple[str, str]],
    max_len: int,
) -> list[tuple[str, str]]:
    if max_len <= 0:
        return []

    remaining = max_len
    clipped: list[tuple[str, str]] = []
    for style, text in fragments:
        if remaining <= 0:
            break
        if len(text) <= remaining:
            clipped.append((style, text))
            remaining -= len(text)
            continue
        clipped.append((style, text[:remaining]))
        break

    return clipped
\n--- comate_cli/terminal_agent/history_printer.py ---
from __future__ import annotations

from collections.abc import Callable
from typing import Any

from rich.console import Console, Group
from rich.text import Text

from comate_cli.terminal_agent.message_style import ASSISTANT_PREFIX
from comate_cli.terminal_agent.models import HistoryEntry


def _render_subtitle_line(subtitle: str, *, error: bool = False) -> Text:
    """Render a ⎿ subtitle line for tool results."""
    line = Text()
    line.append("  ⎿ ", style="#555555")
    if error:
        line.append(f" {subtitle}", style="bold red")
    else:
        line.append(f" {subtitle}", style="dim")
    return line


def _entry_prefix(entry: HistoryEntry) -> str:
    if entry.entry_type == "user":
        return ">"
    if entry.entry_type == "assistant":
        return ASSISTANT_PREFIX
    if entry.entry_type == "tool_call":
        return "→"
\n--- comate_cli/terminal_agent/__init__.py ---
"""Terminal agent example package."""

\n--- comate_cli/terminal_agent/input_geometry.py ---
from __future__ import annotations

from prompt_toolkit.utils import get_cwidth


def compute_visual_line_ranges(text: str, max_cols: int) -> list[tuple[int, int]]:
    """
    Compute visual line ranges [start, end) by wrapping `text` at `max_cols`
    display cells, respecting explicit newlines.
    """
    if max_cols <= 0:
        max_cols = 1

    ranges: list[tuple[int, int]] = []
    start = 0
    col = 0
    for idx, ch in enumerate(text):
        if ch == "\n":
            ranges.append((start, idx))
            start = idx + 1
            col = 0
            continue

        ch_width = get_cwidth(ch)
        if ch_width <= 0:
            ch_width = 1
        if ch_width > max_cols:
            ch_width = max_cols

        if col + ch_width > max_cols:
\n--- comate_cli/terminal_agent/layout_coordinator.py ---
from __future__ import annotations

from rich.console import Console, Group, RenderableType
from rich.live import Live
from rich.segment import Segment, Segments
from rich.text import Text

_TODO_MIN_LINES = 6


def _is_empty_text(renderable: RenderableType | None) -> bool:
    return isinstance(renderable, Text) and not renderable.plain


def _renderable_to_lines(console: Console, renderable: RenderableType | None) -> list[list[Segment]]:
    if renderable is None:
        return []
    if _is_empty_text(renderable):
        return []
    return console.render_lines(renderable, console.options, pad=False, new_lines=False)


def _lines_to_renderable(lines: list[list[Segment]]) -> RenderableType | None:
    if not lines:
        return None
    segments: list[Segment] = []
    last_index = len(lines) - 1
    for idx, line in enumerate(lines):
        segments.extend(line)
        if idx != last_index:
\n--- comate_cli/terminal_agent/logging_adapter.py ---
"""TUI logging adapter - 将日志输出到 prompt_toolkit TUI 而不破坏界面"""
from __future__ import annotations

import logging
import os
import sys
from typing import TYPE_CHECKING

from prompt_toolkit.application import run_in_terminal

if TYPE_CHECKING:
    from comate_cli.terminal_agent.event_renderer import EventRenderer


class TUILoggingHandler(logging.Handler):
    """自定义 logging handler，将日志友好地显示在 TUI 中

    特性：
    - WARNING/ERROR 显示给用户，带颜色标识
    - 首次显示机制：相同消息只显示一次
    - 使用 run_in_terminal() 不破坏 prompt_toolkit 界面
    """

    def __init__(self, renderer: EventRenderer) -> None:
        super().__init__()
        self._renderer = renderer
        self._shown_messages: set[str] = set()  # 跟踪已显示的消息（首次显示）

    def emit(self, record: logging.LogRecord) -> None:
        """处理日志记录"""
\n--- comate_cli/terminal_agent/logo.py ---
from __future__ import annotations

from importlib.metadata import PackageNotFoundError, version
from pathlib import Path

from rich.console import Console
from rich.text import Text

from comate_agent_sdk.utils.paths import PathInput, normalize_path_input

from .codenames import get_codename

_LOGO_LINES: tuple[str, ...] = (
    "",
    "  ██████╗ ██████╗ ███╗   ███╗ █████╗ ████████╗███████╗",
    " ██╔════╝██╔═══██╗████╗ ████║██╔══██╗╚══██╔══╝██╔════╝",
    " ██║     ██║   ██║██╔████╔██║███████║   ██║   █████╗  ",
    " ██║     ██║   ██║██║╚██╔╝██║██╔══██║   ██║   ██╔══╝  ",
    " ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║  ██║   ██║   ███████╗",
    "  ╚═════╝ ╚═════╝ ╚═╝     ╚═╝╚═╝  ╚═╝   ╚═╝   ╚══════╝",
)


def _lerp_rgb(
    start_rgb: tuple[int, int, int],
    end_rgb: tuple[int, int, int],
    ratio: float,
) -> tuple[int, int, int]:
    clamped = max(0.0, min(1.0, ratio))
    r = int(start_rgb[0] + (end_rgb[0] - start_rgb[0]) * clamped)
\n--- comate_cli/terminal_agent/markdown_render.py ---
from __future__ import annotations

from io import StringIO

from rich.console import Console
from rich.markdown import Markdown


def render_markdown_to_plain(content: str, *, width: int) -> str:
    normalized_width = max(int(width), 40)
    try:
        sink = StringIO()
        console = Console(
            file=sink,
            width=normalized_width,
            force_terminal=False,
            color_system=None,
            soft_wrap=True,
        )
        console.print(Markdown(content, code_theme="monokai", hyperlinks=False))
        rendered = sink.getvalue().rstrip("\n")
        return rendered
    except Exception:
        return content
\n--- comate_cli/terminal_agent/mention_completer.py ---
from __future__ import annotations

import os
import re
import subprocess
import time
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path, PurePosixPath

import logging
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
from prompt_toolkit.document import Document

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class MentionContext:
    marker_index: int
    fragment: str


class LocalFileMentionCompleter(Completer):
    """Offer fuzzy `@` path suggestions backed by workspace files."""

    _TRIGGER_GUARDS = frozenset((".", "-", "_", "`", "'", '"', ":", "@", "#", "~"))
    _IGNORED_NAME_GROUPS: dict[str, tuple[str, ...]] = {
        "vcs_metadata": (".DS_Store", ".bzr", ".git", ".hg", ".svn"),
        "tooling_caches": (
\n--- comate_cli/terminal_agent/message_style.py ---
from __future__ import annotations

from rich.console import Console

ASSISTANT_PREFIX = "●"
ASSISTANT_PREFIX_STYLE = "bold cyan"
ASSISTANT_MESSAGE_GAP_LINES = 1
USER_PREFIX = ">"
USER_PREFIX_STYLE = "bold ansicyan"

TOOL_RUNNING_STYLE = "bold blue"
TOOL_SUCCESS_STYLE = "bold green"
TOOL_ERROR_STYLE = "bold red"
TOOL_RUNNING_PREFIX = "→"
TOOL_SUCCESS_PREFIX = "✓"
TOOL_ERROR_PREFIX = "✖"

THINKING_PREFIX = "💭"
THINKING_STYLE = "dim"  # 灰色


def print_assistant_prefix_line(console: Console, content: str) -> None:
    """Print assistant prefix and content on the same visual line."""
    console.print(
        f"[{ASSISTANT_PREFIX_STYLE}]{ASSISTANT_PREFIX}[/] {content}",
        new_line_start=True,
        soft_wrap=True,
    )


\n--- comate_cli/terminal_agent/models.py ---
from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Any, Literal

if TYPE_CHECKING:
    from rich.text import Text

ToolStatus = Literal["running", "success", "error"]
HistoryEntryType = Literal["user", "assistant", "tool_call", "tool_result", "system", "thinking", "elapsed", "file_ref"]


class LoadingStateType(Enum):
    """Loading 状态类型，用于决定渲染策略。"""

    TOOL_CALL = "tool_call"      # 工具调用 - 静态显示
    THINKING = "thinking"        # 思考中 - 流光效果
    ANIMATION = "animation"      # 动画状态（如 vibeing）- 流光效果
    IDLE = "idle"                # 空闲状态 - 无显示


@dataclass
class LoadingState:
    """语义化的 loading 状态，携带类型信息和显示数据。

    Attributes:
        type: Loading 状态类型，决定渲染策略
        text: 显示文本内容
        metadata: 额外元数据（如工具名、耗时、token 数等）
\n--- comate_cli/terminal_agent/path_context_hint.py ---
"""Generate lightweight path-context hints for @path mentions in user messages."""
from __future__ import annotations

import logging
from pathlib import Path

from comate_cli.terminal_agent.custom_slash_commands import FILE_REF_PATTERN

logger = logging.getLogger(__name__)

_MAX_MENTIONS = 10
_LINE_COUNT_MAX_BYTES = 256 * 1024  # 256KB


def build_path_context_hint(text: str, project_root: Path) -> str | None:
    """Parse @path mentions from text and return a hint string, or None if no valid paths."""
    resolved_root = project_root.expanduser().resolve()
    matches = list(FILE_REF_PATTERN.finditer(text))
    if not matches:
        return None

    hint_lines: list[str] = []
    for match in matches[:_MAX_MENTIONS]:
        raw_path = match.group(1).strip()
        if not raw_path:
            continue

        candidate = (resolved_root / raw_path).resolve()
        # Safety: skip paths that escape project root
        try:
\n--- comate_cli/terminal_agent/plugins/components/detail_view.py ---
"""Plugin picker 详情视图组件。

纯状态 + formatted text 渲染，显示插件详细信息和操作菜单。
"""

from __future__ import annotations


class DetailView:
    """插件详情视图：显示名称、来源、版本、描述、信任警告、操作列表。

    渲染为 prompt_toolkit style tuples，由上层 layout 消费。
    """

    def __init__(
        self,
        plugin_info: dict | None = None,
        actions: list[str] | None = None,
    ) -> None:
        self._plugin_info: dict = plugin_info or {}
        self._actions: list[str] = actions or ["Install for user", "Install for project", "Back"]
        self._selected_action = 0

    @property
    def plugin_info(self) -> dict:
        return self._plugin_info

    @plugin_info.setter
    def plugin_info(self, value: dict) -> None:
        self._plugin_info = value or {}
\n--- comate_cli/terminal_agent/plugins/components/__init__.py ---
\n--- comate_cli/terminal_agent/plugins/components/plugin_list.py ---
"""Plugin picker 列表组件。

纯状态 + formatted text 渲染，支持导航、多选、viewport 滚动。
"""

from __future__ import annotations


class PluginListControl:
    """插件列表：支持上下导航、多选、viewport 滚动。

    渲染为 prompt_toolkit style tuples，由上层 layout 消费。
    """

    def __init__(
        self,
        items: list[dict] | None = None,
    ) -> None:
        self._items: list[dict] = list(items) if items else []
        self._selected_index = 0
        self._selected_items: set[int] = set()

    @property
    def items(self) -> list[dict]:
        return self._items

    @items.setter
    def items(self, value: list[dict]) -> None:
        self._items = list(value) if value else []
        self._selected_index = 0
\n--- comate_cli/terminal_agent/plugins/components/search_box.py ---
"""Plugin picker 搜索框组件。

封装 prompt_toolkit TextArea，提供实时过滤回调。
"""

from __future__ import annotations

from typing import Callable

from prompt_toolkit.widgets import TextArea


class SearchBox:
    """搜索框：单行输入，支持实时过滤回调。"""

    def __init__(
        self,
        on_change: Callable[[str], None] | None = None,
        placeholder: str = "Search plugins...",
    ) -> None:
        self._on_change = on_change
        self._text_area = TextArea(
            height=1,
            multiline=False,
            prompt=[("class:plugin-search-icon", "🔍 ")],
            style="class:plugin-search-input",
            dont_extend_height=True,
        )
        self._text_area.buffer.on_text_changed += self._handle_change

\n--- comate_cli/terminal_agent/plugins/components/tab_bar.py ---
"""Plugin picker 标签栏组件。

纯状态 + formatted text 渲染，不依赖 prompt_toolkit Application 即可测试。
"""

from __future__ import annotations


class TabBar:
    """标签栏：支持左右切换、badge 计数显示。

    渲染为 prompt_toolkit style tuples，由上层 layout 消费。
    """

    def __init__(
        self,
        tabs: list[str],
        badge_counts: dict[str, int] | None = None,
    ) -> None:
        self._tabs = list(tabs)
        self._badge_counts = dict(badge_counts) if badge_counts else {}
        self._active_tab = 0

    @property
    def active_tab(self) -> int:
        return self._active_tab

    @active_tab.setter
    def active_tab(self, value: int) -> None:
        if self._tabs:
\n--- comate_cli/terminal_agent/plugins/__init__.py ---
\n--- comate_cli/terminal_agent/plugins/marketplace_install_view.py ---
"""Marketplace 安装面板 — 轻量级 TUI 组件。

状态机：
  CONFIRM  - 显示待安装 repo，等待用户确认（Enter/Esc）
  LOADING  - 正在 git clone，显示进度（Esc 可取消）
  DONE     - 安装完成（由调用方自动退出面板）
  ERROR    - 安装失败，显示错误信息（Esc 退出）
"""

from __future__ import annotations

import asyncio
import logging
import shutil
from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal

from prompt_toolkit.application.current import get_app_or_none
from prompt_toolkit.layout.containers import Window
from prompt_toolkit.layout.controls import FormattedTextControl

if TYPE_CHECKING:
    from comate_agent_sdk.plugins import MarketplaceManager, PluginRegistry

logger = logging.getLogger(__name__)

_State = Literal["CONFIRM", "LOADING", "DONE", "ERROR"]

# 面板固定高度（含上下边框 + footer hint 行）
_PANEL_HEIGHT = 15
\n--- comate_cli/terminal_agent/plugins/plugin_picker.py ---
"""Plugin picker 入口与参数解析。

PluginPickerContext 保存 /plugin 子命令解析结果。
PluginPickerUI 是嵌入式 TUI 组件，供主 TUI ConditionalContainer 使用。
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field

from prompt_toolkit.application.current import get_app_or_none
from prompt_toolkit.document import Document
from prompt_toolkit.filters import Condition
from prompt_toolkit.layout.containers import ConditionalContainer, HSplit, Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.processors import Processor, Transformation, TransformationInput
from prompt_toolkit.widgets import Frame, TextArea

from comate_agent_sdk.plugins import (
    MarketplaceManager,
    PluginLoader,
    PluginManager,
    PluginRegistry,
)

logger = logging.getLogger(__name__)


class _PlaceholderProcessor(Processor):
\n--- comate_cli/terminal_agent/plugins/tabs/discover_tab.py ---
"""Discover 标签 — 浏览 marketplace 中可安装的插件。

状态机：LIST -> DETAIL（Enter）-> LIST（Esc / 安装后）
"""

from __future__ import annotations

import asyncio
import logging
from typing import TYPE_CHECKING, Literal

from prompt_toolkit.application.current import get_app_or_none

if TYPE_CHECKING:
    from comate_agent_sdk.plugins import (
        PluginLoader,
        PluginManager,
        PluginRegistry,
        MarketplaceManager,
    )
    from comate_cli.terminal_agent.plugins.plugin_picker import PluginPickerContext

logger = logging.getLogger(__name__)

_State = Literal["LIST", "DETAIL"]

class DiscoverTab:
    """Discover 标签：浏览 marketplace 插件、批量选择、安装。"""

    def __init__(
\n--- comate_cli/terminal_agent/plugins/tabs/errors_tab.py ---
"""Errors 标签 — 显示插件加载错误。

支持：
  - 列表展示 PluginError 条目
  - Enter 进入详情，选择 Retry / Dismiss / Back 操作
  - Esc 从详情返回列表
  - 文本过滤（apply_filter）
"""

from __future__ import annotations

import asyncio
import logging
from typing import TYPE_CHECKING, Literal

from prompt_toolkit.application.current import get_app_or_none

if TYPE_CHECKING:
    from comate_agent_sdk.plugins import PluginLoader, PluginRegistry
    from comate_agent_sdk.plugins.models import PluginError

logger = logging.getLogger(__name__)

_State = Literal["LIST", "DETAIL"]


class ErrorsTab:
    """Errors 标签：展示插件加载错误，支持重试和忽略。"""

    def __init__(
\n--- comate_cli/terminal_agent/plugins/tabs/__init__.py ---
\n--- comate_cli/terminal_agent/plugins/tabs/installed_tab.py ---
"""Installed 标签 — 管理已安装的插件。

状态机：LIST -> DETAIL（Enter）-> LIST（Esc）
        LIST -> CONFIRM_UNINSTALL（x）-> LIST（y/n）
"""

from __future__ import annotations

import asyncio
import logging
from typing import TYPE_CHECKING, Literal

from prompt_toolkit.application.current import get_app_or_none

if TYPE_CHECKING:
    from comate_agent_sdk.plugins import PluginManager, PluginRegistry
    from comate_cli.terminal_agent.plugins.plugin_picker import PluginPickerContext

logger = logging.getLogger(__name__)

_State = Literal["LIST", "DETAIL", "CONFIRM_UNINSTALL"]


class InstalledTab:
    """Installed 标签：查看、启用/禁用、卸载、更新已安装插件。"""

    def __init__(
        self,
        plugin_mgr: PluginManager,
        registry: PluginRegistry,
\n--- comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py ---
"""Marketplaces 标签 — 管理 marketplace 源。

状态机：
  LIST           - marketplace 列表（含 "+ Add Marketplace" 操作行）
  DETAIL         - marketplace 详情页（Browse / Update / Remove）
  CONFIRM_REMOVE - 删除确认（当 marketplace 有已安装插件时）
  INPUT_ADD      - 等待用户确认添加（简化：显示提示，Enter 提交 context.target_marketplace）
"""

from __future__ import annotations

import asyncio
import logging
from typing import TYPE_CHECKING, Literal

from prompt_toolkit.application.current import get_app_or_none

if TYPE_CHECKING:
    from comate_agent_sdk.plugins import MarketplaceManager, PluginRegistry
    from comate_cli.terminal_agent.plugins.plugin_picker import PluginPickerContext

logger = logging.getLogger(__name__)

_State = Literal["LIST", "DETAIL", "CONFIRM_REMOVE", "INPUT_ADD"]

# "+ Add Marketplace" 哨兵 key
_ADD_ROW_KEY = "__add_marketplace__"


class MarketplacesTab:
\n--- comate_cli/terminal_agent/preflight.py ---
from __future__ import annotations

import json
import logging
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Literal, Mapping

from prompt_toolkit.application import Application
from prompt_toolkit.filters import Condition
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout import HSplit, Layout, Window
from prompt_toolkit.layout.containers import ConditionalContainer, DynamicContainer
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.styles import Style as PTStyle
from prompt_toolkit.widgets import Frame, TextArea
from rich.console import Console

from comate_agent_sdk.agent.llm_levels import ALL_LEVELS
from comate_agent_sdk.agent.settings import (
    LOCAL_SETTINGS_FILENAME,
    USER_SETTINGS_PATH,
    load_settings_file,
    read_settings_json_object,
    write_settings_json_atomic,
)

from comate_cli.terminal_agent.selection_menu import SelectionMenuUI
\n--- comate_cli/terminal_agent/question_view.py ---
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

from prompt_toolkit.document import Document
from prompt_toolkit.filters import Condition
from prompt_toolkit.layout import HSplit, Window
from prompt_toolkit.layout.containers import ConditionalContainer
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.widgets import TextArea

_CANCEL_MESSAGE = "user reject answer this question."
_CHAT_MESSAGE = "Chat about this"
_PREVIEW_CUSTOM_INPUT_MAX_CHARS = 15


@dataclass(frozen=True, slots=True)
class OptionState:
    """单个候选项状态。"""

    label: str
    description: str


@dataclass(slots=True)
class QuestionState:
    """单个问题状态。"""

    question: str
\n--- comate_cli/terminal_agent/resume_selector.py ---
from __future__ import annotations

import json
import logging
import time
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any

from prompt_toolkit.application import Application
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout import Layout
from prompt_toolkit.patch_stdout import patch_stdout
from rich.console import Console

from comate_agent_sdk.agent.session_store import default_sessions_dir

from comate_cli.terminal_agent.selection_menu import SelectionMenuUI

logger = logging.getLogger(__name__)

_PAGE_SIZE = 15


@dataclass(frozen=True, slots=True)
class ResumeSessionItem:
    session_id: str
    context_path: Path
    created_ts: float
\n--- comate_cli/terminal_agent/rewind_store.py ---
from __future__ import annotations

import difflib
import hashlib
import json
import logging
import os
import tempfile
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Literal

from comate_agent_sdk.agent import ChatSession
from comate_agent_sdk.context.items import ItemType

logger = logging.getLogger(__name__)

_TRACKED_TOOLS = {"Write", "Edit", "MultiEdit"}
_SCHEMA_VERSION = 1


@dataclass(frozen=True)
class RewindCheckpoint:
    checkpoint_id: int
    turn_number: int
    user_preview: str
    user_message: str
    created_at: str
    touched_files: tuple[str, ...]
\n--- comate_cli/terminal_agent/rpc_protocol.py ---
from __future__ import annotations

import json
from dataclasses import asdict, is_dataclass
from typing import Any, TypeGuard


class ErrorCodes:
    PARSE_ERROR = -32700
    INVALID_REQUEST = -32600
    METHOD_NOT_FOUND = -32601
    INVALID_PARAMS = -32602
    INTERNAL_ERROR = -32603
    INVALID_STATE = -32000


class JSONRPCProtocolError(Exception):
    def __init__(
        self,
        *,
        code: int,
        message: str,
        request_id: str | int | None = None,
        data: Any = None,
    ) -> None:
        super().__init__(message)
        self.code = code
        self.message = message
        self.request_id = request_id
        self.data = data
\n--- comate_cli/terminal_agent/rpc_stdio.py ---
from __future__ import annotations

import asyncio
import logging
import sys
from typing import Any

from comate_agent_sdk.agent import ChatSession
from comate_agent_sdk.agent.events import SessionInitEvent, StopEvent

from comate_cli.terminal_agent.rpc_protocol import (
    ErrorCodes,
    JSONRPCProtocolError,
    build_error_response,
    build_event_notification,
    build_success_response,
    is_jsonrpc_request_id,
    parse_jsonrpc_message,
)

logger = logging.getLogger(__name__)


class StdioRPCBridge:
    """JSON-RPC 2.0 bridge over stdin/stdout (NDJSON)."""

    def __init__(self, session: ChatSession) -> None:
        self._session = session
        self._write_lock = asyncio.Lock()
        self._closing = False
\n--- comate_cli/terminal_agent/selection_menu.py ---
"""通用选择菜单 UI 组件。

提供简单的单选菜单界面，支持上下选择、Enter 确认、Esc 取消。
可用于模型切换、配置选择等二级选择场景。
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Callable, Literal

from prompt_toolkit.filters import Condition
from prompt_toolkit.layout import HSplit, Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.utils import get_cwidth

from comate_cli.terminal_agent.text_effects import fit_single_line


@dataclass(frozen=True, slots=True)
class SelectionOption:
    """单个选项。"""

    value: str
    label: str
    description: str = ""


@dataclass(slots=True)
class SelectionMenuState:
\n--- comate_cli/terminal_agent/session_view.py ---
from __future__ import annotations

from rich.console import Console
from rich.markdown import Markdown

from comate_agent_sdk.agent import ChatSession
from comate_agent_sdk.context.items import ItemType

from comate_cli.terminal_agent.message_style import print_assistant_gap, print_assistant_prefix_line


def _truncate(content: str, max_len: int = 1400) -> str:
    if len(content) <= max_len:
        return content
    return f"{content[:max_len]}..."


def _extract_assistant_text(item) -> str:
    """从 ContextItem 提取用于显示的文本（不含 tool_calls JSON）"""
    message = getattr(item, "message", None)
    if message is None:
        return ""

    # 优先使用 message.text（纯文本，不含 tool_calls）
    if hasattr(message, "text"):
        text = message.text
        if isinstance(text, str):
            return text

    # 回退：处理非标准 content
\n--- comate_cli/terminal_agent/slash_commands.py ---
from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Callable, Literal

from prompt_toolkit.auto_suggest import AutoSuggest, Suggestion
from prompt_toolkit.completion import (
    Completer,
    Completion,
    FuzzyCompleter,
    WordCompleter,
)
from prompt_toolkit.document import Document


SlashCommandExecutionKind = Literal["local", "llm", "hybrid"]


@dataclass(frozen=True, slots=True)
class SlashCommandSpec:
    name: str
    description: str
    execution_kind: SlashCommandExecutionKind
    aliases: tuple[str, ...] = ()
    argument_hint: str | None = None

    def slash_name(self) -> str:
        if self.aliases:
            return f"/{self.name} ({', '.join(self.aliases)})"
\n--- comate_cli/terminal_agent/startup.py ---
"""Generic startup status display utilities for CLI applications.

Usage:
    from comate_cli.terminal_agent.startup import print_warning, print_success, print_error

    print_warning(console, "MCP server 'ctx7' skipped: header is null")
    print_success(console, "MCP Server loaded: exa_search (4 tools)")
    print_error(console, "Failed to initialize config")
"""

from __future__ import annotations

import asyncio
import time
from contextlib import asynccontextmanager
from typing import AsyncGenerator

from rich.console import Console
from rich.text import Text

from comate_cli.terminal_agent.animations import _cyan_sweep_text, breathing_dot_color, breathing_dot_glyph


def print_status(console: Console, icon: str, style: str, message: str) -> None:
    """Print a styled status line to the console."""
    console.print(f"[{style}]{icon} {message}[/]")


def print_warning(console: Console, message: str) -> None:
    """Print a yellow warning line (⚠)."""
\n--- comate_cli/terminal_agent/status_bar.py ---
from __future__ import annotations

import subprocess
import time
from typing import NamedTuple

from prompt_toolkit.application.current import get_app_or_none

from comate_agent_sdk.agent import ChatSession


class GitDiffStats(NamedTuple):
    added: int
    removed: int


class StatusBar:
    _DEFAULT_TERMINAL_WIDTH: int = 100
    _MIN_TERMINAL_WIDTH: int = 40
    _GIT_DIFF_CACHE_SECONDS: float = 5.0
    _GIT_CMD_TIMEOUT_SECONDS: float = 0.2

    def __init__(self, session: ChatSession):
        self._session = session
        self._model_name: str = self._resolve_model_name(session)
        self._mode: str = "act"
        self._git_branch: str = self._resolve_git_branch()
        self._context_used_pct: float = 0.0
        self._context_left_pct: float = 100.0
        self._git_diff_stats: GitDiffStats | None = None
\n--- comate_cli/terminal_agent/text_effects.py ---
from __future__ import annotations

from prompt_toolkit.utils import get_cwidth

_ELLIPSIS = "…"


def fit_single_line(content: str, width: int) -> str:
    """Truncate string by terminal display width, handling wide chars correctly."""
    max_width = max(width, 8)
    if get_cwidth(content) <= max_width:
        return content
    if max_width <= 1:
        return _ELLIPSIS

    result: list[str] = []
    used_width = 0
    ellipsis_width = get_cwidth(_ELLIPSIS)
    target_width = max_width - ellipsis_width

    for char in content:
        char_width = get_cwidth(char)
        if used_width + char_width > target_width:
            break
        result.append(char)
        used_width += char_width

    return "".join(result) + _ELLIPSIS


\n--- comate_cli/terminal_agent/tips.py ---
"""Tips displayed to users at startup."""
from __future__ import annotations

TIPS: list[str] = [
    "Shift+Tab to switch between act / plan mode",
    "/compact to manually compress context and free up token space",
    "Ctrl+C to interrupt the current AI operation",
    "/rewind to undo the last conversation and return to the previous checkpoint",
    "/clear to clear the current session and start fresh",
    "/context to view current token usage and remaining context",
    "/model to switch model tier (LOW / MID / HIGH)",
    "/mcp to browse loaded MCP services and tools",
    "@filename to reference project files in your input",
    "Tab to trigger command completion, press Tab again to cycle through options",
]
\n--- comate_cli/terminal_agent/tool_view.py ---
from __future__ import annotations

import json
import re
import time
from pathlib import Path
from typing import Any

from rich.console import RenderableType
from rich.panel import Panel
from rich.text import Text


_MEMORY_PATH_RE = re.compile(r"\.agent/sessions/[^/]+/memory")


def _normalize_path_for_display(path_str: str | None, project_root: Path | None) -> str | None:
    """将路径转换为相对于项目根目录的显示格式。memory 路径截断为 memory/xxx。"""
    if path_str is None:
        return None
    try:
        path = Path(path_str)
        result = path_str
        if project_root is not None and path.is_absolute():
            try:
                result = path.relative_to(project_root).as_posix()
            except ValueError:
                pass
        m = _MEMORY_PATH_RE.search(result)
        if m:
\n--- comate_cli/terminal_agent/tui_parts/commands.py ---
from __future__ import annotations

import asyncio
import logging
import time
from pathlib import Path
from typing import Any


from prompt_toolkit.document import Document

from comate_agent_sdk.agent.llm_levels import ALL_LEVELS
from comate_agent_sdk.context.items import ItemType
from comate_agent_sdk.llm.messages import UserMessage

from comate_cli.terminal_agent.rewind_store import RewindCheckpoint, RewindRestorePlan
from comate_cli.terminal_agent.selection_menu import SelectionResult, build_model_level_options
from comate_cli.terminal_agent.custom_slash_commands import (
    CustomSlashExpandError,
    render_custom_slash_prompt,
)
from comate_cli.terminal_agent.slash_commands import parse_slash_command_call
from comate_cli.terminal_agent.tui_parts.ui_mode import UIMode

logger = logging.getLogger(__name__)


class CommandsMixin:
    def _begin_llm_long_task(self) -> None:
        """标记 LLM 远端长任务开始：开启 busy 并记录运行起点。"""
\n--- comate_cli/terminal_agent/tui_parts/history_sync.py ---
from __future__ import annotations

import logging
import sys
from pathlib import Path
from typing import Any

from prompt_toolkit.application import run_in_terminal
from rich.console import Console

from comate_agent_sdk.context.items import ItemType
from comate_agent_sdk.llm.messages import AssistantMessage, UserMessage

from comate_cli.terminal_agent.history_printer import (
    print_history_group_sync,
    render_history_group,
)
from comate_cli.terminal_agent.logo import print_logo
from comate_cli.terminal_agent.markdown_render import render_markdown_to_plain
from comate_cli.terminal_agent.models import HistoryEntry
from comate_cli.terminal_agent.tool_view import should_show_tool_in_scrollback

console = Console()
logger = logging.getLogger(__name__)


class HistorySyncMixin:
    async def _replay_scrollback_after_rewind(self) -> None:
        """清屏并重建 scrollback：Logo + 当前会话历史。"""
        self._renderer.reset_history_view()
\n--- comate_cli/terminal_agent/tui_parts/__init__.py ---
from comate_cli.terminal_agent.tui_parts.commands import CommandsMixin
from comate_cli.terminal_agent.tui_parts.history_sync import HistorySyncMixin
from comate_cli.terminal_agent.tui_parts.input_behavior import InputBehaviorMixin
from comate_cli.terminal_agent.tui_parts.key_bindings import KeyBindingsMixin
from comate_cli.terminal_agent.tui_parts.render_panels import RenderPanelsMixin
from comate_cli.terminal_agent.tui_parts.slash_command_registry import SlashCommandRegistry
from comate_cli.terminal_agent.tui_parts.ui_mode import UIMode

__all__ = [
    "CommandsMixin",
    "HistorySyncMixin",
    "InputBehaviorMixin",
    "KeyBindingsMixin",
    "RenderPanelsMixin",
    "SlashCommandRegistry",
    "UIMode",
]
\n--- comate_cli/terminal_agent/tui_parts/input_behavior.py ---
from __future__ import annotations

from bisect import bisect_right
from typing import Any

from prompt_toolkit.document import Document

from comate_cli.terminal_agent.input_geometry import (
    compute_visual_line_ranges,
    index_for_visual_col,
    visual_col_for_index,
)
from comate_cli.terminal_agent.question_view import QuestionAction
from comate_cli.terminal_agent.slash_commands import parse_slash_command_call
from comate_cli.terminal_agent.tui_parts.ui_mode import UIMode


class InputBehaviorMixin:
    def _should_handle_history(self) -> bool:
        """检查是否应该处理历史浏览(而不是补全导航)

        Returns:
            True 表示可以浏览历史,False 表示应该优先处理补全
        """
        buffer = self._input_area.buffer

        # 如果有活动的补全菜单,不应该浏览历史
        if buffer.complete_state is not None and buffer.complete_state.completions:
            return False

\n--- comate_cli/terminal_agent/tui_parts/key_bindings.py ---
from __future__ import annotations

import time

from prompt_toolkit.filters import Condition, has_completions, has_focus
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.keys import Keys

from comate_cli.terminal_agent.slash_commands import parse_slash_command_call
from comate_cli.terminal_agent.tui_parts.ui_mode import UIMode


class KeyBindingsMixin:
    def _build_key_bindings(self) -> KeyBindings:
        bindings = KeyBindings()
        normal_mode = Condition(lambda: self._ui_mode == UIMode.NORMAL)
        question_mode = Condition(lambda: self._ui_mode == UIMode.QUESTION)
        selection_mode = Condition(lambda: self._ui_mode == UIMode.SELECTION)
        plugin_mode = Condition(lambda: self._ui_mode == UIMode.PLUGIN)
        install_mode = Condition(lambda: self._ui_mode == UIMode.MARKETPLACE_INSTALL)
        mcp_connecting_mode = Condition(lambda: self._ui_mode == UIMode.MCP_CONNECTING)
        diff_panel_open = Condition(lambda: self._diff_panel_visible)
        todo_panel_expanded = Condition(
            lambda: self._todo_panel_expanded and self._renderer.has_active_todos()
        )

        # Selection menu key bindings
        @bindings.add("enter", filter=selection_mode)
        def _selection_enter(event) -> None:
            del event
\n--- comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py ---
"""MCP Server 重连面板 — 轻量级 TUI 组件。

显示连接进度，支持 Esc 取消，20 秒超时。
完成后通过 on_done 回调通知调用方。
"""

from __future__ import annotations

import asyncio
import logging

from prompt_toolkit.application.current import get_app_or_none
from prompt_toolkit.layout.containers import Window
from prompt_toolkit.layout.controls import FormattedTextControl

logger = logging.getLogger(__name__)

_PANEL_HEIGHT = 8

_H = "\u2500"  # ─
_TL = "\u256d"  # ╭
_TR = "\u256e"  # ╮
_BL = "\u2570"  # ╰
_BR = "\u256f"  # ╯
_V = "\u2502"  # │

_CONNECT_TIMEOUT_S = 20.0


class McpConnectingView:
\n--- comate_cli/terminal_agent/tui_parts/render_panels.py ---
from __future__ import annotations

import time
from typing import Any

from prompt_toolkit.utils import get_cwidth
from rich.console import Console

from comate_cli.terminal_agent.animations import _cyan_sweep_text, breathing_dot_color, breathing_dot_glyph
from comate_cli.terminal_agent.text_effects import fit_single_line

console = Console()


class RenderPanelsMixin:
    _COMPLETION_PANEL_MAX_LINES = 8

    def _terminal_width(self) -> int:
        if self._app is None:
            return 100
        try:
            return max(int(self._app.output.get_size().columns), 40)
        except Exception:
            return 100

    def _queue_text(self) -> list[tuple[str, str]]:
        queued_messages = list(self._queued_messages)
        if not queued_messages:
            return [("", " ")]

\n--- comate_cli/terminal_agent/tui_parts/slash_command_registry.py ---
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Callable, Literal

from comate_cli.terminal_agent.slash_commands import SlashCommandSpec


SlashCommandSource = Literal["builtin", "custom"]


@dataclass(frozen=True, slots=True)
class RegisteredSlashCommand:
    spec: SlashCommandSpec
    handler: Callable[[str], Any]
    allow_when_busy: bool = False
    source: SlashCommandSource = "builtin"
    argument_hint: str | None = None


class SlashCommandRegistry:
    def __init__(self) -> None:
        self._entries_by_name: dict[str, RegisteredSlashCommand] = {}
        self._entries_by_spec_name: dict[str, RegisteredSlashCommand] = {}

    def register(
        self,
        *,
        spec: SlashCommandSpec,
        handler: Callable[[str], Any],
\n--- comate_cli/terminal_agent/tui_parts/ui_mode.py ---
from __future__ import annotations

from enum import Enum


class UIMode(Enum):
    NORMAL = "normal"
    QUESTION = "question"
    SELECTION = "selection"
    PLUGIN = "plugin"
    MARKETPLACE_INSTALL = "marketplace_install"
    MCP_CONNECTING = "mcp_connecting"
\n--- comate_cli/terminal_agent/tui.py ---
"""Terminal Agent TUI implementation.

This module was split out from `terminal_agent.app` to keep the entrypoint small.
"""

from __future__ import annotations

import asyncio
import logging
import random
import time
from collections import deque
from collections.abc import Callable
from contextlib import suppress
from pathlib import Path
from typing import Any

from prompt_toolkit.application import Application, run_in_terminal
from prompt_toolkit.completion import (
    ThreadedCompleter,
    merge_completers,
)
from prompt_toolkit.filters import Condition, has_completions, has_focus
from prompt_toolkit.history import FileHistory, InMemoryHistory
from prompt_toolkit.layout import FloatContainer, HSplit, Layout, Window
from prompt_toolkit.layout.containers import ConditionalContainer
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.styles import Style as PTStyle
from prompt_toolkit.utils import get_cwidth
