Skip to content
Open
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
14 changes: 5 additions & 9 deletions api/export_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,13 @@
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_context import resolve_workspace_context_minimal
from services.workspace_db import (
build_composer_id_to_workspace_id,
collect_workspace_entries,
load_bubble_map,
load_code_block_diff_map,
open_global_db,
)
from services.workspace_resolver import (
create_project_name_to_workspace_id_map,
lookup_workspace_display_name,
)
from services.workspace_resolver import lookup_workspace_display_name

bp = Blueprint("export_api", __name__)
_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -102,9 +98,9 @@ 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)
ctx = resolve_workspace_context_minimal(workspace_path)
workspace_entries = ctx.workspace_entries
composer_id_to_ws = ctx.composer_id_to_workspace_id

# Build display-name and slug maps
ws_id_to_slug: dict[str, str] = {}
Expand Down
32 changes: 18 additions & 14 deletions scripts/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,15 @@
cursor_ide_chat_to_markdown,
)
from models import ExportEntry, SchemaError # noqa: E402
from services.workspace_context import ( # noqa: E402
enrich_workspace_context_from_global_db,
resolve_workspace_context,
)
from services.workspace_db import ( # noqa: E402
build_composer_id_to_workspace_id,
collect_invalid_workspace_ids,
collect_workspace_entries,
load_bubble_map,
load_code_block_diff_map,
load_project_layouts_map,
open_global_db,
)
from services.workspace_resolver import ( # noqa: E402
create_project_name_to_workspace_id_map,
create_workspace_path_to_id_map,
determine_project_for_conversation,
infer_invalid_workspace_aliases,
lookup_workspace_display_name,
Expand Down Expand Up @@ -203,11 +200,12 @@ def main():
)

# ── Workspace scanning via service layer ──────────────────────────────────
workspace_entries = collect_workspace_entries(workspace_path)
invalid_workspace_ids = collect_invalid_workspace_ids(workspace_entries)
project_name_map = create_project_name_to_workspace_id_map(workspace_entries)
workspace_path_map = create_workspace_path_to_id_map(workspace_entries)
composer_id_to_ws = build_composer_id_to_workspace_id(workspace_path, workspace_entries)
ctx = resolve_workspace_context(workspace_path)
workspace_entries = ctx.workspace_entries
invalid_workspace_ids = ctx.invalid_workspace_ids
project_name_map = ctx.project_name_to_workspace_id
workspace_path_map = ctx.workspace_path_to_id
composer_id_to_ws = ctx.composer_id_to_workspace_id

# Build display-name and slug maps from workspace entries.
# Entries whose workspace.json cannot be resolved are omitted so the
Expand Down Expand Up @@ -235,8 +233,14 @@ def main():
global_db_path,
)
else:
project_layouts_map = load_project_layouts_map(global_db)
bubble_map = load_bubble_map(global_db)
ctx = enrich_workspace_context_from_global_db(
ctx,
global_db,
populate_project_layouts=True,
populate_bubble_map=True,
)
project_layouts_map = ctx.project_layouts_map
bubble_map = ctx.bubble_map
code_block_diff_map = load_code_block_diff_map(global_db)

try:
Expand Down
135 changes: 135 additions & 0 deletions services/workspace_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""Workspace determination ceremony — single orchestrator for shared maps."""

from __future__ import annotations

import sqlite3
from dataclasses import dataclass, replace

from services.workspace_db import (
build_composer_id_to_workspace_id,
build_composer_id_to_workspace_id_cached,
collect_invalid_workspace_ids,
collect_workspace_entries,
load_bubble_map,
load_project_layouts_map,
)
from services.workspace_resolver import (
create_project_name_to_workspace_id_map,
create_workspace_path_to_id_map,
)


@dataclass(frozen=True)
class WorkspaceContext:
"""Precomputed workspace-resolution maps for conversation assignment."""

workspace_entries: list[dict]
invalid_workspace_ids: set[str]
composer_id_to_workspace_id: dict[str, str]
project_name_to_workspace_id: dict[str, str]
workspace_path_to_id: dict[str, str]
project_layouts_map: dict[str, list]
bubble_map: dict[str, dict]


def _entries(
workspace_path: str,
workspace_entries: list[dict] | None,
) -> list[dict]:
if workspace_entries is not None:
return workspace_entries
return collect_workspace_entries(workspace_path)


def _assemble_context(
entries: list[dict],
*,
invalid_workspace_ids: set[str],
workspace_path_to_id: dict[str, str],
composer_id_to_workspace_id: dict[str, str],
) -> WorkspaceContext:
return WorkspaceContext(
workspace_entries=entries,
invalid_workspace_ids=invalid_workspace_ids,
composer_id_to_workspace_id=composer_id_to_workspace_id,
project_name_to_workspace_id=create_project_name_to_workspace_id_map(entries),
workspace_path_to_id=workspace_path_to_id,
project_layouts_map={},
bubble_map={},
)


def resolve_workspace_context(
workspace_path: str,
*,
workspace_entries: list[dict] | None = None,
) -> WorkspaceContext:
"""Full workspace maps with an uncached composer→workspace scan (CLI export)."""
entries = _entries(workspace_path, workspace_entries)
return _assemble_context(
entries,
invalid_workspace_ids=collect_invalid_workspace_ids(entries),
workspace_path_to_id=create_workspace_path_to_id_map(entries),
composer_id_to_workspace_id=build_composer_id_to_workspace_id(
workspace_path, entries,
),
)


def resolve_workspace_context_cached(
workspace_path: str,
rules: list,
*,
workspace_entries: list[dict] | None = None,
nocache: bool = False,
) -> WorkspaceContext:
"""Full workspace maps with a mtime-keyed composer map (listing / tabs)."""
entries = _entries(workspace_path, workspace_entries)
return _assemble_context(
entries,
invalid_workspace_ids=collect_invalid_workspace_ids(entries),
workspace_path_to_id=create_workspace_path_to_id_map(entries),
composer_id_to_workspace_id=build_composer_id_to_workspace_id_cached(
workspace_path, entries, rules, nocache=nocache,
),
)


def resolve_workspace_context_minimal(
workspace_path: str,
*,
workspace_entries: list[dict] | None = None,
) -> WorkspaceContext:
"""Entries, project-name, and composer maps only (HTTP export).

Args:
workspace_path: Cursor ``workspaceStorage`` root.
workspace_entries: Pre-collected entries; when ``None``, scanned from disk.
"""
entries = _entries(workspace_path, workspace_entries)
return _assemble_context(
entries,
invalid_workspace_ids=set(),
workspace_path_to_id={},
composer_id_to_workspace_id=build_composer_id_to_workspace_id(
workspace_path, entries,
),
)


def enrich_workspace_context_from_global_db(
ctx: WorkspaceContext,
global_db: sqlite3.Connection,
*,
populate_project_layouts: bool = False,
populate_bubble_map: bool = False,
) -> WorkspaceContext:
"""Return *ctx* with global KV maps loaded from an open global DB connection."""
updates: dict = {}
if populate_project_layouts:
updates["project_layouts_map"] = load_project_layouts_map(global_db)
if populate_bubble_map:
updates["bubble_map"] = load_bubble_map(global_db)
if not updates:
return ctx
return replace(ctx, **updates)
20 changes: 10 additions & 10 deletions services/workspace_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@
nocache_enabled,
set_cached_projects,
)
from services.workspace_context import resolve_workspace_context_cached
from services.workspace_db import (
COMPOSER_ROWS_WITH_HEADERS_SQL,
build_composer_id_to_workspace_id_cached,
collect_invalid_workspace_ids,
collect_workspace_entries,
global_storage_db_path,
load_project_layouts_for_composer,
Expand All @@ -36,8 +35,6 @@
)
from utils.workspace_path import get_cli_chats_path
from services.workspace_resolver import (
create_project_name_to_workspace_id_map,
create_workspace_path_to_id_map,
determine_project_for_conversation,
infer_invalid_workspace_aliases,
infer_workspace_name_from_context,
Expand Down Expand Up @@ -125,13 +122,16 @@ def _build_workspace_projects_uncached(
nocache: bool,
) -> tuple[list[dict], list[dict]]:
parse_warnings = ParseWarningCollector()
invalid_workspace_ids = collect_invalid_workspace_ids(workspace_entries)

project_name_map = create_project_name_to_workspace_id_map(workspace_entries)
workspace_path_map = create_workspace_path_to_id_map(workspace_entries)
composer_id_to_ws = build_composer_id_to_workspace_id_cached(
workspace_path, workspace_entries, rules, nocache=nocache,
ctx = resolve_workspace_context_cached(
workspace_path,
rules,
workspace_entries=workspace_entries,
nocache=nocache,
)
invalid_workspace_ids = ctx.invalid_workspace_ids
project_name_map = ctx.project_name_to_workspace_id
workspace_path_map = ctx.workspace_path_to_id
composer_id_to_ws = ctx.composer_id_to_workspace_id

conversation_map: dict[str, list] = {}

Expand Down
53 changes: 26 additions & 27 deletions services/workspace_tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@
nocache_enabled,
set_cached_tab_summaries,
)
from services.workspace_context import resolve_workspace_context_cached
from services.workspace_db import (
COMPOSER_ROWS_WITH_HEADERS_SQL,
build_composer_id_to_workspace_id_cached,
collect_invalid_workspace_ids,
collect_workspace_entries,
global_storage_db_path,
load_bubbles_for_composer,
Expand All @@ -43,8 +42,6 @@
)
from utils.workspace_path import get_cli_chats_path
from services.workspace_resolver import (
create_project_name_to_workspace_id_map,
create_workspace_path_to_id_map,
determine_project_for_conversation,
infer_invalid_workspace_aliases,
lookup_workspace_display_name,
Expand Down Expand Up @@ -489,12 +486,16 @@ def _build_workspace_tab_summaries_uncached(
parse_warnings = ParseWarningCollector()
response: dict = {"tabs": []}

invalid_workspace_ids = collect_invalid_workspace_ids(workspace_entries)
project_name_map = create_project_name_to_workspace_id_map(workspace_entries)
workspace_path_map = create_workspace_path_to_id_map(workspace_entries)
composer_id_to_ws = build_composer_id_to_workspace_id_cached(
workspace_path, workspace_entries, rules, nocache=nocache,
ctx = resolve_workspace_context_cached(
workspace_path,
rules,
workspace_entries=workspace_entries,
nocache=nocache,
)
invalid_workspace_ids = ctx.invalid_workspace_ids
project_name_map = ctx.project_name_to_workspace_id
workspace_path_map = ctx.workspace_path_to_id
composer_id_to_ws = ctx.composer_id_to_workspace_id
matching_ws_ids = _build_matching_ws_ids(workspace_id, workspace_path, workspace_entries)

with open_global_db(workspace_path) as (global_db, _):
Expand Down Expand Up @@ -635,13 +636,12 @@ def assemble_single_tab(
"""
parse_warnings = ParseWarningCollector()

workspace_entries = collect_workspace_entries(workspace_path)
invalid_workspace_ids = collect_invalid_workspace_ids(workspace_entries)
project_name_map = create_project_name_to_workspace_id_map(workspace_entries)
workspace_path_map = create_workspace_path_to_id_map(workspace_entries)
composer_id_to_ws = build_composer_id_to_workspace_id_cached(
workspace_path, workspace_entries, rules,
)
ctx = resolve_workspace_context_cached(workspace_path, rules)
workspace_entries = ctx.workspace_entries
invalid_workspace_ids = ctx.invalid_workspace_ids
project_name_map = ctx.project_name_to_workspace_id
workspace_path_map = ctx.workspace_path_to_id
composer_id_to_ws = ctx.composer_id_to_workspace_id
matching_ws_ids = _build_matching_ws_ids(workspace_id, workspace_path, workspace_entries)

with open_global_db(workspace_path) as (global_db, _):
Expand Down Expand Up @@ -768,13 +768,12 @@ def assemble_workspace_tabs(
parse_warnings = ParseWarningCollector()
response: dict = {"tabs": []}

workspace_entries = collect_workspace_entries(workspace_path)
invalid_workspace_ids = collect_invalid_workspace_ids(workspace_entries)
project_name_map = create_project_name_to_workspace_id_map(workspace_entries)
workspace_path_map = create_workspace_path_to_id_map(workspace_entries)
composer_id_to_ws = build_composer_id_to_workspace_id_cached(
workspace_path, workspace_entries, rules,
)
ctx = resolve_workspace_context_cached(workspace_path, rules)
workspace_entries = ctx.workspace_entries
invalid_workspace_ids = ctx.invalid_workspace_ids
project_name_map = ctx.project_name_to_workspace_id
workspace_path_map = ctx.workspace_path_to_id
composer_id_to_ws = ctx.composer_id_to_workspace_id
matching_ws_ids = _build_matching_ws_ids(workspace_id, workspace_path, workspace_entries)

bubble_map: dict[str, dict] = {}
Expand Down Expand Up @@ -840,20 +839,20 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
if len(parts) < 2:
continue
chat_id = parts[1]
ctx = _loads_kv_value_logged(row["key"], row["value"])
if not isinstance(ctx, dict):
mrc = _loads_kv_value_logged(row["key"], row["value"])
if not isinstance(mrc, dict):
continue

# Per-bubble context map (needs the contextId at parts[2])
if len(parts) >= 3:
context_id = parts[2]
message_request_context_map.setdefault(chat_id, []).append({
**ctx,
**mrc,
"contextId": context_id,
})

# Project-layout map (root paths used by the resolver)
layouts = ctx.get("projectLayouts")
layouts = mrc.get("projectLayouts")
if isinstance(layouts, list):
project_layouts_map.setdefault(chat_id, [])
for layout in layouts:
Expand Down
Loading
Loading