From b1b04bef7e634485cc82250413954fdf5a7c8e80 Mon Sep 17 00:00:00 2001 From: chen Date: Wed, 24 Jun 2026 08:22:53 +0800 Subject: [PATCH 1/5] docs: add Google-style docstrings to public api/services/utils/models surface --- api/composers.py | 15 +++++++++++ api/config_api.py | 22 ++++++++++++++++ api/logs.py | 6 +++++ api/pdf.py | 11 ++++++++ api/search.py | 8 ++++++ api/workspaces.py | 37 ++++++++++++++++++++++++++ models/cli_session.py | 11 ++++++++ models/conversation.py | 35 +++++++++++++++++++++++++ models/export.py | 11 ++++++++ models/workspace.py | 12 +++++++++ services/summary_cache.py | 55 +++++++++++++++++++++++++++++++++++++++ utils/display_bubble.py | 6 +++++ utils/workspace_path.py | 11 ++++++++ 13 files changed, 240 insertions(+) diff --git a/api/composers.py b/api/composers.py index a790bba..1927aff 100644 --- a/api/composers.py +++ b/api/composers.py @@ -30,6 +30,12 @@ def _read_json_file(path: str) -> Any: @bp.route("/api/composers") def list_composers() -> tuple[Response, int] | Response: + """List all composers across workspace databases (GET /api/composers). + + Returns: + JSON array of composer dicts sorted by ``lastUpdatedAt`` descending. + 500 on failure. + """ try: workspace_path = resolve_workspace_path() composers = [] @@ -122,6 +128,15 @@ def list_composers() -> tuple[Response, int] | Response: return json_response({"error": "Failed to get composers"}, 500) @bp.route("/api/composers/") def get_composer(composer_id: str) -> tuple[Response, int] | Response: + """Fetch one composer by ID (GET /api/composers/). + + Args: + composer_id: Composer UUID. + + Returns: + Composer JSON from per-workspace storage or global fallback. 404 when not + found or schema drift blocks serving; 500 on unexpected failure. + """ try: workspace_path = resolve_workspace_path() diff --git a/api/config_api.py b/api/config_api.py index 87e7711..228637b 100644 --- a/api/config_api.py +++ b/api/config_api.py @@ -24,6 +24,12 @@ @bp.route("/api/detect-environment") def detect_environment() -> Response: + """Detect runtime OS, WSL, and SSH-remote context (GET /api/detect-environment). + + Returns: + JSON with ``os``, ``isWSL``, and ``isRemote``. Falls back to safe defaults + on detection errors. + """ try: is_wsl = False is_remote = bool( @@ -98,6 +104,16 @@ def validate_path() -> tuple[Response, int] | Response: return json_response({"valid": False, "error": "Failed to validate path"}, 500) @bp.route("/api/set-workspace", methods=["POST"]) def set_workspace() -> tuple[Response, int] | Response: + """Persist a validated workspace storage path (POST /api/set-workspace). + + Body: ``{"path": ""}``. Path is canonicalized via + :func:`utils.path_validation.validate_workspace_path` before storing the + thread-safe module override. + + Returns: + ``{"success": true, "path": "..."}`` on success. 400 for invalid path or + body; 500 when override storage fails. + """ # Reject non-dict JSON bodies (array / string / number / null). Without # this, get_json returns the value directly, the truthy fallback `or {}` # is bypassed, and `body.get("path", "")` raises AttributeError — which @@ -127,6 +143,12 @@ def set_workspace() -> tuple[Response, int] | Response: @bp.route("/api/get-username") def get_username() -> Response: + """Return the detected Windows/WSL username (GET /api/get-username). + + Returns: + JSON ``{"username": "..."}``. Falls back to ``YOUR_USERNAME`` when + detection fails. + """ try: username = "YOUR_USERNAME" diff --git a/api/logs.py b/api/logs.py index a6bd804..d18162b 100644 --- a/api/logs.py +++ b/api/logs.py @@ -30,6 +30,12 @@ def _extract_chat_id_from_bubble_key(key: str) -> str | None: @bp.route("/api/logs") def get_logs() -> tuple[Response, int] | Response: + """List chat logs from global and per-workspace storage (GET /api/logs). + + Returns: + JSON array of log summary objects (id, title, timestamp, etc.). 500 on + unexpected failure. + """ try: workspace_path = resolve_workspace_path() logs = [] diff --git a/api/pdf.py b/api/pdf.py index b2d107a..8d12c31 100644 --- a/api/pdf.py +++ b/api/pdf.py @@ -47,6 +47,13 @@ def _safe_text(text: str) -> str: @bp.route("/api/generate-pdf", methods=["POST"]) def generate_pdf() -> tuple[Response, int] | Response: + """Render markdown chat content as a PDF download (POST /api/generate-pdf). + + Body: ``{"markdown": "...", "title": "..."}``. + + Returns: + ``application/pdf`` attachment on success. 400/500 JSON errors on failure. + """ try: body = request.get_json(silent=True) or {} markdown_text = body.get("markdown", "") @@ -55,10 +62,14 @@ def generate_pdf() -> tuple[Response, int] | Response: from fpdf import FPDF class PDFDoc(FPDF): + """Minimal fpdf2 document with page numbers in the footer.""" + def header(self) -> None: + """No running header (title is rendered in body).""" pass def footer(self) -> None: + """Render centered page ``n/total`` at the bottom.""" self.set_y(-15) self.set_font("Helvetica", "I", 8) self.cell(0, 10, f"Page {self.page_no()}/{{nb}}", align="C") diff --git a/api/search.py b/api/search.py index da25d42..3a6249a 100644 --- a/api/search.py +++ b/api/search.py @@ -25,6 +25,14 @@ @bp.route("/api/search") def search() -> tuple[Response, int] | Response: + """Search chats, composers, and CLI sessions across Cursor storage. + + Query params: ``q`` (required), ``type`` (``all`` | ``chat`` | ``composer``). + + Returns: + JSON ``{"results": [...]}`` with optional ``warnings``. 400 when ``q`` is + empty; 500 on unexpected failure. + """ try: query = request.args.get("q", "").strip() search_type = request.args.get("type", "all") diff --git a/api/workspaces.py b/api/workspaces.py index 1c3ce87..0994699 100644 --- a/api/workspaces.py +++ b/api/workspaces.py @@ -57,6 +57,13 @@ def _request_nocache() -> bool: @bp.route("/api/workspaces") def list_workspaces() -> tuple[Response, int] | Response: + """List workspace projects for the sidebar (GET /api/workspaces). + + Honors ``?nocache=1`` to bypass the summary disk cache. + + Returns: + JSON with ``projects`` and optional ``warnings``. 500 on failure. + """ try: workspace_path = resolve_workspace_path() rules = exclusion_rules() @@ -76,6 +83,15 @@ def list_workspaces() -> tuple[Response, int] | Response: @bp.route("/api/workspaces/") def get_workspace(workspace_id: str) -> tuple[Response, int] | Response: + """Return metadata for one workspace, global bucket, or CLI project. + + Args: + workspace_id: Storage folder name, ``global``, or ``cli:``. + + Returns: + Workspace JSON (id, name, path, folder, lastModified). 404 when not found; + 500 on unexpected failure. + """ try: if workspace_id == "global": return json_response({ @@ -150,6 +166,17 @@ def get_workspace(workspace_id: str) -> tuple[Response, int] | Response: @bp.route("/api/workspaces//tabs") def get_workspace_tabs(workspace_id: str) -> tuple[Response, int] | Response: + """List conversation tabs for a workspace (GET /api/workspaces//tabs). + + Args: + workspace_id: Storage folder name or ``cli:``. + + Query params: ``summary=1`` for lightweight tab headers only; ``nocache=1`` to + bypass cache on summary requests. + + Returns: + Tabs payload from :func:`services.workspace_tabs` helpers. 500 on failure. + """ if workspace_id.startswith("cli:"): try: return get_cli_workspace_tabs(workspace_id, exclusion_rules()) @@ -176,6 +203,16 @@ def get_workspace_tabs(workspace_id: str) -> tuple[Response, int] | Response: @bp.route("/api/workspaces//tabs/") def get_workspace_tab(workspace_id: str, composer_id: str) -> tuple[Response, int] | Response: + """Lazy-load one conversation tab (GET /api/workspaces//tabs/). + + Args: + workspace_id: IDE workspace folder name (CLI workspaces return 400). + composer_id: Composer UUID to load. + + Returns: + Single-tab JSON from :func:`services.workspace_tabs.assemble_single_tab`. + 400 for CLI workspaces; 500 on unexpected failure. + """ if workspace_id.startswith("cli:"): return json_response({"error": "Per-tab lazy load is not supported for CLI workspaces"}, 400) try: diff --git a/models/cli_session.py b/models/cli_session.py index a2e5196..d220b6f 100644 --- a/models/cli_session.py +++ b/models/cli_session.py @@ -17,6 +17,17 @@ class CliSessionMeta: @classmethod def from_dict(cls, raw: dict[str, Any]) -> "CliSessionMeta": + """Parse CLI session ``meta`` JSON into a validated descriptor. + + Args: + raw: Decoded meta object from a CLI chat session. + + Returns: + Validated :class:`CliSessionMeta`. + + Raises: + SchemaError: When ``latestRootBlobId`` is missing or not a string. + """ raw = require_dict(raw, model="CliSessionMeta", field="meta") latest = require_truthy( raw.get("latestRootBlobId"), diff --git a/models/conversation.py b/models/conversation.py index 98cabbb..495940f 100644 --- a/models/conversation.py +++ b/models/conversation.py @@ -30,6 +30,18 @@ class Composer: @classmethod def from_dict(cls, raw: dict[str, Any], *, composer_id: str) -> "Composer": + """Parse a global ``composerData`` row into a validated composer. + + Args: + raw: Decoded JSON object from cursorDiskKV. + composer_id: Composer UUID from the storage key. + + Returns: + Validated :class:`Composer` with required headers and timestamps. + + Raises: + SchemaError: When required fields are missing or malformed. + """ raw = require_dict(raw, model="Composer", field="composerData") require_non_empty_str(composer_id, model="Composer", field="composerId") require_key(raw, "fullConversationHeadersOnly", model="Composer") @@ -164,6 +176,17 @@ class WorkspaceLocalComposer: @classmethod def from_dict(cls, raw: dict[str, Any]) -> "WorkspaceLocalComposer": + """Parse one ``allComposers`` entry from per-workspace state. + + Args: + raw: Composer summary dict from ``composer.composerData``. + + Returns: + Validated local composer row. + + Raises: + SchemaError: When ``composerId`` is missing or invalid. + """ raw = require_dict(raw, model="WorkspaceLocalComposer", field="composer") composer_id = require_non_empty_str_field( raw, "composerId", model="WorkspaceLocalComposer" @@ -187,6 +210,18 @@ class Bubble: @classmethod def from_dict(cls, raw: dict[str, Any], *, bubble_id: str) -> "Bubble": + """Parse one ``bubbleId:*`` KV value into a validated bubble. + + Args: + raw: Decoded bubble JSON (``bubble_id`` comes from the key, not value). + bubble_id: Bubble UUID from the storage key suffix. + + Returns: + Validated :class:`Bubble`. + + Raises: + SchemaError: When the payload or *bubble_id* is invalid. + """ raw = require_dict(raw, model="Bubble", field="bubble") require_non_empty_str(bubble_id, model="Bubble", field="bubbleId") return cls(bubble_id=bubble_id, raw=raw) diff --git a/models/export.py b/models/export.py index 50f7421..859dcb7 100644 --- a/models/export.py +++ b/models/export.py @@ -31,6 +31,17 @@ class ExportEntry: @classmethod def from_dict(cls, raw: dict[str, Any]) -> "ExportEntry": + """Parse one manifest.jsonl row into a validated export entry. + + Args: + raw: Decoded JSON object for a single manifest line. + + Returns: + Validated :class:`ExportEntry`. + + Raises: + SchemaError: When ``log_id``, ``title``, or ``workspace`` are missing. + """ raw = require_dict(raw, model="ExportEntry", field="entry") require_non_empty_str_fields( raw, diff --git a/models/workspace.py b/models/workspace.py index 8d9cdd9..8338b11 100644 --- a/models/workspace.py +++ b/models/workspace.py @@ -16,6 +16,18 @@ class Workspace: @classmethod def from_dict(cls, raw: dict[str, Any], *, workspace_id: str) -> "Workspace": + """Parse ``workspace.json`` into a validated workspace descriptor. + + Args: + raw: Decoded workspace.json object. + workspace_id: Workspace storage folder name. + + Returns: + Validated :class:`Workspace` (``folder`` may be ``None`` for CLI-only). + + Raises: + SchemaError: When required fields are missing or malformed. + """ raw = require_dict(raw, model="Workspace", field="workspace.json") require_non_empty_str(workspace_id, model="Workspace", field="workspaceId") folder = require_optional_str(raw.get("folder"), model="Workspace", field="folder") diff --git a/services/summary_cache.py b/services/summary_cache.py index 076f13b..c648f54 100644 --- a/services/summary_cache.py +++ b/services/summary_cache.py @@ -26,6 +26,14 @@ def nocache_enabled(*, request_nocache: bool = False) -> bool: + """Return whether summary-cache reads should be bypassed. + + Args: + request_nocache: True when the HTTP request included ``?nocache=1``. + + Returns: + True when bypass is requested or ``CURSOR_CHAT_BROWSER_NOCACHE`` is set. + """ if request_nocache: return True return os.environ.get("CURSOR_CHAT_BROWSER_NOCACHE", "").strip().lower() in ( @@ -132,6 +140,15 @@ def _write_cache_file(path: Path | str, payload: dict[str, Any]) -> None: def get_cached_projects( fingerprint: dict[str, Any], ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]] | None: + """Load cached workspace project list when the fingerprint matches. + + Args: + fingerprint: Storage mtime/rules digest from + :func:`fingerprint_workspace_storage`. + + Returns: + ``(projects, warnings)`` on hit, else ``None``. + """ data = _read_cache_file(PROJECTS_CACHE_FILE) if not data: return None @@ -151,6 +168,13 @@ def set_cached_projects( projects: list[dict[str, Any]], warnings: list[dict[str, Any]], ) -> None: + """Write workspace project list and warnings to the disk cache. + + Args: + fingerprint: Invalidation fingerprint paired with the payload. + projects: Sidebar project dicts. + warnings: Parse warnings emitted while building *projects*. + """ _write_cache_file( PROJECTS_CACHE_FILE, { @@ -164,6 +188,14 @@ def set_cached_projects( def get_cached_composer_id_to_ws( fingerprint: dict[str, Any], ) -> dict[str, str] | None: + """Load cached composer-id → workspace-id map when the fingerprint matches. + + Args: + fingerprint: Storage mtime/rules digest. + + Returns: + Mapping on hit, else ``None``. + """ data = _read_cache_file(COMPOSER_MAP_CACHE_FILE) if not data: return None @@ -179,6 +211,12 @@ def set_cached_composer_id_to_ws( fingerprint: dict[str, Any], mapping: dict[str, str], ) -> None: + """Persist composer-id → workspace-id map under *fingerprint*. + + Args: + fingerprint: Invalidation fingerprint paired with *mapping*. + mapping: Composer UUID to workspace folder name. + """ _write_cache_file( COMPOSER_MAP_CACHE_FILE, { @@ -197,6 +235,15 @@ def get_cached_tab_summaries( fingerprint: dict[str, Any], workspace_id: str, ) -> tuple[dict[str, Any], int] | None: + """Load cached tab-summary response for one workspace when fingerprint matches. + + Args: + fingerprint: Storage mtime/rules digest. + workspace_id: Workspace folder name the payload belongs to. + + Returns: + ``(payload, status)`` on hit, else ``None``. + """ data = _read_cache_file(_tab_summaries_path(workspace_id)) if not data: return None @@ -217,6 +264,14 @@ def set_cached_tab_summaries( payload: dict[str, Any], status: int, ) -> None: + """Persist tab-summary API payload for one workspace. + + Args: + fingerprint: Invalidation fingerprint paired with the response. + workspace_id: Workspace folder name. + payload: JSON body returned to clients. + status: HTTP status code paired with *payload*. + """ _write_cache_file( _tab_summaries_path(workspace_id), { diff --git a/utils/display_bubble.py b/utils/display_bubble.py index 6efb34e..5dbec81 100644 --- a/utils/display_bubble.py +++ b/utils/display_bubble.py @@ -130,10 +130,16 @@ def build_display_bubble_from_storage( def display_bubble_metadata(bubble: DisplayBubble) -> BubbleMetadata: + """Return metadata dict from a display bubble (empty dict when absent).""" return bubble.get("metadata") or {} def display_bubble_tool_calls(bubble: DisplayBubble) -> list[dict[str, Any]]: + """Return tool-call entries from a display bubble's metadata. + + Returns: + List copied from ``metadata.toolCalls``, or ``[]`` when absent. + """ return list(display_bubble_metadata(bubble).get("toolCalls") or []) diff --git a/utils/workspace_path.py b/utils/workspace_path.py index a4cdb03..5d9b364 100644 --- a/utils/workspace_path.py +++ b/utils/workspace_path.py @@ -19,12 +19,23 @@ def set_workspace_path_override(path: str | None) -> None: + """Set the thread-safe module-level workspace path override. + + Args: + path: Canonical workspace storage root from POST /api/set-workspace, or + ``None`` to clear the override. + """ global _workspace_path_override with _workspace_path_lock: _workspace_path_override = path def get_workspace_path_override() -> str | None: + """Return the current module-level workspace path override, if any. + + Returns: + Canonical path set via :func:`set_workspace_path_override`, else ``None``. + """ with _workspace_path_lock: return _workspace_path_override From efd50a07d4fe6df7f3ded7bdecbc6903f5fb3f62 Mon Sep 17 00:00:00 2001 From: chen Date: Wed, 24 Jun 2026 08:32:57 +0800 Subject: [PATCH 2/5] docs: correct inaccurate API and model docstrings from PR review --- api/composers.py | 7 +++++-- api/logs.py | 3 ++- api/pdf.py | 5 +++-- api/workspaces.py | 8 ++++++-- models/cli_session.py | 3 ++- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/api/composers.py b/api/composers.py index 1927aff..7af99c5 100644 --- a/api/composers.py +++ b/api/composers.py @@ -134,8 +134,11 @@ def get_composer(composer_id: str) -> tuple[Response, int] | Response: composer_id: Composer UUID. Returns: - Composer JSON from per-workspace storage or global fallback. 404 when not - found or schema drift blocks serving; 500 on unexpected failure. + Composer JSON from per-workspace storage or global fallback. Per-workspace + schema drift is logged and skipped before global fallback is attempted. + 404 when the composer is absent from both stores (``{"error": "Composer not found"}``) + or when the global row fails validation (``{"error": "Composer schema drift"}``). + 500 on unexpected failure. """ try: workspace_path = resolve_workspace_path() diff --git a/api/logs.py b/api/logs.py index d18162b..3a0e3c8 100644 --- a/api/logs.py +++ b/api/logs.py @@ -33,7 +33,8 @@ def get_logs() -> tuple[Response, int] | Response: """List chat logs from global and per-workspace storage (GET /api/logs). Returns: - JSON array of log summary objects (id, title, timestamp, etc.). 500 on + JSON ``{"logs": [, ...]}`` where each summary has ``id``, + ``title``, ``timestamp``, etc. 500 with ``{"error": ..., "logs": []}`` on unexpected failure. """ try: diff --git a/api/pdf.py b/api/pdf.py index 8d12c31..22af5fb 100644 --- a/api/pdf.py +++ b/api/pdf.py @@ -49,10 +49,11 @@ def _safe_text(text: str) -> str: def generate_pdf() -> tuple[Response, int] | Response: """Render markdown chat content as a PDF download (POST /api/generate-pdf). - Body: ``{"markdown": "...", "title": "..."}``. + Body (optional): ``{"markdown": "...", "title": "..."}``. Missing keys + default to ``""`` and ``"Chat"`` respectively. Returns: - ``application/pdf`` attachment on success. 400/500 JSON errors on failure. + ``application/pdf`` attachment on success. 500 JSON error on failure. """ try: body = request.get_json(silent=True) or {} diff --git a/api/workspaces.py b/api/workspaces.py index 0994699..ae2fd3a 100644 --- a/api/workspaces.py +++ b/api/workspaces.py @@ -169,13 +169,17 @@ def get_workspace_tabs(workspace_id: str) -> tuple[Response, int] | Response: """List conversation tabs for a workspace (GET /api/workspaces//tabs). Args: - workspace_id: Storage folder name or ``cli:``. + workspace_id: Storage folder name, ``global`` for unassigned chats, or + ``cli:``. Query params: ``summary=1`` for lightweight tab headers only; ``nocache=1`` to bypass cache on summary requests. Returns: - Tabs payload from :func:`services.workspace_tabs` helpers. 500 on failure. + Tabs payload from :func:`services.workspace_tabs` helpers (typically + ``{"tabs": [...]}`` with optional ``warnings``). May return 200 with an + empty ``tabs`` list when upstream SQLite reads fail silently; 404 when + global storage is missing; 500 on unexpected route-level failure. """ if workspace_id.startswith("cli:"): try: diff --git a/models/cli_session.py b/models/cli_session.py index d220b6f..4909cb5 100644 --- a/models/cli_session.py +++ b/models/cli_session.py @@ -26,7 +26,8 @@ def from_dict(cls, raw: dict[str, Any]) -> "CliSessionMeta": Validated :class:`CliSessionMeta`. Raises: - SchemaError: When ``latestRootBlobId`` is missing or not a string. + SchemaError: When ``latestRootBlobId`` is missing, falsey (e.g. + empty string or ``None``), or not a string. """ raw = require_dict(raw, model="CliSessionMeta", field="meta") latest = require_truthy( From a41fa84319293c0e1221c73a29086c285c50da79 Mon Sep 17 00:00:00 2001 From: chen Date: Wed, 24 Jun 2026 08:50:58 +0800 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20address=20PR=20119=20review=20?= =?UTF-8?q?=E2=80=94=20Google-style=20sections=20and=20doc=20accuracy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/composers.py | 2 ++ api/config_api.py | 7 ++++--- api/pdf.py | 5 +++-- api/search.py | 6 ++++-- api/workspaces.py | 13 +++++-------- models/cli_session.py | 2 +- services/summary_cache.py | 3 ++- 7 files changed, 21 insertions(+), 17 deletions(-) diff --git a/api/composers.py b/api/composers.py index 7af99c5..1ada343 100644 --- a/api/composers.py +++ b/api/composers.py @@ -126,6 +126,8 @@ def list_composers() -> tuple[Response, int] | Response: except Exception: _logger.exception("Failed to get composers") return json_response({"error": "Failed to get composers"}, 500) + + @bp.route("/api/composers/") def get_composer(composer_id: str) -> tuple[Response, int] | Response: """Fetch one composer by ID (GET /api/composers/). diff --git a/api/config_api.py b/api/config_api.py index 228637b..3adc68a 100644 --- a/api/config_api.py +++ b/api/config_api.py @@ -106,9 +106,10 @@ def validate_path() -> tuple[Response, int] | Response: def set_workspace() -> tuple[Response, int] | Response: """Persist a validated workspace storage path (POST /api/set-workspace). - Body: ``{"path": ""}``. Path is canonicalized via - :func:`utils.path_validation.validate_workspace_path` before storing the - thread-safe module override. + Args: + path: Workspace storage root from JSON body ``{"path": "..."}``. + Canonicalized via :func:`utils.path_validation.validate_workspace_path` + before storing the thread-safe module override. Returns: ``{"success": true, "path": "..."}`` on success. 400 for invalid path or diff --git a/api/pdf.py b/api/pdf.py index 22af5fb..765f1f0 100644 --- a/api/pdf.py +++ b/api/pdf.py @@ -49,8 +49,9 @@ def _safe_text(text: str) -> str: def generate_pdf() -> tuple[Response, int] | Response: """Render markdown chat content as a PDF download (POST /api/generate-pdf). - Body (optional): ``{"markdown": "...", "title": "..."}``. Missing keys - default to ``""`` and ``"Chat"`` respectively. + Args: + markdown: Markdown source text (optional; defaults to ``""``). + title: Document title (optional; defaults to ``"Chat"``). Returns: ``application/pdf`` attachment on success. 500 JSON error on failure. diff --git a/api/search.py b/api/search.py index 3a6249a..1073d46 100644 --- a/api/search.py +++ b/api/search.py @@ -27,11 +27,13 @@ def search() -> tuple[Response, int] | Response: """Search chats, composers, and CLI sessions across Cursor storage. - Query params: ``q`` (required), ``type`` (``all`` | ``chat`` | ``composer``). + Args: + q: Search query string (required; 400 when empty). + type: Filter scope — ``all`` (default), ``chat``, or ``composer``. Returns: JSON ``{"results": [...]}`` with optional ``warnings``. 400 when ``q`` is - empty; 500 on unexpected failure. + empty; 500 with ``{"error": ..., "results": []}`` on unexpected failure. """ try: query = request.args.get("q", "").strip() diff --git a/api/workspaces.py b/api/workspaces.py index ae2fd3a..83383da 100644 --- a/api/workspaces.py +++ b/api/workspaces.py @@ -24,10 +24,7 @@ warn_workspace_json_read, ) from utils.workspace_descriptor import read_json_file -from services.workspace_resolver import ( - infer_workspace_name_from_context, - lookup_workspace_display_name, -) +from services.workspace_resolver import infer_workspace_name_from_context from services.cli_tabs import get_cli_workspace_tabs from services.workspace_listing import list_workspace_projects from services.workspace_tabs import ( @@ -59,7 +56,8 @@ def _request_nocache() -> bool: def list_workspaces() -> tuple[Response, int] | Response: """List workspace projects for the sidebar (GET /api/workspaces). - Honors ``?nocache=1`` to bypass the summary disk cache. + Args: + nocache: When ``1`` or ``true``, bypass the summary disk cache. Returns: JSON with ``projects`` and optional ``warnings``. 500 on failure. @@ -171,9 +169,8 @@ def get_workspace_tabs(workspace_id: str) -> tuple[Response, int] | Response: Args: workspace_id: Storage folder name, ``global`` for unassigned chats, or ``cli:``. - - Query params: ``summary=1`` for lightweight tab headers only; ``nocache=1`` to - bypass cache on summary requests. + summary: When ``1`` or ``true``, return lightweight tab headers only. + nocache: When ``1`` or ``true``, bypass cache on summary requests. Returns: Tabs payload from :func:`services.workspace_tabs` helpers (typically diff --git a/models/cli_session.py b/models/cli_session.py index 4909cb5..ed8eb78 100644 --- a/models/cli_session.py +++ b/models/cli_session.py @@ -27,7 +27,7 @@ def from_dict(cls, raw: dict[str, Any]) -> "CliSessionMeta": Raises: SchemaError: When ``latestRootBlobId`` is missing, falsey (e.g. - empty string or ``None``), or not a string. + empty string or ``None``), or not a string. """ raw = require_dict(raw, model="CliSessionMeta", field="meta") latest = require_truthy( diff --git a/services/summary_cache.py b/services/summary_cache.py index c648f54..298d2bd 100644 --- a/services/summary_cache.py +++ b/services/summary_cache.py @@ -32,7 +32,8 @@ def nocache_enabled(*, request_nocache: bool = False) -> bool: request_nocache: True when the HTTP request included ``?nocache=1``. Returns: - True when bypass is requested or ``CURSOR_CHAT_BROWSER_NOCACHE`` is set. + True when bypass is requested or ``CURSOR_CHAT_BROWSER_NOCACHE`` is set + to ``"1"``, ``"true"``, or ``"yes"`` (case-insensitive). """ if request_nocache: return True From cbbe5940bb4e1052c9f952de2223df7d3b98db87 Mon Sep 17 00:00:00 2001 From: chen Date: Wed, 24 Jun 2026 08:54:21 +0800 Subject: [PATCH 4/5] docs: complete validate_path and get_workspace_tab docstrings --- api/config_api.py | 19 ++++++++++++++++++- api/workspaces.py | 15 ++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/api/config_api.py b/api/config_api.py index 3adc68a..5f190de 100644 --- a/api/config_api.py +++ b/api/config_api.py @@ -65,7 +65,22 @@ def detect_environment() -> Response: @bp.route("/api/validate-path", methods=["POST"]) def validate_path() -> tuple[Response, int] | Response: - """Same path rules as POST /api/set-workspace: realpath, markers (issue #15).""" + """Validate a workspace storage path without persisting it (POST /api/validate-path). + + Uses the same rules as :func:`set_workspace` (realpath, Cursor markers; issue #15). + + Args: + path: Workspace storage root from JSON body ``{"path": "..."}``. + + Returns: + JSON with ``valid``, ``workspaceCount``, and canonical ``path`` on success. + ``valid`` is ``false`` when the path fails validation or contains no + workspace folders with ``state.vscdb``. Invalid JSON body returns + ``{"valid": false, "error": "invalid JSON body", "workspaceCount": 0}``. + Path validation errors return ``{"valid": false, "error": "...", "workspaceCount": 0}``. + 500 with ``{"valid": false, "error": "Failed to validate path"}`` on + unexpected failure. + """ try: body = request.get_json(silent=True) or {} if not isinstance(body, dict): @@ -102,6 +117,8 @@ def validate_path() -> tuple[Response, int] | Response: exc_info=True, ) return json_response({"valid": False, "error": "Failed to validate path"}, 500) + + @bp.route("/api/set-workspace", methods=["POST"]) def set_workspace() -> tuple[Response, int] | Response: """Persist a validated workspace storage path (POST /api/set-workspace). diff --git a/api/workspaces.py b/api/workspaces.py index 83383da..25d5e35 100644 --- a/api/workspaces.py +++ b/api/workspaces.py @@ -75,6 +75,8 @@ def list_workspaces() -> tuple[Response, int] | Response: except Exception: _logger.exception("Failed to get workspaces") return json_response({"error": "Failed to get workspaces"}, 500) + + # --------------------------------------------------------------------------- # GET /api/workspaces/ # --------------------------------------------------------------------------- @@ -158,6 +160,8 @@ def get_workspace(workspace_id: str) -> tuple[Response, int] | Response: except Exception: _logger.exception("Failed to get workspace") return json_response({"error": "Failed to get workspace"}, 500) + + # --------------------------------------------------------------------------- # GET /api/workspaces//tabs # --------------------------------------------------------------------------- @@ -198,6 +202,8 @@ def get_workspace_tabs(workspace_id: str) -> tuple[Response, int] | Response: except Exception: _logger.exception("Failed to get workspace tabs") return json_response({"error": "Failed to get workspace tabs"}, 500) + + # --------------------------------------------------------------------------- # GET /api/workspaces//tabs/ # --------------------------------------------------------------------------- @@ -207,12 +213,15 @@ def get_workspace_tab(workspace_id: str, composer_id: str) -> tuple[Response, in """Lazy-load one conversation tab (GET /api/workspaces//tabs/). Args: - workspace_id: IDE workspace folder name (CLI workspaces return 400). + workspace_id: Storage folder name, ``global`` for unassigned chats, or + ``cli:`` (CLI workspaces return 400). composer_id: Composer UUID to load. Returns: - Single-tab JSON from :func:`services.workspace_tabs.assemble_single_tab`. - 400 for CLI workspaces; 500 on unexpected failure. + Single-tab JSON from :func:`services.workspace_tabs.assemble_single_tab` + (typically ``{"tab": {...}}`` with optional ``warnings``). 400 for CLI + workspaces; 404 when global storage is missing, the composer is not found, + or it is not assigned to *workspace_id*; 500 on unexpected failure. """ if workspace_id.startswith("cli:"): return json_response({"error": "Per-tab lazy load is not supported for CLI workspaces"}, 400) From 83ae44564faca032c212299cfae6693529855d30 Mon Sep 17 00:00:00 2001 From: chen Date: Wed, 24 Jun 2026 20:27:31 +0800 Subject: [PATCH 5/5] revert(workspaces): restore lookup_workspace_display_name import for #118 --- api/workspaces.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/workspaces.py b/api/workspaces.py index 25d5e35..9771f7b 100644 --- a/api/workspaces.py +++ b/api/workspaces.py @@ -24,7 +24,10 @@ warn_workspace_json_read, ) from utils.workspace_descriptor import read_json_file -from services.workspace_resolver import infer_workspace_name_from_context +from services.workspace_resolver import ( + infer_workspace_name_from_context, + lookup_workspace_display_name, +) from services.cli_tabs import get_cli_workspace_tabs from services.workspace_listing import list_workspace_projects from services.workspace_tabs import (