From 404b3b1dbf20af93b3ce610f31cba0ad583751f7 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 10 Apr 2026 22:03:57 +0700 Subject: [PATCH] feat: populate GitHub dropdown on new workspace screen - Fire GitHub search query on mount instead of waiting for 2+ chars - Add staleTime so combobox reuses cached results on open/close - Single-select: replace multi-item chips with a transforming trigger - Trigger shows issue/PR icon, truncated title, and ExternalLink icon - ExternalLink opens GitHub URL via openExternalUrl (desktop-aware) - Add ExternalLink trailingSlot on each combobox option row - Hover on ExternalLink changes color from foregroundMuted to foreground - Constrain badge width (maxWidth: 240) with flexShrink on text --- .../app/src/screens/new-workspace-screen.tsx | 144 ++++++++++-------- 1 file changed, 79 insertions(+), 65 deletions(-) diff --git a/packages/app/src/screens/new-workspace-screen.tsx b/packages/app/src/screens/new-workspace-screen.tsx index 6768060b3..6c53b4cb2 100644 --- a/packages/app/src/screens/new-workspace-screen.tsx +++ b/packages/app/src/screens/new-workspace-screen.tsx @@ -3,7 +3,8 @@ 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, CircleDot, GitBranch, GitPullRequest, Plus, X } from "lucide-react-native"; +import { CircleDot, ChevronDown, ExternalLink, GitBranch, GitPullRequest } from "lucide-react-native"; +import { GitHubIcon } from "@/components/icons/github-icon"; import { Composer } from "@/components/composer"; import { Combobox, ComboboxItem } from "@/components/ui/combobox"; import type { ComboboxOption as ComboboxOptionType } from "@/components/ui/combobox"; @@ -23,6 +24,7 @@ import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session- import { normalizeAgentSnapshot } from "@/utils/agent-snapshots"; import { encodeImages } from "@/utils/encode-images"; import { toErrorMessage } from "@/utils/error-messages"; +import { openExternalUrl } from "@/utils/open-external-url"; import { requireWorkspaceExecutionAuthority, } from "@/utils/workspace-execution"; @@ -30,13 +32,13 @@ 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 { +function buildInitialPrompt(userText: string, githubItem: GitHubSearchItem | null): 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(); + if (githubItem) { + const kind = githubItem.kind === "pr" ? "Pull Request" : "Issue"; + const header = `GitHub ${kind} #${githubItem.number}: ${githubItem.title}`; + const body = githubItem.body?.trim(); parts.push(body ? `${header}\n\n${body}` : header); } @@ -70,7 +72,7 @@ export function NewWorkspaceScreen({ const [selectedBranch, setSelectedBranch] = useState(null); const [branchPickerOpen, setBranchPickerOpen] = useState(false); const branchAnchorRef = useRef(null); - const [githubContextItems, setGithubContextItems] = useState([]); + const [selectedGithubItem, setSelectedGithubItem] = useState(null); const [githubPickerOpen, setGithubPickerOpen] = useState(false); const [githubSearchQuery, setGithubSearchQuery] = useState(""); const githubAnchorRef = useRef(null); @@ -131,14 +133,13 @@ export function NewWorkspaceScreen({ limit: 20, }); }, - enabled: isConnected && !!client && githubSearchQuery_trimmed.length >= 2, + enabled: isConnected && !!client, + staleTime: 30_000, }); 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}`, @@ -146,26 +147,20 @@ export function NewWorkspaceScreen({ // discard server-searched results that match on body but not title. description: githubSearchQuery_trimmed, })); - }, [githubSearchResultsQuery.data?.items, githubContextItems, githubSearchQuery_trimmed]); + }, [githubSearchResultsQuery.data?.items, 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]); - } + setSelectedGithubItem(item ?? null); 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) => ({ @@ -213,7 +208,7 @@ export function NewWorkspaceScreen({ throw new Error("Composer state is required"); } - const initialPrompt = buildInitialPrompt(text.trim(), githubContextItems); + const initialPrompt = buildInitialPrompt(text.trim(), selectedGithubItem); const encodedImages = await encodeImages(images); const workspaceDirectory = requireWorkspaceExecutionAuthority({ workspace }).workspaceDirectory; const agent = await connectedClient.createAgent({ @@ -250,7 +245,7 @@ export function NewWorkspaceScreen({ setPendingAction(null); } }, - [composerState, ensureWorkspace, githubContextItems, serverId, setAgents, toast, withConnectedClient], + [composerState, ensureWorkspace, selectedGithubItem, serverId, setAgents, toast, withConnectedClient], ); const workspaceTitle = @@ -362,25 +357,6 @@ export function NewWorkspaceScreen({ )} /> - {githubContextItems.map((item, index) => ( - - {item.kind === "pr" ? ( - - ) : ( - - )} - - #{item.number} {item.title} - - handleRemoveGithubItem(index)} - accessibilityLabel={`Remove #${item.number}`} - hitSlop={4} - > - - - - ))} - - - Issue or PR - + {selectedGithubItem ? ( + <> + + {selectedGithubItem.kind === "pr" ? ( + + ) : ( + + )} + + + #{selectedGithubItem.number} {selectedGithubItem.title} + + openExternalUrl(selectedGithubItem.url)} + accessibilityLabel="Open on GitHub" + hitSlop={4} + > + {({ hovered }) => ( + + )} + + + ) : ( + <> + + + + + Issue or PR + + + )} { const item = (githubSearchResultsQuery.data?.items ?? []).find( @@ -444,6 +453,22 @@ export function NewWorkspaceScreen({ /> ) } + trailingSlot={ + item?.url ? ( + openExternalUrl(item.url)} + hitSlop={8} + accessibilityLabel="Open on GitHub" + > + {({ hovered }) => ( + + )} + + ) : undefined + } /> ); }} @@ -518,6 +543,7 @@ const styles = StyleSheet.create((theme) => ({ flexDirection: "row", alignItems: "center", height: 28, + maxWidth: 240, paddingHorizontal: theme.spacing[2], borderRadius: theme.borderRadius["2xl"], gap: theme.spacing[1], @@ -528,24 +554,12 @@ const styles = StyleSheet.create((theme) => ({ badgePressed: { backgroundColor: theme.colors.surface0, }, + badgeIcon: { + flexShrink: 0, + }, badgeText: { 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, }, }));