Skip to content

Feat/search errors and ux#121

Merged
wpak-ai merged 4 commits into
masterfrom
feat/search-errors-and-ux
Jul 7, 2026
Merged

Feat/search errors and ux#121
wpak-ai merged 4 commits into
masterfrom
feat/search-errors-and-ux

Conversation

@clean6378-max-it

@clean6378-max-it clean6378-max-it commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Closes #113

Summary

Follow-up to #120. That PR added the FTS index and the 30-day search window. This one is the error codes and search page cleanup.

/api/search used to return 200 [] on an empty query. It now returns 400 with SEARCH_EMPTY_QUERY. Same idea for a query over 500 chars, bad since_days, a projects dir you cannot read, and a locked FTS index (503 on the last one; no live-scan fallback when SQLite is locked).

Search page: short note about the 30-day default and the 50-result cap, checkbox for all history, warning when you hit the limit, separate empty vs error UI, snippet match highlighting after escape.

Stacked on feat/search-fts-index until #120 lands, then rebase and point at master.

Test plan

pytest (search, integration, error propagation, search_index), npm test, mypy, ruff all green locally.

Manual: empty q and bad since_days in curl, /search checkbox and truncation warning, check highlight in a snippet.

Summary by CodeRabbit

  • New Features

    • Search now supports a “search all history” option and shows highlighted matches in results.
    • Search responses now include clearer result counts, truncation notices, and better on-page loading/error states.
  • Bug Fixes

    • Empty, overlong, or invalid search inputs now return clear validation errors.
    • Search now falls back gracefully when the index is unavailable or locked.
    • Unexpected search failures now return a safe server error instead of breaking the page.
  • Documentation

    • Updated search API docs with the latest behavior, error codes, and examples.

@coderabbitai

coderabbitai Bot commented Jul 6, 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

/api/search now validates query length and since_days, checks projects-directory accessibility, prefers an FTS index with live-scan fallback, and returns distinct 400/503/500 error codes. create_app gains a testing flag; the background index worker adds cross-process locking. Search UI adds snippet highlighting and an all-history checkbox.

Changes

Search validation, index locking, and UI updates

Layer / File(s) Summary
Error codes and API documentation
models/error_codes.py, docs/api-reference.md
Adds SEARCH_EMPTY_QUERY, SEARCH_QUERY_TOO_LONG, SEARCH_INVALID_SINCE_DAYS, SEARCH_PROJECTS_UNAVAILABLE enum values and documents them alongside updated /api/search behavior for q, since_days, and all_history.
/api/search validation, index-first search, and fallback
api/search.py
Adds _MAX_QUERY_LEN, since_days validation, _projects_dir_inaccessible, fail-closed exclusion on cache read failure, partial-result return on index_locked, and 400/503/500 error mapping.
Search index cross-process locking and query semantics
utils/search_index.py
Adds a non-blocking lock file guarding the background worker and updates query_index_hits/reset_background_for_tests to track and report index_locked.
create_app testing flag wiring
app.py, .github/workflows/ci.yml, tests/conftest.py, tests/benchmarks/conftest.py, tests/test_api_routes.py
Adds a keyword-only testing parameter to create_app gating the index worker; CI, fixtures, and a route test now pass testing=True directly.
Search UI: highlighting, all-history option, structured errors
static/js/search.js, static/js/search.test.js
Adds highlightSnippet, an all-history checkbox, updated help text, structured non-OK error parsing with data-error-code, and corresponding unit tests.
Backend test coverage
tests/test_search.py, tests/test_error_codes.py, tests/test_error_propagation.py, tests/test_api_integration.py, tests/test_search_index.py
Adds tests for query/since_days validation, projects-dir unavailability, index-locked fallback, fail-closed exclusion, tokenless queries, and internal error non-leakage.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SearchAPI
  participant SearchIndex
  participant LiveScan

  Client->>SearchAPI: GET /api/search?q=...&since_days=...
  SearchAPI->>SearchAPI: validate q length, since_days
  alt validation fails
    SearchAPI-->>Client: 400 error code
  end
  SearchAPI->>SearchAPI: check projects directory accessibility
  alt inaccessible
    SearchAPI-->>Client: 503 SEARCH_PROJECTS_UNAVAILABLE
  end
  SearchAPI->>SearchIndex: _search_via_index(query, since_ms)
  alt index locked, partial hits collected
    SearchIndex-->>SearchAPI: ranked partial hits
    SearchAPI-->>Client: 200 partial results
  else index returns None
    SearchAPI->>LiveScan: _search_live_scan(query, since_ms)
    LiveScan-->>SearchAPI: hits
    SearchAPI-->>Client: 200 hits
  end
  SearchAPI-->>Client: 500 INTERNAL_ERROR on unexpected exception
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: timon0305, wpak-ai

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR misses the required SEARCH_INDEX_UNAVAILABLE 503 path and instead falls back when the index is locked. Add SEARCH_INDEX_UNAVAILABLE, return 503 when the index is locked or unusable, and reserve live-scan fallback for disabled or missing indexes.
✅ 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 matches the PR's main theme: search error handling and UI improvements.
Out of Scope Changes check ✅ Passed The changes stay focused on search API, UI, tests, docs, and startup plumbing for the linked search work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/search-errors-and-ux

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

🧹 Nitpick comments (1)
app.py (1)

109-117: 🧹 Nitpick | 🔵 Trivial

Multiple worker processes will each start their own background rebuild thread.

_background_started is a per-process global, so under a multi-process server (e.g. gunicorn with several workers) every worker independently rebuilds/refreshes the same on-disk SQLite index, causing redundant work and increased lock contention on the shared index file.

🤖 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 `@app.py` around lines 109 - 117, The background search index startup in app
initialization is still happening in every worker process because
`_background_started` only guards within a single process. Update
`start_search_index_background` and/or its call site in `app.py` so only one
process can launch the rebuild thread, using a cross-process lock or master-only
startup check before calling `start_search_index_background(projects_dir,
app.config["EXCLUSION_RULES"])`. Keep the existing `TESTING` guard and the
exception logging, but ensure the background worker is not started redundantly
by each gunicorn worker.
🤖 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 90-112: The exclusion path in _index_hit_excluded currently fails
open when get_cached_session(file_path) raises, which can bypass privacy
filtering. Update this function to match _search_live_scan behavior by treating
session-load failures as excluded/skip-on-error rather than returning False, and
keep the warning in place so the failure remains visible while the index search
stays fail-closed.

In `@app.py`:
- Around line 109-117: The search index startup in create_app is gated by
app.config["TESTING"] too late, so tests that set TESTING after app creation
still spawn the background thread. Add a testing parameter to create_app and use
it before the start_search_index_background branch, then update the call sites
such as tests/test_search.py::_seed_indexed_client to pass testing=True when
constructing the app. Make sure the existing app.config["TESTING"] behavior
still works for callers that already set it, but the startup check should rely
on the new early parameter so the background worker is never launched during
tests.

In `@utils/search_index.py`:
- Around line 635-637: In _search_via_index(), treat tokenless queries from
_fts_match_query(query_lower) as index-unavailable instead of a successful empty
result. Update query_index_hits() so that when fts_q is empty for inputs like
“!!!” or “===”, it returns query_ok=False and an empty response, allowing the
caller to fall back to _search_live_scan() rather than stopping at the index
path.

---

Nitpick comments:
In `@app.py`:
- Around line 109-117: The background search index startup in app initialization
is still happening in every worker process because `_background_started` only
guards within a single process. Update `start_search_index_background` and/or
its call site in `app.py` so only one process can launch the rebuild thread,
using a cross-process lock or master-only startup check before calling
`start_search_index_background(projects_dir, app.config["EXCLUSION_RULES"])`.
Keep the existing `TESTING` guard and the exception logging, but ensure the
background worker is not started redundantly by each gunicorn worker.
🪄 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: c2b92b37-5ada-4126-981d-c896d2428d61

📥 Commits

Reviewing files that changed from the base of the PR and between a05fd29 and 3b22f3f.

📒 Files selected for processing (16)
  • api/search.py
  • app.py
  • benchmarks/README.md
  • benchmarks/baselines.json
  • docs/api-reference.md
  • models/error_codes.py
  • scripts/check_benchmark_regression.py
  • scripts/reduce_baselines.py
  • static/js/search.js
  • static/js/search.test.js
  • tests/test_api_integration.py
  • tests/test_error_codes.py
  • tests/test_error_propagation.py
  • tests/test_search.py
  • tests/test_search_index.py
  • utils/search_index.py

Comment thread api/search.py
Comment thread app.py Outdated
Comment thread utils/search_index.py 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)
tests/test_api_routes.py (1)

42-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant TESTING assignment.

Line 43 (app.config["TESTING"] = True) is now dead code since testing=True is already passed into create_app(...) on Line 42, which sets app.config["TESTING"] internally.

🧹 Proposed cleanup
     app = create_app(base_dir=str(tmp_path), exclusion_rules_path=str(rules_path), testing=True)
-    app.config["TESTING"] = True
     excluded_client = app.test_client()
🤖 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_api_routes.py` around lines 42 - 44, The TESTING config is being
set twice in the test setup, making the explicit app.config["TESTING"]
assignment redundant. In the test that calls create_app(..., testing=True) and
then creates excluded_client, remove the extra TESTING assignment and keep the
create_app testing flag as the single source of truth.
🤖 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 `@tests/test_api_routes.py`:
- Around line 42-44: The TESTING config is being set twice in the test setup,
making the explicit app.config["TESTING"] assignment redundant. In the test that
calls create_app(..., testing=True) and then creates excluded_client, remove the
extra TESTING assignment and keep the create_app testing flag as the single
source of truth.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e046ba83-3e57-4b49-9d95-201955775449

📥 Commits

Reviewing files that changed from the base of the PR and between 3b22f3f and a066ca4.

📒 Files selected for processing (12)
  • .github/workflows/ci.yml
  • api/search.py
  • app.py
  • docs/api-reference.md
  • static/js/search.js
  • static/js/search.test.js
  • tests/benchmarks/conftest.py
  • tests/conftest.py
  • tests/test_api_routes.py
  • tests/test_search.py
  • tests/test_search_index.py
  • utils/search_index.py
✅ Files skipped from review due to trivial changes (1)
  • tests/conftest.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • app.py
  • tests/test_search_index.py
  • static/js/search.test.js
  • static/js/search.js
  • api/search.py
  • docs/api-reference.md
  • utils/search_index.py

Comment thread api/search.py Outdated
Comment thread api/search.py Outdated
@clean6378-max-it clean6378-max-it force-pushed the feat/search-errors-and-ux branch from a066ca4 to 781e1f7 Compare July 7, 2026 09:14

@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: 1

🤖 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 `@static/js/search.js`:
- Around line 103-120: The failed-search handling in search.js is trying to read
the response body twice, so the text fallback never works after res.json()
consumes the stream. Update the error path in the search fetch block to preserve
a separate copy of the response body before parsing, and use that copy for the
raw-text fallback when JSON parsing fails. Keep the existing localRequestId,
res.ok, and container.innerHTML logic unchanged, but ensure the fallback can
surface non-JSON error text instead of always falling back to the generic status
message.
🪄 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: 64855c8d-8e80-423f-8315-ca1c02a2576b

📥 Commits

Reviewing files that changed from the base of the PR and between a066ca4 and 781e1f7.

📒 Files selected for processing (16)
  • .github/workflows/ci.yml
  • api/search.py
  • app.py
  • docs/api-reference.md
  • models/error_codes.py
  • static/js/search.js
  • static/js/search.test.js
  • tests/benchmarks/conftest.py
  • tests/conftest.py
  • tests/test_api_integration.py
  • tests/test_api_routes.py
  • tests/test_error_codes.py
  • tests/test_error_propagation.py
  • tests/test_search.py
  • tests/test_search_index.py
  • utils/search_index.py
✅ Files skipped from review due to trivial changes (3)
  • tests/benchmarks/conftest.py
  • tests/test_api_routes.py
  • docs/api-reference.md
🚧 Files skipped from review as they are similar to previous changes (10)
  • tests/conftest.py
  • models/error_codes.py
  • tests/test_error_codes.py
  • .github/workflows/ci.yml
  • tests/test_api_integration.py
  • tests/test_search_index.py
  • app.py
  • api/search.py
  • static/js/search.test.js
  • utils/search_index.py

Comment thread static/js/search.js

@wpak-ai wpak-ai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Must fix

  1. api/search.py _search_via_index (~L183) — stop breaking on sql_exhausted while the fetched batch has unscanned rows; keep paging (or top-up via live-scan) so the endpoint returns limit results (exclusion-dropped hits on an "exhausted" batch abandon the unscanned tail → silent undercount, no fallback)

  2. utils/search_index.py _indexable_texts_from_entry / _coerce_role (~L344-360) — index the same roles live-scan searches (system/result/progress/unknown), or restrict live-scan to match (index-served queries silently miss matches live-scan finds; results depend on whether the index is warm)

  3. api/search.py search() + query_index_hits (G3, cross-file) — make the fallback detect index UNDER-RETURN, not just unavailability: when the index yields < max_results and isn't provably exhausted (or returns an empty non-None list), fall back to / top-up with live-scan (fixes #1 and #8, mitigates #2, at the point where correctness is decided)

Should fix

  1. api/search.py:87 _message_searchable_text + utils/search_index.py:352 _indexable_texts_from_entry (G1) — unify searchable-text extraction + role policy into one shared helper (two copies that must stay in lock-step already drifted — root cause of #2)

  2. utils/search_index.py query_index_hits + api/search.py _search_via_index (G4) — consolidate pagination-consumption accounting so the tail is guaranteed drained (mechanism of #1)

  3. tests/test_search_index.py — add a test with a usable index, matches > max_results, and ≥1 excluded match, asserting max_results are still returned (nothing exercises #1; the phrase-pagination test passes on buggy and fixed code alike)

  4. tests/test_search_index.py — add a system/unknown-role index-vs-live parity test (no test covers #2)

Fresh Claude installs were getting 503 on search when ~/.claude/projects
did not exist yet. 503 now means the path is there but listdir failed.
CI smoke test uses create_app(testing=True). Search page nags on empty
query and highlights snippets by code point.
@clean6378-max-it clean6378-max-it force-pushed the feat/search-errors-and-ux branch from d8a411b to 96a4ce5 Compare July 7, 2026 19:03
@clean6378-max-it clean6378-max-it requested a review from wpak-ai July 7, 2026 19:36
@wpak-ai wpak-ai merged commit a725bd2 into master Jul 7, 2026
16 checks passed
@wpak-ai wpak-ai deleted the feat/search-errors-and-ux branch July 7, 2026 19:39
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.

claude-code-chat-browser: FTS search index, search UX, and distinct /api/search error codes

3 participants