Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
47 changes: 47 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,52 @@ 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):
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):
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
64 changes: 25 additions & 39 deletions services/workspace_tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@
nocache_enabled,
set_cached_tab_summaries,
)
from services.workspace_context import resolve_workspace_context_cached
from services.workspace_context import (
resolve_invalid_workspace_aliases_cached,
resolve_workspace_context_cached,
)
from services.workspace_db import (
COMPOSER_ROWS_WITH_HEADERS_SQL,
collect_workspace_entries,
Expand All @@ -67,7 +70,6 @@
)
from utils.workspace_path import get_cli_chats_path
from services.workspace_resolver import (
infer_invalid_workspace_aliases,
lookup_workspace_display_name,
matching_workspace_ids_for_folder,
)
Expand Down Expand Up @@ -453,18 +455,14 @@ def _build_workspace_tab_summaries_uncached(

composer_rows = safe_fetchall(global_db, COMPOSER_ROWS_WITH_HEADERS_SQL)
Comment thread
clean6378-max-it marked this conversation as resolved.

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={},
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,
)

for row in composer_rows:
composer = parse_composer_data_row(
Expand Down Expand Up @@ -582,22 +580,15 @@ def assemble_single_tab(
return {"error": "Conversation not found"}, 404

project_layouts_map: dict[str, list[str]] = {}
invalid_workspace_aliases: dict[str, str] = {}
project_layouts_map[composer_id] = load_project_layouts_for_composer(
global_db, composer_id,
)
if invalid_workspace_ids:
composer_rows_for_aliases = safe_fetchall(global_db, COMPOSER_ROWS_WITH_HEADERS_SQL)
invalid_workspace_aliases = infer_invalid_workspace_aliases(
composer_rows=composer_rows_for_aliases,
project_layouts_map=project_layouts_map,
project_name_map=project_name_map,
workspace_path_map=workspace_path_map,
workspace_entries=workspace_entries,
bubble_map={},
composer_id_to_ws=composer_id_to_ws,
invalid_workspace_ids=invalid_workspace_ids,
)
invalid_workspace_aliases = resolve_invalid_workspace_aliases_cached(
Comment thread
clean6378-max-it marked this conversation as resolved.
Outdated
ctx,
global_db,
workspace_path,
rules,
)

bubble_map = load_bubbles_for_composer(
global_db, composer_id, parse_warnings=parse_warnings,
Expand Down Expand Up @@ -723,18 +714,13 @@ def assemble_workspace_tabs(
# Get composer data entries with conversations
composer_rows = safe_fetchall(global_db, COMPOSER_ROWS_WITH_HEADERS_SQL)

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(
Comment thread
clean6378-max-it marked this conversation as resolved.
Outdated
ctx,
global_db,
workspace_path,
rules,
project_layouts_map=project_layouts_map,
)

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