feat: add GitHub issue and PR context picker to new workspace screen
This commit is contained in:
@@ -63,6 +63,8 @@ interface ComposerProps {
|
||||
isInputActive: boolean;
|
||||
onSubmitMessage?: (payload: MessagePayload) => Promise<void>;
|
||||
allowEmptySubmit?: boolean;
|
||||
/** Label to show on the submit button when the input is empty and allowEmptySubmit is true. */
|
||||
emptySubmitLabel?: string;
|
||||
/** Externally controlled loading state. When true, disables the submit button. */
|
||||
isSubmitLoading?: boolean;
|
||||
/** When true, blurs the input immediately when submitting. */
|
||||
@@ -101,6 +103,7 @@ export function Composer({
|
||||
isInputActive,
|
||||
onSubmitMessage,
|
||||
allowEmptySubmit = false,
|
||||
emptySubmitLabel,
|
||||
isSubmitLoading = false,
|
||||
blurOnSubmit = false,
|
||||
value,
|
||||
@@ -737,6 +740,7 @@ export function Composer({
|
||||
onChangeText={setUserInput}
|
||||
onSubmit={handleSubmit}
|
||||
allowEmptySubmit={allowEmptySubmit}
|
||||
emptySubmitLabel={emptySubmitLabel}
|
||||
isSubmitDisabled={isProcessing || isSubmitLoading}
|
||||
isSubmitLoading={isProcessing || isSubmitLoading}
|
||||
images={selectedImages}
|
||||
|
||||
@@ -63,6 +63,8 @@ export interface MessageInputProps {
|
||||
onChangeText: (text: string) => void;
|
||||
onSubmit: (payload: MessagePayload) => void;
|
||||
allowEmptySubmit?: boolean;
|
||||
/** Label to show on the submit button when the input is empty and allowEmptySubmit is true. */
|
||||
emptySubmitLabel?: string;
|
||||
isSubmitDisabled?: boolean;
|
||||
isSubmitLoading?: boolean;
|
||||
images?: ImageAttachment[];
|
||||
@@ -195,6 +197,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
||||
onChangeText,
|
||||
onSubmit,
|
||||
allowEmptySubmit = false,
|
||||
emptySubmitLabel,
|
||||
isSubmitDisabled = false,
|
||||
isSubmitLoading = false,
|
||||
images = [],
|
||||
@@ -558,7 +561,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
||||
|
||||
const handleSendMessage = useCallback(() => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed && images.length === 0) return;
|
||||
if (!trimmed && images.length === 0 && !allowEmptySubmit) return;
|
||||
const payload = {
|
||||
text: trimmed,
|
||||
images: images.length > 0 ? images : undefined,
|
||||
@@ -568,7 +571,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
||||
inputHeightRef.current = MIN_INPUT_HEIGHT;
|
||||
setInputHeight(MIN_INPUT_HEIGHT);
|
||||
onHeightChange?.(MIN_INPUT_HEIGHT);
|
||||
}, [value, images, onSubmit, isAgentRunning, onHeightChange]);
|
||||
}, [value, images, onSubmit, isAgentRunning, onHeightChange, allowEmptySubmit]);
|
||||
|
||||
const handleQueueMessage = useCallback(() => {
|
||||
if (!onQueue) return;
|
||||
@@ -894,8 +897,10 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
||||
}
|
||||
|
||||
const hasImages = images.length > 0;
|
||||
const hasSendableContent = value.trim().length > 0 || hasImages || allowEmptySubmit;
|
||||
const hasRealContent = value.trim().length > 0 || hasImages;
|
||||
const hasSendableContent = hasRealContent || allowEmptySubmit;
|
||||
const shouldShowSendButton = hasSendableContent || isSubmitLoading;
|
||||
const showEmptySubmitLabel = allowEmptySubmit && emptySubmitLabel && !hasRealContent && !isSubmitLoading;
|
||||
const canPressLoadingButton = isSubmitLoading && typeof onSubmitLoadingPress === "function";
|
||||
const isSendButtonDisabled =
|
||||
disabled || (!canPressLoadingButton && (isSubmitDisabled || isSubmitLoading));
|
||||
@@ -1112,19 +1117,24 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
||||
<TooltipTrigger
|
||||
onPress={canPressLoadingButton ? onSubmitLoadingPress : handleSendMessage}
|
||||
disabled={isSendButtonDisabled}
|
||||
accessibilityLabel={submitAccessibilityLabel}
|
||||
accessibilityLabel={showEmptySubmitLabel ? emptySubmitLabel : submitAccessibilityLabel}
|
||||
accessibilityRole="button"
|
||||
style={[styles.sendButton, isSendButtonDisabled && styles.buttonDisabled]}
|
||||
style={[
|
||||
showEmptySubmitLabel ? styles.emptySubmitButton : styles.sendButton,
|
||||
isSendButtonDisabled && styles.buttonDisabled,
|
||||
]}
|
||||
>
|
||||
{isSubmitLoading ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : showEmptySubmitLabel ? (
|
||||
<Text style={styles.emptySubmitLabel}>{emptySubmitLabel}</Text>
|
||||
) : (
|
||||
<ArrowUp size={buttonIconSize} color="white" />
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="center" offset={8}>
|
||||
<View style={styles.tooltipRow}>
|
||||
<Text style={styles.tooltipText}>Send</Text>
|
||||
<Text style={styles.tooltipText}>{showEmptySubmitLabel ? emptySubmitLabel : "Send"}</Text>
|
||||
{sendKeys ? <Shortcut chord={sendKeys} style={styles.tooltipShortcut} /> : null}
|
||||
</View>
|
||||
</TooltipContent>
|
||||
@@ -1304,6 +1314,18 @@ const styles = StyleSheet.create(((theme: any) => ({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
emptySubmitButton: {
|
||||
height: 28,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
backgroundColor: theme.colors.accent,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
},
|
||||
emptySubmitLabel: {
|
||||
color: "white",
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
iconButtonHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Pressable, Text, View } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { createNameId } from "mnemonic-id";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ChevronDown, GitBranch } from "lucide-react-native";
|
||||
import { ChevronDown, CircleDot, GitBranch, GitPullRequest, Plus, X } from "lucide-react-native";
|
||||
import { Composer } from "@/components/composer";
|
||||
import { Combobox, ComboboxItem } from "@/components/ui/combobox";
|
||||
import type { ComboboxOption as ComboboxOptionType } from "@/components/ui/combobox";
|
||||
@@ -28,6 +28,24 @@ import {
|
||||
} from "@/utils/workspace-execution";
|
||||
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
||||
import type { ImageAttachment, MessagePayload } from "@/components/message-input";
|
||||
import type { GitHubSearchItem } from "@server/shared/messages";
|
||||
|
||||
function buildInitialPrompt(userText: string, githubItems: GitHubSearchItem[]): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
for (const item of githubItems) {
|
||||
const kind = item.kind === "pr" ? "Pull Request" : "Issue";
|
||||
const header = `GitHub ${kind} #${item.number}: ${item.title}`;
|
||||
const body = item.body?.trim();
|
||||
parts.push(body ? `${header}\n\n${body}` : header);
|
||||
}
|
||||
|
||||
if (userText) {
|
||||
parts.push(userText);
|
||||
}
|
||||
|
||||
return parts.join("\n\n---\n\n");
|
||||
}
|
||||
|
||||
interface NewWorkspaceScreenProps {
|
||||
serverId: string;
|
||||
@@ -52,6 +70,10 @@ export function NewWorkspaceScreen({
|
||||
const [selectedBranch, setSelectedBranch] = useState<string | null>(null);
|
||||
const [branchPickerOpen, setBranchPickerOpen] = useState(false);
|
||||
const branchAnchorRef = useRef<View>(null);
|
||||
const [githubContextItems, setGithubContextItems] = useState<GitHubSearchItem[]>([]);
|
||||
const [githubPickerOpen, setGithubPickerOpen] = useState(false);
|
||||
const [githubSearchQuery, setGithubSearchQuery] = useState("");
|
||||
const githubAnchorRef = useRef<View>(null);
|
||||
|
||||
const displayName = displayNameProp?.trim() ?? "";
|
||||
const workspace = createdWorkspace;
|
||||
@@ -98,6 +120,52 @@ export function NewWorkspaceScreen({
|
||||
enabled: isConnected && !!client,
|
||||
});
|
||||
|
||||
const githubSearchQuery_trimmed = githubSearchQuery.trim();
|
||||
const githubSearchResultsQuery = useQuery({
|
||||
queryKey: ["github-search", serverId, sourceDirectory, githubSearchQuery_trimmed],
|
||||
queryFn: async () => {
|
||||
const connectedClient = withConnectedClient();
|
||||
return connectedClient.searchGitHub({
|
||||
cwd: sourceDirectory,
|
||||
query: githubSearchQuery_trimmed,
|
||||
limit: 20,
|
||||
});
|
||||
},
|
||||
enabled: isConnected && !!client && githubSearchQuery_trimmed.length >= 2,
|
||||
});
|
||||
|
||||
const githubSearchOptions: ComboboxOptionType[] = useMemo(() => {
|
||||
const items = githubSearchResultsQuery.data?.items ?? [];
|
||||
const selectedNumbers = new Set(githubContextItems.map((i) => `${i.kind}:${i.number}`));
|
||||
return items
|
||||
.filter((item) => !selectedNumbers.has(`${item.kind}:${item.number}`))
|
||||
.map((item) => ({
|
||||
id: `${item.kind}:${item.number}`,
|
||||
label: `#${item.number} ${item.title}`,
|
||||
// Include search query so the Combobox's client-side filter doesn't
|
||||
// discard server-searched results that match on body but not title.
|
||||
description: githubSearchQuery_trimmed,
|
||||
}));
|
||||
}, [githubSearchResultsQuery.data?.items, githubContextItems, githubSearchQuery_trimmed]);
|
||||
|
||||
const handleSelectGithubItem = useCallback(
|
||||
(id: string) => {
|
||||
const items = githubSearchResultsQuery.data?.items ?? [];
|
||||
const [kind, numberStr] = id.split(":");
|
||||
const item = items.find((i) => i.kind === kind && i.number === Number(numberStr));
|
||||
if (item) {
|
||||
setGithubContextItems((prev) => [...prev, item]);
|
||||
}
|
||||
setGithubPickerOpen(false);
|
||||
setGithubSearchQuery("");
|
||||
},
|
||||
[githubSearchResultsQuery.data?.items],
|
||||
);
|
||||
|
||||
const handleRemoveGithubItem = useCallback((index: number) => {
|
||||
setGithubContextItems((prev) => prev.filter((_, i) => i !== index));
|
||||
}, []);
|
||||
|
||||
const branchOptions: ComboboxOptionType[] = useMemo(
|
||||
() =>
|
||||
(branchSuggestionsQuery.data?.branches ?? []).map((branch) => ({
|
||||
@@ -145,6 +213,7 @@ export function NewWorkspaceScreen({
|
||||
throw new Error("Composer state is required");
|
||||
}
|
||||
|
||||
const initialPrompt = buildInitialPrompt(text.trim(), githubContextItems);
|
||||
const encodedImages = await encodeImages(images);
|
||||
const workspaceDirectory = requireWorkspaceExecutionAuthority({ workspace }).workspaceDirectory;
|
||||
const agent = await connectedClient.createAgent({
|
||||
@@ -158,7 +227,7 @@ export function NewWorkspaceScreen({
|
||||
...(composerState.effectiveThinkingOptionId
|
||||
? { thinkingOptionId: composerState.effectiveThinkingOptionId }
|
||||
: {}),
|
||||
...(text.trim() ? { initialPrompt: text.trim() } : {}),
|
||||
...(initialPrompt ? { initialPrompt } : {}),
|
||||
...(encodedImages && encodedImages.length > 0 ? { images: encodedImages } : {}),
|
||||
});
|
||||
|
||||
@@ -181,7 +250,7 @@ export function NewWorkspaceScreen({
|
||||
setPendingAction(null);
|
||||
}
|
||||
},
|
||||
[composerState, ensureWorkspace, serverId, setAgents, toast, withConnectedClient],
|
||||
[composerState, ensureWorkspace, githubContextItems, serverId, setAgents, toast, withConnectedClient],
|
||||
);
|
||||
|
||||
const workspaceTitle =
|
||||
@@ -223,6 +292,8 @@ export function NewWorkspaceScreen({
|
||||
serverId={serverId}
|
||||
isInputActive={true}
|
||||
onSubmitMessage={handleCreateChatAgent}
|
||||
allowEmptySubmit
|
||||
emptySubmitLabel="Create"
|
||||
isSubmitLoading={pendingAction === "chat"}
|
||||
blurOnSubmit={true}
|
||||
value={chatDraft.text}
|
||||
@@ -291,6 +362,93 @@ export function NewWorkspaceScreen({
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
{githubContextItems.map((item, index) => (
|
||||
<View key={`${item.kind}:${item.number}`} style={styles.githubChip}>
|
||||
{item.kind === "pr" ? (
|
||||
<GitPullRequest size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
) : (
|
||||
<CircleDot size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
)}
|
||||
<Text style={styles.githubChipText} numberOfLines={1}>
|
||||
#{item.number} {item.title}
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={() => handleRemoveGithubItem(index)}
|
||||
accessibilityLabel={`Remove #${item.number}`}
|
||||
hitSlop={4}
|
||||
>
|
||||
<X size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
<View>
|
||||
<Pressable
|
||||
ref={githubAnchorRef}
|
||||
onPress={() => setGithubPickerOpen(true)}
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.badge,
|
||||
hovered && styles.badgeHovered,
|
||||
pressed && styles.badgePressed,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Add GitHub issue or PR"
|
||||
>
|
||||
<Plus size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
<Text style={styles.badgeText} numberOfLines={1}>
|
||||
Issue or PR
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Combobox
|
||||
options={githubSearchOptions}
|
||||
value=""
|
||||
onSelect={handleSelectGithubItem}
|
||||
searchable
|
||||
searchPlaceholder="Search issues and PRs..."
|
||||
title="GitHub"
|
||||
open={githubPickerOpen}
|
||||
onOpenChange={(open) => {
|
||||
setGithubPickerOpen(open);
|
||||
if (!open) setGithubSearchQuery("");
|
||||
}}
|
||||
onSearchQueryChange={setGithubSearchQuery}
|
||||
desktopPlacement="bottom-start"
|
||||
anchorRef={githubAnchorRef}
|
||||
emptyText={
|
||||
githubSearchQuery_trimmed.length < 2
|
||||
? "Type to search..."
|
||||
: githubSearchResultsQuery.isFetching
|
||||
? "Searching..."
|
||||
: "No results found."
|
||||
}
|
||||
renderOption={({ option, selected, active, onPress }) => {
|
||||
const item = (githubSearchResultsQuery.data?.items ?? []).find(
|
||||
(i) => `${i.kind}:${i.number}` === option.id,
|
||||
);
|
||||
return (
|
||||
<ComboboxItem
|
||||
key={option.id}
|
||||
label={option.label}
|
||||
selected={selected}
|
||||
active={active}
|
||||
onPress={onPress}
|
||||
leadingSlot={
|
||||
item?.kind === "pr" ? (
|
||||
<GitPullRequest
|
||||
size={theme.iconSize.sm}
|
||||
color={theme.colors.foregroundMuted}
|
||||
/>
|
||||
) : (
|
||||
<CircleDot
|
||||
size={theme.iconSize.sm}
|
||||
color={theme.colors.foregroundMuted}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
||||
</View>
|
||||
@@ -351,6 +509,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
optionsRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[4] + theme.spacing[4] - 6,
|
||||
marginTop: -theme.spacing[2],
|
||||
@@ -373,4 +532,20 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
githubChip: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
height: 28,
|
||||
paddingLeft: theme.spacing[2],
|
||||
paddingRight: theme.spacing[1],
|
||||
borderRadius: theme.borderRadius["2xl"],
|
||||
backgroundColor: theme.colors.surface1,
|
||||
gap: theme.spacing[1],
|
||||
maxWidth: 240,
|
||||
},
|
||||
githubChipText: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
flexShrink: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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