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;
|
isInputActive: boolean;
|
||||||
onSubmitMessage?: (payload: MessagePayload) => Promise<void>;
|
onSubmitMessage?: (payload: MessagePayload) => Promise<void>;
|
||||||
allowEmptySubmit?: boolean;
|
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. */
|
/** Externally controlled loading state. When true, disables the submit button. */
|
||||||
isSubmitLoading?: boolean;
|
isSubmitLoading?: boolean;
|
||||||
/** When true, blurs the input immediately when submitting. */
|
/** When true, blurs the input immediately when submitting. */
|
||||||
@@ -101,6 +103,7 @@ export function Composer({
|
|||||||
isInputActive,
|
isInputActive,
|
||||||
onSubmitMessage,
|
onSubmitMessage,
|
||||||
allowEmptySubmit = false,
|
allowEmptySubmit = false,
|
||||||
|
emptySubmitLabel,
|
||||||
isSubmitLoading = false,
|
isSubmitLoading = false,
|
||||||
blurOnSubmit = false,
|
blurOnSubmit = false,
|
||||||
value,
|
value,
|
||||||
@@ -737,6 +740,7 @@ export function Composer({
|
|||||||
onChangeText={setUserInput}
|
onChangeText={setUserInput}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
allowEmptySubmit={allowEmptySubmit}
|
allowEmptySubmit={allowEmptySubmit}
|
||||||
|
emptySubmitLabel={emptySubmitLabel}
|
||||||
isSubmitDisabled={isProcessing || isSubmitLoading}
|
isSubmitDisabled={isProcessing || isSubmitLoading}
|
||||||
isSubmitLoading={isProcessing || isSubmitLoading}
|
isSubmitLoading={isProcessing || isSubmitLoading}
|
||||||
images={selectedImages}
|
images={selectedImages}
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export interface MessageInputProps {
|
|||||||
onChangeText: (text: string) => void;
|
onChangeText: (text: string) => void;
|
||||||
onSubmit: (payload: MessagePayload) => void;
|
onSubmit: (payload: MessagePayload) => void;
|
||||||
allowEmptySubmit?: boolean;
|
allowEmptySubmit?: boolean;
|
||||||
|
/** Label to show on the submit button when the input is empty and allowEmptySubmit is true. */
|
||||||
|
emptySubmitLabel?: string;
|
||||||
isSubmitDisabled?: boolean;
|
isSubmitDisabled?: boolean;
|
||||||
isSubmitLoading?: boolean;
|
isSubmitLoading?: boolean;
|
||||||
images?: ImageAttachment[];
|
images?: ImageAttachment[];
|
||||||
@@ -195,6 +197,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
onChangeText,
|
onChangeText,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
allowEmptySubmit = false,
|
allowEmptySubmit = false,
|
||||||
|
emptySubmitLabel,
|
||||||
isSubmitDisabled = false,
|
isSubmitDisabled = false,
|
||||||
isSubmitLoading = false,
|
isSubmitLoading = false,
|
||||||
images = [],
|
images = [],
|
||||||
@@ -558,7 +561,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
|
|
||||||
const handleSendMessage = useCallback(() => {
|
const handleSendMessage = useCallback(() => {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
if (!trimmed && images.length === 0) return;
|
if (!trimmed && images.length === 0 && !allowEmptySubmit) return;
|
||||||
const payload = {
|
const payload = {
|
||||||
text: trimmed,
|
text: trimmed,
|
||||||
images: images.length > 0 ? images : undefined,
|
images: images.length > 0 ? images : undefined,
|
||||||
@@ -568,7 +571,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
inputHeightRef.current = MIN_INPUT_HEIGHT;
|
inputHeightRef.current = MIN_INPUT_HEIGHT;
|
||||||
setInputHeight(MIN_INPUT_HEIGHT);
|
setInputHeight(MIN_INPUT_HEIGHT);
|
||||||
onHeightChange?.(MIN_INPUT_HEIGHT);
|
onHeightChange?.(MIN_INPUT_HEIGHT);
|
||||||
}, [value, images, onSubmit, isAgentRunning, onHeightChange]);
|
}, [value, images, onSubmit, isAgentRunning, onHeightChange, allowEmptySubmit]);
|
||||||
|
|
||||||
const handleQueueMessage = useCallback(() => {
|
const handleQueueMessage = useCallback(() => {
|
||||||
if (!onQueue) return;
|
if (!onQueue) return;
|
||||||
@@ -894,8 +897,10 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasImages = images.length > 0;
|
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 shouldShowSendButton = hasSendableContent || isSubmitLoading;
|
||||||
|
const showEmptySubmitLabel = allowEmptySubmit && emptySubmitLabel && !hasRealContent && !isSubmitLoading;
|
||||||
const canPressLoadingButton = isSubmitLoading && typeof onSubmitLoadingPress === "function";
|
const canPressLoadingButton = isSubmitLoading && typeof onSubmitLoadingPress === "function";
|
||||||
const isSendButtonDisabled =
|
const isSendButtonDisabled =
|
||||||
disabled || (!canPressLoadingButton && (isSubmitDisabled || isSubmitLoading));
|
disabled || (!canPressLoadingButton && (isSubmitDisabled || isSubmitLoading));
|
||||||
@@ -1112,19 +1117,24 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
onPress={canPressLoadingButton ? onSubmitLoadingPress : handleSendMessage}
|
onPress={canPressLoadingButton ? onSubmitLoadingPress : handleSendMessage}
|
||||||
disabled={isSendButtonDisabled}
|
disabled={isSendButtonDisabled}
|
||||||
accessibilityLabel={submitAccessibilityLabel}
|
accessibilityLabel={showEmptySubmitLabel ? emptySubmitLabel : submitAccessibilityLabel}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
style={[styles.sendButton, isSendButtonDisabled && styles.buttonDisabled]}
|
style={[
|
||||||
|
showEmptySubmitLabel ? styles.emptySubmitButton : styles.sendButton,
|
||||||
|
isSendButtonDisabled && styles.buttonDisabled,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
{isSubmitLoading ? (
|
{isSubmitLoading ? (
|
||||||
<ActivityIndicator size="small" color="white" />
|
<ActivityIndicator size="small" color="white" />
|
||||||
|
) : showEmptySubmitLabel ? (
|
||||||
|
<Text style={styles.emptySubmitLabel}>{emptySubmitLabel}</Text>
|
||||||
) : (
|
) : (
|
||||||
<ArrowUp size={buttonIconSize} color="white" />
|
<ArrowUp size={buttonIconSize} color="white" />
|
||||||
)}
|
)}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top" align="center" offset={8}>
|
<TooltipContent side="top" align="center" offset={8}>
|
||||||
<View style={styles.tooltipRow}>
|
<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}
|
{sendKeys ? <Shortcut chord={sendKeys} style={styles.tooltipShortcut} /> : null}
|
||||||
</View>
|
</View>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@@ -1304,6 +1314,18 @@ const styles = StyleSheet.create(((theme: any) => ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "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: {
|
iconButtonHovered: {
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Pressable, Text, View } from "react-native";
|
|||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { createNameId } from "mnemonic-id";
|
import { createNameId } from "mnemonic-id";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
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 { Composer } from "@/components/composer";
|
||||||
import { Combobox, ComboboxItem } from "@/components/ui/combobox";
|
import { Combobox, ComboboxItem } from "@/components/ui/combobox";
|
||||||
import type { ComboboxOption as ComboboxOptionType } from "@/components/ui/combobox";
|
import type { ComboboxOption as ComboboxOptionType } from "@/components/ui/combobox";
|
||||||
@@ -28,6 +28,24 @@ import {
|
|||||||
} from "@/utils/workspace-execution";
|
} from "@/utils/workspace-execution";
|
||||||
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
import type { ImageAttachment, MessagePayload } from "@/components/message-input";
|
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 {
|
interface NewWorkspaceScreenProps {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
@@ -52,6 +70,10 @@ export function NewWorkspaceScreen({
|
|||||||
const [selectedBranch, setSelectedBranch] = useState<string | null>(null);
|
const [selectedBranch, setSelectedBranch] = useState<string | null>(null);
|
||||||
const [branchPickerOpen, setBranchPickerOpen] = useState(false);
|
const [branchPickerOpen, setBranchPickerOpen] = useState(false);
|
||||||
const branchAnchorRef = useRef<View>(null);
|
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 displayName = displayNameProp?.trim() ?? "";
|
||||||
const workspace = createdWorkspace;
|
const workspace = createdWorkspace;
|
||||||
@@ -98,6 +120,52 @@ export function NewWorkspaceScreen({
|
|||||||
enabled: isConnected && !!client,
|
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(
|
const branchOptions: ComboboxOptionType[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(branchSuggestionsQuery.data?.branches ?? []).map((branch) => ({
|
(branchSuggestionsQuery.data?.branches ?? []).map((branch) => ({
|
||||||
@@ -145,6 +213,7 @@ export function NewWorkspaceScreen({
|
|||||||
throw new Error("Composer state is required");
|
throw new Error("Composer state is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initialPrompt = buildInitialPrompt(text.trim(), githubContextItems);
|
||||||
const encodedImages = await encodeImages(images);
|
const encodedImages = await encodeImages(images);
|
||||||
const workspaceDirectory = requireWorkspaceExecutionAuthority({ workspace }).workspaceDirectory;
|
const workspaceDirectory = requireWorkspaceExecutionAuthority({ workspace }).workspaceDirectory;
|
||||||
const agent = await connectedClient.createAgent({
|
const agent = await connectedClient.createAgent({
|
||||||
@@ -158,7 +227,7 @@ export function NewWorkspaceScreen({
|
|||||||
...(composerState.effectiveThinkingOptionId
|
...(composerState.effectiveThinkingOptionId
|
||||||
? { thinkingOptionId: composerState.effectiveThinkingOptionId }
|
? { thinkingOptionId: composerState.effectiveThinkingOptionId }
|
||||||
: {}),
|
: {}),
|
||||||
...(text.trim() ? { initialPrompt: text.trim() } : {}),
|
...(initialPrompt ? { initialPrompt } : {}),
|
||||||
...(encodedImages && encodedImages.length > 0 ? { images: encodedImages } : {}),
|
...(encodedImages && encodedImages.length > 0 ? { images: encodedImages } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -181,7 +250,7 @@ export function NewWorkspaceScreen({
|
|||||||
setPendingAction(null);
|
setPendingAction(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[composerState, ensureWorkspace, serverId, setAgents, toast, withConnectedClient],
|
[composerState, ensureWorkspace, githubContextItems, serverId, setAgents, toast, withConnectedClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
const workspaceTitle =
|
const workspaceTitle =
|
||||||
@@ -223,6 +292,8 @@ export function NewWorkspaceScreen({
|
|||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
isInputActive={true}
|
isInputActive={true}
|
||||||
onSubmitMessage={handleCreateChatAgent}
|
onSubmitMessage={handleCreateChatAgent}
|
||||||
|
allowEmptySubmit
|
||||||
|
emptySubmitLabel="Create"
|
||||||
isSubmitLoading={pendingAction === "chat"}
|
isSubmitLoading={pendingAction === "chat"}
|
||||||
blurOnSubmit={true}
|
blurOnSubmit={true}
|
||||||
value={chatDraft.text}
|
value={chatDraft.text}
|
||||||
@@ -291,6 +362,93 @@ export function NewWorkspaceScreen({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</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>
|
</View>
|
||||||
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
||||||
</View>
|
</View>
|
||||||
@@ -351,6 +509,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
optionsRow: {
|
optionsRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
flexWrap: "wrap",
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
paddingHorizontal: theme.spacing[4] + theme.spacing[4] - 6,
|
paddingHorizontal: theme.spacing[4] + theme.spacing[4] - 6,
|
||||||
marginTop: -theme.spacing[2],
|
marginTop: -theme.spacing[2],
|
||||||
@@ -373,4 +532,20 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
color: theme.colors.foregroundMuted,
|
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,
|
CheckoutPrStatusResponse,
|
||||||
ValidateBranchResponse,
|
ValidateBranchResponse,
|
||||||
BranchSuggestionsResponse,
|
BranchSuggestionsResponse,
|
||||||
|
GitHubSearchResponse,
|
||||||
DirectorySuggestionsResponse,
|
DirectorySuggestionsResponse,
|
||||||
PaseoWorktreeListResponse,
|
PaseoWorktreeListResponse,
|
||||||
PaseoWorktreeArchiveResponse,
|
PaseoWorktreeArchiveResponse,
|
||||||
@@ -225,6 +226,7 @@ type CheckoutPrCreatePayload = CheckoutPrCreateResponse["payload"];
|
|||||||
type CheckoutPrStatusPayload = CheckoutPrStatusResponse["payload"];
|
type CheckoutPrStatusPayload = CheckoutPrStatusResponse["payload"];
|
||||||
type ValidateBranchPayload = ValidateBranchResponse["payload"];
|
type ValidateBranchPayload = ValidateBranchResponse["payload"];
|
||||||
type BranchSuggestionsPayload = BranchSuggestionsResponse["payload"];
|
type BranchSuggestionsPayload = BranchSuggestionsResponse["payload"];
|
||||||
|
type GitHubSearchPayload = GitHubSearchResponse["payload"];
|
||||||
type DirectorySuggestionsPayload = DirectorySuggestionsResponse["payload"];
|
type DirectorySuggestionsPayload = DirectorySuggestionsResponse["payload"];
|
||||||
type PaseoWorktreeListPayload = PaseoWorktreeListResponse["payload"];
|
type PaseoWorktreeListPayload = PaseoWorktreeListResponse["payload"];
|
||||||
type PaseoWorktreeArchivePayload = PaseoWorktreeArchiveResponse["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(
|
async getDirectorySuggestions(
|
||||||
options: {
|
options: {
|
||||||
query: string;
|
query: string;
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ import {
|
|||||||
pushCurrentBranch,
|
pushCurrentBranch,
|
||||||
createPullRequest,
|
createPullRequest,
|
||||||
getPullRequestStatus,
|
getPullRequestStatus,
|
||||||
|
searchGitHubIssuesAndPrs,
|
||||||
warmCheckoutShortstatInBackground,
|
warmCheckoutShortstatInBackground,
|
||||||
} from "../utils/checkout-git.js";
|
} from "../utils/checkout-git.js";
|
||||||
import { getProjectIcon } from "../utils/project-icon.js";
|
import { getProjectIcon } from "../utils/project-icon.js";
|
||||||
@@ -1619,6 +1620,10 @@ export class Session {
|
|||||||
await this.handleBranchSuggestionsRequest(msg);
|
await this.handleBranchSuggestionsRequest(msg);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "github_search_request":
|
||||||
|
await this.handleGitHubSearchRequest(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
case "directory_suggestions_request":
|
case "directory_suggestions_request":
|
||||||
await this.handleDirectorySuggestionsRequest(msg);
|
await this.handleDirectorySuggestionsRequest(msg);
|
||||||
break;
|
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> {
|
private async handleDirectorySuggestionsRequest(msg: DirectorySuggestionsRequest): Promise<void> {
|
||||||
const { query, limit, requestId, cwd, includeFiles, includeDirectories } = msg;
|
const { query, limit, requestId, cwd, includeFiles, includeDirectories } = msg;
|
||||||
|
|
||||||
|
|||||||
@@ -1073,6 +1073,24 @@ export const BranchSuggestionsRequestSchema = z.object({
|
|||||||
requestId: z.string(),
|
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({
|
export const DirectorySuggestionsRequestSchema = z.object({
|
||||||
type: z.literal("directory_suggestions_request"),
|
type: z.literal("directory_suggestions_request"),
|
||||||
query: z.string(),
|
query: z.string(),
|
||||||
@@ -1400,6 +1418,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
|||||||
CheckoutPrStatusRequestSchema,
|
CheckoutPrStatusRequestSchema,
|
||||||
ValidateBranchRequestSchema,
|
ValidateBranchRequestSchema,
|
||||||
BranchSuggestionsRequestSchema,
|
BranchSuggestionsRequestSchema,
|
||||||
|
GitHubSearchRequestSchema,
|
||||||
DirectorySuggestionsRequestSchema,
|
DirectorySuggestionsRequestSchema,
|
||||||
PaseoWorktreeListRequestSchema,
|
PaseoWorktreeListRequestSchema,
|
||||||
PaseoWorktreeArchiveRequestSchema,
|
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({
|
export const DirectorySuggestionsResponseSchema = z.object({
|
||||||
type: z.literal("directory_suggestions_response"),
|
type: z.literal("directory_suggestions_response"),
|
||||||
payload: z.object({
|
payload: z.object({
|
||||||
@@ -2696,6 +2725,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
|||||||
CheckoutPrStatusResponseSchema,
|
CheckoutPrStatusResponseSchema,
|
||||||
ValidateBranchResponseSchema,
|
ValidateBranchResponseSchema,
|
||||||
BranchSuggestionsResponseSchema,
|
BranchSuggestionsResponseSchema,
|
||||||
|
GitHubSearchResponseSchema,
|
||||||
DirectorySuggestionsResponseSchema,
|
DirectorySuggestionsResponseSchema,
|
||||||
PaseoWorktreeListResponseSchema,
|
PaseoWorktreeListResponseSchema,
|
||||||
PaseoWorktreeArchiveResponseSchema,
|
PaseoWorktreeArchiveResponseSchema,
|
||||||
@@ -2923,6 +2953,9 @@ export type ValidateBranchRequest = z.infer<typeof ValidateBranchRequestSchema>;
|
|||||||
export type ValidateBranchResponse = z.infer<typeof ValidateBranchResponseSchema>;
|
export type ValidateBranchResponse = z.infer<typeof ValidateBranchResponseSchema>;
|
||||||
export type BranchSuggestionsRequest = z.infer<typeof BranchSuggestionsRequestSchema>;
|
export type BranchSuggestionsRequest = z.infer<typeof BranchSuggestionsRequestSchema>;
|
||||||
export type BranchSuggestionsResponse = z.infer<typeof BranchSuggestionsResponseSchema>;
|
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 DirectorySuggestionsRequest = z.infer<typeof DirectorySuggestionsRequestSchema>;
|
||||||
export type DirectorySuggestionsResponse = z.infer<typeof DirectorySuggestionsResponseSchema>;
|
export type DirectorySuggestionsResponse = z.infer<typeof DirectorySuggestionsResponseSchema>;
|
||||||
export type PaseoWorktreeListRequest = z.infer<typeof PaseoWorktreeListRequestSchema>;
|
export type PaseoWorktreeListRequest = z.infer<typeof PaseoWorktreeListRequestSchema>;
|
||||||
|
|||||||
@@ -2297,3 +2297,110 @@ async function getPullRequestStatusUncached(cwd: string): Promise<PullRequestSta
|
|||||||
throw error;
|
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