Problem
After #88 (lazy-load + summary endpoints), two correctness issues and one performance gap remained on large Cursor installs:
- Count mismatch — The project list page showed a different
conversationCount than the number of tabs on the workspace details page for the same project.
- Noisy logs — Loading workspace summaries logged warnings for
composerData:empty-state-draft when the global KV row had a NULL payload (the JSON object must be str, bytes or bytearray, not NoneType).
- Slow first paint — On machines with many
workspaceStorage folders, project list and sidebar summary took ~50s because composer ownership was rebuilt sequentially from local state.vscdb files, and some paths still over-fetched global composerData.
Expected behavior
- Project card
conversationCount matches GET /api/workspaces/<id>/tabs?summary=1 tab count for the same project.
- Cursor UI placeholders (e.g.
empty-state-draft) are skipped without decode warnings.
- Project list and sidebar summary use workspace-scoped I/O with cached composer registry; first conversation auto-loads after sidebar render (~2s is acceptable).
Root cause (count mismatch)
Project list briefly counted conversations from local allComposers without applying the same global filters as the summary path (non-empty fullConversationHeadersOnly, composer validation, exclusion rules including model names).
A secondary regression dropped conversations during assembly: get_project_from_file_path was accidentally removed while refactoring workspace_resolver.py, causing NameError and parse/assembly warnings on large installs.
Resolution (branch fix/workspace-list-load-and-count)
Shared composer scan
- Extracted
services/workspace_composer_scan.py so list and summary paths share the same parse, exclusion, and workspace-assignment logic.
Count alignment
list_workspace_projects now applies the same filters as list_workspace_tab_summaries (headers, validation, exclusion rules, assignment).
Performance
- Parallelized
build_composer_id_to_workspace_id via ThreadPoolExecutor.
- Bulk
load_project_layouts_map instead of per-composer N+1 queries.
- Reused mtime-keyed disk cache (
summary_cache.py) for project list and tab summaries.
Correctness
- Null/placeholder
composerData rows skipped without warnings.
- Restored
get_project_from_file_path in workspace_resolver.py.
UI
- Workspace page auto-selects the first tab when no
?tab= query param is present.
- Deep links from search (
?tab=<composerId>) fetch the requested conversation directly instead of falling back to the first sidebar item.
Tests
tests/test_workspace_list_count_alignment.py — count parity
tests/test_project_path_boundary.py — path helper / assignment regression
- Updated parse-warning and API tests
Acceptance criteria
Out of scope (tracked separately)
Search performance and indexing work landed on the same branch but is not part of this issue:
- 30-day default search window +
all_history opt-in
- Local FTS search index (
search_index.sqlite) with background refresh
- Search bubble-scan optimization and deep-link workspace assignment fixes
Those should be referenced in the PR and/or a dedicated search issue.
Verify
- Restart
python app.py.
- Home page: project counts match sidebar totals per project.
- No “could not be fully assembled” banner from placeholder composers.
- Open a workspace without
?tab= — first conversation loads automatically.
pytest tests/test_workspace_list_count_alignment.py tests/test_api_workspaces.py
Problem
After #88 (lazy-load + summary endpoints), two correctness issues and one performance gap remained on large Cursor installs:
conversationCountthan the number of tabs on the workspace details page for the same project.composerData:empty-state-draftwhen the global KV row had aNULLpayload (the JSON object must be str, bytes or bytearray, not NoneType).workspaceStoragefolders, project list and sidebar summary took ~50s because composer ownership was rebuilt sequentially from localstate.vscdbfiles, and some paths still over-fetched globalcomposerData.Expected behavior
conversationCountmatchesGET /api/workspaces/<id>/tabs?summary=1tab count for the same project.empty-state-draft) are skipped without decode warnings.Root cause (count mismatch)
Project list briefly counted conversations from local
allComposerswithout applying the same global filters as the summary path (non-emptyfullConversationHeadersOnly, composer validation, exclusion rules including model names).A secondary regression dropped conversations during assembly:
get_project_from_file_pathwas accidentally removed while refactoringworkspace_resolver.py, causingNameErrorand parse/assembly warnings on large installs.Resolution (branch
fix/workspace-list-load-and-count)Shared composer scan
services/workspace_composer_scan.pyso list and summary paths share the same parse, exclusion, and workspace-assignment logic.Count alignment
list_workspace_projectsnow applies the same filters aslist_workspace_tab_summaries(headers, validation, exclusion rules, assignment).Performance
build_composer_id_to_workspace_idviaThreadPoolExecutor.load_project_layouts_mapinstead of per-composer N+1 queries.summary_cache.py) for project list and tab summaries.Correctness
composerDatarows skipped without warnings.get_project_from_file_pathinworkspace_resolver.py.UI
?tab=query param is present.?tab=<composerId>) fetch the requested conversation directly instead of falling back to the first sidebar item.Tests
tests/test_workspace_list_count_alignment.py— count paritytests/test_project_path_boundary.py— path helper / assignment regressionAcceptance criteria
conversationCounton project cards equals summary tab count for the same workspacecomposerDataplaceholders?tab=query paramOut of scope (tracked separately)
Search performance and indexing work landed on the same branch but is not part of this issue:
all_historyopt-insearch_index.sqlite) with background refreshThose should be referenced in the PR and/or a dedicated search issue.
Verify
python app.py.?tab=— first conversation loads automatically.pytest tests/test_workspace_list_count_alignment.py tests/test_api_workspaces.py