feat: add GitHub issue and PR context picker to new workspace screen

This commit is contained in:
Mohamed Boudra
2026-04-09 18:54:25 +07:00
parent 397d454de0
commit 7a9348a5fd
7 changed files with 404 additions and 9 deletions
@@ -30,6 +30,7 @@ import type {
CheckoutPrStatusResponse,
ValidateBranchResponse,
BranchSuggestionsResponse,
GitHubSearchResponse,
DirectorySuggestionsResponse,
PaseoWorktreeListResponse,
PaseoWorktreeArchiveResponse,
@@ -225,6 +226,7 @@ type CheckoutPrCreatePayload = CheckoutPrCreateResponse["payload"];
type CheckoutPrStatusPayload = CheckoutPrStatusResponse["payload"];
type ValidateBranchPayload = ValidateBranchResponse["payload"];
type BranchSuggestionsPayload = BranchSuggestionsResponse["payload"];
type GitHubSearchPayload = GitHubSearchResponse["payload"];
type DirectorySuggestionsPayload = DirectorySuggestionsResponse["payload"];
type PaseoWorktreeListPayload = PaseoWorktreeListResponse["payload"];
type PaseoWorktreeArchivePayload = PaseoWorktreeArchiveResponse["payload"];
@@ -2495,6 +2497,23 @@ export class DaemonClient {
});
}
async searchGitHub(
options: { cwd: string; query: string; limit?: number },
requestId?: string,
): Promise<GitHubSearchPayload> {
return this.sendCorrelatedSessionRequest({
requestId,
message: {
type: "github_search_request",
cwd: options.cwd,
query: options.query,
limit: options.limit,
},
responseType: "github_search_response",
timeout: 15000,
});
}
async getDirectorySuggestions(
options: {
query: string;
+35
View File
@@ -147,6 +147,7 @@ import {
pushCurrentBranch,
createPullRequest,
getPullRequestStatus,
searchGitHubIssuesAndPrs,
warmCheckoutShortstatInBackground,
} from "../utils/checkout-git.js";
import { getProjectIcon } from "../utils/project-icon.js";
@@ -1619,6 +1620,10 @@ export class Session {
await this.handleBranchSuggestionsRequest(msg);
break;
case "github_search_request":
await this.handleGitHubSearchRequest(msg);
break;
case "directory_suggestions_request":
await this.handleDirectorySuggestionsRequest(msg);
break;
@@ -4034,6 +4039,36 @@ export class Session {
}
}
private async handleGitHubSearchRequest(
msg: Extract<SessionInboundMessage, { type: "github_search_request" }>,
): Promise<void> {
const { cwd, query, limit, requestId } = msg;
try {
const resolvedCwd = expandTilde(cwd);
const result = await searchGitHubIssuesAndPrs(resolvedCwd, query, limit);
this.emit({
type: "github_search_response",
payload: {
items: result.items,
githubFeaturesEnabled: result.githubFeaturesEnabled,
error: null,
requestId,
},
});
} catch (error) {
this.emit({
type: "github_search_response",
payload: {
items: [],
githubFeaturesEnabled: true,
error: error instanceof Error ? error.message : String(error),
requestId,
},
});
}
}
private async handleDirectorySuggestionsRequest(msg: DirectorySuggestionsRequest): Promise<void> {
const { query, limit, requestId, cwd, includeFiles, includeDirectories } = msg;
+33
View File
@@ -1073,6 +1073,24 @@ export const BranchSuggestionsRequestSchema = z.object({
requestId: z.string(),
});
export const GitHubSearchItemSchema = z.object({
kind: z.enum(["issue", "pr"]),
number: z.number(),
title: z.string(),
url: z.string(),
state: z.string(),
body: z.string().nullable(),
labels: z.array(z.string()),
});
export const GitHubSearchRequestSchema = z.object({
type: z.literal("github_search_request"),
cwd: z.string(),
query: z.string(),
limit: z.number().int().min(1).max(50).optional(),
requestId: z.string(),
});
export const DirectorySuggestionsRequestSchema = z.object({
type: z.literal("directory_suggestions_request"),
query: z.string(),
@@ -1400,6 +1418,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
CheckoutPrStatusRequestSchema,
ValidateBranchRequestSchema,
BranchSuggestionsRequestSchema,
GitHubSearchRequestSchema,
DirectorySuggestionsRequestSchema,
PaseoWorktreeListRequestSchema,
PaseoWorktreeArchiveRequestSchema,
@@ -2322,6 +2341,16 @@ export const BranchSuggestionsResponseSchema = z.object({
}),
});
export const GitHubSearchResponseSchema = z.object({
type: z.literal("github_search_response"),
payload: z.object({
items: z.array(GitHubSearchItemSchema),
githubFeaturesEnabled: z.boolean(),
error: z.string().nullable(),
requestId: z.string(),
}),
});
export const DirectorySuggestionsResponseSchema = z.object({
type: z.literal("directory_suggestions_response"),
payload: z.object({
@@ -2696,6 +2725,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
CheckoutPrStatusResponseSchema,
ValidateBranchResponseSchema,
BranchSuggestionsResponseSchema,
GitHubSearchResponseSchema,
DirectorySuggestionsResponseSchema,
PaseoWorktreeListResponseSchema,
PaseoWorktreeArchiveResponseSchema,
@@ -2923,6 +2953,9 @@ export type ValidateBranchRequest = z.infer<typeof ValidateBranchRequestSchema>;
export type ValidateBranchResponse = z.infer<typeof ValidateBranchResponseSchema>;
export type BranchSuggestionsRequest = z.infer<typeof BranchSuggestionsRequestSchema>;
export type BranchSuggestionsResponse = z.infer<typeof BranchSuggestionsResponseSchema>;
export type GitHubSearchItem = z.infer<typeof GitHubSearchItemSchema>;
export type GitHubSearchRequest = z.infer<typeof GitHubSearchRequestSchema>;
export type GitHubSearchResponse = z.infer<typeof GitHubSearchResponseSchema>;
export type DirectorySuggestionsRequest = z.infer<typeof DirectorySuggestionsRequestSchema>;
export type DirectorySuggestionsResponse = z.infer<typeof DirectorySuggestionsResponseSchema>;
export type PaseoWorktreeListRequest = z.infer<typeof PaseoWorktreeListRequestSchema>;
+107
View File
@@ -2297,3 +2297,110 @@ async function getPullRequestStatusUncached(cwd: string): Promise<PullRequestSta
throw error;
}
}
export interface GitHubSearchResult {
items: Array<{
kind: "issue" | "pr";
number: number;
title: string;
url: string;
state: string;
body: string | null;
labels: string[];
}>;
githubFeaturesEnabled: boolean;
}
export async function searchGitHubIssuesAndPrs(
cwd: string,
query: string,
limit = 20,
): Promise<GitHubSearchResult> {
await requireGitRepo(cwd);
let ghPath: string;
try {
ghPath = resolveGhPath();
} catch {
return { items: [], githubFeaturesEnabled: false };
}
try {
const [issuesResult, prsResult] = await Promise.allSettled([
execFileAsync(
ghPath,
[
"issue",
"list",
"--search",
query,
"--json",
"number,title,url,state,body,labels",
"--limit",
String(limit),
],
{ cwd, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } },
),
execFileAsync(
ghPath,
[
"pr",
"list",
"--search",
query,
"--json",
"number,title,url,state,body,labels",
"--limit",
String(limit),
],
{ cwd, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } },
),
]);
const items: GitHubSearchResult["items"] = [];
if (issuesResult.status === "fulfilled") {
const parsed = JSON.parse(issuesResult.value.stdout.trim() || "[]");
if (Array.isArray(parsed)) {
for (const item of parsed) {
items.push({
kind: "issue",
number: item.number,
title: item.title ?? "",
url: item.url ?? "",
state: item.state ?? "",
body: item.body ?? null,
labels: Array.isArray(item.labels)
? item.labels.map((l: { name?: string }) => l.name ?? "").filter(Boolean)
: [],
});
}
}
}
if (prsResult.status === "fulfilled") {
const parsed = JSON.parse(prsResult.value.stdout.trim() || "[]");
if (Array.isArray(parsed)) {
for (const item of parsed) {
items.push({
kind: "pr",
number: item.number,
title: item.title ?? "",
url: item.url ?? "",
state: item.state ?? "",
body: item.body ?? null,
labels: Array.isArray(item.labels)
? item.labels.map((l: { name?: string }) => l.name ?? "").filter(Boolean)
: [],
});
}
}
}
return { items, githubFeaturesEnabled: true };
} catch (error) {
if (isGhAuthError(error)) {
return { items: [], githubFeaturesEnabled: false };
}
throw error;
}
}