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
30 changes: 21 additions & 9 deletions api/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from flask import Blueprint, jsonify

from utils.workspace_path import resolve_workspace_path
from utils.path_helpers import to_epoch_ms
from utils.path_helpers import to_epoch_ms, warn_workspace_json_read

bp = Blueprint("logs", __name__)
_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -48,8 +48,12 @@ def get_logs():
try:
bubble = json.loads(row["value"])
chat_map.setdefault(chat_id, []).append(bubble)
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to decode bubble row %s: %s",
row["key"],
e,
)

for chat_id, bubbles in chat_map.items():
bubbles = [b for b in bubbles if isinstance(b, dict)]
Expand Down Expand Up @@ -90,8 +94,8 @@ def get_logs():
with open(wj_path, "r", encoding="utf-8") as f:
wd = json.load(f)
workspace_folder = wd.get("folder")
except Exception:
pass
except Exception as e:
warn_workspace_json_read(_logger, name, e)

try:
# closing() guarantees .close() on scope exit (issue #17).
Expand Down Expand Up @@ -130,10 +134,18 @@ def get_logs():
"type": "composer",
"messageCount": len(c.get("conversation") or []),
})
except Exception:
pass
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to read logs from workspace %s: %s",
name,
e,
)
except Exception as e:
_logger.warning(
"Failed to iterate workspaces under %s: %s",
workspace_path,
e,
)

logs.sort(key=lambda log: log.get("timestamp") or 0, reverse=True)
return jsonify({"logs": logs})
Expand Down
57 changes: 41 additions & 16 deletions api/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules
from utils.workspace_path import resolve_workspace_path, get_cli_chats_path
from utils.path_helpers import to_epoch_ms
from utils.path_helpers import to_epoch_ms, warn_workspace_json_read
from utils.text_extract import extract_text_from_bubble
from utils.cli_chat_reader import list_cli_projects, traverse_blobs, messages_to_bubbles
from models import Bubble, Composer, SchemaError
Expand Down Expand Up @@ -114,10 +114,14 @@ def search():
fn = parts[-1] if parts else None
if fn:
ws_id_to_name[name] = _url_unquote(fn)
except Exception:
pass
except Exception:
pass
except Exception as e:
warn_workspace_json_read(_logger, name, e)
except Exception as e:
_logger.warning(
"Failed to list workspace entries under %s: %s",
workspace_path,
e,
)

# Build composer → workspace mapping
composer_id_to_ws = {}
Expand All @@ -139,8 +143,12 @@ def search():
cid = c.get("composerId") if isinstance(c, dict) else None
if cid:
composer_id_to_ws[cid] = entry["name"]
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to load composer mapping from workspace %s: %s",
entry["name"],
e,
)

# Load bubble text for searching
bubble_map = {}
Expand Down Expand Up @@ -261,8 +269,12 @@ def search():
"matchingText": matching_text,
"type": "composer",
})
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to process Composer from composerData:%s during search: %s",
composer_id,
e,
)

except Exception:
_logger.exception("Error searching global storage")
Expand All @@ -288,8 +300,8 @@ def search():
with open(wj_path, "r", encoding="utf-8") as f:
wd = json.load(f)
workspace_folder = wd.get("folder")
except Exception:
pass
except Exception as e:
warn_workspace_json_read(_logger, name, e)
workspace_name = _workspace_display_name_from_folder(workspace_folder, fallback=name)

# try/finally guarantees .close() on every exit path (issue #17).
Expand Down Expand Up @@ -362,13 +374,21 @@ def search():
"type": "chat",
})

except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to search legacy workspace %s: %s",
name,
e,
)
finally:
if conn is not None:
conn.close()
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to iterate legacy workspaces under %s: %s",
workspace_path,
e,
)

# ---------------------------------------------------------------
# Search Cursor CLI sessions (only for type=all)
Expand All @@ -386,7 +406,12 @@ def search():

try:
messages = traverse_blobs(session["db_path"])
except Exception:
except Exception as e:
_logger.warning(
"Failed to traverse CLI session blobs for %s: %s",
session_id,
e,
)
continue

bubbles = messages_to_bubbles(messages, created_ms)
Expand Down
9 changes: 7 additions & 2 deletions api/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@

from utils.workspace_path import resolve_workspace_path, get_cli_chats_path
from utils.cli_chat_reader import list_cli_projects
from utils.path_helpers import get_workspace_folder_paths, get_workspace_display_name
from utils.path_helpers import (
get_workspace_folder_paths,
get_workspace_display_name,
warn_workspace_json_read,
)
from utils.workspace_descriptor import read_json_file
from services.workspace_resolver import (
_infer_workspace_name_from_context,
Expand Down Expand Up @@ -117,7 +121,8 @@ def get_workspace(workspace_id):
inferred = _infer_workspace_name_from_context(workspace_path, workspace_id)
if inferred:
workspace_name = inferred
except Exception:
except Exception as e:
warn_workspace_json_read(_logger, workspace_id, e)
inferred = _infer_workspace_name_from_context(workspace_path, workspace_id)
if inferred:
workspace_name = inferred
Expand Down
2 changes: 1 addition & 1 deletion requirements-lock.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Lock is generated on Linux (CI / update-lock.yml). Windows-only transitives (e.g.
# colorama via click) are omitted — pip still installs them on Windows when needed.
blinker==1.9.0 # via flask
click==8.4.0 # via flask
click==8.4.1 # via flask
defusedxml==0.7.1 # via fpdf2
flask==3.1.3 # via -r requirements.txt
fonttools==4.63.0 # via fpdf2
Expand Down
55 changes: 47 additions & 8 deletions services/workspace_listing.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
from __future__ import annotations

import json
import logging
import os
import sqlite3
from datetime import datetime, timezone

_logger = logging.getLogger(__name__)

from utils.cli_chat_reader import list_cli_projects
from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules
from utils.path_helpers import (
get_workspace_folder_paths,
normalize_file_path,
to_epoch_ms,
warn_workspace_json_read,
)
from utils.workspace_descriptor import read_json_file
from utils.workspace_path import get_cli_chats_path
from models import Composer, SchemaError
from services.workspace_db import (
_build_composer_id_to_workspace_id,
_collect_invalid_workspace_ids,
Expand Down Expand Up @@ -72,7 +77,32 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
for row in composer_rows:
cid = row["key"].split(":")[1]
try:
cd = json.loads(row["value"])
parsed = json.loads(row["value"])
except (json.JSONDecodeError, TypeError, ValueError) as e:
_logger.warning(
"Failed to decode Composer from composerData:%s: %s",
cid,
e,
)
continue
if not isinstance(parsed, dict):
_logger.warning(
"Failed to parse Composer from composerData:%s: expected object, got %s",
cid,
type(parsed).__name__,
)
continue
try:
composer = Composer.from_dict(parsed, composer_id=cid)
except SchemaError as e:
_logger.warning(
"Failed to parse Composer from composerData:%s: %s",
cid,
e,
)
continue
cd = composer.raw
try:
pid = _determine_project_for_conversation(
cd, cid, project_layouts_map,
project_name_map, workspace_path_map,
Expand All @@ -98,10 +128,14 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
"lastUpdatedAt": to_epoch_ms(cd.get("lastUpdatedAt")) or to_epoch_ms(cd.get("createdAt")) or 0,
"createdAt": to_epoch_ms(cd.get("createdAt")) or 0,
})
except Exception:
pass
except Exception as e:
_logger.warning(
"Failed to process Composer from composerData:%s: %s",
cid,
e,
)
except Exception:
pass
_logger.exception("Failed to load composer rows from global storage")

# Group workspace entries by normalized folder path
folder_to_entries: dict[str, list] = {}
Expand All @@ -114,8 +148,8 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
first_folder = folders[0] if folders else None
if first_folder:
norm_folder = normalize_file_path(first_folder)
except Exception:
pass
except Exception as e:
warn_workspace_json_read(_logger, entry["name"], e)
if not norm_folder:
norm_folder = entry["name"] # fallback to workspace ID
entry_folder_map[entry["name"]] = norm_folder
Expand All @@ -139,7 +173,12 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
for e in group
if os.path.isfile(os.path.join(workspace_path, e["name"], "state.vscdb"))
)
except Exception:
except Exception as e:
_logger.warning(
"Failed to resolve mtime for workspace folder %s: %s",
norm_folder,
e,
)
mtime = 0

workspace_name = _get_workspace_display_name(workspace_path, primary["name"])
Expand Down Expand Up @@ -238,7 +277,7 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
"source": "cli",
})
except Exception as e:
print(f"Failed to load CLI projects: {e}")
_logger.warning("Failed to load CLI projects: %s", e)

projects.sort(key=lambda p: p["lastModified"], reverse=True)
return projects
Loading
Loading