feat: server-side GitHub search for issue/PR pickers (#1352)

Replace local-only filtering in GitHub issue/PR picker dialogs with
server-side GitHub Search API queries. Search text is sent as a query
parameter to the server, which uses the GitHub Search API
(issuesAndPullRequests endpoint) with repo: qualifiers including fork
network support. Results are debounced at 350ms to respect API rate
limits.

- Add query parameter to GitHubAPI issuesList/prsList interface
- Server routes use Search API when query is present, standard list
  endpoint when absent
- Fork networks handled via repo:owner/repo OR repo:owner/upstream
- PR search fetches full PR details after Search API for head/base/draft
  fields
- Remove local filter memos from all three picker dialogs
- Add debounced search effect with abort controller cleanup
- Update VS Code backend and webview API for parity
- Update search placeholders in all locales

Closes #1350

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Tom Rochette
2026-06-08 15:37:42 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 5589ca991a
commit 7b1b3167a4
16 changed files with 457 additions and 149 deletions
+65
View File
@@ -285,12 +285,77 @@ export const listPullRequests = async (
accessToken: string,
directory: string,
page: number = 1,
searchQuery?: string,
): Promise<GitHubPullRequestsListResult> => {
const repo = await resolveRepoFromDirectory(directory);
if (!repo) {
return { connected: true, repo: null, prs: [] };
}
if (searchQuery) {
const q = `repo:${repo.owner}/${repo.repo} ${searchQuery} type:pr state:open`;
const searchUrl = new URL(`${API_BASE}/search/issues`);
searchUrl.searchParams.set('q', q);
searchUrl.searchParams.set('per_page', '50');
searchUrl.searchParams.set('page', String(page));
const searchResp = await githubFetch(searchUrl.toString(), accessToken);
if (searchResp.status === 401) return { connected: false };
const searchJson = await jsonOrNull<{ total_count?: number; items?: unknown[] }>(searchResp);
if (!searchResp.ok || !searchJson) {
throw new Error('Failed to search PRs');
}
const totalCount = typeof searchJson.total_count === 'number' ? searchJson.total_count : 0;
const searchItems = Array.isArray(searchJson.items) ? searchJson.items : [];
const prNumbers = searchItems.map((item) => {
const rec = item && typeof item === 'object' ? (item as JsonRecord) : null;
return typeof rec?.number === 'number' ? rec.number : 0;
}).filter((n) => n > 0);
let prs: GitHubPullRequestSummary[] = [];
if (prNumbers.length > 0) {
const entries = await Promise.all(prNumbers.map(async (number) => {
const prUrl = new URL(`${API_BASE}/repos/${repo.owner}/${repo.repo}/pulls/${number}`);
const prResp = await githubFetch(prUrl.toString(), accessToken);
if (prResp.status === 401) return 'unauthorized' as const;
if (!prResp.ok) return null;
return jsonOrNull<JsonRecord>(prResp);
}));
if (entries.includes('unauthorized')) return { connected: false };
prs = entries
.filter((entry): entry is JsonRecord => Boolean(entry) && entry !== 'unauthorized')
.map((rec) => {
const number = typeof rec?.number === 'number' ? rec.number : 0;
const mergedAt = readString(rec?.merged_at);
const stateRaw = readString(rec?.state);
const state = mergedAt ? 'merged' : (stateRaw === 'closed' ? 'closed' : 'open');
const base = rec?.base && typeof rec.base === 'object' ? (rec.base as JsonRecord) : null;
const head = rec?.head && typeof rec.head === 'object' ? (rec.head as JsonRecord) : null;
return {
number,
title: readString(rec?.title) || '',
url: readString(rec?.html_url) || '',
state,
draft: Boolean(rec?.draft),
base: readString(base?.ref) || '',
head: readString(head?.ref) || '',
headSha: readString(head?.sha) || undefined,
mergeable: typeof rec?.mergeable === 'boolean' ? rec.mergeable : null,
mergeableState: readString(rec?.mergeable_state) || undefined,
author: mapUser(rec?.user),
headLabel: readString(head?.label) || undefined,
headRepo: mapHeadRepo(head?.repo),
} as GitHubPullRequestSummary;
});
}
const fetchedCount = (page - 1) * 50 + searchItems.length;
const hasMore = fetchedCount < totalCount;
return { connected: true, repo, prs, page, hasMore };
}
const url = new URL(`${API_BASE}/repos/${repo.owner}/${repo.repo}/pulls`);
url.searchParams.set('state', 'open');
url.searchParams.set('per_page', '50');