# tools/capture_tools.py — Pattern Inbox capture, processing, and review tools.
from __future__ import annotations

from datetime import datetime, timezone

from langchain_core.tools import tool
from pydantic import BaseModel, Field

from tools._logging import _debug_log, _emit_input_status
from utils.vault_markdown_sanitize import sanitize_vault_markdown_for_storage

# Upper bound for agent-written Vault bodies (chars); avoids runaway appends.
_VAULT_NOTE_MAX_BODY_CHARS = 900_000
# Per-call read cap for get_note (pagination via body_offset).
_GET_NOTE_BODY_CHUNK_MAX = 32_000


class CaptureInboxInput(BaseModel):
    text: str = Field(description="Raw text to save into the user's Pattern Inbox.")
    tags: list[str] = Field(default_factory=list, description="Optional lightweight tags.")
    project_id: str = Field(default="", description="Optional capture project ID to file into immediately.")
    section: str = Field(default="", description="Optional section/folder label inside the project.")


@tool(args_schema=CaptureInboxInput)
def capture_to_inbox(text: str, tags: list[str] | None = None, project_id: str = "", section: str = "") -> str:
    """Save raw user-provided text to the Pattern Inbox without processing or acting on it."""
    _debug_log("[TOOL] capture_to_inbox called", "TOOL")
    _emit_input_status("Saving capture…")
    try:
        from chat_storage import create_capture

        clean = sanitize_vault_markdown_for_storage(text)
        cap = create_capture(clean, source="agent", tags=tags or [], project_id=project_id or "", section=section or "")
        suffix = " (already captured)" if cap.get("duplicate") else ""
        destination = cap.get("project_name") or "Pattern Inbox"
        return f"Saved to {destination}{suffix}: {cap.get('title') or cap.get('id')}"
    finally:
        _emit_input_status("")


class ListCapturesInput(BaseModel):
    status: str = Field(default="inbox", description="Capture status filter: inbox, processed, promoted, archived, deleted, or empty for all.")
    limit: int = Field(default=20, description="Maximum number of captures to return.")
    project_id: str = Field(default="", description="Optional project ID filter.")
    tag: str = Field(default="", description="Optional tag filter.")


@tool(args_schema=ListCapturesInput)
def list_captures(status: str = "inbox", limit: int = 20, project_id: str = "", tag: str = "") -> str:
    """List Pattern Inbox captures with concise metadata."""
    _debug_log("[TOOL] list_captures called", "TOOL")
    _emit_input_status("Reading captures…")
    try:
        from chat_storage import list_captures as _list

        rows = _list(status or None, include_deleted=False, limit=limit, project_id=project_id or None, tag=tag or None)
        if not rows:
            return "No captures found."
        lines = []
        for row in rows:
            kind = row.get("kind") or "raw"
            project = f" → {row.get('project_name')}" if row.get("project_name") else ""
            lines.append(f"- {row.get('id')}: [{row.get('status')}/{kind}] {row.get('title')}{project}")
        return "Pattern Inbox captures:\n" + "\n".join(lines)
    finally:
        _emit_input_status("")


class ProcessCapturesInput(BaseModel):
    ids: list[str] = Field(default_factory=list, description="Optional capture IDs to process. Empty processes recent inbox items.")
    limit: int = Field(default=25, description="Maximum number of captures to process.")
    project_id: str = Field(default="", description="Optional project ID whose inbox should be processed.")


@tool(args_schema=ProcessCapturesInput)
def process_capture_inbox(ids: list[str] | None = None, limit: int = 25, project_id: str = "") -> str:
    """Classify recent Pattern Inbox captures and generate reviewable connection/action-card candidates."""
    _debug_log("[TOOL] process_capture_inbox called", "TOOL")
    _emit_input_status("Processing captures…")
    try:
        from chat_storage import process_captures

        result = process_captures(ids or None, limit=limit, project_id=project_id or None)
        processed = result.get("processed") or []
        skipped = result.get("skipped") or []
        connections = result.get("connections") or []
        lines = [
            f"Processed {len(processed)} capture(s).",
            f"Created {len(connections)} connection(s).",
        ]
        if skipped:
            lines.append(f"Skipped {len(skipped)} likely-sensitive capture(s).")
        for row in processed[:10]:
            lines.append(f"- {row.get('kind')}: {row.get('title')}")
        return "\n".join(lines)
    finally:
        _emit_input_status("")


class ListConnectionsInput(BaseModel):
    status: str = Field(default="suggested", description="Connection status filter, or empty for all.")
    limit: int = Field(default=20, description="Maximum number of connections to return.")
    project_id: str = Field(default="", description="Optional project ID filter.")


@tool(args_schema=ListConnectionsInput)
def list_pattern_connections(status: str = "suggested", limit: int = 20, project_id: str = "") -> str:
    """List reviewable Pattern Inbox connections and source counts."""
    _debug_log("[TOOL] list_pattern_connections called", "TOOL")
    _emit_input_status("Reading connections…")
    try:
        from chat_storage import list_connections

        rows = list_connections(status or None, limit=limit, project_id=project_id or None)
        if not rows:
            return "No Pattern Inbox connections found."
        lines = []
        for row in rows:
            lines.append(f"- {row.get('id')}: {row.get('summary')} ({len(row.get('source_ids') or [])} sources)")
        return "Pattern Inbox connections:\n" + "\n".join(lines)
    finally:
        _emit_input_status("")


class ListActionCardsInput(BaseModel):
    state: str = Field(default="suggested", description="Action-card state filter, or empty for all.")
    limit: int = Field(default=20, description="Maximum number of action cards to return.")
    project_id: str = Field(default="", description="Optional project ID filter.")


@tool(args_schema=ListActionCardsInput)
def list_pattern_action_cards(state: str = "suggested", limit: int = 20, project_id: str = "") -> str:
    """List suggested Pattern Inbox action cards."""
    _debug_log("[TOOL] list_pattern_action_cards called", "TOOL")
    _emit_input_status("Reading action cards…")
    try:
        from chat_storage import list_action_cards

        rows = list_action_cards(state or None, limit=limit, project_id=project_id or None)
        if not rows:
            return "No Pattern Inbox action cards found."
        lines = []
        for row in rows:
            lines.append(f"- {row.get('id')}: [{row.get('trust_level')}/{row.get('state')}] {row.get('title')}")
        return "Pattern Inbox action cards:\n" + "\n".join(lines)
    finally:
        _emit_input_status("")


class ProjectInput(BaseModel):
    include_archived: bool = Field(default=False, description="Whether archived projects should be included.")
    limit: int = Field(default=50, description="Maximum number of projects to list.")


@tool(args_schema=ProjectInput)
def list_capture_projects(include_archived: bool = False, limit: int = 50) -> str:
    """List user-created capture projects."""
    _debug_log("[TOOL] list_capture_projects called", "TOOL")
    _emit_input_status("Reading capture projects…")
    try:
        from chat_storage import list_capture_projects as _list_projects

        rows = _list_projects(include_archived=include_archived, limit=limit)
        if not rows:
            return "No capture projects found."
        lines = []
        for row in rows:
            archived = " archived" if row.get("archived_at") else ""
            lines.append(f"- {row.get('id')}: {row.get('name')}{archived}")
        return "Capture projects:\n" + "\n".join(lines)
    finally:
        _emit_input_status("")


class CreateProjectInput(BaseModel):
    name: str = Field(description="Project name.")
    description: str = Field(default="", description="Optional private project description.")


@tool(args_schema=CreateProjectInput)
def create_capture_project(name: str, description: str = "") -> str:
    """Create a capture project for filing future raw captures."""
    _debug_log("[TOOL] create_capture_project called", "TOOL")
    _emit_input_status("Creating capture project…")
    try:
        from chat_storage import create_capture_project as _create

        project = _create(name, description=description)
        suffix = " (already existed)" if project.get("duplicate") else ""
        return f"Capture project{suffix}: {project.get('name')} ({project.get('id')})"
    finally:
        _emit_input_status("")


class FileCaptureInput(BaseModel):
    capture_id: str = Field(description="Capture ID to file.")
    project_id: str = Field(description="Destination project ID.")
    section: str = Field(default="", description="Optional section/folder label.")
    tags: list[str] = Field(default_factory=list, description="Optional tags to add while filing.")


@tool(args_schema=FileCaptureInput)
def file_capture(capture_id: str, project_id: str, section: str = "", tags: list[str] | None = None) -> str:
    """File an existing capture into a capture project."""
    _debug_log("[TOOL] file_capture called", "TOOL")
    _emit_input_status("Filing capture…")
    try:
        from chat_storage import file_capture as _file

        cap = _file(capture_id, project_id=project_id, section=section, tags=tags or [])
        if not cap:
            return "Capture not found."
        return f"Filed capture under {cap.get('project_name') or project_id}: {cap.get('title')}"
    finally:
        _emit_input_status("")


class TagCaptureInput(BaseModel):
    capture_id: str = Field(description="Capture ID to tag.")
    tags: list[str] = Field(description="Tags to add, remove, or replace.")
    mode: str = Field(default="add", description="add, remove, or replace.")


@tool(args_schema=TagCaptureInput)
def tag_capture(capture_id: str, tags: list[str], mode: str = "add") -> str:
    """Update tags on a capture."""
    _debug_log("[TOOL] tag_capture called", "TOOL")
    _emit_input_status("Updating capture tags…")
    try:
        from chat_storage import tag_capture as _tag

        cap = _tag(capture_id, tags=tags or [], mode=mode)
        if not cap:
            return "Capture not found."
        return f"Capture tags: {', '.join(cap.get('tags') or []) or '(none)'}"
    finally:
        _emit_input_status("")


class ProjectProcessInput(BaseModel):
    project_id: str = Field(description="Project ID whose capture inbox should be processed.")
    limit: int = Field(default=25, description="Maximum number of captures to process.")


@tool(args_schema=ProjectProcessInput)
def process_capture_project(project_id: str, limit: int = 25) -> str:
    """Process unprocessed captures inside one project and generate project-scoped patterns."""
    return process_capture_inbox(ids=[], limit=limit, project_id=project_id)


class ProjectCapturesInput(BaseModel):
    project_id: str = Field(description="Project ID to list.")
    status: str = Field(default="", description="Optional capture status.")
    limit: int = Field(default=30, description="Maximum number of captures.")


@tool(args_schema=ProjectCapturesInput)
def list_project_captures(project_id: str, status: str = "", limit: int = 30) -> str:
    """List captures filed under a project."""
    return list_captures(status=status or "", limit=limit, project_id=project_id)


class ProjectNotesInput(BaseModel):
    project_id: str = Field(description="Project ID to list.")
    limit: int = Field(default=20, description="Maximum number of notes to return.")


@tool(args_schema=ProjectNotesInput)
def list_project_notes(project_id: str, limit: int = 20) -> str:
    """List Vault notes in a project with bounded snippets for chat grounding."""
    _debug_log("[TOOL] list_project_notes called", "TOOL")
    _emit_input_status("Reading Vault notes…")
    try:
        from chat_storage import list_captures

        rows = list_captures(status=None, include_deleted=False, limit=limit, project_id=project_id)
        if not rows:
            return "No Vault notes found for that project."
        lines = []
        for row in rows:
            body = (row.get("body") or "").replace("\n", " ")
            snippet = body[:240] + ("…" if len(body) > 240 else "")
            lines.append(f"- {row.get('id')}: {row.get('title')}\n  {snippet}")
        return "Vault project notes:\n" + "\n".join(lines)
    finally:
        _emit_input_status("")


class SearchVaultNotesInput(BaseModel):
    query: str = Field(description="Substring to find in Vault note titles and bodies (case-insensitive).")
    project_id: str = Field(
        default="",
        description="Optional capture project ID to scope the search; omit to search all non-deleted notes.",
    )
    limit_notes: int = Field(
        default=25,
        ge=1,
        le=80,
        description="Maximum number of matching notes to return.",
    )
    hits_per_note: int = Field(
        default=3,
        ge=1,
        le=12,
        description="Maximum snippet hits per note (each hit includes a short context window).",
    )


@tool(args_schema=SearchVaultNotesInput)
def search_vault_notes(
    query: str,
    project_id: str = "",
    limit_notes: int = 25,
    hits_per_note: int = 3,
) -> str:
    """Search Vault / Pattern Inbox notes by substring in title or body; returns note ids with short snippets.

    Prefer this when the user asks to find, locate, or search their notes without knowing an id.
    Follow up with **get_note** for full content (use **body_offset** for long notes).
    """
    _debug_log("[TOOL] search_vault_notes called", "TOOL")
    _emit_input_status("Searching Vault notes…")
    try:
        from chat_storage import search_vault_captures

        q = " ".join(str(query or "").strip().split())
        if not q:
            return "Provide a non-empty search phrase."
        out = search_vault_captures(
            q,
            project_id=project_id.strip() or None,
            limit_notes=limit_notes,
            hits_per_note=hits_per_note,
        )
        rows = out.get("results") or []
        if not rows:
            return f"No Vault notes matched query {q!r}."
        lines: list[str] = [f"Vault search ({len(rows)} note(s)) for {q!r}:"]
        for row in rows:
            nid = row.get("note_id")
            title = row.get("title") or "(untitled)"
            updated = row.get("updated_at") or ""
            lines.append(f"- {nid}: {title}" + (f" (updated {updated})" if updated else ""))
            for h in (row.get("hits") or [])[:hits_per_note]:
                snip = (h.get("snippet") or "").replace("\n", " ").strip()
                if snip:
                    lines.append(f"  · …{snip}…")
        return "\n".join(lines)
    finally:
        _emit_input_status("")


class ListRecentVaultNotesInput(BaseModel):
    limit: int = Field(default=25, ge=1, le=200, description="Maximum notes to return.")
    project_id: str = Field(
        default="",
        description="Optional capture project id; omit for all projects.",
    )
    since_hours: float = Field(
        default=0,
        ge=0,
        description="If > 0, only notes updated within this many hours.",
    )


@tool(args_schema=ListRecentVaultNotesInput)
def list_recent_vault_notes(limit: int = 25, project_id: str = "", since_hours: float = 0) -> str:
    """List recently updated Vault / Pattern Inbox notes (metadata and timestamps only).

    Use this for a changelog-style view of what changed in the app; call **get_note** for bodies.
    """
    _debug_log("[TOOL] list_recent_vault_notes called", "TOOL")
    _emit_input_status("Listing recent Vault notes…")
    try:
        from chat_storage import list_recent_vault_notes as _list_recent

        out = _list_recent(
            limit=limit,
            project_id=project_id.strip() or None,
            since_hours=float(since_hours) if since_hours and since_hours > 0 else None,
        )
        rows = out.get("notes") or []
        if not rows:
            filt = f" (last {since_hours}h)" if since_hours and since_hours > 0 else ""
            return f"No Vault notes found{filt}."
        lines = [f"Recent Vault notes ({len(rows)}):"]
        for row in rows:
            title = row.get("title") or "(untitled)"
            nid = row.get("id")
            proj = row.get("project_name") or row.get("project_id") or "(unfiled)"
            upd = row.get("updated_at") or ""
            st = row.get("status") or ""
            lines.append(f"- {nid}: {title} [{st}] @ {proj} — updated {upd}")
        return "\n".join(lines)
    finally:
        _emit_input_status("")


class GetNoteInput(BaseModel):
    note_id: str = Field(description="Vault note ID.")
    max_chars: int = Field(
        default=4000,
        description="Maximum characters of the note body to return in this slice (up to 32000).",
    )
    body_offset: int = Field(
        default=0,
        ge=0,
        description="Character offset into the body for long notes; use the continuation hint from a previous get_note result.",
    )


@tool(args_schema=GetNoteInput)
def get_note(note_id: str, max_chars: int = 4000, body_offset: int = 0) -> str:
    """Read one Vault / Pattern Inbox note by id (markdown body).

    Returns up to ``max_chars`` body characters starting at ``body_offset``. For long notes,
    the tail includes a hint with the next ``body_offset`` so you can read the full note in chunks.
    Use **list_recent_vault_notes** to see what changed recently; **search_vault_notes** to find by text.

    Inline Vault images appear in the body as Markdown references like
    ![Vault image](mugi-vault://<attachment_uuid>). Do not replace those with
    localhost URLs, file paths, auth tokens, or remote image URLs when editing
    notes; keep the stable mugi-vault references and rely on the attachment
    list for filenames. If the user is actively editing the same note in the
    Vault UI, treat updates as last-write-wins unless the user asks you to
    coordinate or reload first.
    """
    _debug_log("[TOOL] get_note called", "TOOL")
    _emit_input_status("Reading Vault note…")
    try:
        from chat_storage import get_capture, list_vault_attachments

        note = get_capture(note_id)
        if not note or note.get("deleted_at"):
            return "Vault note not found."
        max_chars = max(200, min(int(max_chars or 4000), _GET_NOTE_BODY_CHUNK_MAX))
        offset = max(0, int(body_offset or 0))
        full_body = note.get("body") or ""
        total = len(full_body)
        if offset >= total and total > 0:
            return (
                f"Note: {note.get('title')} ({note.get('id')})\n"
                f"Body offset {offset} is past end of body ({total} characters). Use body_offset=0 to start over."
            )
        chunk = full_body[offset : offset + max_chars]
        has_more = offset + len(chunk) < total
        next_offset = offset + len(chunk)
        attachments = list_vault_attachments(note_id=note_id, limit=20)
        attachment_names = [
            (a.get("metadata") or {}).get("original_filename") or a.get("id")
            for a in attachments
        ]
        lines = [
            f"Note: {note.get('title')} ({note.get('id')})",
            f"Project: {note.get('project_name') or '(unfiled)'}",
            f"Tags: {', '.join(note.get('tags') or []) or '(none)'}",
            f"Attachments: {', '.join(attachment_names) or '(none)'}",
            f"Body: characters {offset}–{next_offset} of {total} total.",
            "Inline image references: use ![Vault image](mugi-vault://<attachment_uuid>) in body; never persist auth tokens, localhost URLs, file:// paths, or remote image URLs.",
            "",
            chunk,
        ]
        if has_more:
            lines.append("")
            lines.append(
                f"… (body continues; call get_note with note_id={note.get('id')!r}, "
                f"body_offset={next_offset}, max_chars={max_chars})"
            )
        return "\n".join(lines)
    finally:
        _emit_input_status("")


class CreateVaultNoteInput(BaseModel):
    body: str = Field(description="Markdown body for the new Vault note.")
    title: str = Field(default="", description="Optional title; derived from the first line of the body if empty.")
    project_id: str = Field(
        default="",
        description="Optional project ID. New rows stay kind=raw, status=inbox; use file_capture to move/file into a project.",
    )
    section: str = Field(default="", description="Optional section/folder label inside the project.")
    tags: list[str] = Field(default_factory=list, description="Optional tags.")


@tool(args_schema=CreateVaultNoteInput)
def create_vault_note(
    body: str,
    title: str = "",
    project_id: str = "",
    section: str = "",
    tags: list[str] | None = None,
) -> str:
    """Create a new Vault / Pattern Inbox note (stored in the app capture database). On duplicate body-hash, returns the existing note id."""
    _debug_log("[TOOL] create_vault_note called", "TOOL")
    _emit_input_status("Creating Vault note…")
    try:
        from chat_storage import create_capture

        clean = sanitize_vault_markdown_for_storage(body)
        if not clean.strip():
            return "Body is empty after sanitization; nothing saved."
        if len(clean) > _VAULT_NOTE_MAX_BODY_CHARS:
            return f"Body exceeds maximum length ({_VAULT_NOTE_MAX_BODY_CHARS} characters); not saved."
        cap = create_capture(
            clean,
            source="agent",
            tags=tags or [],
            project_id=project_id or "",
            section=section or "",
            title=title or "",
        )
        nid = cap.get("id")
        if cap.get("duplicate"):
            return (
                f"Duplicate: identical content already exists as Vault note id={nid} "
                f"(title={cap.get('title')!r}). Use this id for further updates; no new note was created."
            )
        return f"Created Vault note id={nid} title={cap.get('title')!r}."
    finally:
        _emit_input_status("")


class UpdateVaultNoteInput(BaseModel):
    note_id: str = Field(description="Vault note id (UUID).")
    title: str | None = Field(default=None, description="Set title (omit to leave unchanged).")
    body: str | None = Field(default=None, description="Replace entire note body (omit to leave unchanged).")
    project_id: str | None = Field(default=None, description="Set project id; use empty string to unfile (omit to leave unchanged).")
    section: str | None = Field(default=None, description="Set section (omit to leave unchanged).")
    tags: list[str] | None = Field(default=None, description="Replace tags when provided (omit to leave unchanged).")
    status: str | None = Field(default=None, description="Capture status (omit to leave unchanged).")
    kind: str | None = Field(default=None, description="Capture kind (omit to leave unchanged).")


@tool(args_schema=UpdateVaultNoteInput)
def update_vault_note(
    note_id: str,
    title: str | None = None,
    body: str | None = None,
    project_id: str | None = None,
    section: str | None = None,
    tags: list[str] | None = None,
    status: str | None = None,
    kind: str | None = None,
) -> str:
    """Update a Vault / Pattern Inbox note in the app; only arguments you pass are applied (partial update)."""
    _debug_log("[TOOL] update_vault_note called", "TOOL")
    _emit_input_status("Updating Vault note…")
    try:
        from chat_storage import get_capture, update_capture

        current = get_capture(note_id)
        if not current or current.get("deleted_at"):
            return "Vault note not found."
        updates: dict = {}
        if title is not None:
            updates["title"] = title
        if body is not None:
            clean = sanitize_vault_markdown_for_storage(body)
            if len(clean) > _VAULT_NOTE_MAX_BODY_CHARS:
                return f"Body exceeds maximum length ({_VAULT_NOTE_MAX_BODY_CHARS} characters); not saved."
            updates["body"] = clean
        if project_id is not None:
            updates["project_id"] = project_id
        if section is not None:
            updates["section"] = section
        if tags is not None:
            updates["tags"] = tags
        if status is not None:
            updates["status"] = status
        if kind is not None:
            updates["kind"] = kind
        if not updates:
            return "No fields to update (pass at least one of title, body, project_id, section, tags, status, kind)."
        out = update_capture(note_id, **updates)
        if not out:
            return "Update failed."
        return f"Updated Vault note id={note_id}."
    finally:
        _emit_input_status("")


class AppendVaultNoteInput(BaseModel):
    note_id: str = Field(description="Vault note id (UUID).")
    text: str = Field(description="Markdown or plain text to append to the note body.")
    include_timestamp: bool = Field(
        default=True,
        description="If true, insert a short UTC timestamp separator before the appended text.",
    )


@tool(args_schema=AppendVaultNoteInput)
def append_to_vault_note(note_id: str, text: str, include_timestamp: bool = True) -> str:
    """Append to a Vault / Pattern Inbox note body with a separator; respects maximum body size."""
    _debug_log("[TOOL] append_to_vault_note called", "TOOL")
    _emit_input_status("Appending to Vault note…")
    try:
        from chat_storage import get_capture, update_capture

        current = get_capture(note_id)
        if not current or current.get("deleted_at"):
            return "Vault note not found."
        addition = sanitize_vault_markdown_for_storage(text)
        if not addition.strip():
            return "Nothing to append after sanitization."
        base = current.get("body") or ""
        if include_timestamp:
            ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
            block = f"\n\n---\n*Appended {ts}*\n\n{addition}"
        else:
            block = f"\n\n{addition}"
        merged = base + block
        if len(merged) > _VAULT_NOTE_MAX_BODY_CHARS:
            return (
                f"Append would exceed maximum body length ({_VAULT_NOTE_MAX_BODY_CHARS} characters). "
                "Shorten the text or edit the note in smaller chunks."
            )
        out = update_capture(note_id, body=merged)
        if not out:
            return "Append failed."
        return f"Appended to Vault note id={note_id} (new length {len(merged)} characters)."
    finally:
        _emit_input_status("")


class SummarizeProjectInput(BaseModel):
    project_id: str = Field(description="Project ID to summarize.")


@tool(args_schema=SummarizeProjectInput)
def summarize_capture_project(project_id: str) -> str:
    """Show a lightweight project summary with counts and recent items."""
    _debug_log("[TOOL] summarize_capture_project called", "TOOL")
    _emit_input_status("Summarizing capture project…")
    try:
        from chat_storage import get_capture_project_summary

        summary = get_capture_project_summary(project_id)
        if not summary:
            return "Project not found."
        project = summary.get("project") or {}
        counts = summary.get("counts") or {}
        by_status = counts.get("by_status") or {}
        lines = [
            f"Project: {project.get('name')}",
            f"Captures: {counts.get('captures', 0)}",
            f"Connections: {counts.get('connections', 0)}",
            f"Action cards: {counts.get('action_cards', 0)}",
            "By status: " + (", ".join(f"{k}={v}" for k, v in by_status.items()) or "(none)"),
        ]
        for cap in (summary.get("recent_captures") or [])[:5]:
            lines.append(f"- {cap.get('status')}: {cap.get('title')}")
        return "\n".join(lines)
    finally:
        _emit_input_status("")
