From 69ecc0b7e4de5e2a37458ef905741d7c9b32bea6 Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Thu, 26 Mar 2026 10:51:32 -0700 Subject: [PATCH 1/5] fix(desktop): implement server-side PR search in new workspace modal Replace client-side PR search with server-side GitHub search to fix performance and completeness issues in the new workspace modal. Previously: - Fetched all PRs upfront (slow 5-10s initial load) - Limited to 30 PRs total (hardcoded backend limit) - Client-side Fuse.js search only searched fetched PRs - Pasting PR URLs didn't work Now: - Fast initial load: shows 30 recent PRs for browsing (~1-2s) - Unlimited search: server-side GitHub search via gh CLI - Searches ALL PRs in the repo (no limits) - Debounced search (300ms) for better UX - URL detection: extracts PR number from pasted GitHub URLs Changes: - Add searchPullRequests tRPC endpoint using gh pr list --search - Update PRLinkCommand to use conditional queries - Add URL parsing to extract PR numbers from GitHub URLs - Show result count and appropriate loading states Fixes the regression from v1.1.7 where PR search could access all PRs via synced database. This approach works without requiring ElectricSQL sync and provides better performance. --- .../src/lib/trpc/routers/projects/projects.ts | 65 +++++++++++++ .../components/PromptGroup/PromptGroup.tsx | 2 + .../PRLinkCommand/PRLinkCommand.tsx | 92 ++++++++++--------- bun.lock | 2 +- 4 files changed, 117 insertions(+), 44 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index 28ca08d8d98..21c63ea1d5f 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -360,6 +360,71 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { } }), + searchPullRequests: publicProcedure + .input( + z.object({ + projectId: z.string(), + query: z.string(), + }), + ) + .query(async ({ input }) => { + const project = localDb + .select() + .from(projects) + .where(eq(projects.id, input.projectId)) + .get(); + if (!project) return []; + + try { + const { stdout } = await execWithShellEnv( + "gh", + [ + "pr", + "list", + "--search", + input.query, + "--limit", + "100", + "--json", + "number,title,url,state,isDraft", + ], + { cwd: project.mainRepoPath }, + ); + const raw: unknown = JSON.parse(stdout.trim() || "[]"); + if (!Array.isArray(raw)) return []; + return raw + .filter( + ( + item: unknown, + ): item is { + number: number; + title: string; + url: string; + state: string; + isDraft: boolean; + } => + typeof item === "object" && + item !== null && + "number" in item && + "title" in item && + "url" in item, + ) + .map((pr) => ({ + prNumber: pr.number, + title: pr.title, + url: pr.url, + state: pr.isDraft + ? "draft" + : pr.state === "OPEN" + ? "open" + : pr.state.toLowerCase(), + })); + } catch (err) { + console.warn("[searchPullRequests] Failed to search PRs:", err); + return []; + } + }), + listIssues: publicProcedure .input(z.object({ projectId: z.string() })) .query(async ({ input }) => { diff --git a/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx b/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx index 419e682a25b..7509ee2e98f 100644 --- a/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx +++ b/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx @@ -1334,6 +1334,8 @@ ${sanitizeText(truncatedBody)}`; onOpenChange={setPRLinkOpen} onSelect={setLinkedPR} projectId={projectId} + githubOwner={null} + repoName={null} anchorRef={plusMenuRef} /> void; onSelect: (pr: SelectedPR) => void; projectId: string | null; + githubOwner: string | null; + repoName: string | null; anchorRef: RefObject; } @@ -39,56 +39,54 @@ export function PRLinkCommand({ onOpenChange, onSelect, projectId, + githubOwner: _githubOwner, + repoName: _repoName, anchorRef, }: PRLinkCommandProps) { const [searchQuery, setSearchQuery] = useState(""); + const debouncedQuery = useDebouncedValue(searchQuery, 300); + const trimmedQuery = debouncedQuery.trim(); + + // Extract PR number from GitHub URL if pasted + const prNumberFromUrl = useMemo(() => { + const match = trimmedQuery.match( + /github\.com\/[\w-]+\/[\w.-]+\/pull\/(\d+)/i, + ); + return match ? match[1] : null; + }, [trimmedQuery]); - const { data: pullRequests, isLoading } = + // Use PR number for search if URL was pasted, otherwise use the query as-is + const effectiveQuery = prNumberFromUrl ?? trimmedQuery; + + // Fetch recent PRs for browsing (only when no search query) + const { data: recentPRs, isLoading: isLoadingRecent } = electronTrpc.projects.listPullRequests.useQuery( { projectId: projectId ?? "" }, - { enabled: !!projectId && open }, + { enabled: !!projectId && open && !trimmedQuery }, ); - const prsWithSearchField = useMemo( - () => - (pullRequests ?? []).map((pr) => ({ - ...pr, - prNumberStr: String(pr.prNumber), - })), - [pullRequests], - ); - - const prFuse = useMemo( - () => - new Fuse(prsWithSearchField, { - keys: [ - { name: "prNumberStr", weight: 3 }, - { name: "title", weight: 2 }, - ], - threshold: 0.4, - ignoreLocation: true, - }), - [prsWithSearchField], - ); + // Server-side search when user types + const { data: searchResults, isLoading: isSearching } = + electronTrpc.projects.searchPullRequests.useQuery( + { projectId: projectId ?? "", query: effectiveQuery }, + { enabled: !!projectId && open && !!effectiveQuery }, + ); - const searchResults = useMemo(() => { - if (!prsWithSearchField.length) return []; - if (!searchQuery) { - return prsWithSearchField.slice(0, MAX_RESULTS); + const pullRequests = useMemo(() => { + if (trimmedQuery) { + return searchResults ?? []; } - const urlMatch = prsWithSearchField.find((pr) => pr.url === searchQuery); - if (urlMatch) return [urlMatch]; - return prFuse - .search(searchQuery, { limit: MAX_RESULTS }) - .map((r) => r.item); - }, [prsWithSearchField, searchQuery, prFuse]); + return recentPRs ?? []; + }, [trimmedQuery, searchResults, recentPRs]); + + const isLoading = trimmedQuery ? isSearching : isLoadingRecent; const handleClose = () => { setSearchQuery(""); onOpenChange(false); }; - const handleSelect = (pr: (typeof searchResults)[number]) => { + const handleSelect = (pr: (typeof pullRequests)[number]) => { onSelect({ prNumber: pr.prNumber, title: pr.title, @@ -117,18 +115,26 @@ export function PRLinkCommand({ onValueChange={setSearchQuery} /> - {searchResults.length === 0 && ( + {pullRequests.length === 0 && ( {isLoading - ? "Loading pull requests..." - : "No open pull requests found."} + ? trimmedQuery + ? "Searching..." + : "Loading pull requests..." + : trimmedQuery + ? "No pull requests found." + : "No open pull requests."} )} - {searchResults.length > 0 && ( + {pullRequests.length > 0 && ( - {searchResults.map((pr) => ( + {pullRequests.map((pr) => ( Date: Fri, 27 Mar 2026 00:20:22 -0700 Subject: [PATCH 2/5] fix(desktop): use immediate trim for PR search UI decisions Fix stale UI behavior caused by debouncing the trimmed query value. Previously: - trimmedQuery was derived from debouncedQuery.trim() - This caused 300ms delay before UI decisions (show browse/search mode) - Pressing Enter had stale behavior due to delayed trim Now: - trimmedQuery = searchQuery.trim() (immediate, for UI decisions) - debouncedTrimmed = debouncedQuery.trim() (debounced, for RPC calls) - URL detection and search RPC use debounced value - UI state (enabled flags, display logic) use immediate trim - No more stale behavior on Enter or mode switching --- .../components/PRLinkCommand/PRLinkCommand.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx b/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx index 3536ae28df1..195606c8fdb 100644 --- a/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx +++ b/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx @@ -45,27 +45,28 @@ export function PRLinkCommand({ }: PRLinkCommandProps) { const [searchQuery, setSearchQuery] = useState(""); const debouncedQuery = useDebouncedValue(searchQuery, 300); - const trimmedQuery = debouncedQuery.trim(); + const trimmedQuery = searchQuery.trim(); // Immediate trim for UI decisions + const debouncedTrimmed = debouncedQuery.trim(); // Debounced trim for RPC calls - // Extract PR number from GitHub URL if pasted + // Extract PR number from GitHub URL if pasted (use debounced for RPC) const prNumberFromUrl = useMemo(() => { - const match = trimmedQuery.match( + const match = debouncedTrimmed.match( /github\.com\/[\w-]+\/[\w.-]+\/pull\/(\d+)/i, ); return match ? match[1] : null; - }, [trimmedQuery]); + }, [debouncedTrimmed]); // Use PR number for search if URL was pasted, otherwise use the query as-is - const effectiveQuery = prNumberFromUrl ?? trimmedQuery; + const effectiveQuery = prNumberFromUrl ?? debouncedTrimmed; - // Fetch recent PRs for browsing (only when no search query) + // Fetch recent PRs for browsing (only when no search query - use immediate trim) const { data: recentPRs, isLoading: isLoadingRecent } = electronTrpc.projects.listPullRequests.useQuery( { projectId: projectId ?? "" }, { enabled: !!projectId && open && !trimmedQuery }, ); - // Server-side search when user types + // Server-side search when user types (use debounced for RPC) const { data: searchResults, isLoading: isSearching } = electronTrpc.projects.searchPullRequests.useQuery( { projectId: projectId ?? "", query: effectiveQuery }, From 55fb43a86ecc4ab5203256727a3201613bb6e96f Mon Sep 17 00:00:00 2001 From: AviPeltz Date: Sat, 28 Mar 2026 21:04:04 -0700 Subject: [PATCH 3/5] fix(desktop): eliminate 300ms empty results gap in PR search Previously, search mode toggled immediately on trimmedQuery while data fetching was debounced on debouncedTrimmed, causing a 300ms window where the UI showed "No pull requests found" even though results were still loading. Changes: - Add isPendingDebounce state detection (trimmedQuery !== debouncedTrimmed) - Use debouncedTrimmed consistently for all mode decisions and UI display - Include isPendingDebounce in isLoading calculation - Keep trimmedQuery only for pending state detection This ensures the mode doesn't switch until debounced data is ready, eliminating the empty results gap and providing consistent UI behavior. --- .../PRLinkCommand/PRLinkCommand.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx b/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx index 195606c8fdb..4d6c1e4562e 100644 --- a/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx +++ b/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx @@ -48,6 +48,9 @@ export function PRLinkCommand({ const trimmedQuery = searchQuery.trim(); // Immediate trim for UI decisions const debouncedTrimmed = debouncedQuery.trim(); // Debounced trim for RPC calls + // Detect if we're in the pending debounce state + const isPendingDebounce = trimmedQuery !== debouncedTrimmed; + // Extract PR number from GitHub URL if pasted (use debounced for RPC) const prNumberFromUrl = useMemo(() => { const match = debouncedTrimmed.match( @@ -59,11 +62,11 @@ export function PRLinkCommand({ // Use PR number for search if URL was pasted, otherwise use the query as-is const effectiveQuery = prNumberFromUrl ?? debouncedTrimmed; - // Fetch recent PRs for browsing (only when no search query - use immediate trim) + // Fetch recent PRs for browsing (only when no search query) const { data: recentPRs, isLoading: isLoadingRecent } = electronTrpc.projects.listPullRequests.useQuery( { projectId: projectId ?? "" }, - { enabled: !!projectId && open && !trimmedQuery }, + { enabled: !!projectId && open && !debouncedTrimmed }, ); // Server-side search when user types (use debounced for RPC) @@ -74,13 +77,16 @@ export function PRLinkCommand({ ); const pullRequests = useMemo(() => { - if (trimmedQuery) { + // Use debounced value for mode decision to avoid empty gap + if (debouncedTrimmed) { return searchResults ?? []; } return recentPRs ?? []; - }, [trimmedQuery, searchResults, recentPRs]); + }, [debouncedTrimmed, searchResults, recentPRs]); - const isLoading = trimmedQuery ? isSearching : isLoadingRecent; + const isLoading = debouncedTrimmed + ? isSearching || isPendingDebounce + : isLoadingRecent; const handleClose = () => { setSearchQuery(""); @@ -119,10 +125,10 @@ export function PRLinkCommand({ {pullRequests.length === 0 && ( {isLoading - ? trimmedQuery + ? debouncedTrimmed ? "Searching..." : "Loading pull requests..." - : trimmedQuery + : debouncedTrimmed ? "No pull requests found." : "No open pull requests."} @@ -130,7 +136,7 @@ export function PRLinkCommand({ {pullRequests.length > 0 && ( Date: Sun, 29 Mar 2026 01:02:05 -0700 Subject: [PATCH 4/5] refactor(desktop): extract shared PR parsing logic Extract parsePullRequests() helper to eliminate duplication between listPullRequests and searchPullRequests endpoints. Both now use the same filter/map logic for consistent PR shape and state mapping. Benefits: - Single source of truth for PR parsing - Future changes only need updating in one place - Consistent behavior across browse and search modes - Reduced code by 18 lines Resolves cubic.dev P3 refactoring suggestion --- .../src/lib/trpc/routers/projects/projects.ts | 94 ++++++++----------- 1 file changed, 38 insertions(+), 56 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index 21c63ea1d5f..3aee6d192a4 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -58,6 +58,42 @@ type OpenNewResult = | { canceled: false; needsGitInit: true; selectedPath: string } | OpenNewError; +/** + * Parses and transforms raw GitHub PR data from CLI output. + * Filters valid PR objects and maps them to our internal format. + */ +function parsePullRequests(raw: unknown) { + if (!Array.isArray(raw)) return []; + + return raw + .filter( + ( + item: unknown, + ): item is { + number: number; + title: string; + url: string; + state: string; + isDraft: boolean; + } => + typeof item === "object" && + item !== null && + "number" in item && + "title" in item && + "url" in item, + ) + .map((pr) => ({ + prNumber: pr.number, + title: pr.title, + url: pr.url, + state: pr.isDraft + ? "draft" + : pr.state === "OPEN" + ? "open" + : pr.state.toLowerCase(), + })); +} + type FolderOutcome = | { status: "success"; project: Project } | { status: "needsGitInit"; selectedPath: string } @@ -326,34 +362,7 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { { cwd: project.mainRepoPath }, ); const raw: unknown = JSON.parse(stdout.trim() || "[]"); - if (!Array.isArray(raw)) return []; - return raw - .filter( - ( - item: unknown, - ): item is { - number: number; - title: string; - url: string; - state: string; - isDraft: boolean; - } => - typeof item === "object" && - item !== null && - "number" in item && - "title" in item && - "url" in item, - ) - .map((pr) => ({ - prNumber: pr.number, - title: pr.title, - url: pr.url, - state: pr.isDraft - ? "draft" - : pr.state === "OPEN" - ? "open" - : pr.state.toLowerCase(), - })); + return parsePullRequests(raw); } catch (err) { console.warn("[listPullRequests] Failed to list PRs:", err); return []; @@ -391,34 +400,7 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { { cwd: project.mainRepoPath }, ); const raw: unknown = JSON.parse(stdout.trim() || "[]"); - if (!Array.isArray(raw)) return []; - return raw - .filter( - ( - item: unknown, - ): item is { - number: number; - title: string; - url: string; - state: string; - isDraft: boolean; - } => - typeof item === "object" && - item !== null && - "number" in item && - "title" in item && - "url" in item, - ) - .map((pr) => ({ - prNumber: pr.number, - title: pr.title, - url: pr.url, - state: pr.isDraft - ? "draft" - : pr.state === "OPEN" - ? "open" - : pr.state.toLowerCase(), - })); + return parsePullRequests(raw); } catch (err) { console.warn("[searchPullRequests] Failed to search PRs:", err); return []; From cb905d6c02089595de837398709a32e2ab6c7013 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 29 Mar 2026 14:43:28 -0700 Subject: [PATCH 5/5] fix(desktop): address PR search review feedback --- .../src/lib/trpc/routers/projects/projects.ts | 60 +++++++------- .../components/PromptGroup/PromptGroup.tsx | 4 +- .../PRLinkCommand/PRLinkCommand.tsx | 81 ++++++++++++++----- 3 files changed, 97 insertions(+), 48 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index 3aee6d192a4..169fa8cc7d4 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -62,36 +62,38 @@ type OpenNewResult = * Parses and transforms raw GitHub PR data from CLI output. * Filters valid PR objects and maps them to our internal format. */ +function isRawPullRequest(item: unknown): item is { + number: number; + title: string; + url: string; + state: string; + isDraft: boolean; +} { + if (typeof item !== "object" || item === null) return false; + + const value = item as Record; + return ( + typeof value.number === "number" && + typeof value.title === "string" && + typeof value.url === "string" && + typeof value.state === "string" && + typeof value.isDraft === "boolean" + ); +} + function parsePullRequests(raw: unknown) { if (!Array.isArray(raw)) return []; - return raw - .filter( - ( - item: unknown, - ): item is { - number: number; - title: string; - url: string; - state: string; - isDraft: boolean; - } => - typeof item === "object" && - item !== null && - "number" in item && - "title" in item && - "url" in item, - ) - .map((pr) => ({ - prNumber: pr.number, - title: pr.title, - url: pr.url, - state: pr.isDraft - ? "draft" - : pr.state === "OPEN" - ? "open" - : pr.state.toLowerCase(), - })); + return raw.filter(isRawPullRequest).map((pr) => ({ + prNumber: pr.number, + title: pr.title, + url: pr.url, + state: pr.isDraft + ? "draft" + : pr.state === "OPEN" + ? "open" + : pr.state.toLowerCase(), + })); } type FolderOutcome = @@ -390,6 +392,8 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { [ "pr", "list", + "--state", + "all", "--search", input.query, "--limit", @@ -397,7 +401,7 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { "--json", "number,title,url,state,isDraft", ], - { cwd: project.mainRepoPath }, + { cwd: project.mainRepoPath, timeout: 10_000 }, ); const raw: unknown = JSON.parse(stdout.trim() || "[]"); return parsePullRequests(raw); diff --git a/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx b/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx index 7509ee2e98f..79212e5b7bc 100644 --- a/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx +++ b/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx @@ -1334,8 +1334,8 @@ ${sanitizeText(truncatedBody)}`; onOpenChange={setPRLinkOpen} onSelect={setLinkedPR} projectId={projectId} - githubOwner={null} - repoName={null} + githubOwner={project?.githubOwner ?? null} + repoName={project?.mainRepoPath.split("/").pop() ?? null} anchorRef={plusMenuRef} /> ; } +function parseGitHubPullRequestUrl(query: string): { + owner: string; + repo: string; + prNumber: string; +} | null { + const match = query.match( + /^https?:\/\/(?:www\.)?github\.com\/([\w.-]+)\/([\w.-]+)\/pull\/(\d+)(?:[/?#].*)?$/i, + ); + + if (!match) return null; + + return { + owner: match[1], + repo: match[2], + prNumber: match[3], + }; +} + export function PRLinkCommand({ open, onOpenChange, onSelect, projectId, - githubOwner: _githubOwner, - repoName: _repoName, + githubOwner, + repoName, anchorRef, }: PRLinkCommandProps) { const [searchQuery, setSearchQuery] = useState(""); @@ -51,16 +69,32 @@ export function PRLinkCommand({ // Detect if we're in the pending debounce state const isPendingDebounce = trimmedQuery !== debouncedTrimmed; - // Extract PR number from GitHub URL if pasted (use debounced for RPC) - const prNumberFromUrl = useMemo(() => { - const match = debouncedTrimmed.match( - /github\.com\/[\w-]+\/[\w.-]+\/pull\/(\d+)/i, - ); - return match ? match[1] : null; + const parsedPullRequestUrl = useMemo(() => { + return parseGitHubPullRequestUrl(debouncedTrimmed); }, [debouncedTrimmed]); - // Use PR number for search if URL was pasted, otherwise use the query as-is - const effectiveQuery = prNumberFromUrl ?? debouncedTrimmed; + const selectedRepositoryLabel = useMemo(() => { + if (!githubOwner || !repoName) return null; + return `${githubOwner}/${repoName}`; + }, [githubOwner, repoName]); + + const pastedRepository = useMemo(() => { + if (!parsedPullRequestUrl) return null; + return `${parsedPullRequestUrl.owner}/${parsedPullRequestUrl.repo}`.toLowerCase(); + }, [parsedPullRequestUrl]); + + const isCrossRepositoryUrl = Boolean( + selectedRepositoryLabel && + pastedRepository && + pastedRepository !== selectedRepositoryLabel.toLowerCase(), + ); + + // Search by PR number when the pasted URL matches the selected repository. + const effectiveQuery = parsedPullRequestUrl + ? isCrossRepositoryUrl + ? "" + : parsedPullRequestUrl.prNumber + : debouncedTrimmed; // Fetch recent PRs for browsing (only when no search query) const { data: recentPRs, isLoading: isLoadingRecent } = @@ -73,20 +107,29 @@ export function PRLinkCommand({ const { data: searchResults, isLoading: isSearching } = electronTrpc.projects.searchPullRequests.useQuery( { projectId: projectId ?? "", query: effectiveQuery }, - { enabled: !!projectId && open && !!effectiveQuery }, + { + enabled: + !!projectId && open && !!effectiveQuery && !isCrossRepositoryUrl, + }, ); const pullRequests = useMemo(() => { + if (isCrossRepositoryUrl) { + return []; + } + // Use debounced value for mode decision to avoid empty gap if (debouncedTrimmed) { return searchResults ?? []; } return recentPRs ?? []; - }, [debouncedTrimmed, searchResults, recentPRs]); + }, [debouncedTrimmed, isCrossRepositoryUrl, searchResults, recentPRs]); - const isLoading = debouncedTrimmed - ? isSearching || isPendingDebounce - : isLoadingRecent; + const isLoading = isCrossRepositoryUrl + ? false + : debouncedTrimmed + ? isSearching || isPendingDebounce + : isLoadingRecent; const handleClose = () => { setSearchQuery(""); @@ -128,9 +171,11 @@ export function PRLinkCommand({ ? debouncedTrimmed ? "Searching..." : "Loading pull requests..." - : debouncedTrimmed - ? "No pull requests found." - : "No open pull requests."} + : isCrossRepositoryUrl + ? `PR URL must match ${selectedRepositoryLabel}.` + : debouncedTrimmed + ? "No pull requests found." + : "No open pull requests."} )} {pullRequests.length > 0 && (