Skip to content

Commit cac9e53

Browse files
committed
fix: addressed reviewer's comment
1 parent 3841188 commit cac9e53

7 files changed

Lines changed: 91 additions & 15 deletions

File tree

api/export_api.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,12 @@ def _get_export_state() -> dict:
4747
try:
4848
with open(state_path, "r", encoding="utf-8") as f:
4949
return json.load(f)
50-
except Exception:
51-
pass
50+
except (json.JSONDecodeError, ValueError, OSError) as e:
51+
_logger.warning(
52+
"Could not read export state from %s: %s",
53+
state_path,
54+
e,
55+
)
5256
return {}
5357

5458

api/workspaces.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
_logger = logging.getLogger(__name__)
4141

4242

43+
def _exclusion_rules() -> list:
44+
"""Return loaded exclusion rules from app config (empty list when unset)."""
45+
return current_app.config.get("EXCLUSION_RULES") or []
46+
47+
4348
# ---------------------------------------------------------------------------
4449
# GET /api/workspaces
4550
# ---------------------------------------------------------------------------
@@ -48,7 +53,7 @@
4853
def list_workspaces():
4954
try:
5055
workspace_path = resolve_workspace_path()
51-
rules = current_app.config.get("EXCLUSION_RULES") or []
56+
rules = _exclusion_rules()
5257
projects, warnings = list_workspace_projects(workspace_path, rules)
5358
payload: dict = {"projects": projects}
5459
if warnings:
@@ -144,10 +149,14 @@ def get_workspace(workspace_id):
144149
@bp.route("/api/workspaces/<workspace_id>/tabs")
145150
def get_workspace_tabs(workspace_id):
146151
if workspace_id.startswith("cli:"):
147-
return get_cli_workspace_tabs(workspace_id)
152+
try:
153+
return get_cli_workspace_tabs(workspace_id)
154+
except Exception:
155+
_logger.exception("Failed to get CLI workspace tabs")
156+
return jsonify({"error": "Failed to get workspace tabs"}), 500
148157
try:
149158
workspace_path = resolve_workspace_path()
150-
rules = current_app.config.get("EXCLUSION_RULES") or []
159+
rules = _exclusion_rules()
151160
payload, status = assemble_workspace_tabs(workspace_id, workspace_path, rules)
152161
return jsonify(payload), status
153162
except Exception:

scripts/export.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
extract_text_from_bubble,
4040
slug,
4141
)
42-
from utils.tool_parser import parse_tool_call # noqa: E402
4342
from utils.workspace_path import ( # noqa: E402
4443
get_cli_chats_path,
4544
resolve_workspace_path,
@@ -197,8 +196,11 @@ def main():
197196
ts = st.get("lastExportTime")
198197
if ts:
199198
last_export = int(datetime.fromisoformat(ts.replace("Z", "+00:00")).timestamp() * 1000)
200-
except Exception:
201-
pass
199+
except Exception as e:
200+
_logger.warning(
201+
"Could not read last export timestamp; defaulting to full export: %s",
202+
e,
203+
)
202204

203205
# ── Workspace scanning via service layer ──────────────────────────────────
204206
workspace_entries = collect_workspace_entries(workspace_path)

services/workspace_listing.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,20 @@
3838

3939

4040
def list_workspace_projects(workspace_path: str, rules: list) -> tuple[list[dict], list[dict]]:
41-
"""Return (projects, warnings) for GET /api/workspaces."""
41+
"""List workspace projects for GET /api/workspaces.
42+
43+
Args:
44+
workspace_path: Cursor ``workspaceStorage`` root.
45+
rules: Exclusion rule token lists from :func:`utils.exclusion_rules.load_rules`.
46+
47+
Returns:
48+
``(projects, warnings)``. Each project dict has ``id``, ``name``,
49+
``path`` (``workspace.json`` path), ``conversationCount``,
50+
``lastModified`` (ISO 8601), and optional ``aliasIds`` / ``source``
51+
(``"cli"`` for Cursor CLI projects). *warnings* is a list of structured
52+
parse-error dicts (``type``, ``count``, ``detail``) from
53+
:meth:`models.ParseWarningCollector.to_api_list`; empty when no skips.
54+
"""
4255
parse_warnings = ParseWarningCollector()
4356
workspace_entries = collect_workspace_entries(workspace_path)
4457
invalid_workspace_ids = collect_invalid_workspace_ids(workspace_entries)

services/workspace_resolver.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,18 @@ def determine_project_for_conversation(
227227
) -> str | None:
228228
"""Resolve which workspace folder owns a composer conversation.
229229
230-
Uses per-workspace composer maps, project layouts, file paths in bubbles,
231-
and path-segment heuristics in priority order.
230+
Args:
231+
composer_data: Parsed ``composerData`` JSON for *composer_id*.
232+
composer_id: Composer UUID from the global DB key.
233+
project_layouts_map: ``{composer_id: [root_path, ...]}`` from global KV.
234+
project_name_to_workspace_id: Basename-to-workspace-folder map.
235+
workspace_path_to_id: Normalized root path to workspace folder map.
236+
workspace_entries: Output of :func:`services.workspace_db.collect_workspace_entries`.
237+
bubble_map: ``{bubble_id: bubble_dict}`` from global KV.
238+
composer_id_to_workspace_id: Definitive per-workspace composer map; when
239+
``None``, layout and path heuristics are used without this shortcut.
240+
invalid_workspace_ids: Workspace folders marked invalid; mapped IDs in
241+
this set are ignored when using *composer_id_to_workspace_id*.
232242
233243
Returns:
234244
Workspace folder name, or ``None`` when no project can be determined.
@@ -360,7 +370,27 @@ def infer_invalid_workspace_aliases(
360370
composer_id_to_ws: dict,
361371
invalid_workspace_ids: set[str],
362372
) -> dict[str, str]:
363-
"""Majority-vote each invalid workspace ID to its most likely valid replacement."""
373+
"""Map invalid workspace IDs to valid replacements by majority vote.
374+
375+
For each composer assigned to an *invalid_workspace_ids* entry, calls
376+
:func:`determine_project_for_conversation` without the definitive composer map
377+
and counts votes for inferred valid workspace folders.
378+
379+
Args:
380+
composer_rows: Global ``composerData:*`` SQLite rows.
381+
project_layouts_map: Layout map passed to :func:`determine_project_for_conversation`.
382+
project_name_map: Basename map for path resolution.
383+
workspace_path_map: Normalized path map for path resolution.
384+
workspace_entries: Workspace folder entries from storage scan.
385+
bubble_map: Bubble KV map for path resolution.
386+
composer_id_to_ws: Composer-to-workspace map (may point at invalid IDs).
387+
invalid_workspace_ids: Workspace folder names to reassign.
388+
389+
Returns:
390+
``{invalid_id: replacement_id}`` for IDs with at least one vote. Ties
391+
break by choosing the replacement with the highest vote count (first
392+
max in iteration order). Returns ``{}`` when no invalid ID receives votes.
393+
"""
364394
votes: dict[str, dict[str, int]] = {}
365395
for row in composer_rows:
366396
cid = row["key"].split(":")[1]

services/workspace_tabs.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,20 @@ def assemble_workspace_tabs(
8282
workspace_path: str,
8383
rules: list,
8484
) -> tuple[dict, int]:
85-
"""Build (payload, status) for GET /api/workspaces/<id>/tabs; status=404 if global storage is missing."""
85+
"""Build tabs payload for GET /api/workspaces/<id>/tabs (IDE workspaces).
86+
87+
Args:
88+
workspace_id: Workspace folder name, or ``"global"`` for unassigned chats.
89+
workspace_path: Cursor ``workspaceStorage`` root.
90+
rules: Exclusion rule token lists from :func:`utils.exclusion_rules.load_rules`.
91+
92+
Returns:
93+
``(payload, status)``. On success (``200``), *payload* contains ``tabs``
94+
(list of tab dicts with ``id``, ``title``, ``timestamp``, ``bubbles``,
95+
optional ``metadata`` / ``codeBlockDiffs``) and optional ``warnings``
96+
when parse failures were skipped. On failure (``404``), *payload* is
97+
``{"error": "Global storage not found"}``.
98+
"""
8699
parse_warnings = ParseWarningCollector()
87100
response: dict = {"tabs": []}
88101

@@ -359,7 +372,12 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
359372

360373
# Context window
361374
ctx_window = raw.get("contextWindowStatusAtCreation") or {}
362-
ctx_pct = ctx_window.get("percentageRemainingFloat") or ctx_window.get("percentageRemaining")
375+
ctx_pct = None
376+
if isinstance(ctx_window, dict):
377+
if ctx_window.get("percentageRemainingFloat") is not None:
378+
ctx_pct = ctx_window.get("percentageRemainingFloat")
379+
elif ctx_window.get("percentageRemaining") is not None:
380+
ctx_pct = ctx_window.get("percentageRemaining")
363381

364382
# Display text fallbacks
365383
display_text = full_text.strip()

tests/test_workspace_tabs_malformed_nested.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def test_non_dict_parse_result_does_not_drop_composer(self) -> None:
201201

202202
with tempfile.TemporaryDirectory() as tmp:
203203
ws_root = _seed_workspace_with_tool_former(tmp)
204-
# Force _parse_tool_call to return None — the previous code
204+
# Force parse_tool_call to return None — the previous code
205205
# would have stored ``tool_calls = [None]`` and crashed in the
206206
# display-text fallback with ``NoneType.get``.
207207
with patch("services.workspace_tabs.parse_tool_call", return_value=None):

0 commit comments

Comments
 (0)