feat: add GitHub issue and PR context picker to new workspace screen
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user