Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 25 additions & 15 deletions api/export_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,25 @@
from datetime import datetime
from pathlib import Path

from flask import Blueprint, Response, current_app, jsonify, request
from flask import Blueprint, Response, jsonify, request

from api.flask_config import exclusion_rules

from utils.workspace_path import resolve_workspace_path
from utils.path_helpers import to_epoch_ms
from utils.text_extract import extract_text_from_bubble, slug
from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules
from utils.cursor_md_exporter import cursor_ide_chat_to_markdown
from services.workspace_db import (
_build_composer_id_to_workspace_id,
_collect_workspace_entries,
build_composer_id_to_workspace_id,
collect_workspace_entries,
load_bubble_map,
load_code_block_diff_map,
_open_global_db,
open_global_db,
)
from services.workspace_resolver import (
_get_workspace_display_name,
_create_project_name_to_workspace_id_map,
create_project_name_to_workspace_id_map,
lookup_workspace_display_name,
)

bp = Blueprint("export_api", __name__)
Expand All @@ -47,8 +49,12 @@ def _get_export_state() -> dict:
try:
with open(state_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
except (json.JSONDecodeError, ValueError, OSError) as e:
_logger.warning(
"Could not read export state from %s: %s",
state_path,
e,
)
return {}


Expand Down Expand Up @@ -96,25 +102,25 @@ def export_chats():
last_export_ms = to_epoch_ms(ts_str)

# ── Workspace scanning via service layer ──────────────────────────────
workspace_entries = _collect_workspace_entries(workspace_path)
composer_id_to_ws = _build_composer_id_to_workspace_id(workspace_path, workspace_entries)
project_name_map = _create_project_name_to_workspace_id_map(workspace_entries)
workspace_entries = collect_workspace_entries(workspace_path)
composer_id_to_ws = build_composer_id_to_workspace_id(workspace_path, workspace_entries)
project_name_map = create_project_name_to_workspace_id_map(workspace_entries)

# Build display-name and slug maps
ws_id_to_slug: dict[str, str] = {}
ws_id_to_display_name: dict[str, str] = {}
for e in workspace_entries:
display = _get_workspace_display_name(workspace_path, e["name"])
display = lookup_workspace_display_name(workspace_path, e["name"])
if display != e["name"]:
ws_id_to_display_name[e["name"]] = display
ws_id_to_slug[e["name"]] = slug(display)

today = datetime.now().strftime("%Y-%m-%d")
exported = []
rules = current_app.config.get("EXCLUSION_RULES") or []
rules = exclusion_rules()

# ── Database reading via service layer ────────────────────────────────
with _open_global_db(workspace_path) as (global_db, global_db_path):
with open_global_db(workspace_path) as (global_db, _):
if global_db is None:
return jsonify({"error": "Cursor global storage not found"}), 404

Expand All @@ -138,7 +144,11 @@ def export_chats():
if not headers:
continue

updated_at_ms = to_epoch_ms(cd.get("lastUpdatedAt")) or to_epoch_ms(cd.get("createdAt")) or 0
updated_at_ms = to_epoch_ms(cd.get("lastUpdatedAt"))
if updated_at_ms is None:
updated_at_ms = to_epoch_ms(cd.get("createdAt"))
if updated_at_ms is None:
updated_at_ms = 0
if since == "last" and updated_at_ms and updated_at_ms <= last_export_ms:
continue

Expand Down
10 changes: 10 additions & 0 deletions api/flask_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Shared Flask request/config helpers for API blueprints."""

from __future__ import annotations

from flask import current_app


def exclusion_rules() -> list:
"""Return loaded exclusion rules from app config (empty list when unset)."""
return current_app.config.get("EXCLUSION_RULES") or []
30 changes: 15 additions & 15 deletions api/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import os
from datetime import datetime, timezone

from flask import Blueprint, current_app, jsonify
from flask import Blueprint, jsonify

from api.flask_config import exclusion_rules

from utils.workspace_path import resolve_workspace_path, get_cli_chats_path
from utils.cli_chat_reader import list_cli_projects
Expand All @@ -22,16 +24,10 @@
)
from utils.workspace_descriptor import read_json_file
from services.workspace_resolver import (
_infer_workspace_name_from_context,
# Re-exported for back-compat with existing tests that import from api.workspaces
# directly (test_invalid_workspace_aliases, test_workspace_assignment_fallback,
# test_workspace_name_inference, test_models_wired_at_read_sites).
# Production callers should import from services.workspace_resolver instead.
_determine_project_for_conversation, # noqa: F401
_infer_invalid_workspace_aliases, # noqa: F401
_get_workspace_display_name, # noqa: F401
infer_workspace_name_from_context,
lookup_workspace_display_name,
)
from services.cli_tabs import _get_cli_workspace_tabs
from services.cli_tabs import get_cli_workspace_tabs
from services.workspace_listing import list_workspace_projects
from services.workspace_tabs import assemble_workspace_tabs

Expand All @@ -54,7 +50,7 @@
def list_workspaces():
try:
workspace_path = resolve_workspace_path()
rules = current_app.config.get("EXCLUSION_RULES") or []
rules = exclusion_rules()
projects, warnings = list_workspace_projects(workspace_path, rules)
payload: dict = {"projects": projects}
if warnings:
Expand Down Expand Up @@ -121,12 +117,12 @@ def get_workspace(workspace_id):
if derived_name:
workspace_name = derived_name
elif workspace_name == workspace_id:
inferred = _infer_workspace_name_from_context(workspace_path, workspace_id)
inferred = infer_workspace_name_from_context(workspace_path, workspace_id)
if inferred:
workspace_name = inferred
except Exception as e:
warn_workspace_json_read(_logger, workspace_id, e)
inferred = _infer_workspace_name_from_context(workspace_path, workspace_id)
inferred = infer_workspace_name_from_context(workspace_path, workspace_id)
if inferred:
workspace_name = inferred

Expand All @@ -150,10 +146,14 @@ def get_workspace(workspace_id):
@bp.route("/api/workspaces/<workspace_id>/tabs")
def get_workspace_tabs(workspace_id):
if workspace_id.startswith("cli:"):
return _get_cli_workspace_tabs(workspace_id)
try:
return get_cli_workspace_tabs(workspace_id, exclusion_rules())
except Exception:
_logger.exception("Failed to get CLI workspace tabs")
return jsonify({"error": "Failed to get workspace tabs"}), 500
try:
workspace_path = resolve_workspace_path()
rules = current_app.config.get("EXCLUSION_RULES") or []
rules = exclusion_rules()
payload, status = assemble_workspace_tabs(workspace_id, workspace_path, rules)
return jsonify(payload), status
except Exception:
Expand Down
Loading
Loading