Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
61 changes: 61 additions & 0 deletions apps/desktop/src/lib/trpc/routers/projects/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
sanitizeAuthorPrefix,
} from "../workspaces/utils/git";
import { getSimpleGitWithShellPath } from "../workspaces/utils/git-client";
import { execWithShellEnv } from "../workspaces/utils/shell-env";
import { getDefaultProjectColor } from "./utils/colors";
import { discoverAndSaveProjectIcon } from "./utils/favicon-discovery";
import { fetchGitHubOwner, getGitHubAvatarUrl } from "./utils/github";
Expand Down Expand Up @@ -296,6 +297,66 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
.all();
}),

listPullRequests: publicProcedure
.input(z.object({ projectId: 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",
"--state",
"open",
"--limit",
"30",
"--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(),
}));
Comment on lines +325 to +353
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

🧩 Analysis chain

🌐 Web query:

In GitHub CLI docs for gh pr list --json, are stateandisDraft guaranteed to be present and non-null for each returned PR item?

💡 Result:

GitHub CLI’s gh pr list manual lists state and isDraft as valid --json fields, but it does not explicitly state any guarantee about those fields being “always present and non-null” in the output. [1]

That said, those fields come from GitHub’s PullRequest GraphQL object, where both are defined as non-nullable (isDraft: Boolean! and state: PullRequestState!). So, when gh pr list --json state,isDraft succeeds, each returned PR item should include them with non-null values (otherwise the GraphQL response would error rather than yield null for those fields). [2]

Answer: Not guaranteed by the gh docs themselves, but effectively yes on successful output, because the underlying GraphQL fields are non-nullable. [1][2]


🏁 Script executed:

# Check if zod is a dependency
fd package.json --exec cat {} \; | grep -A 50 '"dependencies"' | grep -i zod || echo "Checking root package.json..."
cat package.json | grep -A 100 '"dependencies"' | grep -i zod

Repository: superset-sh/superset

Length of output: 460


🏁 Script executed:

# Search for existing zod usage in the codebase
rg 'from.*zod|import.*z.*from' --type ts --type tsx | head -20

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

# Check the actual file and context around the problematic code
sed -n '320,360p' apps/desktop/src/lib/trpc/routers/projects/projects.ts

Repository: superset-sh/superset

Length of output: 1018


🏁 Script executed:

# Search for other gh command usages in projects.ts to see if there's a pattern
rg -B5 -A15 'gh.*pr\|gh.*issue' apps/desktop/src/lib/trpc/routers/projects/projects.ts | head -80

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

# Check for zod schema usage patterns in the desktop app
rg 'z\.object|z\.string|z\.number|z\.boolean' apps/desktop/src --max-count=10 -A2

Repository: superset-sh/superset

Length of output: 50376


Harden PR payload validation to match type guard declarations.

The type predicate declares state and isDraft as required fields (lines 331–337), but the filter only validates the presence of number, title, and url. This creates a type safety gap where pr.state could be undefined when .toLowerCase() is called on line 352, despite the type guard suggesting otherwise.

While GitHub's CLI returns non-nullable fields in practice (GraphQL guarantees), the code should validate all declared fields for consistency. Consider using a zod schema to ensure complete validation:

Proposed fix
+const PullRequestItemSchema = z.object({
+	number: z.number().int(),
+	title: z.string(),
+	url: z.string(),
+	state: z.string(),
+	isDraft: z.boolean(),
+});
+
 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) => ({
+const parsed = z.array(PullRequestItemSchema).safeParse(raw);
+if (!parsed.success) return [];
+return parsed.data.map((pr) => ({
 		prNumber: pr.number,
 		title: pr.title,
 		url: pr.url,
 		state: pr.isDraft
 			? "draft"
 			: pr.state === "OPEN"
 				? "open"
 				: pr.state.toLowerCase(),
 	}));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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(),
}));
const PullRequestItemSchema = z.object({
number: z.number().int(),
title: z.string(),
url: z.string(),
state: z.string(),
isDraft: z.boolean(),
});
const raw: unknown = JSON.parse(stdout.trim() || "[]");
const parsed = z.array(PullRequestItemSchema).safeParse(raw);
if (!parsed.success) return [];
return parsed.data.map((pr) => ({
prNumber: pr.number,
title: pr.title,
url: pr.url,
state: pr.isDraft
? "draft"
: pr.state === "OPEN"
? "open"
: pr.state.toLowerCase(),
}));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/projects/projects.ts` around lines 325 -
353, The filter/type-guard for parsed CLI output (variable raw) currently only
checks number/title/url but the map assumes pr.state and pr.isDraft exist,
causing a potential runtime error; update the guard used in the Array.filter (or
replace it with a zod schema) to also validate that "state" is a string and
"isDraft" is a boolean before mapping, or run a zod.parse on raw to guarantee
all fields, then map using pr.state and pr.isDraft safely.

} catch (err) {
console.warn("[listPullRequests] Failed to list PRs:", err);
return [];
}
}),

selectDirectory: publicProcedure
.input(
z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ export function NewWorkspaceModal() {
<Dialog open={isOpen} onOpenChange={(open) => !open && closeModal()}>
<DialogHeader className="sr-only">
<DialogTitle>New Workspace</DialogTitle>
<DialogDescription>
Create a new workspace from a PR, branch, issue, or prompt.
</DialogDescription>
<DialogDescription>Create a new workspace</DialogDescription>
</DialogHeader>
<DialogContent
showCloseButton={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,46 @@ import { useCreateWorkspace } from "renderer/react-query/workspaces/useCreateWor
import { useOpenExternalWorktree } from "renderer/react-query/workspaces/useOpenExternalWorktree";
import { useOpenTrackedWorktree } from "renderer/react-query/workspaces/useOpenTrackedWorktree";

export type NewWorkspaceModalTab =
| "prompt"
| "issues"
| "pull-requests"
| "branches";
export type LinkedIssue = {
slug: string;
title: string;
};

export type LinkedPR = {
prNumber: number;
title: string;
url: string;
state: string;
};

export interface NewWorkspaceModalDraft {
activeTab: NewWorkspaceModalTab;
selectedProjectId: string | null;
prompt: string;
branchName: string;
branchNameEdited: boolean;
baseBranch: string | null;
showAdvanced: boolean;
runSetupScript: boolean;
branchSearch: string;
issuesQuery: string;
pullRequestsQuery: string;
branchesQuery: string;
workspaceName: string;
workspaceNameEdited: boolean;
branchName: string;
branchNameEdited: boolean;
linkedIssues: LinkedIssue[];
linkedPR: LinkedPR | null;
}

interface NewWorkspaceModalDraftState extends NewWorkspaceModalDraft {
draftVersion: number;
}

const initialDraft: NewWorkspaceModalDraft = {
activeTab: "prompt",
selectedProjectId: null,
prompt: "",
branchName: "",
branchNameEdited: false,
baseBranch: null,
showAdvanced: false,
runSetupScript: true,
branchSearch: "",
issuesQuery: "",
pullRequestsQuery: "",
branchesQuery: "",
workspaceName: "",
workspaceNameEdited: false,
branchName: "",
branchNameEdited: false,
linkedIssues: [],
linkedPR: null,
};

function buildInitialDraftState(): NewWorkspaceModalDraftState {
Expand Down Expand Up @@ -151,18 +153,16 @@ export function NewWorkspaceModalDraftProvider({
const value = useMemo<NewWorkspaceModalDraftContextValue>(
() => ({
draft: {
activeTab: state.activeTab,
selectedProjectId: state.selectedProjectId,
prompt: state.prompt,
branchName: state.branchName,
branchNameEdited: state.branchNameEdited,
baseBranch: state.baseBranch,
showAdvanced: state.showAdvanced,
runSetupScript: state.runSetupScript,
branchSearch: state.branchSearch,
issuesQuery: state.issuesQuery,
pullRequestsQuery: state.pullRequestsQuery,
branchesQuery: state.branchesQuery,
workspaceName: state.workspaceName,
workspaceNameEdited: state.workspaceNameEdited,
branchName: state.branchName,
branchNameEdited: state.branchNameEdited,
linkedIssues: state.linkedIssues,
linkedPR: state.linkedPR,
},
draftVersion: state.draftVersion,
closeModal: onClose,
Expand Down

This file was deleted.

Loading
Loading