Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 75 additions & 28 deletions apps/desktop/src/lib/trpc/routers/projects/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

type FolderOutcome =
| { status: "success"; project: Project }
| { status: "needsGitInit"; selectedPath: string }
Expand Down Expand Up @@ -326,40 +362,51 @@ 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 [];
}
}),

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",
],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{ cwd: project.mainRepoPath },
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const raw: unknown = JSON.parse(stdout.trim() || "[]");
return parsePullRequests(raw);
} catch (err) {
console.warn("[searchPullRequests] Failed to search PRs:", err);
return [];
}
}),

listIssues: publicProcedure
.input(z.object({ projectId: z.string() }))
.query(async ({ input }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,8 @@ ${sanitizeText(truncatedBody)}`;
onOpenChange={setPRLinkOpen}
onSelect={setLinkedPR}
projectId={projectId}
githubOwner={null}
repoName={null}
anchorRef={plusMenuRef}
/>
<PromptInputSubmit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,16 @@ import {
CommandList,
} from "@superset/ui/command";
import { Popover, PopoverAnchor, PopoverContent } from "@superset/ui/popover";
import Fuse from "fuse.js";
import type React from "react";
import type { RefObject } from "react";
import { useMemo, useState } from "react";
import { useDebouncedValue } from "renderer/hooks/useDebouncedValue";
import { electronTrpc } from "renderer/lib/electron-trpc";
import {
PRIcon,
type PRState,
} from "renderer/screens/main/components/PRIcon/PRIcon";

const MAX_RESULTS = 20;

export interface SelectedPR {
prNumber: number;
title: string;
Expand All @@ -31,6 +29,8 @@ interface PRLinkCommandProps {
onOpenChange: (open: boolean) => void;
onSelect: (pr: SelectedPR) => void;
projectId: string | null;
githubOwner: string | null;
repoName: string | null;
anchorRef: RefObject<HTMLElement | null>;
}

Expand All @@ -39,56 +39,61 @@ export function PRLinkCommand({
onOpenChange,
onSelect,
projectId,
githubOwner: _githubOwner,
repoName: _repoName,
anchorRef,
}: PRLinkCommandProps) {
const [searchQuery, setSearchQuery] = useState("");
const debouncedQuery = useDebouncedValue(searchQuery, 300);
const trimmedQuery = searchQuery.trim(); // Immediate trim for UI decisions
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
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(
/github\.com\/[\w-]+\/[\w.-]+\/pull\/(\d+)/i,
);
return match ? match[1] : null;
}, [debouncedTrimmed]);

// Use PR number for search if URL was pasted, otherwise use the query as-is
const effectiveQuery = prNumberFromUrl ?? debouncedTrimmed;

const { data: pullRequests, isLoading } =
// 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 && !debouncedTrimmed },
);

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 (use debounced for RPC)
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(() => {
// Use debounced value for mode decision to avoid empty gap
if (debouncedTrimmed) {
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 ?? [];
}, [debouncedTrimmed, searchResults, recentPRs]);

const isLoading = debouncedTrimmed
? isSearching || isPendingDebounce
: 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,
Expand Down Expand Up @@ -117,18 +122,26 @@ export function PRLinkCommand({
onValueChange={setSearchQuery}
/>
<CommandList className="max-h-[280px]">
{searchResults.length === 0 && (
{pullRequests.length === 0 && (
<CommandEmpty>
{isLoading
? "Loading pull requests..."
: "No open pull requests found."}
? debouncedTrimmed
? "Searching..."
: "Loading pull requests..."
: debouncedTrimmed
? "No pull requests found."
: "No open pull requests."}
</CommandEmpty>
)}
{searchResults.length > 0 && (
{pullRequests.length > 0 && (
<CommandGroup
heading={searchQuery ? "Results" : "Open pull requests"}
heading={
debouncedTrimmed
? `${pullRequests.length} result${pullRequests.length === 1 ? "" : "s"}`
: "Recent pull requests"
}
>
{searchResults.map((pr) => (
{pullRequests.map((pr) => (
<CommandItem
key={pr.prNumber}
value={`${pr.prNumber}-${pr.title}`}
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.