"""Shared session-listing helpers for CLI and gateway slash surfaces.""" from __future__ import annotations from typing import Any def parse_session_listing_args(raw_args: str) -> tuple[bool, bool, str, str | None]: """Parse `/sessions`-style args into listing flags, a resume target, and a search query. Returns ``(include_all_sources, include_unnamed, target, search_query)``. ``all`` widens source scope, ``full`` keeps unnamed sessions, ``search``/``find`` makes the rest a query (``None`` = not requested, ``""`` = requested with no terms). Flags are honored only before the first positional word so titles containing "all" aren't misparsed; anything else is a target so `/sessions ` can delegate to `/resume`. """ import shlex parts = shlex.split(raw_args or "") include_all = False include_unnamed = False target_parts: list[str] = [] for i, part in enumerate(parts): lower = part.strip().lower() if not target_parts: if lower in {"list", "ls", "browse"}: continue if lower in {"all", "--all"}: include_all = True continue if lower in {"full", "--full"}: include_unnamed = True continue if lower in {"search", "find"}: query = " ".join(parts[i + 1:]).strip() return include_all, include_unnamed, "", query target_parts.append(part) return include_all, include_unnamed, " ".join(target_parts).strip(), None def query_session_listing( session_db: Any, *, source: str | None, session_key: str | None = None, current_session_id: str | None = None, include_current_session: bool = False, include_all_sources: bool = False, include_unnamed: bool = False, search_query: str | None = None, limit: int = 10, exclude_sources: list[str] | None = None, ) -> list[dict[str, Any]]: """Return session rows for interactive listing surfaces. Shared CLI/gateway policy: source-scoped unless global is requested, unnamed hidden unless a full listing is asked for, current session hidden unless requested (then marked ``is_current_session``); ``session_key`` restricts gateway callers to one lane before the DB limit applies. With ``search_query`` rows are filtered by title/id in SQL, ordered by recent activity, and unnamed sessions stay visible since an id match may be the only handle. """ query_source = None if include_all_sources else source fetch_limit = max(limit * 4, limit) search = (search_query or "").strip() rows = session_db.list_sessions_rich( source=query_source, session_key=session_key, exclude_sources=exclude_sources, limit=fetch_limit, search_query=search or None, order_by_last_active=bool(search), ) result: list[dict[str, Any]] = [] for row in rows: is_current = bool(current_session_id and row.get("id") == current_session_id) if is_current and not include_current_session: continue if not include_unnamed and not row.get("title") and not search and not is_current: continue if is_current: row = dict(row) row["is_current_session"] = True result.append(row) if len(result) >= limit: break return result def format_gateway_session_listing( rows: list[dict[str, Any]], *, include_source: bool = False, title: str = "Sessions", notice: str | None = None, ) -> str: """Render a compact Markdown-ish session list for gateway messengers. ``notice`` appends an explanatory line above the footer — used e.g. when a requested scope widening (``all``) was declined so the caller isn't left guessing why sessions are missing. """ if not rows: parts = [ "No sessions found.\n" "Use `/title My Session` to name this chat, or `/sessions full` " "to include unnamed sessions." ] if notice: parts.append(notice) return "\n".join(parts) lines = [f"📋 **{title}**", ""] for idx, row in enumerate(rows, start=1): session_id = str(row.get("id") or "") title_text = str(row.get("title") or "—") current_part = " (current)" if row.get("is_current_session") else "" preview = str(row.get("preview") or "")[:40] source = str(row.get("source") or "") source_part = f" `{source}`" if include_source and source else "" preview_part = f" — _{preview}_" if preview else "" lines.append(f"{idx}. **{title_text}**{current_part}{source_part} — `{session_id}`{preview_part}") lines.append("") if notice: lines.append(notice) lines.append("Resume: `/resume ` or `/resume ` from `/resume`.") lines.append("More: `/sessions all`, `/sessions full`, `/sessions search `.") return "\n".join(lines)