"""Command dispatcher and handler mixin for IeumApp."""
# ruff: noqa: BLE001, PLR0912, PLR2004, S110

from __future__ import annotations

import asyncio
import re
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    pass

from ieum.i18n import t, set_locale, get_locale, get_supported_locales
from ieum.approval_policy import (
    APPROVAL_POLICIES,
    approval_policy_labels,
    is_valid_approval_policy,
    normalize_approval_policy,
)


@dataclass
class CommandInfo:
    """Information about a registered command."""

    handler: Callable[..., Awaitable[None]]
    show_user_message: bool = True  # Whether to show UserMessage before handling
    accepts_args: bool = False  # Whether this command accepts arguments
    early_return: bool = False  # Whether to return after handling (skip default flow)


# Prompt for /remember command - triggers agent to review conversation and update memory/skills
REMEMBER_PROMPT = """Review our conversation and capture valuable knowledge. Focus especially on **best practices** we discussed or discovered—these are the most important things to preserve.

## Step 1: Identify Best Practices and Key Learnings

Scan the conversation for:

### Best Practices (highest priority)
- **Patterns that worked well** - approaches, techniques, or solutions we found effective
- **Anti-patterns to avoid** - mistakes, gotchas, or approaches that caused problems
- **Quality standards** - criteria we established for good code, documentation, or processes
- **Decision rationale** - why we chose one approach over another

### Other Valuable Knowledge
- Coding conventions and style preferences
- Project architecture decisions
- Workflows and processes we developed
- Tools, libraries, or techniques worth remembering
- Feedback I gave about your behavior or outputs

## Step 2: Decide Where to Store Each Learning

For each best practice or learning, choose the right destination:

### → Memory (AGENTS.md) for preferences and guidelines
Use memory when the knowledge is:
- A preference or guideline (not a multi-step process)
- Something to always keep in mind
- A simple rule or pattern

**Global** (`~/.ieum/agent/AGENTS.md`): Universal preferences across all projects
**Project** (`.ieum/AGENTS.md`): Project-specific conventions and decisions

### → Skill for reusable workflows and methodologies
**Create a skill when** we developed:
- A multi-step process worth reusing
- A methodology for a specific type of task
- A workflow with best practices baked in
- A procedure that should be followed consistently

Skills are more powerful than memory entries because they can encode **how** to do something well, not just **what** to remember.

## Step 3: Create Skills for Significant Best Practices

If we established best practices around a workflow or process, capture them in a skill.

**Example:** If we discussed best practices for code review, create a `code-review` skill that encodes those practices into a reusable workflow.

### Skill Location
`~/.ieum/agent/skills/<skill-name>/SKILL.md`

### Skill Structure
```
skill-name/
├── SKILL.md          (required - main instructions with best practices)
├── scripts/          (optional - executable code)
├── references/       (optional - detailed documentation)
└── assets/           (optional - templates, examples)
```

### SKILL.md Format
```markdown
---
name: skill-name
description: "What this skill does AND when to use it. Include triggers like 'when the user asks to X' or 'when working with Y'. This description determines when the skill activates."
---

# Skill Name

## Overview
Brief explanation of what this skill accomplishes.

## Best Practices
Capture the key best practices upfront:
- Best practice 1: explanation
- Best practice 2: explanation

## Process
Step-by-step instructions (imperative form):
1. First, do X
2. Then, do Y
3. Finally, do Z

## Common Pitfalls
- Pitfall to avoid and why
- Another anti-pattern we discovered
```

### Key Principles
1. **Encode best practices prominently** - Put them near the top so they guide the entire workflow
2. **Concise is key** - Only include non-obvious knowledge. Every paragraph should justify its token cost.
3. **Clear triggers** - The description determines when the skill activates. Be specific.
4. **Imperative form** - Write as commands: "Create a file" not "You should create a file"
5. **Include anti-patterns** - What NOT to do is often as valuable as what to do

## Step 4: Update Memory for Simpler Learnings

For preferences, guidelines, and simple rules that don't warrant a full skill:

```markdown
## Best Practices
- When doing X, always Y because Z
- Avoid A because it leads to B
```

Use `edit_file` to update existing files or `write_file` to create new ones.

## Step 5: Summarize Changes

List what you captured and where you stored it:
- Skills created (with key best practices encoded)
- Memory entries added (with location)
"""


class CommandHandlerMixin:
    """Command dispatching and handler methods for IeumApp."""

    # =========================================================================
    # Command Dispatcher - Registration and Routing
    # =========================================================================

    def _get_command_registry(self) -> dict[str, CommandInfo]:
        """Build command registry mapping command names to handlers.

        Returns:
            Dictionary mapping command names to CommandInfo objects.
        """
        return {
            # Exit commands
            "/quit": CommandInfo(self._cmd_quit, show_user_message=False),
            "/exit": CommandInfo(self._cmd_quit, show_user_message=False),
            "/q": CommandInfo(self._cmd_quit, show_user_message=False),
            # Help & info
            "/help": CommandInfo(self._cmd_help),
            "/version": CommandInfo(self._cmd_version),
            "/threads": CommandInfo(self._cmd_threads),
            "/tokens": CommandInfo(self._cmd_tokens),
            "/cost": CommandInfo(self._cmd_cost),
            # Session management
            "/new": CommandInfo(self._cmd_new_session, show_user_message=False),
            "/clear": CommandInfo(self._cmd_new_session, show_user_message=False),
            "/resume": CommandInfo(self._cmd_resume, accepts_args=True, early_return=True),
            # Memory & skills
            "/remember": CommandInfo(
                self._cmd_remember, accepts_args=True, show_user_message=False, early_return=True
            ),
            "/skills": CommandInfo(self._cmd_skills_list, early_return=True),
            "/skill": CommandInfo(self._cmd_skill_invoke, accepts_args=True, early_return=True),
            # Model management
            "/models": CommandInfo(self._cmd_models_list, early_return=True),
            "/model": CommandInfo(self._cmd_model_select, accepts_args=True, early_return=True),
            # Ralph loop mode
            "/ralph-loop": CommandInfo(
                self._cmd_ralph_loop, accepts_args=True, early_return=True
            ),
            # MCP server management
            "/mcp": CommandInfo(
                self._cmd_mcp, show_user_message=False, accepts_args=True, early_return=True
            ),
            # Environment configuration
            "/env": CommandInfo(
                self._cmd_env, show_user_message=False, accepts_args=True, early_return=True
            ),
            # Language
            "/lang": CommandInfo(
                self._cmd_lang, show_user_message=False, accepts_args=True, early_return=True
            ),
            "/approval": CommandInfo(
                self._cmd_approval, show_user_message=False, accepts_args=True, early_return=True
            ),
            # Tool invocation
            "/tools": CommandInfo(self._cmd_tools_list, early_return=True),
            "/tool": CommandInfo(self._cmd_tool_invoke, accepts_args=True, early_return=True),
            # Planning
            "/plan": CommandInfo(
                self._cmd_plan, accepts_args=True, show_user_message=False, early_return=True
            ),
            # Git auto-commit
            "/undo": CommandInfo(
                self._cmd_undo, show_user_message=False, early_return=True
            ),
            "/autocommit": CommandInfo(
                self._cmd_autocommit, show_user_message=False, early_return=True
            ),
            # Verify loop
            "/verify": CommandInfo(
                self._cmd_verify, accepts_args=True, show_user_message=False, early_return=True
            ),
            # Checkpoints
            "/checkpoint": CommandInfo(
                self._cmd_checkpoint, accepts_args=True, show_user_message=False, early_return=True
            ),
            # Memory management
            "/memory": CommandInfo(
                self._cmd_memory, accepts_args=True, show_user_message=False, early_return=True
            ),
            # Background tasks
            "/task": CommandInfo(
                self._cmd_task, accepts_args=True, show_user_message=False, early_return=True
            ),
            # Utility
            "/refresh": CommandInfo(self._cmd_refresh, show_user_message=False),
        }

    async def _handle_command(self, command: str) -> None:
        """Handle a slash command using the command dispatcher.

        Args:
            command: The slash command (including /)
        """
        from ieum.widgets.messages import SystemMessage, UserMessage

        cmd_lower = command.lower().strip()
        registry = self._get_command_registry()

        # Try exact match first
        if cmd_lower in registry:
            info = registry[cmd_lower]
            if info.show_user_message:
                await self._mount_message(UserMessage(command))
            await info.handler(command)
            return

        # Try prefix match for commands that accept arguments
        for cmd_name, info in registry.items():
            if info.accepts_args and cmd_lower.startswith(f"{cmd_name} "):
                if info.show_user_message:
                    await self._mount_message(UserMessage(command))
                await info.handler(command)
                return

        # Try matching against tool names (e.g., /git_status_tool, /web_search)
        tool_match = await self._try_tool_shorthand(command)
        if tool_match:
            return

        # Unknown command
        await self._mount_message(UserMessage(command))
        await self._mount_message(SystemMessage(f"Unknown command: {cmd_lower}"))

    # =========================================================================
    # Command Handlers - Individual command implementations
    # =========================================================================

    async def _cmd_quit(self, _command: str) -> None:
        """Handle /quit, /exit, /q commands."""
        self.exit()

    async def _cmd_help(self, _command: str) -> None:
        """Handle /help command."""
        from ieum.widgets.messages import SystemMessage

        help_text = (
            f"{t('tui.help.header')}\n"
            f"{t('tui.help.help')}\n"
            f"{t('tui.help.new')}\n"
            f"{t('tui.help.resume')}\n"
            f"{t('tui.help.clear')}\n"
            f"{t('tui.help.remember')}\n"
            f"{t('tui.help.tokens')}\n"
            "/cost                   - Show session cost ($)\n"
            f"{t('tui.help.threads')}\n"
            f"{t('tui.help.version')}\n"
            f"{t('tui.help.quit')}\n\n"
            f"{t('tui.help.skills_header')}\n"
            f"{t('tui.help.skills')}\n"
            f"{t('tui.help.skill')}\n\n"
            f"{t('tui.help.models_header')}\n"
            f"{t('tui.help.models')}\n"
            f"{t('tui.help.model')}\n\n"
            f"{t('tui.help.mcp_header')}\n"
            f"{t('tui.help.mcp')}\n"
            f"{t('tui.help.mcp_add')}\n"
            f"{t('tui.help.mcp_remove')}\n\n"
            f"{t('tui.help.settings_header')}\n"
            f"{t('tui.help.env')}\n"
            f"{t('tui.help.env_set')}\n"
            f"{t('tui.help.lang')}\n\n"
            "Tools:\n"
            "/tools                  - List all available tools\n"
            "/tool <name> [args]     - Invoke a tool directly\n\n"
            "Planning:\n"
            "/plan                   - Toggle plan mode on/off\n"
            "/plan <task>            - Start planning a task\n"
            "/plan show              - Show current plan\n"
            "/plan approve           - Approve and execute plan\n"
            "/plan cancel            - Cancel plan mode\n\n"
            "Git:\n"
            "/undo                   - Revert last git commit\n"
            "/autocommit             - Toggle auto-commit on/off\n\n"
            "Verification:\n"
            "/verify                 - Toggle auto-verify on/off\n"
            "/verify run             - Run tests/lint now\n"
            "/verify status          - Show verify config\n"
            "/verify set-test <cmd>  - Set test command\n"
            "/verify set-lint <cmd>  - Set lint command\n\n"
            "Memory:\n"
            "/memory                 - List stored memories\n"
            "/memory show <key>      - Show a memory entry\n"
            "/memory add <key> <val> - Add a memory entry\n"
            "/memory delete <key>    - Delete a memory entry\n"
            "/memory clear           - Clear all memories\n\n"
            "Background Tasks:\n"
            "/task <description>     - Run a task in the background\n"
            "/task list              - List background tasks\n"
            "/task status <id>       - Show task status\n"
            "/task cancel <id>       - Cancel a running task\n\n"
            "Checkpoints:\n"
            "/checkpoint save <name> - Save current state with a label\n"
            "/checkpoint list        - List saved checkpoints\n"
            "/checkpoint restore <n> - Restore to a checkpoint\n"
            "/checkpoint delete <n>  - Delete a checkpoint label\n\n"
            "Approval presets:\n"
            "/approval [manual|read_only|safe_write|full_trusted]\n\n"
            f"{t('tui.help.mode_header')}\n"
            f"{t('tui.help.ralph_loop')}"
        )
        await self._mount_message(SystemMessage(help_text))

    async def _cmd_version(self, _command: str) -> None:
        """Handle /version command."""
        from ieum.widgets.messages import SystemMessage

        try:
            from ieum._version import __version__

            await self._mount_message(SystemMessage(f"ieum version: {__version__}"))
        except Exception:
            await self._mount_message(SystemMessage("ieum version: unknown"))

    async def _cmd_threads(self, _command: str) -> None:
        """Handle /threads command."""
        from ieum.widgets.messages import SystemMessage

        if self._session_state:
            await self._mount_message(
                SystemMessage(f"Current session: {self._session_state.thread_id}")
            )
        else:
            await self._mount_message(SystemMessage("No active session"))

    async def _cmd_tokens(self, _command: str) -> None:
        """Handle /tokens command."""
        from ieum.widgets.messages import SystemMessage

        if self._token_tracker and self._token_tracker.current_context > 0:
            count = self._token_tracker.current_context
            if count >= 1000:
                formatted = f"{count / 1000:.1f}K"
            else:
                formatted = str(count)
            await self._mount_message(SystemMessage(f"Current context: {formatted} tokens"))
        else:
            await self._mount_message(SystemMessage("No token usage yet"))

    async def _cmd_cost(self, _command: str) -> None:
        """Handle /cost command — show session cost in dollars."""
        from ieum.widgets.messages import SystemMessage

        if self._token_tracker:
            cost = self._token_tracker.session_cost
            context = self._token_tracker.current_context
            model = getattr(self._token_tracker, "_model_name", None) or "unknown"

            if context >= 1000:
                ctx_fmt = f"{context / 1000:.1f}K"
            else:
                ctx_fmt = str(context)

            if cost > 0:
                await self._mount_message(
                    SystemMessage(
                        f"Model: {model}\n"
                        f"Context: {ctx_fmt} tokens\n"
                        f"Session cost: ${cost:.4f}"
                    )
                )
            else:
                await self._mount_message(
                    SystemMessage(
                        f"Model: {model}\n"
                        f"Context: {ctx_fmt} tokens\n"
                        f"Session cost: $0.00 (no pricing data or local model)"
                    )
                )
        else:
            await self._mount_message(SystemMessage("No cost data yet"))

    async def _cmd_new_session(self, _command: str) -> None:
        """Handle /new and /clear commands."""
        from ieum.widgets.messages import SystemMessage

        await self._clear_messages()
        if self._token_tracker:
            self._token_tracker.reset()
        self._update_status("")
        if self._session_state:
            new_thread_id = self._session_state.reset_thread()
            await self._mount_message(SystemMessage(f"Started new session: {new_thread_id}"))

    async def _cmd_resume(self, command: str) -> None:
        """Handle /resume command."""
        await self._handle_resume(command)

    async def _cmd_remember(self, command: str) -> None:
        """Handle /remember command."""
        cmd_lower = command.lower().strip()
        additional_context = ""
        if cmd_lower.startswith("/remember "):
            additional_context = command.strip()[len("/remember ") :].strip()

        if additional_context:
            final_prompt = (
                f"{REMEMBER_PROMPT}\n\n**Additional context from user:** {additional_context}"
            )
        else:
            final_prompt = REMEMBER_PROMPT

        await self._handle_user_message(final_prompt)

    async def _cmd_skills_list(self, command: str) -> None:
        """Handle /skills command."""
        await self._handle_skills_list(command)

    async def _cmd_skill_invoke(self, command: str) -> None:
        """Handle /skill <name> command."""
        cmd_lower = command.lower().strip()
        if cmd_lower == "/skill":
            await self._handle_skills_list(command)
        else:
            await self._handle_skill_invoke(command)

    async def _cmd_models_list(self, command: str) -> None:
        """Handle /models command."""
        await self._handle_models_list(command)

    async def _cmd_model_select(self, command: str) -> None:
        """Handle /model <name> command."""
        await self._handle_model_select(command)

    async def _cmd_ralph_loop(self, command: str) -> None:
        """Handle /ralph-loop command."""
        await self._handle_ralph_loop(command)

    async def _cmd_undo(self, _command: str) -> None:
        """Handle /undo command - revert the last git commit."""
        from ieum.widgets.messages import SystemMessage
        from ieum.git.auto_commit import AutoCommitManager

        mgr = AutoCommitManager(repo_path=self._cwd)
        result = mgr.undo_last_commit()
        if result.success:
            await self._mount_message(SystemMessage(f"Undo: {result.message}"))
        else:
            await self._mount_message(SystemMessage(f"Undo failed: {result.error}"))

    async def _cmd_autocommit(self, _command: str) -> None:
        """Handle /autocommit command - toggle auto-commit on/off."""
        from ieum.widgets.messages import SystemMessage

        if self._session_state is None:
            await self._mount_message(SystemMessage("No active session"))
            return

        self._session_state.auto_commit = not self._session_state.auto_commit
        state = "ON" if self._session_state.auto_commit else "OFF"
        await self._mount_message(
            SystemMessage(f"Auto-commit: {state}")
        )

    async def _cmd_verify(self, command: str) -> None:
        """Handle /verify command - auto test/lint verification loop.

        Subcommands:
            /verify             - Toggle auto-verify on/off
            /verify run         - Run tests/lint immediately
            /verify status      - Show current verify config
            /verify set-test <cmd> - Set custom test command
            /verify set-lint <cmd> - Set custom lint command
        """
        from ieum.widgets.messages import SystemMessage
        from ieum.verify_loop import VerifyConfig, detect_verify_commands, run_verify

        if self._session_state is None:
            await self._mount_message(SystemMessage("No active session"))
            return

        cmd = command.strip()
        args = cmd[len("/verify"):].strip() if len(cmd) > len("/verify") else ""
        sub = args.lower()

        # /verify status
        if sub == "status":
            cfg = self._session_state.verify_config
            if cfg is None:
                await self._mount_message(SystemMessage("Verify not configured. Use /verify to enable."))
            else:
                lines = [
                    f"Auto-verify: {'ON' if self._session_state.verify_enabled else 'OFF'}",
                    f"Test command: {cfg.test_command or '(none)'}",
                    f"Lint command: {cfg.lint_command or '(none)'}",
                    f"Max retries: {cfg.max_retries}",
                ]
                await self._mount_message(SystemMessage("\n".join(lines)))
            return

        # /verify run
        if sub == "run":
            cfg = self._session_state.verify_config
            if cfg is None:
                cfg = detect_verify_commands(self._cwd)
                self._session_state.verify_config = cfg
            if not cfg.test_command and not cfg.lint_command:
                await self._mount_message(
                    SystemMessage("No test or lint commands detected. Use /verify set-test <cmd>.")
                )
                return
            await self._mount_message(SystemMessage("Running verification..."))
            result = await run_verify(cfg, cwd=self._cwd)
            await self._mount_message(SystemMessage(result.summary))
            if not result.all_passed and result.errors:
                error_lines = "\n".join(result.errors[:5])
                await self._mount_message(SystemMessage(f"Errors:\n```\n{error_lines}\n```"))
            return

        # /verify set-test <cmd>
        if sub.startswith("set-test "):
            test_cmd = args[len("set-test "):].strip()
            if not test_cmd:
                await self._mount_message(SystemMessage("Usage: /verify set-test <command>"))
                return
            if self._session_state.verify_config is None:
                self._session_state.verify_config = VerifyConfig()
            self._session_state.verify_config.test_command = test_cmd
            await self._mount_message(SystemMessage(f"Test command set: {test_cmd}"))
            return

        # /verify set-lint <cmd>
        if sub.startswith("set-lint "):
            lint_cmd = args[len("set-lint "):].strip()
            if not lint_cmd:
                await self._mount_message(SystemMessage("Usage: /verify set-lint <command>"))
                return
            if self._session_state.verify_config is None:
                self._session_state.verify_config = VerifyConfig()
            self._session_state.verify_config.lint_command = lint_cmd
            await self._mount_message(SystemMessage(f"Lint command set: {lint_cmd}"))
            return

        # /verify (no args) - toggle
        if not args:
            self._session_state.verify_enabled = not self._session_state.verify_enabled
            if self._session_state.verify_enabled and self._session_state.verify_config is None:
                cfg = detect_verify_commands(self._cwd)
                self._session_state.verify_config = cfg
            state = "ON" if self._session_state.verify_enabled else "OFF"
            cfg = self._session_state.verify_config
            details = ""
            if cfg and self._session_state.verify_enabled:
                parts = []
                if cfg.test_command:
                    parts.append(f"test: {cfg.test_command}")
                if cfg.lint_command:
                    parts.append(f"lint: {cfg.lint_command}")
                if parts:
                    details = f" ({', '.join(parts)})"
                elif not parts:
                    details = " (no commands detected)"
            await self._mount_message(SystemMessage(f"Auto-verify: {state}{details}"))
            return

        await self._mount_message(
            SystemMessage("Unknown /verify subcommand. Use /verify, /verify run, /verify status, "
                         "/verify set-test <cmd>, or /verify set-lint <cmd>.")
        )

    async def _cmd_memory(self, command: str) -> None:
        """Handle /memory command - cross-session memory management.

        Subcommands:
            /memory                  - List stored memories
            /memory show <key>       - Show a memory entry
            /memory add <key> <val>  - Add/update a memory entry
            /memory delete <key>     - Delete a memory entry
            /memory clear            - Clear all memories
        """
        from ieum.auto_memory import (
            clear_memories,
            delete_memory,
            list_memories,
            read_memory,
            write_memory,
        )
        from ieum.widgets.messages import SystemMessage

        if not self._store:
            await self._mount_message(
                SystemMessage("Memory store not available. Start with --agent flag.")
            )
            return

        cmd = command.strip()
        args = cmd[len("/memory"):].strip() if len(cmd) > len("/memory") else ""
        sub = args.lower()

        # /memory (no args) or /memory list - list all entries
        if not args or sub == "list" or sub == "ls":
            items = await list_memories(self._store)
            if not items:
                await self._mount_message(
                    SystemMessage(
                        "No memories stored yet.\n"
                        "Use /memory add <key> <content> or /remember to capture learnings."
                    )
                )
                return
            lines = ["Stored memories:\n"]
            for item in items:
                preview = item["preview"]
                if len(preview) > 60:
                    preview = preview[:60] + "..."
                lines.append(f"  {item['key']:30} {preview}")
            lines.append(f"\nTotal: {len(items)} entries")
            lines.append("Use /memory show <key> to view full content.")
            await self._mount_message(SystemMessage("\n".join(lines)))
            return

        # /memory show <key>
        if sub.startswith("show "):
            key = args[len("show "):].strip()
            if not key:
                await self._mount_message(SystemMessage("Usage: /memory show <key>"))
                return
            content = await read_memory(self._store, key)
            if content is None:
                await self._mount_message(SystemMessage(f"Memory '{key}' not found."))
                return
            # Truncate very long content
            if len(content) > 3000:
                content = content[:3000] + "\n... (truncated)"
            await self._mount_message(
                SystemMessage(f"Memory: {key}\n\n{content}")
            )
            return

        # /memory add <key> <content>
        if sub.startswith("add "):
            parts = args[len("add "):].strip().split(maxsplit=1)
            if len(parts) < 2:
                await self._mount_message(
                    SystemMessage("Usage: /memory add <key> <content>")
                )
                return
            key, content = parts[0], parts[1]
            await write_memory(self._store, key, content)
            await self._mount_message(SystemMessage(f"Memory '{key}' saved."))
            return

        # /memory delete <key>
        if sub.startswith("delete ") or sub.startswith("rm "):
            prefix = "delete " if sub.startswith("delete ") else "rm "
            key = args[len(prefix):].strip()
            if not key:
                await self._mount_message(SystemMessage("Usage: /memory delete <key>"))
                return
            deleted = await delete_memory(self._store, key)
            if deleted:
                await self._mount_message(SystemMessage(f"Memory '{key}' deleted."))
            else:
                await self._mount_message(SystemMessage(f"Memory '{key}' not found."))
            return

        # /memory clear
        if sub == "clear":
            count = await clear_memories(self._store)
            await self._mount_message(SystemMessage(f"Cleared {count} memory entries."))
            return

        await self._mount_message(
            SystemMessage(
                "Usage:\n"
                "  /memory                  - List memories\n"
                "  /memory show <key>       - Show entry\n"
                "  /memory add <key> <val>  - Add entry\n"
                "  /memory delete <key>     - Delete entry\n"
                "  /memory clear            - Clear all"
            )
        )

    async def _cmd_checkpoint(self, command: str) -> None:
        """Handle /checkpoint command - save/list/restore/delete checkpoints.

        Subcommands:
            /checkpoint save <name> [desc]  - Label current state
            /checkpoint list                - List saved checkpoints
            /checkpoint restore <name>      - Restore to a checkpoint
            /checkpoint delete <name>       - Delete a checkpoint label
        """
        from ieum.sessions import (
            delete_checkpoint_label,
            get_checkpoint_by_label,
            list_checkpoint_labels,
            save_checkpoint_label,
        )
        from ieum.widgets.messages import SystemMessage

        if self._session_state is None:
            await self._mount_message(SystemMessage("No active session"))
            return

        thread_id = self._session_state.thread_id
        cmd = command.strip()
        args = cmd[len("/checkpoint"):].strip() if len(cmd) > len("/checkpoint") else ""
        sub = args.lower()

        # /checkpoint list
        if sub == "list" or sub == "ls":
            labels = await list_checkpoint_labels(thread_id)
            if not labels:
                await self._mount_message(SystemMessage("No saved checkpoints."))
                return
            lines = ["Saved checkpoints:\n"]
            for cp in labels:
                desc = f" - {cp['description']}" if cp['description'] else ""
                ts = cp['created_at'][:19].replace("T", " ")
                lines.append(f"  {cp['label']:20} {ts}{desc}")
            await self._mount_message(SystemMessage("\n".join(lines)))
            return

        # /checkpoint save <name> [description]
        if sub.startswith("save "):
            parts = args[len("save "):].strip().split(maxsplit=1)
            if not parts:
                await self._mount_message(
                    SystemMessage("Usage: /checkpoint save <name> [description]")
                )
                return
            label = parts[0]
            desc = parts[1] if len(parts) > 1 else ""
            cp_id = await save_checkpoint_label(thread_id, label, desc)
            if cp_id:
                await self._mount_message(
                    SystemMessage(f"Checkpoint '{label}' saved (id: {cp_id[:12]}...)")
                )
            else:
                await self._mount_message(
                    SystemMessage("No checkpoint found for current session.")
                )
            return

        # /checkpoint restore <name>
        if sub.startswith("restore "):
            label = args[len("restore "):].strip()
            if not label:
                await self._mount_message(
                    SystemMessage("Usage: /checkpoint restore <name>")
                )
                return
            cp_id = await get_checkpoint_by_label(thread_id, label)
            if not cp_id:
                await self._mount_message(
                    SystemMessage(f"Checkpoint '{label}' not found. Use /checkpoint list.")
                )
                return

            # Restore by setting checkpoint_id in the config for next agent invocation
            # LangGraph will resume from this checkpoint_id
            self._session_state._restore_checkpoint_id = cp_id
            await self._mount_message(
                SystemMessage(
                    f"Restored to checkpoint '{label}'.\n"
                    "Next agent message will continue from this point."
                )
            )
            return

        # /checkpoint delete <name>
        if sub.startswith("delete ") or sub.startswith("rm "):
            prefix = "delete " if sub.startswith("delete ") else "rm "
            label = args[len(prefix):].strip()
            if not label:
                await self._mount_message(
                    SystemMessage("Usage: /checkpoint delete <name>")
                )
                return
            deleted = await delete_checkpoint_label(thread_id, label)
            if deleted:
                await self._mount_message(
                    SystemMessage(f"Checkpoint '{label}' deleted.")
                )
            else:
                await self._mount_message(
                    SystemMessage(f"Checkpoint '{label}' not found.")
                )
            return

        # /checkpoint (no args) - show usage
        await self._mount_message(
            SystemMessage(
                "Usage:\n"
                "  /checkpoint save <name> [desc] - Save current state\n"
                "  /checkpoint list               - List checkpoints\n"
                "  /checkpoint restore <name>     - Restore to checkpoint\n"
                "  /checkpoint delete <name>      - Delete checkpoint"
            )
        )

    async def _cmd_task(self, command: str) -> None:
        """Handle /task command - background agent tasks.

        Subcommands:
            /task <description>     - Submit a background task
            /task list              - List background tasks
            /task status <id>       - Show task status
            /task cancel <id>       - Cancel a running task
        """
        from ieum.widgets.messages import SystemMessage

        if self._session_state is None:
            await self._mount_message(SystemMessage("No active session"))
            return

        cmd = command.strip()
        args = cmd[len("/task"):].strip() if len(cmd) > len("/task") else ""
        sub = args.lower()

        # /task list
        if sub == "list" or sub == "ls":
            await self._task_list()
            return

        # /task status <id>
        if sub.startswith("status "):
            task_id = args[len("status "):].strip()
            if not task_id:
                await self._mount_message(SystemMessage("Usage: /task status <task_id>"))
                return
            await self._task_status(task_id)
            return

        # /task cancel <id>
        if sub.startswith("cancel "):
            task_id = args[len("cancel "):].strip()
            if not task_id:
                await self._mount_message(SystemMessage("Usage: /task cancel <task_id>"))
                return
            await self._task_cancel(task_id)
            return

        # /task <description> - submit a new background task
        if args:
            await self._task_submit(args)
            return

        # /task (no args) - show usage
        await self._mount_message(
            SystemMessage(
                "Usage:\n"
                "  /task <description>     - Run a task in the background\n"
                "  /task list              - List background tasks\n"
                "  /task status <id>       - Show task status\n"
                "  /task cancel <id>       - Cancel a running task"
            )
        )

    async def _task_submit(self, description: str) -> None:
        """Submit a new background task."""
        from ieum.widgets.messages import SystemMessage

        if not self._agent or not self._session_state:
            await self._mount_message(SystemMessage("Agent not configured."))
            return

        try:
            from ieum.server.services.task_service import BackgroundTask, TaskService

            # Get or create a task service for TUI mode
            if not hasattr(self, "_task_service") or self._task_service is None:
                self._task_service = TaskService(max_concurrent=2)
                self._task_service._session_service = None
                await self._task_service.start(worker_count=1)

            task = await self._task_service.create_task(
                session_id=self._session_state.thread_id,
                user_id="local",
                description=description,
            )
            await self._mount_message(
                SystemMessage(
                    f"Background task created: {task.id}\n"
                    f"Description: {description}\n"
                    "Use /task list to check progress."
                )
            )
        except Exception as e:
            await self._mount_message(SystemMessage(f"Failed to create task: {e}"))

    async def _task_list(self) -> None:
        """List background tasks."""
        from ieum.widgets.messages import SystemMessage

        if not hasattr(self, "_task_service") or self._task_service is None:
            await self._mount_message(SystemMessage("No background tasks yet."))
            return

        tasks = self._task_service.list_tasks(
            self._session_state.thread_id,
        )
        if not tasks:
            await self._mount_message(SystemMessage("No background tasks."))
            return

        lines = ["Background tasks:\n"]
        for task in tasks:
            status_icon = {
                "queued": "⏳",
                "running": "🔄",
                "completed": "✅",
                "failed": "❌",
                "cancelled": "⛔",
            }.get(task.status, "?")
            desc = task.description[:50]
            if len(task.description) > 50:
                desc += "..."
            lines.append(f"  {status_icon} {task.id}  {task.status:10}  {desc}")
        await self._mount_message(SystemMessage("\n".join(lines)))

    async def _task_status(self, task_id: str) -> None:
        """Show status of a specific task."""
        from ieum.widgets.messages import SystemMessage

        if not hasattr(self, "_task_service") or self._task_service is None:
            await self._mount_message(SystemMessage("No task service available."))
            return

        task = self._task_service.get_task(task_id)
        if task is None:
            await self._mount_message(SystemMessage(f"Task '{task_id}' not found."))
            return

        lines = [
            f"Task: {task.id}",
            f"Status: {task.status}",
            f"Description: {task.description}",
            f"Created: {task.created_at.isoformat()[:19]}",
        ]
        if task.started_at:
            lines.append(f"Started: {task.started_at.isoformat()[:19]}")
        if task.completed_at:
            lines.append(f"Completed: {task.completed_at.isoformat()[:19]}")
        if task.result_summary:
            lines.append(f"Result: {task.result_summary}")
        if task.error:
            lines.append(f"Error: {task.error}")
        await self._mount_message(SystemMessage("\n".join(lines)))

    async def _task_cancel(self, task_id: str) -> None:
        """Cancel a background task."""
        from ieum.widgets.messages import SystemMessage

        if not hasattr(self, "_task_service") or self._task_service is None:
            await self._mount_message(SystemMessage("No task service available."))
            return

        cancelled = await self._task_service.cancel_task(task_id)
        if cancelled:
            await self._mount_message(SystemMessage(f"Task '{task_id}' cancelled."))
        else:
            task = self._task_service.get_task(task_id)
            if task is None:
                await self._mount_message(SystemMessage(f"Task '{task_id}' not found."))
            else:
                await self._mount_message(
                    SystemMessage(f"Cannot cancel task '{task_id}' (status: {task.status})")
                )

    async def _cmd_refresh(self, _command: str) -> None:
        """Handle /refresh command - redraw screen."""
        self.refresh(repaint=True, layout=True)

    async def _cmd_plan(self, command: str) -> None:
        """Handle /plan command - plan mode management.

        Subcommands:
            /plan             - Toggle plan mode on/off
            /plan <task>      - Enter plan mode and start planning a task
            /plan show        - Show the current captured plan
            /plan approve     - Approve plan and execute it
            /plan cancel      - Cancel plan mode and discard plan
        """
        from ieum.widgets.messages import SystemMessage

        if self._session_state is None:
            await self._mount_message(SystemMessage("No active session"))
            return

        cmd = command.strip()
        args = cmd[len("/plan"):].strip() if len(cmd) > len("/plan") else ""
        sub = args.lower()

        # /plan show
        if sub == "show":
            if self._session_state.current_plan:
                await self._mount_message(
                    SystemMessage(f"Current Plan:\n\n{self._session_state.current_plan}")
                )
            else:
                await self._mount_message(SystemMessage("No plan captured yet."))
            return

        # /plan approve
        if sub == "approve":
            plan = self._session_state.current_plan
            task = self._session_state._plan_original_task
            if not plan:
                await self._mount_message(
                    SystemMessage("No plan to approve. Use '/plan <task>' first.")
                )
                return
            # Auto-checkpoint before plan execution
            try:
                from ieum.sessions import save_checkpoint_label
                from datetime import datetime, UTC as _UTC
                ts = datetime.now(_UTC).strftime("%Y%m%d-%H%M%S")
                await save_checkpoint_label(
                    self._session_state.thread_id,
                    f"pre-plan-{ts}",
                    "Auto-saved before plan execution",
                )
            except Exception:
                pass  # Auto-checkpoint is best-effort
            # Exit plan mode → restore full tool access
            self._session_state.exit_plan_mode()
            self._auto_approve = self._session_state.auto_approve
            if self._status_bar:
                self._status_bar.set_approval_mode(self._session_state.get_current_mode())
            await self._mount_message(SystemMessage("Plan approved. Executing..."))
            # Execute with full tools
            from ieum.plan_mode import PLAN_EXECUTE_TEMPLATE

            execute_prompt = PLAN_EXECUTE_TEMPLATE.format(
                plan=plan, task=task or "(not specified)"
            )
            await self._handle_user_message(execute_prompt)
            return

        # /plan cancel
        if sub == "cancel":
            if not self._session_state.plan_mode:
                await self._mount_message(SystemMessage("Not in plan mode."))
                return
            self._session_state.exit_plan_mode()
            self._session_state.current_plan = None
            self._session_state._plan_original_task = None
            self._auto_approve = self._session_state.auto_approve
            if self._status_bar:
                self._status_bar.set_approval_mode(self._session_state.get_current_mode())
            await self._mount_message(SystemMessage("Plan mode cancelled."))
            return

        # /plan (no args) - toggle
        if not args:
            if self._session_state.plan_mode:
                self._session_state.exit_plan_mode()
                self._auto_approve = self._session_state.auto_approve
                if self._status_bar:
                    self._status_bar.set_approval_mode(self._session_state.get_current_mode())
                await self._mount_message(SystemMessage("Plan mode OFF."))
            else:
                self._session_state.enter_plan_mode()
                self._auto_approve = self._session_state.auto_approve
                if self._status_bar:
                    self._status_bar.set_approval_mode("plan")
                await self._mount_message(
                    SystemMessage(
                        "Plan mode ON. Agent will only read/analyze.\n"
                        "Type your task or use '/plan cancel' to exit."
                    )
                )
            return

        # /plan <task> - enter plan mode and start planning
        if not self._session_state.plan_mode:
            self._session_state.enter_plan_mode()
            self._auto_approve = self._session_state.auto_approve
            if self._status_bar:
                self._status_bar.set_approval_mode("plan")
        self._session_state._plan_original_task = args
        await self._mount_message(
            SystemMessage(f"Planning: {args}")
        )
        await self._handle_user_message(args)

    async def _cmd_lang(self, command: str) -> None:
        """Handle /lang command - change language."""
        from ieum.widgets.messages import SystemMessage

        parts = command.strip().split()
        if len(parts) < 2:
            # Show current language and usage
            current = get_locale()
            lang_name = "English" if current == "en" else "한국어"
            await self._mount_message(
                SystemMessage(
                    f"{t('tui.lang.current', lang=lang_name)}\n"
                    f"{t('tui.lang.usage')}"
                )
            )
            return

        new_lang = parts[1].lower().strip()
        supported = get_supported_locales()

        if new_lang not in supported:
            await self._mount_message(
                SystemMessage(f"{t('tui.lang.usage')}\nSupported: {', '.join(supported)}")
            )
            return

        # Set locale
        set_locale(new_lang)

        # Save to .env
        import os
        os.environ["IEUM_LANG"] = new_lang
        from ieum.sessions import save_env_file, load_env_file
        env_settings = load_env_file("global") or {}
        env_settings["lang"] = new_lang
        save_env_file(env_settings, "global")

        lang_name = "English" if new_lang == "en" else "한국어"
        await self._mount_message(
            SystemMessage(
                f"{t('tui.lang.changed', lang=lang_name)}\n"
                f"{t('tui.lang.restart_hint')}"
            )
        )

    async def _cmd_approval(self, command: str) -> None:
        """Handle /approval command - show or set approval preset policy."""
        from ieum.widgets.messages import SystemMessage

        if self._session_state is None:
            await self._mount_message(SystemMessage("No active session"))
            return

        parts = command.strip().split()
        if len(parts) < 2:
            labels = "\n".join(f"- {label}" for label in approval_policy_labels())
            await self._mount_message(
                SystemMessage(
                    f"Current approval policy: {self._session_state.approval_policy}\n"
                    "Usage: /approval <policy>\n"
                    f"Available policies:\n{labels}"
                )
            )
            return

        if not is_valid_approval_policy(parts[1]):
            await self._mount_message(
                SystemMessage(
                    "Unknown approval policy.\n"
                    "Usage: /approval <manual|read_only|safe_write|full_trusted>"
                )
            )
            return
        requested = normalize_approval_policy(parts[1])
        if requested not in APPROVAL_POLICIES:
            requested = "manual"

        self._session_state.set_approval_policy(requested)
        await self._mount_message(
            SystemMessage(
                f"Approval policy updated: {requested}\n"
                "This affects future tool approval decisions."
            )
        )

    async def _cmd_tools_list(self, _command: str) -> None:
        """Handle /tools command - list all available tools."""
        from ieum.widgets.messages import SystemMessage, UserMessage

        await self._mount_message(UserMessage("/tools"))

        try:
            from ieum.tools import ALL_TOOLS
            from ieum.tools.basic import (
                fetch_url, http_request, python_repl, ripgrep, web_search,
            )

            basic_tools = [http_request, fetch_url, ripgrep, python_repl, web_search]
            all_tools = basic_tools + list(ALL_TOOLS)

            # Deduplicate by name
            seen: set[str] = set()
            unique_tools = []
            for t in all_tools:
                name = getattr(t, "name", str(t))
                if name not in seen:
                    seen.add(name)
                    unique_tools.append(t)

            # Group by category
            categories: dict[str, list] = {
                "Git": [],
                "Search": [],
                "Code Analysis": [],
                "Review & Docs": [],
                "Other": [],
            }
            for t in unique_tools:
                name = getattr(t, "name", str(t))
                desc = getattr(t, "description", "")
                if isinstance(desc, str):
                    desc = desc.split("\n")[0][:60]
                entry = f"  {name:30} {desc}"
                if "git" in name:
                    categories["Git"].append(entry)
                elif name in ("ripgrep", "web_search", "fetch_url", "http_request"):
                    categories["Search"].append(entry)
                elif "lsp" in name or "analyze" in name or "call_graph" in name or "dependencies" in name or "index" in name or "search_symbols" in name:
                    categories["Code Analysis"].append(entry)
                elif "review" in name or "security" in name or "doc" in name or "readme" in name:
                    categories["Review & Docs"].append(entry)
                else:
                    categories["Other"].append(entry)

            lines = ["Available Tools:\n"]
            for cat, entries in categories.items():
                if entries:
                    lines.append(f"  [{cat}]")
                    lines.extend(entries)
                    lines.append("")

            lines.append("Usage: /tool <name> [key=value ...] or /<tool_name> [args]")
            await self._mount_message(SystemMessage("\n".join(lines)))
        except ImportError:
            await self._mount_message(
                SystemMessage("Tools module not available. Check ieum installation.")
            )

    async def _cmd_tool_invoke(self, command: str) -> None:
        """Handle /tool <name> [args] command - invoke a tool directly."""
        from ieum.widgets.messages import SystemMessage, UserMessage

        await self._mount_message(UserMessage(command))

        args_str = command.strip()[len("/tool "):].strip()
        if not args_str:
            await self._mount_message(
                SystemMessage("Usage: /tool <name> [key=value ...]\nExample: /tool git_status_tool")
            )
            return

        parts = args_str.split(maxsplit=1)
        tool_name = parts[0]
        tool_args_str = parts[1] if len(parts) > 1 else ""

        await self._invoke_tool_by_name(tool_name, tool_args_str)

    async def _try_tool_shorthand(self, command: str) -> bool:
        """Try to match an unknown command against tool names.

        Returns True if matched and handled.
        """
        cmd = command.strip()
        # Split /tool_name args
        parts = cmd.split(maxsplit=1)
        tool_name = parts[0].lstrip("/")
        tool_args_str = parts[1] if len(parts) > 1 else ""

        # Try to find a matching tool
        tool = self._find_tool_by_name(tool_name)
        if tool is None:
            return False

        from ieum.widgets.messages import UserMessage
        await self._mount_message(UserMessage(cmd))
        await self._invoke_tool_by_name(tool_name, tool_args_str)
        return True

    def _find_tool_by_name(self, name: str):
        """Find a tool object by name from ALL_TOOLS + basic tools."""
        try:
            from ieum.tools import ALL_TOOLS
            from ieum.tools.basic import (
                fetch_url, http_request, python_repl, ripgrep, web_search,
            )
            all_tools = [http_request, fetch_url, ripgrep, python_repl, web_search] + list(ALL_TOOLS)
            for t in all_tools:
                if getattr(t, "name", "") == name:
                    return t
        except ImportError:
            pass
        return None

    async def _invoke_tool_by_name(self, tool_name: str, args_str: str) -> None:
        """Look up a tool by name, parse arguments, invoke it, and display the result."""
        import json as _json

        from ieum.widgets.messages import SystemMessage

        tool = self._find_tool_by_name(tool_name)
        if tool is None:
            await self._mount_message(
                SystemMessage(f"Tool '{tool_name}' not found. Use /tools to list available tools.")
            )
            return

        # Parse arguments
        args_dict: dict = {}
        args_str = args_str.strip()
        if args_str:
            if args_str.startswith("{"):
                # JSON object
                try:
                    args_dict = _json.loads(args_str)
                except _json.JSONDecodeError as e:
                    await self._mount_message(SystemMessage(f"Invalid JSON arguments: {e}"))
                    return
            elif "=" in args_str:
                # key=value pairs
                for pair in args_str.split():
                    if "=" in pair:
                        k, v = pair.split("=", 1)
                        args_dict[k] = v
                    else:
                        # Positional - map to first required param
                        args_dict = self._map_positional_arg(tool, pair)
                        break
            else:
                # Single positional argument
                args_dict = self._map_positional_arg(tool, args_str)

        try:
            result = await asyncio.get_event_loop().run_in_executor(
                None, lambda: tool.invoke(args_dict)
            )
            # Format output
            if isinstance(result, dict):
                output = _json.dumps(result, indent=2, ensure_ascii=False)
            else:
                output = str(result)
            # Truncate very long output
            if len(output) > 5000:
                output = output[:5000] + "\n... (truncated)"
            await self._mount_message(SystemMessage(f"[{tool_name}]\n{output}"))
        except Exception as e:
            await self._mount_message(SystemMessage(f"Tool error: {e}"))

    @staticmethod
    def _map_positional_arg(tool, value: str) -> dict:
        """Map a single positional argument to the first required parameter."""
        schema = getattr(tool, "args_schema", None)
        if schema:
            # Pydantic model - find first required field
            fields = schema.model_fields if hasattr(schema, "model_fields") else {}
            for field_name, field_info in fields.items():
                if field_info.is_required():
                    return {field_name: value}
        return {"__arg1": value}

    # =========================================================================
    # Complex Command Implementations
    # =========================================================================

    async def _handle_resume(self, command: str) -> None:
        """Handle /resume command - resume a previous conversation.

        Args:
            command: The full command string (e.g., "/resume" or "/resume abc123")
        """
        from ieum.sessions import list_threads, thread_exists
        from ieum.widgets.messages import SystemMessage, UserMessage

        await self._mount_message(UserMessage(command))

        # Check if a specific thread ID was provided
        cmd = command.strip()
        if cmd.startswith("/resume "):
            thread_id = cmd[len("/resume "):].strip()
            if thread_id:
                # Resume specific thread
                if await thread_exists(thread_id):
                    await self._resume_thread(thread_id)
                else:
                    await self._mount_message(
                        SystemMessage(f"Thread '{thread_id}' not found. Use /resume to see available threads.")
                    )
                return

        # List recent threads for selection
        threads = await list_threads(limit=10)

        if not threads:
            await self._mount_message(
                SystemMessage("No previous conversations found. Use /new to start a new conversation.")
            )
            return

        # Format thread list with message previews
        from ieum.sessions import get_conversation_preview, get_thread_message_count

        lines = ["Recent conversations:\n"]
        for i, thr in enumerate(threads, 1):
            thread_id = thr["thread_id"]
            updated = thr.get("updated_at", "")
            if updated:
                try:
                    from datetime import datetime
                    dt = datetime.fromisoformat(updated).astimezone()
                    time_str = dt.strftime("%b %d, %I:%M%p").lower()
                except (ValueError, TypeError):
                    time_str = ""
            else:
                time_str = ""

            # Fetch message count and preview
            msg_count = await get_thread_message_count(thread_id)
            preview = await get_conversation_preview(thread_id)

            count_str = f"({msg_count} msgs)" if msg_count > 0 else ""
            preview_str = ""
            if preview:
                truncated = preview[:40].replace("\n", " ")
                if len(preview) > 40:
                    truncated += "..."
                preview_str = f'"{truncated}"'

            line = f"  {i}. {thread_id}  {time_str}"
            if count_str:
                line += f"  {count_str}"
            if preview_str:
                line += f"  {preview_str}"
            lines.append(line)

        lines.append("\nUsage: /resume <thread_id> to resume a conversation")
        await self._mount_message(SystemMessage("\n".join(lines)))

    async def _resume_thread(self, thread_id: str) -> None:
        """Resume a specific thread by switching to it.

        Args:
            thread_id: The thread ID to resume
        """
        from langchain_core.messages import AIMessage, HumanMessage

        from ieum.widgets.messages import AssistantMessage, SystemMessage, UserMessage

        # Clear current messages
        await self._clear_messages()
        if self._token_tracker:
            self._token_tracker.reset()
        self._update_status("")

        # Update session state with the new thread ID
        if self._session_state:
            self._session_state.thread_id = thread_id

        await self._mount_message(SystemMessage(f"Resumed session: {thread_id}"))

        # Load and display previous messages from the checkpoint
        if self._agent and self._checkpointer:
            try:
                config = {"configurable": {"thread_id": thread_id}}
                state = await self._agent.aget_state(config)
                if state and state.values:
                    messages = state.values.get("messages", [])
                    if messages:
                        # Display last few messages for context
                        recent_messages = messages[-10:]  # Show last 10 messages
                        for msg in recent_messages:
                            if hasattr(msg, "content") and msg.content:
                                if isinstance(msg, HumanMessage):
                                    await self._mount_message(UserMessage(str(msg.content)))
                                elif isinstance(msg, AIMessage):
                                    await self._mount_message(AssistantMessage(str(msg.content)))
                        await self._mount_message(
                            SystemMessage(f"Showing last {len(recent_messages)} messages from previous session.")
                        )
            except Exception:
                # Failed to load previous messages, but session is still resumed
                pass

    async def _handle_ralph_loop(self, command: str) -> None:
        """Handle Ralph loop mode - autonomous iterations.

        Args:
            command: The full command string (e.g., "/ralph-loop Build a REST API")
        """
        from ieum.textual_adapter import execute_task_textual
        from ieum.widgets.messages import ErrorMessage, SystemMessage, UserMessage

        # Parse the command
        cmd_lower = command.lower().strip()

        if cmd_lower == "/ralph-loop":
            # No task provided, show usage
            await self._mount_message(UserMessage(command))
            await self._mount_message(
                SystemMessage(
                    "Usage: /ralph-loop <task> [--iterations N]\n\n"
                    "Examples:\n"
                    "  /ralph-loop Build a REST API\n"
                    "  /ralph-loop Create a Python CLI tool --iterations 5\n"
                    "  /ralph-loop Write unit tests for this project\n\n"
                    "Press Ctrl+C to stop the loop."
                )
            )
            return

        # Extract task and options
        args = command.strip()[len("/ralph-loop "):].strip()

        # Parse --iterations flag
        max_iterations = 0  # unlimited by default
        if "--iterations " in args:
            parts = args.split("--iterations ")
            task = parts[0].strip()
            try:
                max_iterations = int(parts[1].split()[0])
            except (ValueError, IndexError):
                max_iterations = 0
        elif "-i " in args:
            parts = args.split("-i ")
            task = parts[0].strip()
            try:
                max_iterations = int(parts[1].split()[0])
            except (ValueError, IndexError):
                max_iterations = 0
        else:
            task = args

        if not task:
            await self._mount_message(UserMessage(command))
            await self._mount_message(SystemMessage("Error: Please provide a task for Ralph loop."))
            return

        await self._mount_message(UserMessage(command))

        # Show Ralph mode header
        iter_display = f"{max_iterations} iterations" if max_iterations > 0 else "unlimited (Ctrl+C to stop)"
        await self._mount_message(
            SystemMessage(
                f"╔══════════════════════════════════════════════════════════╗\n"
                f"║                     RALPH MODE                           ║\n"
                f"╚══════════════════════════════════════════════════════════╝\n\n"
                f"Task: {task}\n"
                f"Iterations: {iter_display}\n"
                f"Auto-approve: ✓ enabled"
            )
        )

        # Run Ralph loop in a worker to avoid blocking the UI event loop
        self._agent_worker = self.run_worker(
            self._run_ralph_loop(task, max_iterations),
            exclusive=False,
        )

    async def _run_ralph_loop(self, task: str, max_iterations: int) -> None:
        """Run the Ralph loop iterations in a background worker.

        Args:
            task: The task description for Ralph mode
            max_iterations: Maximum iterations (0 for unlimited)
        """
        from ieum.textual_adapter import execute_task_textual
        from ieum.widgets.messages import ErrorMessage, SystemMessage

        iteration = 1
        original_auto_approve = self._session_state.auto_approve if self._session_state else False
        try:
            # Ralph mode always runs with auto-approve
            if self._session_state:
                self._session_state.auto_approve = True

            while max_iterations == 0 or iteration <= max_iterations:
                # Check if agent is available
                if not self._agent or not self._ui_adapter or not self._session_state:
                    await self._mount_message(SystemMessage("Error: Agent not configured."))
                    break

                await self._mount_message(
                    SystemMessage(f"═══ RALPH ITERATION {iteration} ═══")
                )

                # Build the Ralph prompt
                iter_str = f"{iteration}/{max_iterations}" if max_iterations > 0 else str(iteration)
                ralph_prompt = f"""## Ralph Mode - Iteration {iter_str}

Your previous work is in the filesystem. Check what exists and keep building.

**TASK:**
{task}

**Instructions:**
1. First, check the current state of files (use ls, read_file, glob)
2. Review any existing work from previous iterations
3. Make meaningful progress on the task
4. Use git to track your changes if appropriate
5. You'll be called again in the next iteration

Make progress. Focus on the task."""

                # Run the agent for this iteration
                self._agent_running = True
                if self._chat_input:
                    self._chat_input.set_cursor_active(active=False)
                    self._chat_input.set_submit_enabled(enabled=False)

                try:
                    await execute_task_textual(
                        user_input=ralph_prompt,
                        agent=self._agent,
                        assistant_id=self._assistant_id,
                        session_state=self._session_state,
                        adapter=self._ui_adapter,
                        backend=self._backend,
                    )
                except Exception as e:
                    await self._mount_message(ErrorMessage(f"Error in iteration {iteration}: {e}"))
                    break

                iteration += 1

                if max_iterations == 0 or iteration <= max_iterations:
                    await self._mount_message(SystemMessage(f"...continuing to iteration {iteration}"))

        except asyncio.CancelledError:
            await self._mount_message(SystemMessage(f"Ralph stopped after {iteration - 1} iteration(s)"))
        finally:
            # Restore original auto_approve setting
            if self._session_state:
                self._session_state.auto_approve = original_auto_approve
            await self._cleanup_agent_task()
            await self._mount_message(
                SystemMessage(f"Ralph Mode Complete. Total iterations: {iteration - 1}")
            )

    async def _handle_skills_list(self, command: str) -> None:
        """Handle /skills command - list available skills.

        Args:
            command: The original command string
        """
        from ieum.skills.load import list_skills
        from ieum.config import settings
        from ieum.widgets.messages import SystemMessage, UserMessage

        await self._mount_message(UserMessage(command))

        # Load skills from user and project directories
        user_skills_dir = settings.get_user_skills_dir(self._assistant_id or "agent")
        project_skills_dir = settings.get_project_skills_dir()

        skills = list_skills(
            user_skills_dir=user_skills_dir,
            project_skills_dir=project_skills_dir,
        )

        if not skills:
            await self._mount_message(
                SystemMessage(
                    "No skills found.\n\n"
                    "Install example skills:\n"
                    "  cp -r examples/skills/tdd ~/.ieum/agent/skills/\n\n"
                    "Or create your own:\n"
                    "  ieum skills create my-skill"
                )
            )
            return

        # Format skills list
        lines = ["Available Skills:\n"]
        for skill in sorted(skills, key=lambda s: s["name"]):
            source = "[user]" if skill["source"] == "user" else "[project]"
            lines.append(f"  {skill['name']:20} {source} - {skill['description'][:50]}...")

        lines.append(f"\nUse /skill <name> <task> to invoke a skill.")
        await self._mount_message(SystemMessage("\n".join(lines)))

    async def _handle_skill_invoke(self, command: str) -> None:
        """Handle /skill <name> <task> - invoke a skill.

        Args:
            command: The full command string (e.g., "/skill tdd Write tests for auth")
        """
        from ieum.skills.load import list_skills
        from ieum.config import settings
        from ieum.widgets.messages import SystemMessage, UserMessage

        from pathlib import Path

        # Parse command: /skill <name> <task>
        args = command.strip()[len("/skill "):].strip()
        parts = args.split(maxsplit=1)

        if len(parts) < 1:
            await self._mount_message(UserMessage(command))
            await self._mount_message(
                SystemMessage("Usage: /skill <skill-name> [task]\n\nExample: /skill tdd Write tests for login")
            )
            return

        skill_name = parts[0]
        task = parts[1] if len(parts) > 1 else ""

        # Load skills
        user_skills_dir = settings.get_user_skills_dir(self._assistant_id or "agent")
        project_skills_dir = settings.get_project_skills_dir()
        skills = list_skills(user_skills_dir=user_skills_dir, project_skills_dir=project_skills_dir)

        # Find the skill
        skill = next((s for s in skills if s["name"] == skill_name), None)
        if not skill:
            await self._mount_message(UserMessage(command))
            await self._mount_message(SystemMessage(f"Skill '{skill_name}' not found. Use /skills to list available skills."))
            return

        # Read the skill content
        skill_path = Path(skill["path"])
        try:
            skill_content = skill_path.read_text(encoding="utf-8")
            # Extract content after frontmatter
            match = re.match(r"^---\s*\n.*?\n---\s*\n(.*)$", skill_content, re.DOTALL)
            if match:
                skill_prompt = match.group(1).strip()
            else:
                skill_prompt = skill_content.strip()
        except Exception as e:
            await self._mount_message(UserMessage(command))
            await self._mount_message(SystemMessage(f"Error reading skill: {e}"))
            return

        # Combine skill prompt with user task
        if task:
            full_prompt = f"[SKILL: {skill_name}]\n\n{skill_prompt}\n\n---\n\nUser Task: {task}"
        else:
            full_prompt = f"[SKILL: {skill_name}]\n\n{skill_prompt}"

        # Send to agent
        await self._handle_user_message(full_prompt)

    async def _handle_models_list(self, command: str) -> None:
        """Handle /models command - list available models from Ollama."""
        import requests
        from ieum.config import settings
        from ieum.widgets.messages import SystemMessage, UserMessage

        await self._mount_message(UserMessage(command))

        # Try to get models from Ollama
        models_info = []

        # Check Ollama
        if settings.has_ollama:
            try:
                # Ollama OpenAI-compatible endpoint requires /v1 suffix
                ollama_api = settings.ollama_base_url.rstrip("/")
                if not ollama_api.endswith("/v1"):
                    ollama_api += "/v1"
                resp = requests.get(f"{ollama_api}/models", timeout=5)
                if resp.status_code == 200:
                    data = resp.json()
                    for model in data.get("data", []):
                        model_id = model.get("id", "")
                        models_info.append(f"  ollama/{model_id}")
            except Exception:
                models_info.append("  [Ollama: connection failed]")

        # Check LMStudio
        if settings.has_lmstudio:
            try:
                resp = requests.get(f"{settings.lmstudio_base_url}/models", timeout=5)
                if resp.status_code == 200:
                    data = resp.json()
                    for model in data.get("data", []):
                        model_id = model.get("id", "")
                        models_info.append(f"  lmstudio/{model_id}")
            except Exception:
                models_info.append("  [LMStudio: connection failed]")

        # Check vLLM
        if settings.has_vllm:
            try:
                resp = requests.get(f"{settings.vllm_base_url}/models", timeout=5)
                if resp.status_code == 200:
                    data = resp.json()
                    for model in data.get("data", []):
                        model_id = model.get("id", "")
                        models_info.append(f"  vllm/{model_id}")
            except Exception:
                models_info.append("  [vLLM: connection failed]")

        if not models_info:
            await self._mount_message(
                SystemMessage(
                    "No LLM servers configured.\n\n"
                    "Set one of these in your .env:\n"
                    "  OLLAMA_BASE_URL=http://localhost:11434/v1\n"
                    "  LMSTUDIO_BASE_URL=http://localhost:1234/v1\n"
                    "  VLLM_BASE_URL=http://localhost:8000/v1"
                )
            )
            return

        # Show current model
        current = settings.model_name or "Not set"

        msg = f"Current model: {current}\n\nAvailable models:\n" + "\n".join(models_info)
        msg += "\n\nTo switch: /model <provider>/<model-name>"
        await self._mount_message(SystemMessage(msg))

    async def _handle_model_select(self, command: str) -> None:
        """Handle /model <name> command - select a different model."""
        from ieum.widgets.messages import SystemMessage, UserMessage

        await self._mount_message(UserMessage(command))

        parts = command.strip()[len("/model "):].strip()
        if not parts:
            await self._mount_message(
                SystemMessage("Usage: /model <provider>/<model-name>\nExample: /model ollama/qwen3-coder:30b")
            )
            return

        model_name = parts

        # Validate model name format
        if "/" not in model_name:
            await self._mount_message(
                SystemMessage(
                    f"Invalid model format: {model_name}\n"
                    "Use format: <provider>/<model-name>\n"
                    "Examples:\n"
                    "  /model ollama/qwen3-coder:30b\n"
                    "  /model lmstudio/qwen2.5-coder\n"
                    "  /model vllm/mistral-7b"
                )
            )
            return

        # Try to recreate the agent with the new model
        try:
            from ieum.agent import create_cli_agent
            from ieum.config import create_model, settings
            from ieum.tools import fetch_url, http_request, python_repl, ripgrep, web_search

            # Create new model instance
            new_model = create_model(model_name)

            # Build tools list
            tools = [http_request, fetch_url, ripgrep, python_repl]
            if settings.has_tavily:
                tools.append(web_search)

            # Recreate agent with new model
            new_agent, new_backend = create_cli_agent(
                model=new_model,
                assistant_id=self._assistant_id or "agent",
                tools=tools,
                auto_approve=self._auto_approve,
                checkpointer=self._checkpointer,
                store=self._store,
                enable_skills=False,  # TUI handles /skill commands directly
            )

            # Update the app's agent
            self._agent = new_agent
            self._backend = new_backend
            self._current_model_name = model_name

            # Update token tracker model name for cost calculation
            if self._token_tracker:
                self._token_tracker.set_model_name(model_name)

            # Reset the global token extractor so it re-detects the new provider format
            from ieum.textual_adapter import _token_extractor
            _token_extractor.reset()

            # Update status bar to show new model name and context size
            if self._status_bar:
                self._status_bar.set_model_name(model_name)
                # Update max context tokens from new model profile
                profile = getattr(new_model, "profile", None)
                if profile and isinstance(profile, dict):
                    max_tokens = profile.get("max_input_tokens")
                    if max_tokens:
                        self._max_context_tokens = max_tokens
                        self._status_bar.set_max_tokens(max_tokens)
                    else:
                        self._max_context_tokens = None
                        self._status_bar.set_max_tokens(0)
                else:
                    self._max_context_tokens = None
                    self._status_bar.set_max_tokens(0)

            await self._mount_message(
                SystemMessage(t("tui.model.changed", model=model_name))
            )
        except SystemExit:
            await self._mount_message(
                SystemMessage(f"Failed to load model: {model_name}\nCheck if the model exists and server is running.")
            )
        except Exception as e:
            await self._mount_message(
                SystemMessage(f"Error switching model: {e}")
            )

    async def _load_thread_history(self) -> None:
        """Load and render message history when resuming a thread.

        This retrieves the checkpoint state from the agent and converts
        stored messages into UI widgets.
        """
        from langchain_core.messages import AIMessage, HumanMessage, ToolMessage

        from ieum.widgets.messages import (
            AssistantMessage,
            SystemMessage,
            ToolCallMessage,
            UserMessage,
        )
        from textual.containers import VerticalScroll

        if not self._agent or not self._lc_thread_id:
            return

        config = {"configurable": {"thread_id": self._lc_thread_id}}

        try:
            # Get the state snapshot from the agent
            state = await self._agent.aget_state(config)
            if not state or not state.values:
                return

            messages = state.values.get("messages", [])
            if not messages:
                return

            # Track tool calls from AIMessages to match with ToolMessages
            pending_tool_calls: dict[str, dict] = {}

            for msg in messages:
                if isinstance(msg, HumanMessage):
                    # Skip system messages that were auto-injected
                    content = msg.content if isinstance(msg.content, str) else str(msg.content)
                    if content.startswith("[SYSTEM]"):
                        continue
                    await self._mount_message(UserMessage(content))

                elif isinstance(msg, AIMessage):
                    # Render text content if present
                    content = msg.content
                    # Handle both string content and list of content blocks
                    text_content = ""
                    if isinstance(content, str):
                        text_content = content.strip()
                    elif isinstance(content, list):
                        # Extract text from content blocks
                        for block in content:
                            if isinstance(block, dict) and block.get("type") == "text":
                                text_content += block.get("text", "")
                            elif isinstance(block, str):
                                text_content += block
                        text_content = text_content.strip()

                    if text_content:
                        widget = AssistantMessage(text_content)
                        await self._mount_message(widget)
                        await widget.write_initial_content()

                    # Track tool calls for later matching with ToolMessages
                    tool_calls = getattr(msg, "tool_calls", [])
                    for tc in tool_calls:
                        tc_id = tc.get("id")
                        if tc_id:
                            pending_tool_calls[tc_id] = {
                                "name": tc.get("name", "unknown"),
                                "args": tc.get("args", {}),
                            }
                            # Mount tool call widget
                            tool_widget = ToolCallMessage(
                                tc.get("name", "unknown"),
                                tc.get("args", {}),
                            )
                            await self._mount_message(tool_widget)
                            # Store widget reference for result matching
                            pending_tool_calls[tc_id]["widget"] = tool_widget

                elif isinstance(msg, ToolMessage):
                    # Match with pending tool call and show result
                    tc_id = getattr(msg, "tool_call_id", None)
                    if tc_id and tc_id in pending_tool_calls:
                        tool_info = pending_tool_calls.pop(tc_id)
                        widget = tool_info.get("widget")
                        if widget:
                            status = getattr(msg, "status", "success")
                            content = (
                                msg.content if isinstance(msg.content, str) else str(msg.content)
                            )
                            if status == "success":
                                widget.set_success(content)
                            else:
                                widget.set_error(content)

            # Mark any unmatched tool calls as interrupted (no ToolMessage result)
            for tool_info in pending_tool_calls.values():
                widget = tool_info.get("widget")
                if widget:
                    widget.set_rejected()  # Shows as interrupted/rejected in UI

            # Show system message indicating this is a resumed session
            await self._mount_message(SystemMessage(f"Resumed session: {self._lc_thread_id}"))

            # Scroll to bottom after UI fully renders
            # Use set_timer to ensure layout is complete (Markdown rendering is async)
            def scroll_to_end() -> None:
                chat = self.query_one("#chat", VerticalScroll)
                chat.scroll_end(animate=False, immediate=True)

            self.set_timer(0.1, scroll_to_end)

        except Exception as e:
            # Don't fail the app if history loading fails
            await self._mount_message(SystemMessage(f"Could not load history: {e}"))

    async def _show_setup_message(self) -> None:
        """Show setup instructions when no LLM is configured."""
        from ieum.widgets.messages import SystemMessage

        setup_msg = t("tui.setup.message")
        await self._mount_message(SystemMessage(setup_msg))
