Skip to content

Search UX tooltip and distinct /api/search error codes (#117)#126

Open
clean6378-max-it wants to merge 2 commits into
masterfrom
fix/search-ux-and-api-errors
Open

Search UX tooltip and distinct /api/search error codes (#117)#126
clean6378-max-it wants to merge 2 commits into
masterfrom
fix/search-ux-and-api-errors

Conversation

@clean6378-max-it

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

Copy link
Copy Markdown
Collaborator

Summary

  • Add search-window info tooltip to templates/search.html explaining the default 30-day indexed window, undated-chat inclusion, and the all-history checkbox.
  • Replace the broad except Exception handler in api/search.py with narrowed handlers returning structured {"error", "code"} bodies for 400, 404, 503, and 500.
  • Frontend search handler displays the human-readable error field from failed API responses.
  • Add max query length validation (500 chars → 400) and optional workspace hash validation (404 when unknown).

Problem

Part A: Users did not understand why some search results were missing (30-day window, undated chats always included).
Part B: /api/search returned generic 500 for all failures, preventing clients from distinguishing malformed input, missing workspace, index lock, and internal errors.

Test plan

  • pytest tests/test_api_search.py -q (19 passed)
  • Full pytest -q (533 passed, 21 skipped)
  • mypy api/search.py
  • Parametrized 400 cases: empty query, invalid type, invalid since_days, query too long
  • 404: unknown workspace hash
  • 503: simulated sqlite3.OperationalError
  • 500: unexpected internal error with internal_error code
  • No regression in TestSearchWindow / happy-path behavior

Out of scope

  • FTS vs live-scan refactor
  • Repo-wide structured errors for all blueprints
  • Search module duplication extraction (T21)

Closes #117

Summary by CodeRabbit

  • New Features

    • Added search guidance in the interface explaining the default recent-history window and how to expand results to older chats.
    • Search results now respect an optional workspace filter, helping narrow searches to a specific workspace.
  • Bug Fixes

    • Improved search error handling with clearer messages and status codes.
    • Search failures now show more useful feedback instead of a generic error, including cases where the search index is unavailable.
    • Search behavior is more consistent when older-history search is enabled.

Add a search-window info tooltip, return structured 400/404/503/500
responses with machine-readable codes, validate query length and
workspace hash, and show API error messages in the search UI.
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 40 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f4565178-00c6-45c4-8398-3b5676d7c6f8

📥 Commits

Reviewing files that changed from the base of the PR and between 4e8320e and fbd9ba1.

📒 Files selected for processing (4)
  • api/search.py
  • static/css/style.css
  • templates/search.html
  • tests/test_api_search.py
📝 Walkthrough

Walkthrough

Restructures /api/search to perform explicit parameter validation (query, type, since_days, workspace) with distinct 400/404/503/500 HTTP error codes and structured {"error", "code"} JSON responses. Adds workspace post-filtering of ranked results. Introduces a search-window tooltip in the UI and defensive JSON error parsing on the frontend.

Search API hardening and UI tooltip

Layer / File(s) Summary
Search helper functions and constants
api/search.py
Adds _MAX_SEARCH_QUERY_LEN, _VALID_SEARCH_TYPES, _MAX_SEARCH_SINCE_DAYS constants and four helpers: _parse_since_days_param, _search_error, _workspace_exists, and _filter_results_by_workspace.
Restructured search() route with tiered error handling
api/search.py
Moves all parameter validation outside the main try block; assembles results from multiple backends; ranks and optionally filters by workspace; splits exception handling into sqlite3.OperationalError → 503 and generic Exception → 500; success payload now includes allHistory and searchWindowDays.
Updated and new search API tests
tests/test_api_search.py
Adds _assert_error_body helper and parameterized test_search_error_status_codes; revises TestSearchErrorResponses to assert 400/500/503 codes without a results key; adds test_workspace_filter_limits_results.
Tooltip UI and defensive JSON parsing
static/css/style.css, templates/search.html
Adds .search-info-tooltip styles with hover/focus visibility toggling; inserts tooltip HTML explaining default 30-day window behavior; updates performSearch to parse JSON defensively and display data.error when a string.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • cppalliance/cppa-cursor-browser#102: Refactored the search handler to delegate to services/search including rank_results, which this PR builds on when restructuring the route's result assembly and ranking flow.

Suggested reviewers

  • wpak-ai

🐇 A search once returned just "failed" and shrugged,
Now it speaks in codes—400, 503!
A tooltip peeks out with a wink and a nudge,
"Undated chats appear, but now you can see!"
Old errors are gone, new structure's the plan,
Hooray for the rabbit who tests what it can! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% 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 clearly summarizes the main changes: search UX tooltip and distinct /api/search error codes.
Linked Issues check ✅ Passed The PR covers the tooltip, preserves search behavior, adds distinct 400/404/503/500 search errors, and updates tests.
Out of Scope Changes check ✅ Passed The changes stay focused on search UX, API error handling, and related tests; no clearly unrelated scope is introduced.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/search-ux-and-api-errors

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: 3

🤖 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 57-66: The _workspace_exists helper currently joins
client-controlled workspace_id values directly into os.path.isdir, which can be
abused with absolute paths or .. segments. Update _workspace_exists to
explicitly allow only known-safe workspace IDs before joining paths: keep the
existing "global" and "cli:" handling, and for regular IDs validate they match
the expected workspace hash/ID format before calling
os.path.join(workspace_path, workspace_id). Reject or return false for anything
outside that allowed pattern so workspace existence checks cannot be used as a
local path oracle.
- Around line 107-112: Move workspace discovery into the existing
structured-error guard in the search flow so failures are converted to the JSON
error contract. Specifically, in the request handling around workspace_filter,
resolve_workspace_path should be called inside the try block (the same guard
used by the search endpoint), and any discovery/storage exceptions should be
caught and routed through _search_error instead of escaping before the handler
can format a 500/503 response.

In `@templates/search.html`:
- Around line 17-24: The search help tooltip is not exposed to assistive
technologies because the trigger in search.html uses a focusable span with only
aria-label, while the guidance text is just visual content. Update the tooltip
trigger to a semantic control such as a button, and connect it to the help text
using aria-describedby with the tooltip content marked appropriately (for
example, role="tooltip") so screen readers can access the 30-day/full-history
guidance. Keep the existing search-info-tooltip, search-info-tooltip-text, and
search-info-icon structure as the anchor points while changing the accessibility
semantics.
🪄 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: fdfda060-7323-48ea-8cff-b4a8c4d5fc20

📥 Commits

Reviewing files that changed from the base of the PR and between fb97d03 and 4e8320e.

📒 Files selected for processing (4)
  • api/search.py
  • static/css/style.css
  • templates/search.html
  • tests/test_api_search.py

Comment thread api/search.py Outdated
Comment thread api/search.py Outdated
Comment thread templates/search.html Outdated

@bradjin8 bradjin8 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please check the following issues.

[Optional] templates/search.html line 123-126:

result-count banner could mention undated chats when windowed ("last 30 days; undated chats included"). Tooltip covers discoverability; banner would reinforce after search.

Comment thread templates/search.html
Comment on lines +17 to +22
<button
type="button"
class="search-info-tooltip"
id="search-window-help"
aria-describedby="search-window-help-text"
>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Second commit dropped aria-label="Search window help" when switching span → button. aria-describedby alone may not give the control an accessible name in all AT. Restore aria-label on the button.

Comment thread api/search.py
Comment on lines +114 to +121
query = request.args.get("q", "").strip()
if not query:
return _search_error("No search query provided", "empty_query", 400)
if len(query) > _MAX_SEARCH_QUERY_LEN:
return _search_error("Search query is too long", "query_too_long", 400)

search_type = request.args.get("type", "all")
if search_type not in _VALID_SEARCH_TYPES:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

test_workspace_path_resolution_failure_returns_structured_500 documents OSError → 500. Issue #117 suggested 404/503 for storage unavailability — consider 503 + storage_unavailable if you want clients to retry config/discovery failures differently from bugs. Current choice is defensible; note in PR description.

Comment thread api/search.py
Comment on lines +199 to +208
except sqlite3.OperationalError:
_logger.exception("Search index unavailable")
return _search_error(
"Search index is temporarily unavailable",
"search_index_unavailable",
503,
)
except Exception:
_logger.exception("Search failed")
return json_response({"error": "Search failed", "results": []}, 500) No newline at end of file
return _search_error("Search failed", "internal_error", 500)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Only sqlite3.OperationalError maps to 503. Other DB errors (e.g. sqlite3.DatabaseError) fall through to 500. Fine for now; widen only if search layer raises them in practice.

Comment thread api/search.py
Comment on lines 132 to +185
@@ -101,18 +180,30 @@ def search() -> tuple[Response, int] | Response:
)
)

ranked = rank_results(results)
if workspace_filter:
ranked = _filter_results_by_workspace(ranked, workspace_filter)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

workspace query param + post-filter is useful but outside #117 scope (API-only; UI does not expose it). Call out in PR as bonus scope or split to a follow-up if you want strict issue alignment.

Comment thread templates/search.html
Comment on lines 111 to 117
if (!res.ok) {
showIncompleteResultsBanner('parse-warnings-host', []);
loading.style.display = 'none';
container.innerHTML = '<p class="text-danger">Search failed.</p>';
const message = typeof data.error === 'string' ? data.error : 'Search failed.';
container.innerHTML = `<p class="text-danger">${escapeHtml(message)}</p>`;
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Frontend shows data.error on failure — good. Consider surfacing data.code in a data-* attribute or dev-only suffix for operators debugging 503 vs 500.

Comment thread api/search.py
"""
query = request.args.get("q", "").strip()
if not query:
return _search_error("No search query provided", "empty_query", 400)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Old 400/500 bodies included no code / 500 included "results": []. New contract is cleaner; note breaking change for any external client that expected results on error.

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.

cppa-cursor-browser: Search UX polish and distinct /api/search error codes

2 participants