Refactor mobile PR creation to run intent and open URL directly

Remove MobilePrComposeSheet and the local compose form, moving instead
to a direct PR creation workflow that matches the desktop experience.

- Add runMobileHostedReviewCreateIntent to handle the full prepare,
  push, and create sequence.
- Replace useMobileOpenPrSheetRunner with useMobileCreatePrRunner to
  trigger the creation workflow and directly open the created PR URL.
- Simplify state management by removing showPrSheet, prPrefill, and
  associated local compose sheets.
This commit is contained in:
Jinjing
2026-06-27 15:04:43 -07:00
parent 7d229f58f1
commit aebc730a9e
11 changed files with 161 additions and 129 deletions
@@ -14,6 +14,7 @@ import type { RpcSuccess } from '../../transport/types'
import { triggerError, triggerSuccess } from '../../platform/haptics'
import {
createMobilePr,
getMobilePrCreateSuccessWarning,
shouldPushBeforeMobilePrCreate,
type MobilePrPrefill
} from '../../source-control/mobile-pr-create'
@@ -132,13 +133,7 @@ export function MobilePrComposeForm({
})
if (outcome.ok) {
triggerSuccess()
const warning = outcome.existing
? outcome.number
? `${copy.titleLabel} #${outcome.number} is already open.`
: `${copy.titleLabel} is already open.`
: outcome.linkError
? `${copy.titleLabel} created, but Orca could not refresh it yet.`
: undefined
const warning = getMobilePrCreateSuccessWarning(outcome, prefill.provider)
if (warning) {
setError(warning)
}
@@ -3,18 +3,11 @@ import { ActivityIndicator, Pressable, Text, View } from 'react-native'
import { GitPullRequestArrow, Link2, RefreshCw } from 'lucide-react-native'
import { colors } from '../../theme/mobile-theme'
import type { RpcClient } from '../../transport/rpc-client'
import {
getMobilePrCreateBlockMessage,
type MobilePrPrefill
} from '../../source-control/mobile-pr-create'
import type { MobileGitStatusResult } from '../../source-control/mobile-git-status'
import {
mobileHostedReviewCreateIntentProgressMessage,
prepareMobileHostedReviewCreateIntent
} from '../../source-control/mobile-hosted-review-create-intent'
import { mobileHostedReviewCreateIntentProgressMessage } from '../../source-control/mobile-hosted-review-create-intent'
import { runMobileHostedReviewCreateIntent } from '../../source-control/mobile-hosted-review-create-intent-runner'
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'
@@ -27,7 +20,7 @@ type Props = {
onCreated: () => void
}
type Mode = 'choose' | 'create' | 'link'
type Mode = 'choose' | 'link'
// 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
@@ -39,7 +32,6 @@ export function PrSidebarCreateEmptyState({
gitStatus,
onCreated
}: Props) {
const [prefill, setPrefill] = useState<MobilePrPrefill | null>(null)
const [mode, setMode] = useState<Mode>('choose')
const [loading, setLoading] = useState(false)
const [createWarning, setCreateWarning] = useState<string | null>(null)
@@ -80,25 +72,20 @@ export function PrSidebarCreateEmptyState({
setCreateWarning('Check out a branch before creating a pull request.')
return
}
const prepared = await prepareMobileHostedReviewCreateIntent(client, worktreeId, {
const outcome = await runMobileHostedReviewCreateIntent(client, worktreeId, {
branch: gitBranch,
title: gitBranch,
status: gitStatus,
onProgress: (progress) =>
setCreateWarning(mobileHostedReviewCreateIntentProgressMessage(progress))
})
if (!prepared.ok) {
setCreateWarning(prepared.error)
if (!outcome.ok) {
setCreateWarning(outcome.error)
return
}
const resolved = prepared.prefill
const blockedMessage = getMobilePrCreateBlockMessage(resolved)
if (blockedMessage) {
setCreateWarning(blockedMessage)
return
}
setPrefill(resolved)
setMode('create')
setCreateWarning(outcome.warning ?? null)
openMobilePrUrl(outcome.url)
onCreated()
} catch {
// Best-effort: if prefill resolution rejects, leave the empty state so the
// user can retry rather than surfacing an unhandled rejection.
@@ -109,26 +96,6 @@ export function PrSidebarCreateEmptyState({
const canCreate = !!client && !!gitBranch
if (mode === 'create' && prefill) {
return (
<View style={styles.composerArea}>
<MobilePrComposeForm
client={client}
worktreeId={worktreeId}
prefill={prefill}
head={gitBranch}
onCancel={() => setMode('choose')}
onCreated={(url, warning) => {
setMode('choose')
setCreateWarning(warning ?? null)
openMobilePrUrl(url)
onCreated()
}}
/>
</View>
)
}
if (mode === 'link') {
return (
<View style={styles.composerArea}>
@@ -1,28 +1,23 @@
import { ActionSheetModal, type ActionSheetAction } from '../components/ActionSheetModal'
import { ConfirmModal } from '../components/ConfirmModal'
import { PickerModal } from '../components/PickerModal'
import { MobilePrComposeSheet, openMobilePrUrl } from '../components/MobilePrComposeSheet'
import { openMobilePrUrl } from '../components/MobilePrComposeSheet'
import { MobileBranchDiffPreviewDrawer } from './MobileBranchDiffPreviewDrawer'
import type { MobileSourceControlState } from './use-mobile-source-control-state'
type Props = {
state: MobileSourceControlState
worktreeId: string
actionSheetActions: ActionSheetAction[]
}
export function MobileSourceControlModals({ state, worktreeId, actionSheetActions }: Props) {
export function MobileSourceControlModals({ state, actionSheetActions }: Props) {
const {
client,
branchDiffPreview,
setBranchDiffPreview,
showActionSheet,
setShowActionSheet,
discardTarget,
setDiscardTarget,
showPrSheet,
setShowPrSheet,
prPrefill,
showBranchPicker,
setShowBranchPicker,
localBranches,
@@ -30,9 +25,7 @@ export function MobileSourceControlModals({ state, worktreeId, actionSheetAction
setCreatedPrUrl,
createdPrWarning,
setCreatedPrWarning,
status,
branchLabel,
loadStatus,
checkoutBranch,
runGitAction
} = state
@@ -74,21 +67,6 @@ export function MobileSourceControlModals({ state, worktreeId, actionSheetAction
onCancel={() => setDiscardTarget(null)}
/>
<MobilePrComposeSheet
visible={showPrSheet}
client={client}
worktreeId={worktreeId ?? ''}
prefill={prPrefill ?? { provider: 'github', base: 'main', title: branchLabel, body: '' }}
head={status?.branch ?? null}
onClose={() => setShowPrSheet(false)}
onCreated={(url, warning) => {
setShowPrSheet(false)
setCreatedPrUrl(url)
setCreatedPrWarning(warning ?? null)
void loadStatus({ preserveReadyOnFailure: true, force: true })
}}
/>
<PickerModal
visible={showBranchPicker}
title="Switch Branch"
@@ -112,11 +112,7 @@ export function MobileSourceControlPanel({
/>
)}
<MobileSourceControlModals
state={state}
worktreeId={worktreeId}
actionSheetActions={actionSheetActions}
/>
<MobileSourceControlModals state={state} actionSheetActions={actionSheetActions} />
</View>
)
}
@@ -0,0 +1,86 @@
import type { RpcClient } from '../transport/rpc-client'
import type { MobileGitStatusResult } from './mobile-git-status'
import {
createMobilePr,
getMobilePrCreateBlockMessage,
getMobilePrCreateSuccessWarning,
shouldPushBeforeMobilePrCreate,
type MobilePrPrefill
} from './mobile-pr-create'
import {
prepareMobileHostedReviewCreateIntent,
type MobileHostedReviewCreateIntentProgress
} from './mobile-hosted-review-create-intent'
type RunInput = {
branch: string
title: string
status: MobileGitStatusResult | null
commitMessage?: string
onProgress?: (progress: MobileHostedReviewCreateIntentProgress) => void
}
export type MobileHostedReviewCreateIntentRunOutcome =
| {
ok: true
url: string
warning?: string
prefill: MobilePrPrefill
status: MobileGitStatusResult | null
committed: boolean
}
| {
ok: false
error: string
committed?: boolean
status?: MobileGitStatusResult | null
}
export async function runMobileHostedReviewCreateIntent(
client: Pick<RpcClient, 'sendRequest'>,
worktreeId: string,
input: RunInput
): Promise<MobileHostedReviewCreateIntentRunOutcome> {
const prepared = await prepareMobileHostedReviewCreateIntent(client, worktreeId, input)
if (!prepared.ok) {
return prepared
}
const blockedMessage = getMobilePrCreateBlockMessage(prepared.prefill)
if (blockedMessage) {
return {
ok: false,
error: blockedMessage,
committed: prepared.committed,
status: prepared.status
}
}
input.onProgress?.('creating_review')
const created = await createMobilePr(client, worktreeId, {
provider: prepared.prefill.provider,
base: prepared.prefill.base,
head: input.branch,
title: prepared.prefill.title,
body: prepared.prefill.body,
draft: false,
pushBeforeCreate: shouldPushBeforeMobilePrCreate(prepared.prefill)
})
if (!created.ok) {
return {
ok: false,
error: created.error,
committed: prepared.committed,
status: prepared.status
}
}
return {
ok: true,
url: created.url,
warning: getMobilePrCreateSuccessWarning(created, prepared.prefill.provider),
prefill: prepared.prefill,
status: prepared.status,
committed: prepared.committed
}
}
@@ -13,6 +13,7 @@ export type MobileHostedReviewCreateIntentProgress =
| 'publishing'
| 'pushing'
| 'force_pushing'
| 'creating_review'
export type MobileHostedReviewCreateIntentOutcome =
| {
@@ -47,6 +48,8 @@ export function mobileHostedReviewCreateIntentProgressMessage(
return 'Pushing commits...'
case 'force_pushing':
return 'Force pushing with lease...'
case 'creating_review':
return 'Creating review...'
}
}
@@ -26,6 +26,22 @@ export {
shouldPushBeforeMobileHostedReviewCreate as shouldPushBeforeMobilePrCreate
}
export function getMobilePrCreateSuccessWarning(
outcome: Extract<MobilePrCreateOutcome, { ok: true }>,
provider: MobilePrPrefill['provider']
): string | undefined {
const copy = hostedReviewCopy(provider)
if (outcome.existing) {
return outcome.number
? `${copy.titleLabel} #${outcome.number} is already open.`
: `${copy.titleLabel} is already open.`
}
if (outcome.linkError) {
return `${copy.titleLabel} created, but Orca could not refresh it yet.`
}
return undefined
}
export function getMobilePrCreateBlockMessage(prefill: MobilePrPrefill): string | null {
if (prefill.canCreate !== false || shouldPushBeforeMobileHostedReviewCreate(prefill)) {
return null
@@ -2,12 +2,14 @@ import { useCallback, type MutableRefObject } from 'react'
import type { RpcClient } from '../transport/rpc-client'
import { triggerError } from '../platform/haptics'
import type { MobileGitStatusResult } from './mobile-git-status'
import { getMobilePrCreateBlockMessage, type MobilePrPrefill } from './mobile-pr-create'
import {
mobileHostedReviewCreateIntentProgressMessage,
prepareMobileHostedReviewCreateIntent,
type MobileHostedReviewCreateIntentOutcome
type MobileHostedReviewCreateIntentProgress
} from './mobile-hosted-review-create-intent'
import {
runMobileHostedReviewCreateIntent,
type MobileHostedReviewCreateIntentRunOutcome
} from './mobile-hosted-review-create-intent-runner'
type RunGitWorkflow = (actionId: string, runner: () => Promise<void>) => Promise<boolean>
@@ -22,11 +24,11 @@ type Params = {
setActionError: (next: string | null) => void
setCommitMessage: (next: string) => void
setShowActionSheet: (next: boolean) => void
setPrPrefill: (next: MobilePrPrefill | null) => void
setShowPrSheet: (next: boolean) => void
setCreatedPrUrl: (next: string | null) => void
setCreatedPrWarning: (next: string | null) => void
}
export function useMobileOpenPrSheetRunner({
export function useMobileCreatePrRunner({
client,
worktreeId,
status,
@@ -37,8 +39,8 @@ export function useMobileOpenPrSheetRunner({
setActionError,
setCommitMessage,
setShowActionSheet,
setPrPrefill,
setShowPrSheet
setCreatedPrUrl,
setCreatedPrWarning
}: Params) {
return useCallback(
async (pushFirst: boolean) => {
@@ -49,37 +51,32 @@ export function useMobileOpenPrSheetRunner({
setActionError('Check out a branch before creating a pull request.')
return
}
const prepared: { current: MobileHostedReviewCreateIntentOutcome | null } = { current: null }
const created: { current: MobileHostedReviewCreateIntentRunOutcome | null } = {
current: null
}
const ran = await runGitWorkflow(pushFirst ? 'push-create-pr' : 'create-pr', async () => {
prepared.current = await prepareMobileHostedReviewCreateIntent(client, worktreeId, {
created.current = await runMobileHostedReviewCreateIntent(client, worktreeId, {
branch,
title: branchLabel,
status,
commitMessage,
onProgress: (progress) =>
onProgress: (progress: MobileHostedReviewCreateIntentProgress) =>
setActionError(mobileHostedReviewCreateIntentProgressMessage(progress))
})
if (!prepared.current.ok) {
throw new Error(prepared.current.error)
if (!created.current.ok) {
throw new Error(created.current.error)
}
})
const outcome = prepared.current
const outcome = created.current
if (outcome?.committed && mountedRef.current) {
setCommitMessage('')
}
if (!ran || !mountedRef.current || !outcome || !outcome.ok) {
return
}
const prefill = outcome.prefill
if (outcome.committed) {
setCommitMessage('')
}
const blockedMessage = getMobilePrCreateBlockMessage(prefill)
if (blockedMessage) {
triggerError()
setActionError(blockedMessage)
return
}
setActionError(null)
setPrPrefill(prefill)
setShowPrSheet(true)
setCreatedPrUrl(outcome.url)
setCreatedPrWarning(outcome.warning ?? null)
},
[
branchLabel,
@@ -89,9 +86,9 @@ export function useMobileOpenPrSheetRunner({
runGitWorkflow,
setActionError,
setCommitMessage,
setPrPrefill,
setCreatedPrUrl,
setCreatedPrWarning,
setShowActionSheet,
setShowPrSheet,
status,
worktreeId
]
@@ -23,7 +23,7 @@ export function useMobileSourceControlActionSheet(
runActionSheetGitSequence,
runActionSheetGitSync,
runActionSheetRebase,
openPrSheet,
createPr,
openBranchPicker,
openHistory
} = state
@@ -38,7 +38,8 @@ export function useMobileSourceControlActionSheet(
busyAction,
openingPath,
openingBranchPath,
prAvailable: upstreamKnown && upstream?.hasUpstream === true,
// Why: the create intent can publish/push before creating, matching desktop.
prAvailable: upstreamKnown,
handlers: {
commit: () => void runActionSheetCommit(),
commitPush: () =>
@@ -55,8 +56,8 @@ export function useMobileSourceControlActionSheet(
fastForward: () =>
void runActionSheetGitSequence('fast-forward', [{ method: 'git.fastForward' }]),
rebase: () => void runActionSheetRebase(),
createPr: () => void openPrSheet(false),
pushAndCreatePr: () => void openPrSheet(true),
createPr: () => void createPr(false),
pushAndCreatePr: () => void createPr(true),
checkout: () => void openBranchPicker(),
history: () => void openHistory()
}
@@ -64,11 +65,11 @@ export function useMobileSourceControlActionSheet(
[
busyAction,
commitMessage,
createPr,
openBranchPicker,
openHistory,
openingBranchPath,
openingPath,
openPrSheet,
runActionSheetCommit,
runActionSheetCommitSequence,
runActionSheetCommitSync,
@@ -2,11 +2,10 @@ import { useCallback, type MutableRefObject } from 'react'
import { useRouter } from 'expo-router'
import type { RpcClient } from '../transport/rpc-client'
import { triggerError, triggerSuccess } from '../platform/haptics'
import type { MobilePrPrefill } from './mobile-pr-create'
import { useMobileCommitMessageGeneration } from './use-mobile-commit-message-generation'
import { useMobileSourceControlCommitRunners } from './use-mobile-source-control-commit-runners'
import { useMobileSourceControlActionSheetRunners } from './use-mobile-source-control-action-sheet-runners'
import { useMobileOpenPrSheetRunner } from './use-mobile-open-pr-sheet-runner'
import { useMobileCreatePrRunner } from './use-mobile-create-pr-runner'
import type { RuntimeGitLocalBranches } from '../../../src/shared/runtime-types'
import type { MobileGitStatusResult } from './mobile-git-status'
import type { LoadStatusOptions } from './mobile-source-control-screen-state'
@@ -38,8 +37,8 @@ type Params = {
setShowActionSheet: (next: boolean) => void
setLocalBranches: (next: RuntimeGitLocalBranches | null) => void
setShowBranchPicker: (next: boolean) => void
setPrPrefill: (next: MobilePrPrefill | null) => void
setShowPrSheet: (next: boolean) => void
setCreatedPrUrl: (next: string | null) => void
setCreatedPrWarning: (next: string | null) => void
}
// All git workflow + action-sheet runners for the source-control panel. Split
@@ -70,8 +69,8 @@ export function useMobileSourceControlRunners(params: Params) {
setShowActionSheet,
setLocalBranches,
setShowBranchPicker,
setPrPrefill,
setShowPrSheet
setCreatedPrUrl,
setCreatedPrWarning
} = params
const runGitWorkflow = useCallback(
@@ -184,7 +183,7 @@ export function useMobileSourceControlRunners(params: Params) {
setActionError
})
const openPrSheet = useMobileOpenPrSheetRunner({
const createPr = useMobileCreatePrRunner({
client,
worktreeId,
status,
@@ -195,8 +194,8 @@ export function useMobileSourceControlRunners(params: Params) {
setActionError,
setCommitMessage,
setShowActionSheet,
setPrPrefill,
setShowPrSheet
setCreatedPrUrl,
setCreatedPrWarning
})
const openBranchPicker = useCallback(() => {
@@ -278,7 +277,7 @@ export function useMobileSourceControlRunners(params: Params) {
commit,
generateCommitMessage,
cancelGenerateCommitMessage,
openPrSheet,
createPr,
openBranchPicker,
openHistory,
checkoutBranch,
@@ -3,7 +3,6 @@ import { Keyboard, Platform } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useHostClient, useForceReconnect } from '../transport/client-context'
import { getWorktreeLabel } from '../session/worktree-label'
import type { MobilePrPrefill } from './mobile-pr-create'
import { useMobileGitRequests } from './use-mobile-git-requests'
import { useMobileSourceControlLoaders } from './use-mobile-source-control-loaders'
import { useMobileSourceControlOpeners } from './use-mobile-source-control-openers'
@@ -50,12 +49,10 @@ export function useMobileSourceControlState(params: MobileSourceControlStatePara
const [busyAction, setBusyAction] = useState<string | null>(null)
const [commitMessage, setCommitMessage] = useState('')
const [generatingMessage, setGeneratingMessage] = useState(false)
const [showPrSheet, setShowPrSheet] = useState(false)
const [showBranchPicker, setShowBranchPicker] = useState(false)
const [localBranches, setLocalBranches] = useState<MobileGitLocalBranches | null>(null)
const [createdPrUrl, setCreatedPrUrl] = useState<string | null>(null)
const [createdPrWarning, setCreatedPrWarning] = useState<string | null>(null)
const [prPrefill, setPrPrefill] = useState<MobilePrPrefill | null>(null)
const [discardTarget, setDiscardTarget] = useState<MobileGitStatusEntry | null>(null)
const [showActionSheet, setShowActionSheet] = useState(false)
const [actionError, setActionError] = useState<string | null>(null)
@@ -203,8 +200,8 @@ export function useMobileSourceControlState(params: MobileSourceControlStatePara
setShowActionSheet,
setLocalBranches,
setShowBranchPicker,
setPrPrefill,
setShowPrSheet
setCreatedPrUrl,
setCreatedPrWarning
})
const primaryAction = useMemo(
() =>
@@ -261,8 +258,6 @@ export function useMobileSourceControlState(params: MobileSourceControlStatePara
commitMessage,
setCommitMessage,
generatingMessage,
showPrSheet,
setShowPrSheet,
showBranchPicker,
setShowBranchPicker,
localBranches,
@@ -270,7 +265,6 @@ export function useMobileSourceControlState(params: MobileSourceControlStatePara
setCreatedPrUrl,
createdPrWarning,
setCreatedPrWarning,
prPrefill,
discardTarget,
setDiscardTarget,
showActionSheet,