From fcab77051806097978b080aaa37980c3498c12f9 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 11 Apr 2026 06:49:23 -0500 Subject: [PATCH] Add inbox issue search fallback --- ui/src/lib/inbox.test.ts | 60 +++++++++++++++++++++++++++++++++++++++ ui/src/lib/inbox.ts | 25 ++++++++++++++++ ui/src/pages/Inbox.tsx | 61 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 140 insertions(+), 6 deletions(-) diff --git a/ui/src/lib/inbox.test.ts b/ui/src/lib/inbox.test.ts index 2bd2d74b..002946b4 100644 --- a/ui/src/lib/inbox.test.ts +++ b/ui/src/lib/inbox.test.ts @@ -20,6 +20,7 @@ import { getApprovalsForTab, getInboxWorkItems, getInboxKeyboardSelectionIndex, + getInboxSearchFallbackIssues, getRecentTouchedIssues, getUnreadTouchedIssues, groupInboxWorkItems, @@ -611,6 +612,65 @@ describe("inbox helpers", () => { ).toEqual(["newer", "older"]); }); + it("uses remote issue results only when local inbox search has no matches", () => { + const remoteMatch = makeIssue("remote-match", false); + remoteMatch.status = "in_progress"; + + expect( + getInboxSearchFallbackIssues({ + query: "pull/3303", + filteredWorkItems: [], + archivedSearchIssues: [], + remoteIssues: [remoteMatch], + issueFilters: { + statuses: ["in_progress"], + priorities: [], + assignees: [], + labels: [], + projects: [], + workspaces: [], + showRoutineExecutions: false, + }, + }).map((issue) => issue.id), + ).toEqual(["remote-match"]); + + expect( + getInboxSearchFallbackIssues({ + query: "pull/3303", + filteredWorkItems: [{ kind: "issue", timestamp: 1, issue: makeIssue("local", false) }], + archivedSearchIssues: [], + remoteIssues: [remoteMatch], + issueFilters: { + statuses: [], + priorities: [], + assignees: [], + labels: [], + projects: [], + workspaces: [], + showRoutineExecutions: false, + }, + }), + ).toEqual([]); + + expect( + getInboxSearchFallbackIssues({ + query: "pull/3303", + filteredWorkItems: [], + archivedSearchIssues: [makeIssue("archived", false)], + remoteIssues: [remoteMatch], + issueFilters: { + statuses: [], + priorities: [], + assignees: [], + labels: [], + projects: [], + workspaces: [], + showRoutineExecutions: false, + }, + }), + ).toEqual([]); + }); + it("defaults the remembered inbox tab to mine and persists all", () => { localStorage.clear(); expect(loadLastInboxTab()).toBe("mine"); diff --git a/ui/src/lib/inbox.ts b/ui/src/lib/inbox.ts index 5e496ee4..f31adf6e 100644 --- a/ui/src/lib/inbox.ts +++ b/ui/src/lib/inbox.ts @@ -7,6 +7,7 @@ import type { JoinRequest, } from "@paperclipai/shared"; import { + applyIssueFilters, defaultIssueFilterState, type IssueFilterState, } from "./issue-filters"; @@ -370,6 +371,30 @@ export function getArchivedInboxSearchIssues({ .sort(sortIssuesByMostRecentActivity); } +export function getInboxSearchFallbackIssues({ + query, + filteredWorkItems, + archivedSearchIssues, + remoteIssues, + issueFilters, + currentUserId, + enableRoutineVisibilityFilter = false, +}: { + query: string; + filteredWorkItems: InboxWorkItem[]; + archivedSearchIssues: Issue[]; + remoteIssues: Issue[]; + issueFilters: IssueFilterState; + currentUserId?: string | null; + enableRoutineVisibilityFilter?: boolean; +}): Issue[] { + const normalizedQuery = query.trim(); + if (!normalizedQuery) return []; + if (filteredWorkItems.length > 0) return []; + if (archivedSearchIssues.length > 0) return []; + return applyIssueFilters(remoteIssues, issueFilters, currentUserId, enableRoutineVisibilityFilter); +} + export function resolveIssueWorkspaceName( issue: Pick, { diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index b28938ca..30891363 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -98,6 +98,7 @@ import { getArchivedInboxSearchIssues, getInboxWorkItems, getInboxKeyboardSelectionIndex, + getInboxSearchFallbackIssues, getLatestFailedRunsByAgent, matchesInboxIssueSearch, getRecentTouchedIssues, @@ -642,6 +643,7 @@ export function Inbox() { retry: false, }); const [searchQuery, setSearchQuery] = useState(""); + const normalizedSearchQuery = searchQuery.trim(); const [filterPreferences, setFilterPreferences] = useState( () => loadInboxFilterPreferences(selectedCompanyId), ); @@ -945,7 +947,7 @@ export function Inbox() { ); const filteredWorkItems = useMemo(() => { - const q = searchQuery.trim().toLowerCase(); + const q = normalizedSearchQuery.toLowerCase(); if (!q) return workItemsToRender; return workItemsToRender.filter((item) => { if (item.kind === "issue") { @@ -987,12 +989,12 @@ export function Inbox() { }); }, [ workItemsToRender, - searchQuery, agentById, defaultProjectWorkspaceIdByProjectId, executionWorkspaceById, issueById, isolatedWorkspacesEnabled, + normalizedSearchQuery, projectWorkspaceById, ]); @@ -1002,7 +1004,7 @@ export function Inbox() { ? getArchivedInboxSearchIssues({ visibleIssues: visibleMineIssues, searchableIssues: visibleTouchedIssues, - query: searchQuery, + query: normalizedSearchQuery, isolatedWorkspacesEnabled, executionWorkspaceById, projectWorkspaceById, @@ -1013,13 +1015,60 @@ export function Inbox() { defaultProjectWorkspaceIdByProjectId, executionWorkspaceById, isolatedWorkspacesEnabled, + normalizedSearchQuery, projectWorkspaceById, - searchQuery, tab, visibleMineIssues, visibleTouchedIssues, ], ); + const shouldUseIssueSearchFallback = + !!selectedCompanyId + && normalizedSearchQuery.length > 0 + && filteredWorkItems.length === 0 + && archivedSearchIssues.length === 0; + const { data: remoteIssueSearchResults = [] } = useQuery({ + queryKey: [ + ...queryKeys.issues.search(selectedCompanyId!, normalizedSearchQuery, undefined, 25), + "inbox-fallback", + issueFilters, + ], + queryFn: () => + issuesApi.list(selectedCompanyId!, { + q: normalizedSearchQuery, + limit: 25, + includeRoutineExecutions: true, + }), + enabled: shouldUseIssueSearchFallback, + placeholderData: (previousData) => previousData, + }); + const issueSearchFallbackResults = useMemo( + () => + getInboxSearchFallbackIssues({ + query: normalizedSearchQuery, + filteredWorkItems, + archivedSearchIssues, + remoteIssues: remoteIssueSearchResults, + issueFilters, + currentUserId, + enableRoutineVisibilityFilter: true, + }), + [ + archivedSearchIssues, + currentUserId, + filteredWorkItems, + issueFilters, + normalizedSearchQuery, + remoteIssueSearchResults, + ], + ); + const effectiveWorkItems = useMemo( + () => + issueSearchFallbackResults.length > 0 + ? getInboxWorkItems({ issues: issueSearchFallbackResults, approvals: [] }) + : filteredWorkItems, + [filteredWorkItems, issueSearchFallbackResults], + ); const archivedSearchIssueIds = useMemo( () => new Set(archivedSearchIssues.map((issue) => issue.id)), [archivedSearchIssues], @@ -1037,14 +1086,14 @@ export function Inbox() { }, []); const [collapsedInboxParents, setCollapsedInboxParents] = useState>(new Set()); const groupedSections = useMemo(() => [ - ...buildGroupedInboxSections(filteredWorkItems, groupBy, nestingEnabled), + ...buildGroupedInboxSections(effectiveWorkItems, groupBy, nestingEnabled), ...buildGroupedInboxSections( getInboxWorkItems({ issues: archivedSearchIssues, approvals: [] }), groupBy, nestingEnabled, { keyPrefix: "archived-search:", isArchivedSearch: true }, ), - ], [archivedSearchIssues, filteredWorkItems, groupBy, nestingEnabled]); + ], [archivedSearchIssues, effectiveWorkItems, groupBy, nestingEnabled]); const totalVisibleWorkItems = useMemo( () => groupedSections.reduce((count, group) => count + group.displayItems.length, 0), [groupedSections],