Skip to content

feat(wework): add conversation search navigation - #1257

Merged
qdaxb merged 2 commits into
wecode-ai:mainfrom
FicoHu:codex/wework-search-navigation
Jun 3, 2026
Merged

qdaxb merged 2 commits into
wecode-ai:mainfrom
FicoHu:codex/wework-search-navigation

Conversation

@FicoHu

@FicoHu FicoHu commented Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Conversation/task search with keyword, pagination, and desktop search dialog integrated into sidebar and workbench.
    • Optionally fetches remote search results and task detail text; supports keyboard interaction and localized UI strings.
  • Tests

    • Added unit and integration tests covering conversation search behavior, detail-fetching, and UI interactions.
  • Documentation / Localization

    • Added English and Simplified Chinese strings for the search UI.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 41e73f2e-ca4a-467f-867f-e76c0bc0eb4f

📥 Commits

Reviewing files that changed from the base of the PR and between 108ddfe and 233a2e8.

📒 Files selected for processing (2)
  • wework/src/i18n/locales/en/common.json
  • wework/src/i18n/locales/zh-CN/common.json
✅ Files skipped from review due to trivial changes (2)
  • wework/src/i18n/locales/en/common.json
  • wework/src/i18n/locales/zh-CN/common.json

📝 Walkthrough

Walkthrough

Adds an end-to-end WeWork-scoped conversation search: backend search adapter and GET /wework/conversation-search endpoint; frontend task API, a DesktopSearchDialog merging local/remote results with lazy detail fetching; sidebar/layout/provider wiring; tests; and i18n strings.

Changes

Task Search Feature

Layer / File(s) Summary
Backend search service and API endpoint
backend/app/services/adapters/wework_conversation_search.py, backend/app/api/endpoints/adapter/tasks.py
New WeWork conversation search implementation: title (JSON) and subtask matching via DB ilike plus Python fallback, pagination/ordering, formatting, and GET /wework/conversation-search endpoint.
Backend search tests
backend/tests/services/adapters/test_task_archive_operations.py
Three pytest cases: title search vs subtask content, client_origin filtering, and DB-first vs Python-fallback search path.
Frontend API client
wework/src/api/tasks.ts, wework/src/api/adapters.test.ts
Added SearchTaskParams and searchTasks(keyword, params?) calling /tasks/wework/conversation-search; adapter test asserts GET URL and params.
Search dialog component
wework/src/components/layout/DesktopSearchDialog.tsx
New portal dialog merging local project/recent task index with optional remote results; normalized query filtering, debounced remote search with cancellation, and lazy per-task detail fetching for conversation message searches.
Sidebar and layout integration
wework/src/components/layout/DesktopSidebar.tsx, wework/src/components/layout/DesktopWorkbenchLayout.tsx
Sidebar accepts search callbacks, manages dialog open state, auto-expands/scrolls to current task, and renders DesktopSearchDialog; layout forwards callbacks to sidebar.
Provider and page wiring
wework/src/features/workbench/WorkbenchProvider.tsx, wework/src/pages/WorkbenchPage.tsx
WorkbenchProvider exposes searchTasks and searchTaskDetail delegating to optional taskApi methods (with empty-result fallback); WorkbenchPage forwards these callbacks into the layout.
Integration tests
wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
Tests for dialog interactions, local vs remote search, detail-backed message search, and current-task auto-selection with mocked scrollIntoView.
i18n translations
wework/src/i18n/locales/en/common.json, wework/src/i18n/locales/zh-CN/common.json
Added search_conversations, recent_conversations, and search_no_results keys for English and Simplified Chinese.

Sequence Diagram

sequenceDiagram
  participant User as User
  participant SearchDialog as DesktopSearchDialog
  participant RemoteAPI as searchTasks API
  participant DetailAPI as searchTaskDetail API
  participant Sidebar as DesktopSidebar

  User->>SearchDialog: Opens dialog and types keyword
  SearchDialog->>SearchDialog: Filter local projects/recent tasks
  alt Remote search available
    SearchDialog->>RemoteAPI: Debounced searchTasks(keyword,page,limit)
    RemoteAPI-->>SearchDialog: TaskListResponse
    SearchDialog->>SearchDialog: Merge remote + local by taskId
  end
  SearchDialog->>DetailAPI: searchTaskDetail(taskId) for missing items (parallel)
  DetailAPI-->>SearchDialog: TaskDetail with searchable text
  User->>SearchDialog: Clicks result
  SearchDialog->>Sidebar: onOpenTask(taskId, projectId)
  Sidebar->>Sidebar: Expand project and scroll row into view
  SearchDialog->>SearchDialog: Close dialog
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • wecode-ai/Wegent#1199: Related to passing projectId into onOpenTask(...) used by the search dialog results.
  • wecode-ai/Wegent#1206: Related client_origin scoping and backend origin-filtered task behavior used by the new endpoint.

Suggested reviewers

  • qdaxb

Poem

🐰 I hopped through tasks both near and far,
Whispered keywords like a tiny star.
Backend sniffs titles and messages too,
Frontend shows results, fresh as dew.
Hooray — search found it! thump thump 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 'feat(wework): add conversation search navigation' clearly and directly summarizes the main change: adding a conversation search feature with navigation capability to the WeWork adapter.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
wework/src/components/layout/DesktopWorkbenchLayout.test.tsx (1)

568-616: 💤 Low value

Test name doesn't match its behavior.

This test never opens the search dialog (no search-button click); it only renders with currentTask set and asserts the auto-expand/scroll-into-view effect. The phrase "after opening from search" is misleading for future maintainers — consider renaming to reflect that it verifies the current-project task is expanded/highlighted/scrolled on render.

♻️ Suggested rename
-  test('expands and scrolls to the current project task after opening from search', async () => {
+  test('expands, highlights, and scrolls to the current project task on render', async () => {
🤖 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 `@wework/src/components/layout/DesktopWorkbenchLayout.test.tsx` around lines
568 - 616, The test named "expands and scrolls to the current project task after
opening from search" is misleading because it never clicks the search button;
update the test name to reflect that it verifies automatic
expansion/highlighting/scrolling on render (e.g., "expands and scrolls to the
current project task on render when currentTask is set"), and ensure references
to DesktopWorkbenchLayout's state props (currentTask/currentProject/projects),
the queried test id `project-chat-row-{task_id}`, and the `search-button` are
not implied; leave the test logic that asserts Element.prototype.scrollIntoView
was called and the row has the active class and correct text intact.
🤖 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 `@wework/src/pages/WorkbenchPage.tsx`:
- Around line 79-80: Layout is being rendered with
onSearchTasks/onSearchTaskDetail from WorkbenchPage
(searchTasks/searchTaskDetail) but MobileWorkbenchLayoutProps doesn't declare
them; update MobileWorkbenchLayoutProps to include optional onSearchTasks and
onSearchTaskDetail callback signatures and propagate those props through the
MobileWorkbenchLayout component (accept props in MobileWorkbenchLayout and pass
them to the internal handler or child that needs them), or alternatively render
those props only when Layout resolves to the desktop layout. Modify
MobileWorkbenchLayoutProps and the MobileWorkbenchLayout component to accept and
forward onSearchTasks/onSearchTaskDetail (matching the types used by
WorkbenchPage: searchTasks/searchTaskDetail).

---

Nitpick comments:
In `@wework/src/components/layout/DesktopWorkbenchLayout.test.tsx`:
- Around line 568-616: The test named "expands and scrolls to the current
project task after opening from search" is misleading because it never clicks
the search button; update the test name to reflect that it verifies automatic
expansion/highlighting/scrolling on render (e.g., "expands and scrolls to the
current project task on render when currentTask is set"), and ensure references
to DesktopWorkbenchLayout's state props (currentTask/currentProject/projects),
the queried test id `project-chat-row-{task_id}`, and the `search-button` are
not implied; leave the test logic that asserts Element.prototype.scrollIntoView
was called and the row has the active class and correct text intact.
🪄 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: fac0066b-0ac8-4ece-8cdd-747a351d0e5b

📥 Commits

Reviewing files that changed from the base of the PR and between a16db2f and c225ca6.

📒 Files selected for processing (17)
  • backend/app/api/endpoints/adapter/tasks.py
  • backend/app/services/adapters/task_kinds/queries.py
  • backend/tests/services/adapters/test_task_archive_operations.py
  • docs/en/user-guide/chat/managing-tasks.md
  • docs/zh/user-guide/chat/managing-tasks.md
  • frontend/src/__tests__/contexts/SocketContext.reconnect.test.tsx
  • frontend/src/__tests__/features/resource-library/ResourceCreateButton.test.tsx
  • wework/src/api/adapters.test.ts
  • wework/src/api/tasks.ts
  • wework/src/components/layout/DesktopSearchDialog.tsx
  • wework/src/components/layout/DesktopSidebar.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.tsx
  • wework/src/features/workbench/WorkbenchProvider.tsx
  • wework/src/i18n/locales/en/common.json
  • wework/src/i18n/locales/zh-CN/common.json
  • wework/src/pages/WorkbenchPage.tsx

Comment on lines +79 to +80
onSearchTasks={searchTasks}
onSearchTaskDetail={searchTaskDetail}

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether MobileWorkbenchLayout declares the new search props
fd -i 'MobileWorkbenchLayout.tsx' wework/src --exec sh -c 'echo "== {} =="; rg -nP -C2 "onSearchTasks|onSearchTaskDetail|interface .*LayoutProps" "{}"'

Repository: wecode-ai/Wegent

Length of output: 277


Fix prop mismatch: wire or conditionally pass onSearchTasks / onSearchTaskDetail on mobile
wework/src/pages/WorkbenchPage.tsx renders Layout with onSearchTasks and onSearchTaskDetail (lines 79-80), but wework/src/components/layout/MobileWorkbenchLayout.tsx’s MobileWorkbenchLayoutProps does not declare these props. Add them to the mobile layout props (and pass them through) or only pass these callbacks when rendering the desktop layout.

🤖 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 `@wework/src/pages/WorkbenchPage.tsx` around lines 79 - 80, Layout is being
rendered with onSearchTasks/onSearchTaskDetail from WorkbenchPage
(searchTasks/searchTaskDetail) but MobileWorkbenchLayoutProps doesn't declare
them; update MobileWorkbenchLayoutProps to include optional onSearchTasks and
onSearchTaskDetail callback signatures and propagate those props through the
MobileWorkbenchLayout component (accept props in MobileWorkbenchLayout and pass
them to the internal handler or child that needs them), or alternatively render
those props only when Layout resolves to the desktop layout. Modify
MobileWorkbenchLayoutProps and the MobileWorkbenchLayout component to accept and
forward onSearchTasks/onSearchTaskDetail (matching the types used by
WorkbenchPage: searchTasks/searchTaskDetail).

@FicoHu
FicoHu force-pushed the codex/wework-search-navigation branch from c225ca6 to 108ddfe Compare June 3, 2026 14:57

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
backend/app/services/adapters/wework_conversation_search.py (1)

119-139: Unbounded task load on the request path may not scale.

_load_visible_wework_tasks fetches every active WeWork task for the user via .all() with no cap, and _match_task_titles then validates each task's JSON in Python (a second Task.model_validate pass also happens in _format_task_results). For users with large histories this loads and parses the full task set on every search. Consider pushing the title match into SQL (JSON field access, as the other lite queries do) and bounding the candidate set, or caching the per-task Task parse to avoid double validation.

🤖 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 `@backend/app/services/adapters/wework_conversation_search.py` around lines 119
- 139, _load_visible_wework_tasks currently loads all TaskResource rows then
filters and causes repeated Task.model_validate passes in _match_task_titles and
_format_task_results; change it to push the title-match into the DB query (use
JSON/->>'title' access on TaskResource.model or the DB-specific JSON path used
elsewhere) and add a bounded candidate set (e.g. .limit(n) with sensible page
size and order_by TaskResource.updated_at.desc()) so we only load a finite
recent subset, and additionally introduce an in-memory cache of parsed Task
objects (keyed by TaskResource.id) used by _match_task_titles and
_format_task_results to avoid double Task.model_validate for the same resource.
🤖 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 `@backend/app/services/adapters/wework_conversation_search.py`:
- Around line 168-187: In _match_subtasks_in_database: escape SQL LIKE wildcards
in the incoming keyword before building like_pattern by first replacing
backslashes with double backslashes then prefixing any '%' and '_' with a
backslash (or chosen escape char), build like_pattern = f"%{escaped_keyword}%",
and pass the same escape char into the ilike calls (e.g., .ilike(like_pattern,
escape='\\')) for Subtask.prompt, Subtask.error_message and cast(Subtask.result,
String) so user input is treated literally.
- Around line 44-53: This function is a public service entrypoint that lacks
OpenTelemetry tracing; add the tracing decorator `@trace_sync` to both
search_wework_conversation_tasks and the new
search_wework_conversation_task_list (they are synchronous), and also add the
corresponding import for trace_sync at the top of the module so the decorator is
available. Ensure the decorator wraps the entire function definition (above def)
so the entrypoints are traced end-to-end.
- Around line 66-116: The current bug is that _collect_matched_task_ids() gates
calling _match_subtasks_with_python_fallback() on a page-dependent
required_count (skip+limit), causing matched_task_ids/total to differ between
pages; fix by making the fallback deterministic and skip-independent: introduce
a fixed SEARCH_RESULT_FALLBACK_TASK_LIMIT constant (or hardcap value) and always
run _match_subtasks_with_python_fallback() against
task_ids[:SEARCH_RESULT_FALLBACK_TASK_LIMIT] (or pass that cap into the
function) to extend matched_task_ids, then compute ordered_matched_task_ids from
the full task_ids using that final matched_task_ids so total and paging are
stable; update the _collect_matched_task_ids() logic to remove the
skip-dependent gating and reference _match_subtasks_with_python_fallback,
_match_subtasks_in_database, _match_task_titles, and restore_task_order
accordingly.

---

Nitpick comments:
In `@backend/app/services/adapters/wework_conversation_search.py`:
- Around line 119-139: _load_visible_wework_tasks currently loads all
TaskResource rows then filters and causes repeated Task.model_validate passes in
_match_task_titles and _format_task_results; change it to push the title-match
into the DB query (use JSON/->>'title' access on TaskResource.model or the
DB-specific JSON path used elsewhere) and add a bounded candidate set (e.g.
.limit(n) with sensible page size and order_by TaskResource.updated_at.desc())
so we only load a finite recent subset, and additionally introduce an in-memory
cache of parsed Task objects (keyed by TaskResource.id) used by
_match_task_titles and _format_task_results to avoid double Task.model_validate
for the same resource.
🪄 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: 16376d52-4fe0-4145-8fda-eb29e9597199

📥 Commits

Reviewing files that changed from the base of the PR and between c225ca6 and 108ddfe.

📒 Files selected for processing (13)
  • backend/app/api/endpoints/adapter/tasks.py
  • backend/app/services/adapters/wework_conversation_search.py
  • backend/tests/services/adapters/test_task_archive_operations.py
  • wework/src/api/adapters.test.ts
  • wework/src/api/tasks.ts
  • wework/src/components/layout/DesktopSearchDialog.tsx
  • wework/src/components/layout/DesktopSidebar.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.tsx
  • wework/src/features/workbench/WorkbenchProvider.tsx
  • wework/src/i18n/locales/en/common.json
  • wework/src/i18n/locales/zh-CN/common.json
  • wework/src/pages/WorkbenchPage.tsx
✅ Files skipped from review due to trivial changes (1)
  • wework/src/i18n/locales/zh-CN/common.json
🚧 Files skipped from review as they are similar to previous changes (10)
  • wework/src/api/tasks.ts
  • wework/src/api/adapters.test.ts
  • wework/src/components/layout/DesktopSearchDialog.tsx
  • wework/src/i18n/locales/en/common.json
  • wework/src/components/layout/DesktopWorkbenchLayout.tsx
  • backend/tests/services/adapters/test_task_archive_operations.py
  • wework/src/pages/WorkbenchPage.tsx
  • wework/src/features/workbench/WorkbenchProvider.tsx
  • wework/src/components/layout/DesktopSidebar.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.test.tsx

Comment on lines +44 to +53
def search_wework_conversation_tasks(
*,
db: Session,
user_id: int,
keyword: str,
skip: int = 0,
limit: int = 100,
client_origin: str = CLIENT_ORIGIN_WEWORK,
) -> Tuple[List[Dict[str, Any]], int]:
"""Search WeWork-owned tasks by title or conversation content."""

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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add an OpenTelemetry tracing decorator to this entrypoint.

search_wework_conversation_tasks is a public service entrypoint but is not traced. The new endpoint search_wework_conversation_task_list is likewise untraced. As per coding guidelines: "Use OpenTelemetry tracing decorators: @trace_async or @trace_sync for entire functions".

Proposed change
+from shared.telemetry.decorators import trace_sync
+
+@trace_sync("search_wework_conversation_tasks", "tasks.search")
 def search_wework_conversation_tasks(
     *,
     db: Session,
🤖 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 `@backend/app/services/adapters/wework_conversation_search.py` around lines 44
- 53, This function is a public service entrypoint that lacks OpenTelemetry
tracing; add the tracing decorator `@trace_sync` to both
search_wework_conversation_tasks and the new
search_wework_conversation_task_list (they are synchronous), and also add the
corresponding import for trace_sync at the top of the module so the decorator is
available. Ensure the decorator wraps the entire function definition (above def)
so the entrypoints are traced end-to-end.

Comment on lines +66 to +116
matched_task_ids, ordered_matched_task_ids = _collect_matched_task_ids(
db=db,
task_ids=task_ids,
visible_task_by_id=visible_task_by_id,
keyword=keyword,
required_count=skip + limit,
)
id_to_task = {
task_id: visible_task_by_id[task_id]
for task_id in matched_task_ids
if task_id in visible_task_by_id
}
total = len(id_to_task)
filtered_tasks = restore_task_order(
ordered_matched_task_ids[skip : skip + limit], id_to_task, limit
)
if not filtered_tasks:
return [], total

return _format_task_results(db, filtered_tasks, user_id), total


def _collect_matched_task_ids(
*,
db: Session,
task_ids: List[int],
visible_task_by_id: Dict[int, TaskResource],
keyword: str,
required_count: int,
) -> Tuple[set[int], List[int]]:
keyword_lower = keyword.lower()
matched_task_ids = _match_task_titles(visible_task_by_id, keyword_lower)
matched_task_ids.update(_match_subtasks_in_database(db, task_ids, keyword))
ordered_matched_task_ids = [
task_id for task_id in task_ids if task_id in matched_task_ids
]

if len(ordered_matched_task_ids) < required_count:
matched_task_ids.update(
_match_subtasks_with_python_fallback(
db=db,
task_ids=task_ids,
matched_task_ids=matched_task_ids,
keyword_lower=keyword_lower,
)
)
ordered_matched_task_ids = [
task_id for task_id in task_ids if task_id in matched_task_ids
]

return matched_task_ids, ordered_matched_task_ids

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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep --pattern 'def restore_task_order($$$):
  $$$'
rg -nP --type=py -C3 '\brestore_task_order\b'

Repository: wecode-ai/Wegent

Length of output: 6303


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant part of wework_conversation_search.py (around the snippet)
sed -n '1,140p' backend/app/services/adapters/wework_conversation_search.py

# Show the fallback/matching helpers referenced in the snippet
rg -n "def _collect_matched_task_ids|_match_subtasks_in_database|_match_subtasks_with_python_fallback|def _match_task_titles" backend/app/services/adapters/wework_conversation_search.py
sed -n '140,320p' backend/app/services/adapters/wework_conversation_search.py

# Inspect restore_task_order implementation
sed -n '360,460p' backend/app/services/adapters/task_kinds/query_utils.py

Repository: wecode-ai/Wegent

Length of output: 8141


Stabilize pagination by making the matched-task set (and total) independent of skip

  • _collect_matched_task_ids() runs _match_subtasks_with_python_fallback() only when len(ordered_matched_task_ids) < (skip + limit), so matched_task_ids (and therefore total = len(id_to_task)) changes between page 1 and page 2.
  • Since search_wework_conversation_tasks() slices ordered_matched_task_ids[skip : skip + limit] using that page-dependent matched set, tasks can be duplicated or skipped across pages when fallback gets added on later page requests.
  • restore_task_order() does not reorder—it only projects the provided task_ids slice in order until limit, so the instability originates from the fallback gating.

Compute fallback/matched IDs once using a skip-independent criterion (e.g., always apply fallback for the same bounded ID set like task_ids[:SEARCH_RESULT_FALLBACK_TASK_LIMIT], or gate on a fixed cap not based on page offset) and derive both ordered_matched_task_ids and total from that same final matched set.

🤖 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 `@backend/app/services/adapters/wework_conversation_search.py` around lines 66
- 116, The current bug is that _collect_matched_task_ids() gates calling
_match_subtasks_with_python_fallback() on a page-dependent required_count
(skip+limit), causing matched_task_ids/total to differ between pages; fix by
making the fallback deterministic and skip-independent: introduce a fixed
SEARCH_RESULT_FALLBACK_TASK_LIMIT constant (or hardcap value) and always run
_match_subtasks_with_python_fallback() against
task_ids[:SEARCH_RESULT_FALLBACK_TASK_LIMIT] (or pass that cap into the
function) to extend matched_task_ids, then compute ordered_matched_task_ids from
the full task_ids using that final matched_task_ids so total and paging are
stable; update the _collect_matched_task_ids() logic to remove the
skip-dependent gating and reference _match_subtasks_with_python_fallback,
_match_subtasks_in_database, _match_task_titles, and restore_task_order
accordingly.

Comment on lines +168 to +187
def _match_subtasks_in_database(
db: Session,
task_ids: List[int],
keyword: str,
) -> set[int]:
like_pattern = f"%{keyword}%"
rows = (
db.query(Subtask.task_id)
.filter(Subtask.task_id.in_(task_ids))
.filter(
or_(
Subtask.prompt.ilike(like_pattern),
Subtask.error_message.ilike(like_pattern),
cast(Subtask.result, String).ilike(like_pattern),
)
)
.distinct()
.all()
)
return {row[0] for row in rows}

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape LIKE wildcards in the keyword.

% and _ in the user keyword are treated as wildcards inside ilike, so a query like 100% or a_b matches more broadly than the literal text the user typed. Escape them (and set an escape char) before interpolating into like_pattern.

Proposed change
-    like_pattern = f"%{keyword}%"
+    escaped = keyword.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
+    like_pattern = f"%{escaped}%"
     rows = (
         db.query(Subtask.task_id)
         .filter(Subtask.task_id.in_(task_ids))
         .filter(
             or_(
-                Subtask.prompt.ilike(like_pattern),
-                Subtask.error_message.ilike(like_pattern),
-                cast(Subtask.result, String).ilike(like_pattern),
+                Subtask.prompt.ilike(like_pattern, escape="\\"),
+                Subtask.error_message.ilike(like_pattern, escape="\\"),
+                cast(Subtask.result, String).ilike(like_pattern, escape="\\"),
             )
         )
🤖 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 `@backend/app/services/adapters/wework_conversation_search.py` around lines 168
- 187, In _match_subtasks_in_database: escape SQL LIKE wildcards in the incoming
keyword before building like_pattern by first replacing backslashes with double
backslashes then prefixing any '%' and '_' with a backslash (or chosen escape
char), build like_pattern = f"%{escaped_keyword}%", and pass the same escape
char into the ilike calls (e.g., .ilike(like_pattern, escape='\\')) for
Subtask.prompt, Subtask.error_message and cast(Subtask.result, String) so user
input is treated literally.

@qdaxb
qdaxb merged commit a4c4eeb into wecode-ai:main Jun 3, 2026
22 checks passed
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.

2 participants