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
16 changes: 13 additions & 3 deletions api/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ def get_workspace_tabs(workspace_id: str) -> tuple[Response, int] | Response:
workspace_id: Storage folder name, ``global`` for unassigned chats, or
``cli:<project_id>``.
summary: When ``1`` or ``true``, return lightweight tab headers only.
nocache: When ``1`` or ``true``, bypass cache on summary requests.
nocache: When ``1`` or ``true``, bypass cache on summary and full-tab
requests (alias disk cache on per-tab lazy load).

Returns:
Tabs payload from :func:`services.workspace_tabs` helpers (typically
Expand All @@ -190,7 +191,9 @@ def get_workspace_tabs(workspace_id: str) -> tuple[Response, int] | Response:
workspace_id, workspace_path, rules, nocache=_request_nocache(),
)
else:
payload, status = assemble_workspace_tabs(workspace_id, workspace_path, rules)
payload, status = assemble_workspace_tabs(
workspace_id, workspace_path, rules, nocache=_request_nocache(),
)
return json_response(payload, status)
except Exception:
_logger.exception("Failed to get workspace tabs")
Expand All @@ -209,6 +212,7 @@ def get_workspace_tab(workspace_id: str, composer_id: str) -> tuple[Response, in
workspace_id: Storage folder name, ``global`` for unassigned chats, or
``cli:<project_id>`` (CLI workspaces return 400).
composer_id: Composer UUID to load.
nocache: When ``1`` or ``true``, bypass alias disk cache.

Returns:
Single-tab JSON from :func:`services.workspace_tabs.assemble_single_tab`
Expand All @@ -221,7 +225,13 @@ def get_workspace_tab(workspace_id: str, composer_id: str) -> tuple[Response, in
try:
workspace_path = resolve_workspace_path()
rules = exclusion_rules()
payload, status = assemble_single_tab(workspace_id, composer_id, workspace_path, rules)
payload, status = assemble_single_tab(
workspace_id,
composer_id,
workspace_path,
rules,
nocache=_request_nocache(),
)
return json_response(payload, status)
except Exception:
_logger.exception("Failed to get workspace tab")
Expand Down
23 changes: 13 additions & 10 deletions services/export_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from services.workspace_context import (
WorkspaceContext,
enrich_workspace_context_from_global_db,
resolve_invalid_workspace_aliases_cached,
resolve_workspace_context_cached,
)
from services.workspace_db import (
Expand All @@ -28,7 +29,6 @@
)
from services.workspace_resolver import (
determine_project_for_conversation,
infer_invalid_workspace_aliases,
lookup_workspace_display_name,
)
from utils.cli_chat_reader import (
Expand Down Expand Up @@ -169,6 +169,9 @@ def prepare_workspace_orchestration(

def load_global_db_export_data(
orch: WorkspaceOrchestration,
rules: list[Any],
*,
nocache: bool = False,
) -> GlobalDbExportData | None:
"""Load global DB maps needed for IDE composer export."""
ctx = orch.ctx
Expand Down Expand Up @@ -197,15 +200,13 @@ def load_global_db_export_data(
code_block_diff_map = load_code_block_diff_map(global_db)
ide_composer_rows = safe_fetchall(global_db, COMPOSER_ROWS_WITH_HEADERS_SQL)

invalid_workspace_aliases = infer_invalid_workspace_aliases(
composer_rows=ide_composer_rows,
invalid_workspace_aliases = resolve_invalid_workspace_aliases_cached(
ctx,
global_db,
orch.workspace_path,
rules,
nocache=nocache,
project_layouts_map=project_layouts_map,
project_name_map=ctx.project_name_to_workspace_id,
workspace_path_map=ctx.workspace_path_to_id,
workspace_entries=orch.workspace_entries,
bubble_map=bubble_map,
composer_id_to_ws=ctx.composer_id_to_workspace_id,
invalid_workspace_ids=ctx.invalid_workspace_ids,
)

return GlobalDbExportData(
Expand Down Expand Up @@ -503,7 +504,9 @@ def collect_export_entries(
exported: list[CollectedExportEntry] = []

if include_composer:
db_data = load_global_db_export_data(orch)
db_data = load_global_db_export_data(
orch, exclusion_rules, nocache=effective_nocache,
)
if db_data is not None:
exported.extend(
_collect_ide_export_entries(
Expand Down
3 changes: 3 additions & 0 deletions services/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,9 @@ def _load_search_workspace_assigner(
)
invalid_workspace_aliases: dict[str, str] = {}
if ctx.invalid_workspace_ids:
# Issue #116 follow-up: search assigner still cold-scans composerData:*
# rows here; sharing resolve_invalid_workspace_aliases_cached is
# intentionally deferred (operator scope — see issue Out of scope).
composer_rows = safe_fetchall(global_db, COMPOSER_ROWS_WITH_HEADERS_SQL)
invalid_workspace_aliases = infer_invalid_workspace_aliases(
composer_rows=composer_rows,
Expand Down
55 changes: 55 additions & 0 deletions services/summary_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
CACHE_DIR = Path.home() / ".cache" / "cursor-chat-browser"
PROJECTS_CACHE_FILE = CACHE_DIR / "projects.json"
COMPOSER_MAP_CACHE_FILE = CACHE_DIR / "composer-id-to-ws.json"
INVALID_WORKSPACE_ALIASES_CACHE_FILE = CACHE_DIR / "invalid-workspace-aliases.json"
TAB_SUMMARIES_PREFIX = "tab-summaries-"


Expand Down Expand Up @@ -238,6 +239,60 @@ def set_cached_composer_id_to_ws(
)


def get_cached_invalid_workspace_aliases(
fingerprint: dict[str, Any],
) -> dict[str, str] | None:
"""Load cached invalid-workspace alias map when the fingerprint matches.

Args:
fingerprint: Storage mtime/rules digest.

Returns:
``{invalid_id: replacement_id}`` on hit, else ``None``.
"""
data = _read_cache_file(INVALID_WORKSPACE_ALIASES_CACHE_FILE)
if not data:
return None
if not _fingerprint_equal(data.get("fingerprint"), fingerprint):
return None
aliases = data.get("invalid_workspace_aliases")
if not isinstance(aliases, dict):
_logger.debug(
"Invalid workspace aliases cache rejected: invalid_workspace_aliases is not a dict",
)
return None
validated: dict[str, str] = {}
Comment thread
clean6378-max-it marked this conversation as resolved.
for key, value in aliases.items():
if not isinstance(key, str) or not isinstance(value, str):
_logger.debug(
"Invalid workspace aliases cache rejected: non-string entry (%r -> %r)",
key,
value,
)
return None
validated[key] = value
return validated


def set_cached_invalid_workspace_aliases(
fingerprint: dict[str, Any],
aliases: dict[str, str],
) -> None:
"""Persist invalid-workspace alias map under *fingerprint*.

Args:
fingerprint: Invalidation fingerprint paired with *aliases*.
aliases: ``{invalid_id: replacement_id}`` from alias inference.
"""
_write_cache_file(
INVALID_WORKSPACE_ALIASES_CACHE_FILE,
{
"fingerprint": fingerprint,
"invalid_workspace_aliases": aliases,
},
)


def _tab_summaries_path(workspace_id: str) -> Path:
safe = hashlib.sha256(workspace_id.encode("utf-8")).hexdigest()[:16]
return CACHE_DIR / f"{TAB_SUMMARIES_PREFIX}{safe}.json"
Expand Down
105 changes: 105 additions & 0 deletions services/workspace_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@

from __future__ import annotations

import os
import sqlite3
from dataclasses import dataclass, replace
from typing import Any

from models import Bubble
from services.workspace_db import (
COMPOSER_ROWS_WITH_HEADERS_SQL,
build_composer_id_to_workspace_id,
build_composer_id_to_workspace_id_cached,
collect_invalid_workspace_ids,
collect_workspace_entries,
global_storage_db_path,
load_bubble_map,
load_project_layouts_map,
safe_fetchall,
)
from services.workspace_resolver import (
create_project_name_to_workspace_id_map,
create_workspace_path_to_id_map,
infer_invalid_workspace_aliases,
)


Expand All @@ -32,6 +37,7 @@ class WorkspaceContext:
workspace_path_to_id: dict[str, str]
project_layouts_map: dict[str, list[str]]
bubble_map: dict[str, Bubble]
invalid_workspace_aliases: dict[str, str] | None = None


def _entries(
Expand Down Expand Up @@ -135,3 +141,102 @@ def enrich_workspace_context_from_global_db(
if not updates:
return ctx
return replace(ctx, **updates)


def resolve_invalid_workspace_aliases_cached(
ctx: WorkspaceContext,
global_db: sqlite3.Connection,
workspace_path: str,
rules: list[Any],
*,
nocache: bool = False,
project_layouts_map: dict[str, list[str]] | None = None,
) -> dict[str, str]:
"""Return invalid-workspace alias map, using the summary-cache fingerprint.

Computes ``infer_invalid_workspace_aliases`` at most once per storage
fingerprint (same mtime key as composer-map / tab-summary caches). When
*ctx* already carries a populated ``invalid_workspace_aliases`` field,
that value is returned without touching disk or the global DB roster.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Args:
ctx: Workspace maps from :func:`resolve_workspace_context_cached`.
global_db: Open global ``state.vscdb`` connection.
workspace_path: Cursor ``workspaceStorage`` root.
rules: Exclusion rule token lists (fingerprint input).
nocache: When ``True``, bypass disk cache reads and writes.
project_layouts_map: Pre-loaded layouts; loaded from *global_db* when
``None``.

Returns:
``{invalid_id: replacement_id}``, or ``{}`` when every workspace is valid.
"""
if ctx.invalid_workspace_aliases is not None:
return ctx.invalid_workspace_aliases
if not ctx.invalid_workspace_ids:
return {}

from services.summary_cache import (
fingerprint_workspace_storage,
get_cached_invalid_workspace_aliases,
nocache_enabled,
set_cached_invalid_workspace_aliases,
)
from utils.workspace_path import get_cli_chats_path

gdb = global_storage_db_path(workspace_path)
cli_path = get_cli_chats_path()
fingerprint = fingerprint_workspace_storage(
workspace_path,
ctx.workspace_entries,
global_db_path=gdb if os.path.isfile(gdb) else None,
rules=rules,
cli_chats_path=cli_path if os.path.isdir(cli_path) else None,
)
if not nocache_enabled(request_nocache=nocache):
cached = get_cached_invalid_workspace_aliases(fingerprint)
if cached is not None:
return cached

layouts = (
project_layouts_map
if project_layouts_map is not None
else load_project_layouts_map(global_db)
)
composer_rows = safe_fetchall(global_db, COMPOSER_ROWS_WITH_HEADERS_SQL)
aliases = infer_invalid_workspace_aliases(
composer_rows=composer_rows,
project_layouts_map=layouts,
project_name_map=ctx.project_name_to_workspace_id,
workspace_path_map=ctx.workspace_path_to_id,
workspace_entries=ctx.workspace_entries,
bubble_map={},
composer_id_to_ws=ctx.composer_id_to_workspace_id,
invalid_workspace_ids=ctx.invalid_workspace_ids,
)
if not nocache_enabled(request_nocache=nocache):
set_cached_invalid_workspace_aliases(fingerprint, aliases)
return aliases


def with_invalid_workspace_aliases(
Comment thread
clean6378-max-it marked this conversation as resolved.
ctx: WorkspaceContext,
global_db: sqlite3.Connection,
workspace_path: str,
rules: list[Any],
*,
nocache: bool = False,
project_layouts_map: dict[str, list[str]] | None = None,
) -> WorkspaceContext:
"""Return *ctx* with ``invalid_workspace_aliases`` populated from cache."""
if ctx.invalid_workspace_aliases is not None:
return ctx
aliases = resolve_invalid_workspace_aliases_cached(
ctx,
global_db,
workspace_path,
rules,
nocache=nocache,
project_layouts_map=project_layouts_map,
)
return replace(ctx, invalid_workspace_aliases=aliases)
26 changes: 12 additions & 14 deletions services/workspace_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
nocache_enabled,
set_cached_projects,
)
from services.workspace_context import resolve_invalid_workspace_aliases_cached
from services.workspace_db import (
COMPOSER_ROWS_WITH_HEADERS_SQL,
collect_workspace_entries,
Expand All @@ -41,7 +42,6 @@
from utils.workspace_path import get_cli_chats_path
from services.workspace_resolver import (
build_composer_ids_by_workspace,
infer_invalid_workspace_aliases,
infer_workspace_name_from_layouts,
lookup_workspace_display_name,
)
Expand Down Expand Up @@ -93,7 +93,7 @@ def list_workspace_projects(
)

projects, warnings = _build_workspace_projects_uncached(
workspace_path, rules, orch,
workspace_path, rules, orch, nocache=effective_nocache,
)
if not effective_nocache:
set_cached_projects(orch.fingerprint, projects, warnings)
Expand All @@ -104,6 +104,8 @@ def _build_workspace_projects_uncached(
workspace_path: str,
rules: list[Any],
orch: WorkspaceOrchestration,
*,
nocache: bool = False,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
parse_warnings = ParseWarningCollector()
ctx = orch.ctx
Expand All @@ -124,18 +126,14 @@ def _build_workspace_projects_uncached(
project_layouts_map = load_project_layouts_map(global_db)

bubble_map: dict[str, Bubble] = {}
invalid_workspace_aliases: dict[str, str] = {}
if invalid_workspace_ids:
invalid_workspace_aliases = infer_invalid_workspace_aliases(
composer_rows=composer_rows,
project_layouts_map=project_layouts_map,
project_name_map=project_name_map,
workspace_path_map=workspace_path_map,
workspace_entries=workspace_entries,
bubble_map=bubble_map,
composer_id_to_ws=composer_id_to_ws,
invalid_workspace_ids=invalid_workspace_ids,
)
invalid_workspace_aliases = resolve_invalid_workspace_aliases_cached(
ctx,
global_db,
workspace_path,
rules,
nocache=nocache,
project_layouts_map=project_layouts_map,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

for row in composer_rows:
composer = parse_composer_data_row(
Expand Down
Loading
Loading