Skip to content

Speed up workspace listing and search; add FTS index and fix search d…#113

Open
clean6378-max-it wants to merge 5 commits into
masterfrom
fix/workspace-list-load-and-count
Open

Speed up workspace listing and search; add FTS index and fix search d…#113
clean6378-max-it wants to merge 5 commits into
masterfrom
fix/workspace-list-load-and-count

Conversation

@clean6378-max-it

@clean6378-max-it clean6378-max-it commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

CLoses #95

Summary

Improves workspace list/navigation performance and makes search fast enough for daily use, while fixing incorrect conversation links from search results.

Workspace listing & navigation (#95)

  • Extract shared composer parsing/assignment into workspace_composer_scan.py
  • Parallelize build_composer_id_to_workspace_id and bulk-load project layouts
  • Align conversationCount with the same filters used by tab summaries
  • Skip null/placeholder composerData rows without spurious warnings
  • Restore get_project_from_file_path (fixes NameError that dropped ~66 conversations)

Search performance

  • 30-day default window with UI checkbox and all_history=1 API flag
  • Phase 1: single-pass bubble LIKE scan (fixes scoped-query regression that made 30-day search slower than full history)
  • Phase 2: local search_index.sqlite with FTS5, background rebuild on startup + 60s poll, fingerprint invalidation (same mtime scheme as listing cache)
  • Windows: versioned index files + search_index.active pointer (avoids PermissionError when replacing an open DB)

Search correctness

  • Search workspaceId now uses assign_composer_workspace (same as sidebar), not roster map alone
  • Workspace page honors ?tab= deep links; fetches conversation directly instead of opening the first sidebar item; redirects to global on 404

Test plan

  • Restart python app.py; confirm log: Search index rebuilt: N composers, M bubbles -> search_index.*.sqlite
  • Home page: projects load quickly; counts match sidebar conversation totals
  • Search default (30 days): completes in a few seconds; checkbox searches all history
  • Click a search result: opens the matching thread (not the first conversation in the project)
  • pytest tests/test_workspace_list_count_alignment.py tests/test_api_search.py tests/test_search_index.py tests/test_search_helpers.py tests/test_project_path_boundary.py
  • After Cursor adds a new chat, wait ~60s or restart; search finds new content

Notes

  • Index/cache lives under ~/.cache/cursor-chat-browser/
  • Bypass: CURSOR_CHAT_BROWSER_NOCACHE=1 or CURSOR_CHAT_BROWSER_NO_SEARCH_INDEX=1
  • Dev-only: scripts/profile_search*.py

Summary by CodeRabbit

Release Notes

  • New Features

    • Added an “Include chats older than 30 days” search mode that expands results to all history.
    • Search API now supports a configurable history window and returns allHistory / searchWindowDays for accurate result labeling.
  • Performance/Background

    • Search indexing can now start automatically during app initialization to keep search results current.
  • Bug Fixes

    • Improved workspace tab navigation: missing tabs now redirect to the global workspace.
  • Tests

    • Added/updated coverage for search windows, all-history API behavior, and end-to-end search index querying.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a local SQLite FTS5 search index with background daemon-thread refresh and atomic pointer-based publication. Extends all three search sources (search_global_storage, search_legacy_workspaces, search_cli_sessions) with a since_ms time-window parameter exposed through a new all_history API query parameter and UI checkbox. Extracts shared composer-row parsing, workspace assignment, and exclusion filtering into a new services/workspace_composer_scan.py module to fix workspace list/tab count mismatch (#95). Parallelizes workspace registry and mtime scanning using ThreadPoolExecutor. Refactors workspace name inference to use a single preloaded project_layouts_map instead of per-composer global KV lookups. Adds comprehensive test coverage including FTS index lifecycle, time-window filtering, and count-alignment regressions.

Changes

Search Index, Time-Windowed Search, and Workspace List/Tab Count Alignment

Layer / File(s) Summary
Shared composer-scan helpers and workspace resolver refactor
services/workspace_composer_scan.py, services/workspace_resolver.py
New module workspace_composer_scan exports parse_composer_data_row (validates full_conversation_headers_only, skips null placeholders silently), assign_composer_workspace (via determine_project_for_conversation with alias fallback), is_composer_excluded, composer_chat_title, and composer_model_names. workspace_resolver replaces per-composer global KV messageRequestContext scanning with load_project_layouts_map (single load), infer_workspace_name_from_layouts (aggregate basenames), build_composer_ids_by_workspace (invert mapping), matching_workspace_ids_for_folder (match workspace folders), and _composer_ids_from_workspace_db (extract from local state.vscdb).
Parallel workspace registry scanning
services/workspace_db.py, services/summary_cache.py
build_composer_id_to_workspace_id uses ThreadPoolExecutor with per-entry helper _composer_ids_for_workspace_entry to scan workspace folders in parallel; fingerprint_workspace_storage applies the same pattern for mtime collection. Both preserve existing "first mapping wins" behavior.
Workspace listing and tabs refactored to shared helpers
services/workspace_listing.py, services/workspace_tabs.py
Composer-row loops delegate to parse_composer_data_row, assign_composer_workspace, is_composer_excluded, composer_chat_title, and composer_model_names (from workspace_composer_scan), removing inline JSON decode/validate logic. Switches workspace name inference to infer_workspace_name_from_layouts (with preloaded project_layouts_map). Adds modelNames to conversation objects. Ensures list and tab summary paths apply identical filters, fixing count mismatch.
Workspace page tab selection and deep-link fallback
templates/workspace.html
selectTab now always awaits (for both ?tab= param and default-to-first paths); constructs minimal fallback summary object when tab absent from allTabs; on HTTP 404 from full-tab fetch, redirects to global workspace. Enables workspace deep-links from search results.
Workspace count-alignment and placeholder regressions
tests/test_workspace_list_count_alignment.py
Adds dedicated regression test module that seeds global/workspace SQLite fixtures and validates project-card/tab count parity, null placeholder composer handling, parallel registry coverage, summary/full tab assignment consistency, single-tab 404 behavior for placeholders, and exclusion-rule count consistency.
SQLite FTS5 search index module
services/search_index.py
New module with active-pointer atomic publication (_publish_active_index via temp file with replace/write fallback), workspace fingerprint-based staleness detection, build_search_index() (schema creation + composer/bubble load), ensure_search_index() (sync rebuild when missing/stale/unreadable), start_search_index_background() (daemon thread, 60s poll), and read-only query APIs for bubble hits, title hits, and composer rows with since_ms filtering. Index location is ~/.cache/cursor-chat-browser/search_index.sqlite with pointer file search_index.active.
Time-windowed search core
services/search.py
Exports DEFAULT_SEARCH_WINDOW_DAYS (30), adds resolve_search_since_ms() (derives epoch-ms cutoff from all_history/since_days params), and _timestamp_in_search_window() (unknown/unparseable timestamps remain searchable in-window). search_global_storage() dispatches to _search_global_storage_via_index() (FTS path when usable) or _search_global_storage_live_scan() (SQLite fallback), both filtering by since_ms. search_legacy_workspaces() defensively decodes ItemTable payloads (bytes/bytearray support) and filters tabs by since_ms. search_cli_sessions() gains effective_ms (max of meta.createdAt and DB mtime), early session filtering, and a title-match fast path. Adds _SearchWorkspaceAssigner (frozen dataclass with workspace context) and lightweight bubble-text indexing helpers.
Search API route, query params, and UI all-history toggle
api/search.py, templates/search.html
api/search.py parses all_history query param ('1' or 'true'), derives optional since_days, calls resolve_search_since_ms, passes since_ms to all three search functions, and returns allHistory (bool) and searchWindowDays (int | null) in JSON response. search.html adds "Include chats older than 30 days" checkbox, manages all_history URL param via URLSearchParams, and displays window note "(all history)" or "(last X days)" in result count.
App startup wiring and test configuration
app.py, tests/conftest.py
create_app() wires start_search_index_background at startup with exception logging (not propagated). conftest.py autouse fixture conditionally sets CURSOR_CHAT_BROWSER_NO_SEARCH_INDEX=1 to disable indexing outside index-specific test modules.
Search index lifecycle and query tests
tests/test_search_index.py, tests/test_search_index_windows.py, tests/test_search_helpers.py
test_search_index.py end-to-end FTS build/query validation with isolated storage and index lifecycle checks; test_search_index_windows.py Windows locked-file regression for atomic publish without open-file replacement. Extended test_search_helpers.py with resolve_search_since_ms, _timestamp_in_search_window, search_global_storage window filtering, and bubble-only-match scenarios.
Search API and integration test updates
tests/test_api_search.py, tests/test_models_wired_at_read_sites.py, tests/test_parse_failure_logging.py, tests/test_project_path_boundary.py, tests/test_search_exclusion_filtering.py
Updated search tests to use all_history=1 param and validate response allHistory/searchWindowDays fields; logger renames in parse-warning tests to services.workspace_composer_scan; added regression for determine_project_for_conversation with newlyCreatedFiles; changed schema-drift trigger from /api/search to /api/workspaces/{id}/tabs.
Standalone search profiling scripts
scripts/profile_search.py, scripts/profile_search_breakdown.py, scripts/profile_search_window.py
Three new CLI profilers for performance analysis: profile_search.py times global-storage queries and hit counts; profile_search_breakdown.py stages 30-day vs all-history bubble index filtering; profile_search_window.py pre-scans stats and times 30-day vs all-history across global/legacy/CLI sources.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant SearchRoute as /api/search
    participant resolve as resolve_search_since_ms
    participant GlobalSearch as search_global_storage
    participant IndexCheck as index_is_usable
    participant FTSIndex as _search_global_storage_via_index
    participant LiveScan as _search_global_storage_live_scan

    Browser->>SearchRoute: GET /api/search?q=export&all_history=1
    SearchRoute->>resolve: resolve_search_since_ms(all_history=True)
    resolve-->>SearchRoute: since_ms = None
    SearchRoute->>GlobalSearch: search_global_storage(q, since_ms=None)
    GlobalSearch->>IndexCheck: index_is_usable(workspace_path, rules)
    alt FTS index enabled and ready
        IndexCheck-->>GlobalSearch: True
        GlobalSearch->>FTSIndex: query_composer_bubble_hits + query_composer_title_hits
        FTSIndex-->>GlobalSearch: matched composers (since_ms=None → unwindowed)
    else FTS index disabled or stale
        IndexCheck-->>GlobalSearch: False
        GlobalSearch->>LiveScan: full SQLite scan with since_ms=None filter
        LiveScan-->>GlobalSearch: all composers (no timestamp filter)
    end
    GlobalSearch-->>SearchRoute: list[SearchResult] with timestamps
    SearchRoute-->>Browser: {results: [...], allHistory: true, searchWindowDays: null}
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • cppalliance/cppa-cursor-browser#102: Refactors the /api/search route and services/search.py search entry points that this PR extends with since_ms/all_history time-window filtering.
  • cppalliance/cppa-cursor-browser#88: Introduces the disk-backed summary cache and fingerprinting mechanism in services/summary_cache.py that this PR parallelizes and uses for FTS index staleness detection.
  • cppalliance/cppa-cursor-browser#31: Establishes the workspace/search service-layer structure that this PR builds upon with shared composer-scan helpers and unified filtering logic.

Suggested reviewers

  • wpak-ai

🐇 A rabbit built an index one night,
FTS5 bubbles gleaming bright.
With threads that scan and pointers that point,
Count mismatch fixed at the joint.
all_history=1 shines in the light!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: workspace listing speedup, search functionality improvements, and FTS index implementation.
Linked Issues check ✅ Passed All five acceptance criteria from issue #95 are fully satisfied: conversation count parity via shared parsing helpers, null-placeholder handling prevents errors, parallelized registry build, comprehensive regression tests, and workspace UI auto-selection.
Out of Scope Changes check ✅ Passed All changes align with issue #95 and the PR objectives. The search performance work (FTS index, 30-day window) is explicitly noted as part of the same branch and properly documented.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/workspace-list-load-and-count

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_search_index.py (1)

135-147: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Test intent and assertion are currently mismatched.

Line 135 says this verifies index usage, but Lines 138-147 only prove a hit exists; the same assertion still passes if search_global_storage falls back to live scan. Add an explicit assertion that live-scan fallback is not invoked.

🧪 Proposed hardening
 class TestSearchGlobalStorageUsesIndex:
     def test_search_global_storage_uses_index(self, indexed_storage):
         patches = _index_patches(indexed_storage["cache_dir"])
         with patches[0], patches[1], patches[2]:
-            results = search_global_storage(
-                indexed_storage["ws_root"],
-                indexed_storage["term"],
-                indexed_storage["term"].lower(),
-                [],
-                ParseWarningCollector(),
-                since_ms=None,
-            )
-            assert any(r["chatId"] == indexed_storage["composer_id"] for r in results)
+            with patch("services.search._search_global_storage_live_scan") as live_scan:
+                results = search_global_storage(
+                    indexed_storage["ws_root"],
+                    indexed_storage["term"],
+                    indexed_storage["term"].lower(),
+                    [],
+                    ParseWarningCollector(),
+                    since_ms=None,
+                )
+                live_scan.assert_not_called()
+                assert any(r["chatId"] == indexed_storage["composer_id"] for r in results)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_search_index.py` around lines 135 - 147, The test method
test_search_global_storage_uses_index sets up patches to enable index usage but
the current assertion only verifies that results were found, which could still
pass if the code falls back to live scan instead of using the index. Add an
explicit assertion after the results are returned that verifies the live-scan
fallback mechanism was not invoked, such as checking that a specific mock or
patch related to the fallback was not called, to ensure the test actually proves
that search_global_storage used the index as intended.
🧹 Nitpick comments (3)
services/workspace_resolver.py (1)

112-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Narrow the workspace.json exception handling.

Ruff flags both broad except Exception handlers here. Catch the expected read/parse/path failures explicitly so unexpected defects still surface.

♻️ Proposed fix
-    except Exception as e:
+    except (OSError, ValueError, TypeError, KeyError) as e:
         warn_workspace_json_read(_logger, workspace_id, e)
@@
-        except Exception as e:
+        except (OSError, ValueError, TypeError, KeyError) as e:
             warn_workspace_json_read(_logger, entry["name"], e)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/workspace_resolver.py` around lines 112 - 124, Replace the two broad
except Exception handlers with specific exception handling. In the first except
block around the warn_workspace_json_read call for the initial workspace.json
read, catch the specific exceptions that read_json_file and
get_workspace_folder_paths would raise (such as FileNotFoundError,
JSONDecodeError, KeyError, and ValueError). In the second except block within
the loop, similarly catch only the expected exceptions from read_json_file,
get_workspace_folder_paths, normalize_file_path operations rather than catching
all Exception types, so that unexpected errors will still propagate and surface
during debugging instead of being silently caught.

Source: Linters/SAST tools

tests/conftest.py (1)

30-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Broaden the index-test exemption matcher.

The exemption only matches module names ending with test_search_index, so index-focused modules like test_search_index_windows still get CURSOR_CHAT_BROWSER_NO_SEARCH_INDEX=1. That can silently alter what those tests actually validate.

Suggested tweak
-    if request.module.__name__.endswith("test_search_index"):
+    if request.module.__name__.endswith(
+        ("test_search_index", "test_search_index_windows")
+    ):
         return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/conftest.py` around lines 30 - 31, The endswith check in the
conftest.py file only matches module names ending with exactly
"test_search_index", so index-focused modules like "test_search_index_windows"
bypass the exemption and still get the CURSOR_CHAT_BROWSER_NO_SEARCH_INDEX
environment variable applied. Broaden the condition that checks
request.module.__name__ to use a pattern match (such as the `in` operator or a
substring search) that will catch any module name containing "test_search_index"
as part of its name, not just those ending with it exactly, so all
search-index-related test modules are properly exempted.
tests/test_models_wired_at_read_sites.py (1)

99-115: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Re-pin the “search does not rely on Bubble.from_dict” contract.

This test now verifies only output shape/content. It no longer guards against regressions that reintroduce Bubble.from_dict into /api/search. Add a spy on the symbol used by the search path and assert it is not called, while keeping the current result assertion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_models_wired_at_read_sites.py` around lines 99 - 115, The
test_search_endpoint_finds_bubble_text method only validates the search results
content but does not actively guard against the regression of using
Bubble.from_dict in the search path. Add a spy or mock on the Bubble.from_dict
method before calling the /api/search endpoint via client.get(), then assert
that this spy/mock was not called during the request. Retain all existing
assertions about the response status code, results length, and matching text to
maintain the current validation logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/search.py`:
- Around line 28-34: The `_parse_since_days_param` function accepts unbounded
integer values that can cause overflow errors when passed to timedelta
operations, resulting in 500 responses. Add bounds checking to validate that the
parsed integer falls within an acceptable range (e.g., return None or a capped
value if the number exceeds a reasonable maximum like 36500 days or similar).
Apply the same bounds validation to any similar parameter parsing functions
referenced at lines 44-47.

In `@scripts/profile_search_breakdown.py`:
- Around line 74-77: The mode label at line 74 incorrectly marks the scan as
"scoped" when the condition checks window_ids, but the actual implementation of
_index_bubble_texts_matching_query() still performs a full bubble LIKE SQL pass
and only filters composer IDs in Python afterward. Change the mode label from
"scoped" to something more accurate that reflects the actual behavior of a full
database scan with Python-level filtering (not a truly scoped database query),
so the profiler output correctly represents the query strategy being used.

In `@scripts/profile_search_window.py`:
- Line 61: The variable name `scoped_bubble_scan` in the print statement is
misleading because it suggests an actual executed strategy when it only
represents a threshold heuristic check. Rename this variable to a more accurate
name that clearly indicates it is a threshold-based heuristic or condition check
(such as `in_small_window` or `below_bubble_threshold`) to prevent
misinterpretation in profiling output.

In `@services/search_index.py`:
- Around line 245-247: When src_conn is None in the open_global_db context
manager (indicating the source database cannot be opened), the function returns
False immediately without cleaning up the temporary search_index file that was
already created at new_path. Before returning False, route this through the
existing cleanup path to remove the temporary index file, ensuring orphan files
are not left behind on the filesystem during failed background polls.
- Around line 376-407: The query_composer_bubble_hits function accepts a
since_ms parameter at line 379 but does not use it in the SQL query at lines
394-397, causing all matching bubbles to be fetched before Python-side
filtering. Push the time window filter into the FTS query by conditionally
joining the composers table and adding a WHERE clause with (updated_ms >= ? OR
updated_ms <= 0) when since_ms is not None, following the same pattern used in
the query_composer_title_hits function, to reduce unnecessary I/O.

In `@services/search.py`:
- Around line 151-164: The _quick_bubble_text function only checks for top-level
keys "text", "rawText", and "content" when extracting searchable text from
bubble KV values, but misses the "richText" key which is a documented text
source in the Bubble model. Add "richText" to the tuple of keys being checked in
the for loop to ensure rich-text-only messages are properly indexed and
discoverable in searches.
- Around line 222-230: The function `_light_composer_exclusion_text` and related
fast path implementations currently apply exclusion rules only against limited
content (title/metadata for title matches, query-matching bubbles for content
matches), allowing excluded terms in non-matching bubbles to slip through
results. Modify the exclusion checking logic to load all visible bubble and tool
text for each candidate after pruning, then apply exclusion rules against the
full conversation content rather than just title or query-specific snippets.
Reserve the matchingText field exclusively for query-specific snippets while
ensuring exclusion validation happens against the complete conversation data.
- Around line 317-328: The workspace_for_composer method is passing an empty
dictionary for the bubble_map parameter instead of the resolver's actual bubble
map, which can cause workspace assignment to diverge from path-based resolution.
Replace the bubble_map={} argument with the resolver's actual bubble map from
the context (likely self.ctx.bubble_map or similar). Apply this same fix to any
other call sites mentioned in the review that also pass an empty bubble_map
dictionary.
- Around line 965-985: The search-window filtering at the point where created_ms
is calculated uses only createdAt, which can skip recently updated sessions.
Modify the created_ms assignment to use the maximum of createdAt and the
session's database modification timestamp (session_ts or mtime). Ensure this
updated created_ms value is used both in the _timestamp_in_search_window check
and when setting the timestamp in the results.append dictionary. Additionally,
when converting bubbles elsewhere in the code, use created_ms or session_ts as a
fallback to account for updated sessions that may have been created before the
search cutoff.

In `@services/workspace_db.py`:
- Around line 416-423: The current code uses as_completed() to iterate through
futures, which returns them in completion order based on thread timing rather
than submission order. This causes non-deterministic behavior where duplicate
composerIds in the mapping dictionary can be assigned to different workspace
names across runs. To fix this, modify the loop that processes futures to
iterate in the original workspace_entries order instead of as_completed() order.
You can do this by creating a list of futures that maintains correspondence with
workspace_entries (using zip or enumerate), then iterate through this list in
order while extracting results from each completed future, ensuring that earlier
workspace_entries always take precedence when the same composerId appears in
multiple entries.

In `@services/workspace_resolver.py`:
- Around line 144-149: The code calls data.get("allComposers") without first
verifying that data is a dictionary. If json.loads() successfully parses the
JSON but returns a non-dict value (like a list or string), calling .get() will
raise an AttributeError. Add a guard check to ensure data is a dictionary before
calling .get("allComposers"), similar to the existing isinstance(c, dict) check
used for the composer object c. The guard should allow the code to safely handle
malformed payloads without breaking workspace-name inference.

In `@templates/search.html`:
- Around line 42-43: The URL parameter parsing for all_history only recognizes
the value '1', but the backend API accepts both '1' and 'true'. Update the
condition in the line where allHistory is assigned to check if the all_history
parameter equals either '1' or 'true' (instead of only checking for '1'), so
that both shared link formats correctly restore the toggle state for the element
with id all-history.

---

Outside diff comments:
In `@tests/test_search_index.py`:
- Around line 135-147: The test method test_search_global_storage_uses_index
sets up patches to enable index usage but the current assertion only verifies
that results were found, which could still pass if the code falls back to live
scan instead of using the index. Add an explicit assertion after the results are
returned that verifies the live-scan fallback mechanism was not invoked, such as
checking that a specific mock or patch related to the fallback was not called,
to ensure the test actually proves that search_global_storage used the index as
intended.

---

Nitpick comments:
In `@services/workspace_resolver.py`:
- Around line 112-124: Replace the two broad except Exception handlers with
specific exception handling. In the first except block around the
warn_workspace_json_read call for the initial workspace.json read, catch the
specific exceptions that read_json_file and get_workspace_folder_paths would
raise (such as FileNotFoundError, JSONDecodeError, KeyError, and ValueError). In
the second except block within the loop, similarly catch only the expected
exceptions from read_json_file, get_workspace_folder_paths, normalize_file_path
operations rather than catching all Exception types, so that unexpected errors
will still propagate and surface during debugging instead of being silently
caught.

In `@tests/conftest.py`:
- Around line 30-31: The endswith check in the conftest.py file only matches
module names ending with exactly "test_search_index", so index-focused modules
like "test_search_index_windows" bypass the exemption and still get the
CURSOR_CHAT_BROWSER_NO_SEARCH_INDEX environment variable applied. Broaden the
condition that checks request.module.__name__ to use a pattern match (such as
the `in` operator or a substring search) that will catch any module name
containing "test_search_index" as part of its name, not just those ending with
it exactly, so all search-index-related test modules are properly exempted.

In `@tests/test_models_wired_at_read_sites.py`:
- Around line 99-115: The test_search_endpoint_finds_bubble_text method only
validates the search results content but does not actively guard against the
regression of using Bubble.from_dict in the search path. Add a spy or mock on
the Bubble.from_dict method before calling the /api/search endpoint via
client.get(), then assert that this spy/mock was not called during the request.
Retain all existing assertions about the response status code, results length,
and matching text to maintain the current validation logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 154040f4-ed3c-4763-91f5-f16f5bf18dda

📥 Commits

Reviewing files that changed from the base of the PR and between ee6a6e7 and 31ac7e2.

📒 Files selected for processing (25)
  • api/search.py
  • app.py
  • scripts/profile_search.py
  • scripts/profile_search_breakdown.py
  • scripts/profile_search_window.py
  • services/search.py
  • services/search_index.py
  • services/summary_cache.py
  • services/workspace_composer_scan.py
  • services/workspace_db.py
  • services/workspace_listing.py
  • services/workspace_resolver.py
  • services/workspace_tabs.py
  • templates/search.html
  • templates/workspace.html
  • tests/conftest.py
  • tests/test_api_search.py
  • tests/test_models_wired_at_read_sites.py
  • tests/test_parse_failure_logging.py
  • tests/test_project_path_boundary.py
  • tests/test_search_exclusion_filtering.py
  • tests/test_search_helpers.py
  • tests/test_search_index.py
  • tests/test_search_index_windows.py
  • tests/test_workspace_list_count_alignment.py

Comment thread api/search.py
Comment thread scripts/profile_search_breakdown.py Outdated
Comment thread scripts/profile_search_window.py Outdated
Comment thread services/search_index.py Outdated
Comment thread services/search_index.py
Comment thread services/search.py
Comment thread services/search.py Outdated
Comment thread services/workspace_db.py Outdated
Comment thread services/workspace_resolver.py
Comment thread templates/search.html Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
services/search.py (1)

516-530: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Hoist the full-index query out of the candidate loop.

query_composer_rows_in_window(None) runs a full composers table scan (and opens a fresh read-only connection) every time it's called. Line 528 invokes it once per missing candidate inside the for composer_id in candidate_ids loop, so a batch of cache misses degrades to repeated full scans. Resolve it once and reuse.

♻️ Proposed fix
         search_pool = composers_in_window if since_ms is not None else query_composer_rows_in_window(None)
+        all_rows: dict[str, sqlite3.Row] | None = None
         for composer_id, row in search_pool.items():
             raw_lower = (row["raw_json"] or "").lower()
             if query_lower in raw_lower:
                 candidate_ids.add(composer_id)
 
         for composer_id in candidate_ids:
             composer_row: sqlite3.Row | None = (
                 composers_in_window.get(composer_id) if since_ms is not None
                 else search_pool.get(composer_id)
             )
             if composer_row is None:
-                composer_row = query_composer_rows_in_window(None).get(composer_id)
+                if all_rows is None:
+                    all_rows = query_composer_rows_in_window(None)
+                composer_row = all_rows.get(composer_id)
             if composer_row is None:
                 continue

Note: when since_ms is not None, this fallback also pulls in composers outside the window; confirm that's intended versus skipping them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/search.py` around lines 516 - 530, The
query_composer_rows_in_window(None) call inside the for loop over candidate_ids
is inefficient because it executes a full table scan for every missing
candidate. Move the query_composer_rows_in_window(None) call outside and before
the for loop over candidate_ids, store the result in a variable (e.g.,
full_window_data), and then reuse that variable inside the loop where the
fallback lookup occurs on line 528. This eliminates repeated database scans and
connection overhead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@services/search.py`:
- Around line 516-530: The query_composer_rows_in_window(None) call inside the
for loop over candidate_ids is inefficient because it executes a full table scan
for every missing candidate. Move the query_composer_rows_in_window(None) call
outside and before the for loop over candidate_ids, store the result in a
variable (e.g., full_window_data), and then reuse that variable inside the loop
where the fallback lookup occurs on line 528. This eliminates repeated database
scans and connection overhead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e1128d68-b30b-4071-b9f0-6daa1af54714

📥 Commits

Reviewing files that changed from the base of the PR and between 31ac7e2 and bc5e6ea.

📒 Files selected for processing (11)
  • api/search.py
  • scripts/profile_search_breakdown.py
  • scripts/profile_search_window.py
  • services/search.py
  • services/search_index.py
  • services/workspace_db.py
  • services/workspace_resolver.py
  • templates/search.html
  • tests/conftest.py
  • tests/test_models_wired_at_read_sites.py
  • tests/test_search_index.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • tests/conftest.py
  • tests/test_search_index.py
  • scripts/profile_search_window.py
  • tests/test_models_wired_at_read_sites.py
  • scripts/profile_search_breakdown.py
  • templates/search.html
  • services/search_index.py
  • services/workspace_resolver.py

@clean6378-max-it

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit, please review this PR in more depth again based on issue description(latest updated version)

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the latest issue `#95` description
gh issue view 95 --repo cppalliance/cppa-cursor-browser --json title,body,comments | jq '{title, body, lastComment: .comments[-1]}'

Length of output: 4635


🏁 Script executed:

#!/bin/bash
# Get PR `#113` details and files changed
gh pr view 113 --repo cppalliance/cppa-cursor-browser --json title,body,files | jq '{title, body: (.body | .[0:2000]), files: [.files[].path]}'

Length of output: 3403


🏁 Script executed:

#!/bin/bash
# Read key new/changed files
echo "=== services/workspace_composer_scan.py ==="
cat services/workspace_composer_scan.py

echo ""
echo "=== services/search_index.py (first 200 lines) ==="
head -200 services/search_index.py

echo ""
echo "=== services/search.py (first 150 lines) ==="
head -150 services/search.py

Length of output: 15529


🏁 Script executed:

#!/bin/bash
echo "=== services/workspace_db.py ==="
cat services/workspace_db.py

echo ""
echo "=== services/workspace_listing.py ==="
cat services/workspace_listing.py

echo ""
echo "=== services/workspace_resolver.py ==="
cat services/workspace_resolver.py

Length of output: 49210


🏁 Script executed:

#!/bin/bash
echo "=== services/workspace_tabs.py ==="
cat services/workspace_tabs.py

echo ""
echo "=== app.py ==="
cat app.py

echo ""
echo "=== api/search.py ==="
cat api/search.py

Length of output: 42236


🏁 Script executed:

#!/bin/bash
echo "=== services/search_index.py (full) ==="
cat services/search_index.py

echo ""
echo "=== services/search.py (key sections) ==="
cat services/search.py

Length of output: 60367


🏁 Script executed:

#!/bin/bash
echo "=== tests/test_workspace_list_count_alignment.py ==="
cat tests/test_workspace_list_count_alignment.py

echo ""
echo "=== tests/test_search_index.py ==="
cat tests/test_search_index.py

echo ""
echo "=== tests/conftest.py ==="
cat tests/conftest.py

Length of output: 17524


🏁 Script executed:

#!/bin/bash
echo "=== services/summary_cache.py (key fingerprint section) ==="
cat services/summary_cache.py

echo ""
echo "=== templates/workspace.html ==="
cat templates/workspace.html

echo ""
echo "=== templates/search.html (tail) ==="
tail -100 templates/search.html

Length of output: 29654



@clean6378-max-it Here is a thorough review of PR #113 against the latest issue #95 description and acceptance criteria.


Acceptance Criteria Status

Criterion Status
conversationCount on project cards equals summary tab count for same workspace ✅ Both paths now use parse_composer_data_row + assign_composer_workspace
No warnings for null/empty composerData placeholders parse_composer_data_row returns None silently for None values
Parallel local registry build + scoped global fetches ThreadPoolExecutor in build_composer_id_to_workspace_id and fingerprint_workspace_storage
Regression tests for count alignment, workspace scope, null payload handling test_workspace_list_count_alignment.py, test_project_path_boundary.py
Workspace UI auto-selects first tab when no ?tab= query param workspace.html DOMContentLoaded always calls await selectTab(...)

Issues Found

1. search_legacy_workspaces — Timestamp Window Filter Placed After Expensive Match Work

In services/search.py, the _timestamp_in_search_window check comes after is_excluded_by_rules and _find_match, which means per-tab bubble-text match work is done for tabs that will ultimately be discarded for being outside the date window:

# Current order (expensive work before cheap filter)
has_match, matching_text = _find_match(ct, tab_bubble_texts, query_lower, query)
if not has_match:
    continue
tab_ts = to_epoch_ms(tab.get("lastSendTime")) or _UNKNOWN_SEARCH_TIMESTAMP
if not _timestamp_in_search_window(tab_ts, since_ms):   # ← should be earlier
    continue

Suggested order: compute tab_ts and window-filter immediately after the prefilter if query_lower not in str(raw_chat).lower().


2. assemble_workspace_tabs (Full Tabs Path) Not Migrated to Shared Helpers

list_workspace_tab_summaries and list_workspace_projects were both migrated to use parse_composer_data_row, but assemble_workspace_tabs (used by GET /api/workspaces/<id>/tabs without ?summary=1) still has its own inline JSON decode loop:

# assemble_workspace_tabs — still inline (not using parse_composer_data_row)
try:
    parsed = json.loads(row["value"])
except (json.JSONDecodeError, TypeError, ValueError) as e:
    ...
    parse_warnings.record_composer_skipped()
    continue

Consequence: a NULL value in composerData:empty-state-draft on this path would still raise TypeError: the JSON object must be str, bytes or bytearray, not NoneType rather than being silently skipped. Also, the SQL used differs slightly — it's missing the AND value NOT LIKE '%fullConversationHeadersOnly": []%' variant (with space after colon) that COMPOSER_ROWS_WITH_HEADERS_SQL includes.


3. Redundant Index DB Opens in _search_global_storage_via_index

When since_ms is None:

composers_in_window = query_composer_rows_in_window(since_ms)  # opens DB, returns all rows
# ...
search_pool = composers_in_window if since_ms is not None else query_composer_rows_in_window(None)
#                                                              ↑ opens DB again unnecessarily

Since since_ms is None makes composers_in_window identical to query_composer_rows_in_window(None), search_pool could simply be composers_in_window in all cases:

search_pool = composers_in_window  # already loaded above

Additionally, the per-candidate fallback also opens the DB again:

if composer_row is None:
    composer_row = query_composer_rows_in_window(None).get(composer_id)  # 3rd open

This could be addressed by building search_pool once and reusing it.


4. Minor: parse_composer_data_row — Type Check Order Records Spurious Warning

if raw_value is None:
    return None
if not isinstance(raw_value, (str, bytes, bytearray)):
    parse_warnings.record_composer_skipped()   # ← records even if key isn't composerData:*
    return None
if not row_key.startswith("composerData:"):    # ← checked too late
    return None

If a non-string value is passed for a non-composerData:* key, a spurious composer_skipped warning is recorded. In practice all callers pass composerData:* rows, so this has no real impact, but the startswith check should move before the type check for correctness:

if not row_key.startswith("composerData:"):
    return None
if raw_value is None:
    return None
if not isinstance(raw_value, (str, bytes, bytearray)):
    parse_warnings.record_composer_skipped()
    return None

5. Minor: SEARCH_INDEX_POINTER_FILE Not in __all__

services/search_index.py defines and uses SEARCH_INDEX_POINTER_FILE prominently (it's the active-index mechanism), but it's absent from __all__. The test patches it correctly via direct attribute access, but adding it to __all__ would make the public surface clearer:

__all__ = [
    "SEARCH_INDEX_FILE",
    "SEARCH_INDEX_POINTER_FILE",  # add this
    "ensure_search_index",
    ...
]

6. assemble_single_tab — Full Composer Scan When Invalid Workspace IDs Exist

if invalid_workspace_ids:
    composer_rows_for_aliases = safe_fetchall(global_db, COMPOSER_ROWS_WITH_HEADERS_SQL)
    invalid_workspace_aliases = infer_invalid_workspace_aliases(...)

This triggers a full scan of all composerData:* rows on every single-tab fetch when there are any invalid workspace IDs. On large installs this can be slow. Since infer_invalid_workspace_aliases is deterministic given the same workspace state, it could be memoized or served from the workspace context cache.


Summary

The PR fully satisfies all issue #95 acceptance criteria. The most actionable items are (2) migrating assemble_workspace_tabs to use parse_composer_data_row (to make the null-placeholder fix complete) and (1) moving the timestamp window filter earlier in search_legacy_workspaces. Items (3)–(6) are performance or cosmetic improvements that could be addressed in a follow-up.

Comment thread services/workspace_tabs.py
Comment thread services/workspace_tabs.py Outdated
Comment thread services/search.py
Comment thread services/workspace_tabs.py Outdated
Comment thread services/search.py Outdated
Comment thread services/workspace_tabs.py Outdated
Comment thread tests/test_workspace_list_count_alignment.py
Comment thread tests/test_workspace_list_count_alignment.py
Comment thread services/search.py
Comment thread api/search.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
services/search_index.py (1)

270-315: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

indexed_composer_ids is populated before the composer is actually inserted, so orphan bubbles can still be indexed.

Line 272 adds composer_id to the set before the json.loads/isinstance guards at 275–279. Composers whose payload fails to parse or isn't a dict hit continue (277/279) and are never inserted into the composers table, yet their id remains in indexed_composer_ids. As a result the bubble filter at 306–307 still admits bubbles for those composers, producing FTS rows with no backing composers row — the exact orphan scenario the earlier fix targeted. These rows add rebuild cost and can surface bubble hits that are dropped downstream when no since_ms join/composer_ids_filter is applied.

Move the add to after the successful insert so only persisted composers gate bubble indexing.

Proposed fix
                     for row in composer_rows:
                         composer_id = row["key"].split(":")[1]
-                        indexed_composer_ids.add(composer_id)
                         raw_text = _composer_row_raw_text(row)
                         try:
                             cd = json.loads(raw_text)
                         except (json.JSONDecodeError, TypeError, ValueError):
                             continue
                         if not isinstance(cd, dict):
                             continue
                         title = cd.get("name") or ""
                         if not isinstance(title, str):
                             title = str(title) if title else ""
                         conn.execute(
                             "INSERT OR REPLACE INTO composers"
                             " (composer_id, title, created_ms, updated_ms, raw_json)"
                             " VALUES (?, ?, ?, ?, ?)",
                             (
                                 composer_id,
                                 title,
                                 to_epoch_ms(cd.get("createdAt")) or 0,
                                 _composer_dict_timestamp_ms(cd),
                                 raw_text,
                             ),
                         )
                         composer_count += 1
+                        indexed_composer_ids.add(composer_id)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/search_index.py` around lines 270 - 315, The indexed_composer_ids
set is being populated with composer_id immediately after extracting it from the
row, before the json.loads validation and isinstance checks that occur at lines
275-279. If these validation checks fail and hit continue statements, the
composer is never inserted into the composers table but its id remains in
indexed_composer_ids. This allows orphan bubbles to still be indexed later when
the filter at lines 306-307 checks if composer_id is in indexed_composer_ids.
Move the indexed_composer_ids.add(composer_id) call from its current position to
after the successful conn.execute INSERT statement that adds to the composers
table, ensuring only composers that are actually persisted have their ids
tracked for bubble indexing.
🧹 Nitpick comments (1)
tests/test_workspace_list_count_alignment.py (1)

138-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Equality assertion passes vacuously if both sets are empty.

summary_ids == full_ids would still hold if both paths returned zero tabs (e.g., a regression that drops the seeded composer from both). Add a non-empty guard so the test actually proves parity over real data.

💚 Strengthen the assertion
     summary_ids = {t["id"] for t in summary["tabs"]}
     full_ids = {t["id"] for t in full["tabs"]}
+    assert summary_ids  # guard against vacuous parity when both are empty
     assert summary_ids == full_ids
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_workspace_list_count_alignment.py` around lines 138 - 140, The
equality assertion between summary_ids and full_ids passes vacuously when both
sets are empty, which does not actually prove parity. Add an assertion to verify
that both summary_ids and full_ids are non-empty (contain at least one tab)
before or in conjunction with the equality assertion, ensuring the test
validates parity over real data rather than just confirming both endpoints
return no tabs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@services/search_index.py`:
- Around line 270-315: The indexed_composer_ids set is being populated with
composer_id immediately after extracting it from the row, before the json.loads
validation and isinstance checks that occur at lines 275-279. If these
validation checks fail and hit continue statements, the composer is never
inserted into the composers table but its id remains in indexed_composer_ids.
This allows orphan bubbles to still be indexed later when the filter at lines
306-307 checks if composer_id is in indexed_composer_ids. Move the
indexed_composer_ids.add(composer_id) call from its current position to after
the successful conn.execute INSERT statement that adds to the composers table,
ensuring only composers that are actually persisted have their ids tracked for
bubble indexing.

---

Nitpick comments:
In `@tests/test_workspace_list_count_alignment.py`:
- Around line 138-140: The equality assertion between summary_ids and full_ids
passes vacuously when both sets are empty, which does not actually prove parity.
Add an assertion to verify that both summary_ids and full_ids are non-empty
(contain at least one tab) before or in conjunction with the equality assertion,
ensuring the test validates parity over real data rather than just confirming
both endpoints return no tabs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 527e4b70-3695-406f-8620-449b4adfe9bb

📥 Commits

Reviewing files that changed from the base of the PR and between 55c4531 and d4d7d76.

📒 Files selected for processing (6)
  • services/search.py
  • services/search_index.py
  • services/workspace_listing.py
  • services/workspace_resolver.py
  • services/workspace_tabs.py
  • tests/test_workspace_list_count_alignment.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • services/workspace_resolver.py
  • services/workspace_listing.py
  • services/search.py

@clean6378-max-it

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit, All @bradjin8 's comments are addressed or answered correctly?

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== PR `#113` review comments by bradjin8 ==="
gh api repos/cppalliance/cppa-cursor-browser/pulls/113/comments \
  --paginate \
  | jq '[.[] | select(.user.login == "bradjin8") | {id, path, line, body, created_at}]'

echo ""
echo "=== PR `#113` issue-level comments by bradjin8 ==="
gh api repos/cppalliance/cppa-cursor-browser/issues/113/comments \
  --paginate \
  | jq '[.[] | select(.user.login == "bradjin8") | {id, body, created_at}]'

echo ""
echo "=== PR `#113` reviews by bradjin8 ==="
gh api repos/cppalliance/cppa-cursor-browser/pulls/113/reviews \
  --paginate \
  | jq '[.[] | select(.user.login == "bradjin8") | {id, state, body, submitted_at}]'

Length of output: 7942


🏁 Script executed:

#!/bin/bash
echo "=== PR `#113` commits after bradjin8's review (2026-06-23) ==="
gh api repos/cppalliance/cppa-cursor-browser/pulls/113/commits \
  --paginate \
  | jq '[.[] | {sha: .sha[:8], message: .commit.message | split("\n")[0], date: .commit.committer.date}]'

echo ""
echo "=== All review comments on PR `#113` (including replies to bradjin8) ==="
gh api repos/cppalliance/cppa-cursor-browser/pulls/113/comments \
  --paginate \
  | jq '[.[] | {id, in_reply_to_id, user: .user.login, path, line, body: .body[:300], created_at}]'

Length of output: 21122



@clean6378-max-it Here is a breakdown of @bradjin8's 13 comments and their current status, based on the replies and commit d4d7d761 ("Address feedback from brad"):


✅ Fully Addressed (10/13)

# Comment Resolution
1a assemble_single_tab uses raw json.loads → 500 on NULL Fixed — now uses parse_composer_data_row; test test_single_tab_null_composer_placeholder_returns_404 added
1b Bubble-attached path assignment before scoped bubbles loaded → wrong 404 Fixed — single-tab now loads scoped bubbles before calling assign_composer_workspace
2 assemble_workspace_tabs inlines duplicate assignment logic Fixed — now calls assign_composer_workspace with bubble_map={}, matching list/summary
3 Same bubble_map={} assignment issue in search.py line 362 Fixed — same alignment applied
4 infer_invalid_workspace_aliases called unconditionally in full /tabs Fixed — wrapped in if invalid_workspace_ids:, mirroring summary path
5 N+1 I/O for query_all_composer_bubble_texts Answered as intentional (performance trade-off); comment added to prevent reversion
6 Bubble indexing inserts orphan FTS rows for missing composers Fixed — index build now skips bubble rows whose composer isn't in the indexed set
7 bubble_map={} in workspace_listing.py without explanatory comment Fixed — comment added explaining the deliberate trade-off
8 Broad except Exception swallowing errors in workspace_resolver.py Fixed — narrowed to (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError)
9 Duplicated assignment block instead of assign_composer_workspace Fixed — assemble_workspace_tabs no longer inlines the logic
10 No test for assemble_single_tab with NULL composerData Fixed — test_single_tab_null_composer_placeholder_returns_404 added
11 No test for summary vs full /tabs assignment parity Fixed — test_summary_and_full_tabs_share_assignment added

⚠️ Deferred / Partially Addressed (3 items)

# Comment Status
1c Full composer scan for alias inference on every assemble_single_tab call Deferred — "too large a change, separate PR"
12 _INCLUDE_UNKNOWN_TIMESTAMPS_IN_WINDOW behavior should be visible to users (UI tooltip/API docs) Partially addressed — documented in code comment; UI tooltip explicitly deferred to a separate PR
13 api/search.py outer except Exception returns generic 500; should distinguish 400 from 500 Deferred — "left as-is, separate PR"

🔄 Review Status

@bradjin8's formal review is still marked CHANGES_REQUESTED (they have not re-reviewed after the d4d7d761 push). The three deferred items above are the open gaps. The two deferred-to-separate-PR items (1c, 13) are genuine gaps — would it help to open follow-up issues for them so they're tracked and don't get lost?

@clean6378-max-it

Copy link
Copy Markdown
Collaborator Author

Deferred / Partially Addressed (3 items)

Comment Status

1c Full composer scan for alias inference on every assemble_single_tab call Deferred — "too large a change, separate PR"
12 _INCLUDE_UNKNOWN_TIMESTAMPS_IN_WINDOW behavior should be visible to users (UI tooltip/API docs) Partially addressed — documented in code comment; UI tooltip explicitly deferred to a separate PR
13 api/search.py outer except Exception returns generic 500; should distinguish 400 from 500 Deferred — "left as-is, separate PR"

These will be implemented by follow up PRs by next week

@clean6378-max-it clean6378-max-it force-pushed the fix/workspace-list-load-and-count branch from 0dde9d9 to 9927ba9 Compare June 23, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Project list and workspace details show different conversation counts; workspace load still slow on large installs

2 participants