From 375c4df8b3447dcee6bfbd631ab269b899c1db2c Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 22 Jun 2026 15:16:42 -0700 Subject: [PATCH] feat(mobile): surface "Link an existing PR" in PR sidebar empty state (#5963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(mobile): surface "Link an existing PR" in PR sidebar empty state The mobile link-PR building blocks (MobileLinkPrForm, linkMobilePr, parseGitHubPrReference) existed and were tested, but had no entry point — only unlink was wired up. Add a "Link an existing PR" action to the no-PR empty state that opens MobileLinkPrForm and refetches the sidebar on success, mirroring desktop's link flow (GitHub-scoped via worktree.set). Co-Authored-By: Claude Opus 4.8 (1M context) * Polish mobile PR link empty state --------- Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Jinwoo-H --- .../pr-sidebar/PrSidebarCreateEmptyState.tsx | 49 ++++++++++++++++--- .../pr-create-empty-state-styles.ts | 20 ++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/mobile/src/components/pr-sidebar/PrSidebarCreateEmptyState.tsx b/mobile/src/components/pr-sidebar/PrSidebarCreateEmptyState.tsx index a11bdec59..3f3b74eeb 100644 --- a/mobile/src/components/pr-sidebar/PrSidebarCreateEmptyState.tsx +++ b/mobile/src/components/pr-sidebar/PrSidebarCreateEmptyState.tsx @@ -1,12 +1,13 @@ import { useEffect, useState } from 'react' import { ActivityIndicator, Pressable, Text, View } from 'react-native' -import { GitPullRequestArrow, RefreshCw } from 'lucide-react-native' +import { GitPullRequestArrow, Link2, RefreshCw } from 'lucide-react-native' import { colors } from '../../theme/mobile-theme' import type { RpcClient } from '../../transport/rpc-client' import { resolveMobilePrPrefill, type MobilePrPrefill } from '../../source-control/mobile-pr-create' import { fetchWorktreeLinkedPR } from '../../source-control/mobile-pr-link' import { openMobilePrUrl } from '../MobilePrComposeSheet' import { MobilePrComposeForm } from './MobilePrComposeForm' +import { MobileLinkPrForm } from './MobileLinkPrForm' import { prCreateEmptyStateStyles as styles } from './pr-create-empty-state-styles' type Props = { @@ -17,18 +18,18 @@ type Props = { onCreated: () => void } -type Mode = 'choose' | 'create' +type Mode = 'choose' | 'create' | 'link' -// Empty state for a branch with no PR. Keep this scoped to desktop's no-PR -// surface: create/refresh here; linked-PR edits belong outside this panel. +// Empty state for a branch with no PR: create a new PR, or link an existing one +// (the no-PR surface is the natural home for linking — desktop's link entry lives +// on its PR card, but on mobile this is where a user lands with nothing linked). export function PrSidebarCreateEmptyState({ client, worktreeId, gitBranch, onCreated }: Props) { const [prefill, setPrefill] = useState(null) const [mode, setMode] = useState('choose') const [loading, setLoading] = useState(false) const [createWarning, setCreateWarning] = useState(null) // A persisted linkedPR while the branch shows no PR means the linked PR could - // not be resolved. Mention it, but keep link editing out of this desktop-parity - // create surface. + // not be resolved. Mention it while still allowing the user to relink. const [orphanLinkedPR, setOrphanLinkedPR] = useState(null) useEffect(() => { @@ -103,6 +104,22 @@ export function PrSidebarCreateEmptyState({ client, worktreeId, gitBranch, onCre ) } + if (mode === 'link') { + return ( + + setMode('choose')} + onLinked={() => { + setMode('choose') + onCreated() + }} + /> + + ) + } + return ( @@ -148,6 +165,26 @@ export function PrSidebarCreateEmptyState({ client, worktreeId, gitBranch, onCre : 'The current branch is not linked to an open PR.'} {createWarning ? {createWarning} : null} + [ + styles.linkButton, + !client && styles.linkButtonDisabled, + pressed && styles.linkButtonPressed + ]} + onPress={() => setMode('link')} + disabled={!client} + accessibilityRole="button" + accessibilityLabel="Link an existing pull request" + accessibilityState={{ disabled: !client }} + hitSlop={6} + > + + Link an existing PR + ) diff --git a/mobile/src/components/pr-sidebar/pr-create-empty-state-styles.ts b/mobile/src/components/pr-sidebar/pr-create-empty-state-styles.ts index ffc77804b..486fbd1a8 100644 --- a/mobile/src/components/pr-sidebar/pr-create-empty-state-styles.ts +++ b/mobile/src/components/pr-sidebar/pr-create-empty-state-styles.ts @@ -83,5 +83,25 @@ export const prCreateEmptyStateStyles = StyleSheet.create({ borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.borderSubtle, padding: spacing.md + }, + // Secondary link-an-existing-PR affordance, set apart from the body copy. + linkButton: { + marginTop: spacing.xs, + minHeight: 32, + alignSelf: 'flex-start', + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xs + }, + linkButtonDisabled: { + opacity: 0.5 + }, + linkButtonPressed: { + opacity: 0.6 + }, + linkButtonText: { + color: colors.textSecondary, + fontSize: typography.metaSize, + fontWeight: '600' } })