diff --git a/mobile/src/components/pr-sidebar/MobilePrComposeForm.tsx b/mobile/src/components/pr-sidebar/MobilePrComposeForm.tsx index 6837686907e..618e399d1a0 100644 --- a/mobile/src/components/pr-sidebar/MobilePrComposeForm.tsx +++ b/mobile/src/components/pr-sidebar/MobilePrComposeForm.tsx @@ -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) } diff --git a/mobile/src/components/pr-sidebar/PrSidebarCreateEmptyState.tsx b/mobile/src/components/pr-sidebar/PrSidebarCreateEmptyState.tsx index 0a70ae0a22d..8687d911807 100644 --- a/mobile/src/components/pr-sidebar/PrSidebarCreateEmptyState.tsx +++ b/mobile/src/components/pr-sidebar/PrSidebarCreateEmptyState.tsx @@ -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(null) const [mode, setMode] = useState('choose') const [loading, setLoading] = useState(false) const [createWarning, setCreateWarning] = useState(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 ( - - setMode('choose')} - onCreated={(url, warning) => { - setMode('choose') - setCreateWarning(warning ?? null) - openMobilePrUrl(url) - onCreated() - }} - /> - - ) - } - if (mode === 'link') { return ( diff --git a/mobile/src/source-control/MobileSourceControlModals.tsx b/mobile/src/source-control/MobileSourceControlModals.tsx index bd47a1901aa..b58d2520723 100644 --- a/mobile/src/source-control/MobileSourceControlModals.tsx +++ b/mobile/src/source-control/MobileSourceControlModals.tsx @@ -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)} /> - setShowPrSheet(false)} - onCreated={(url, warning) => { - setShowPrSheet(false) - setCreatedPrUrl(url) - setCreatedPrWarning(warning ?? null) - void loadStatus({ preserveReadyOnFailure: true, force: true }) - }} - /> - )} - + ) } diff --git a/mobile/src/source-control/mobile-hosted-review-create-intent-runner.ts b/mobile/src/source-control/mobile-hosted-review-create-intent-runner.ts new file mode 100644 index 00000000000..16e6a117f04 --- /dev/null +++ b/mobile/src/source-control/mobile-hosted-review-create-intent-runner.ts @@ -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, + worktreeId: string, + input: RunInput +): Promise { + 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 + } +} diff --git a/mobile/src/source-control/mobile-hosted-review-create-intent.ts b/mobile/src/source-control/mobile-hosted-review-create-intent.ts index 2bf01eb509c..c93b1fd2621 100644 --- a/mobile/src/source-control/mobile-hosted-review-create-intent.ts +++ b/mobile/src/source-control/mobile-hosted-review-create-intent.ts @@ -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...' } } diff --git a/mobile/src/source-control/mobile-pr-create.ts b/mobile/src/source-control/mobile-pr-create.ts index 49d1a50e6e8..675df20bedb 100644 --- a/mobile/src/source-control/mobile-pr-create.ts +++ b/mobile/src/source-control/mobile-pr-create.ts @@ -26,6 +26,22 @@ export { shouldPushBeforeMobileHostedReviewCreate as shouldPushBeforeMobilePrCreate } +export function getMobilePrCreateSuccessWarning( + outcome: Extract, + 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 diff --git a/mobile/src/source-control/use-mobile-open-pr-sheet-runner.ts b/mobile/src/source-control/use-mobile-create-pr-runner.ts similarity index 64% rename from mobile/src/source-control/use-mobile-open-pr-sheet-runner.ts rename to mobile/src/source-control/use-mobile-create-pr-runner.ts index dc8a9793cd3..9cd7d978834 100644 --- a/mobile/src/source-control/use-mobile-open-pr-sheet-runner.ts +++ b/mobile/src/source-control/use-mobile-create-pr-runner.ts @@ -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) => Promise @@ -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 ] diff --git a/mobile/src/source-control/use-mobile-source-control-action-sheet.ts b/mobile/src/source-control/use-mobile-source-control-action-sheet.ts index 8511996fe82..cedf3dad521 100644 --- a/mobile/src/source-control/use-mobile-source-control-action-sheet.ts +++ b/mobile/src/source-control/use-mobile-source-control-action-sheet.ts @@ -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, diff --git a/mobile/src/source-control/use-mobile-source-control-runners.ts b/mobile/src/source-control/use-mobile-source-control-runners.ts index 8f543838eb3..6bddf9bdd1f 100644 --- a/mobile/src/source-control/use-mobile-source-control-runners.ts +++ b/mobile/src/source-control/use-mobile-source-control-runners.ts @@ -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, diff --git a/mobile/src/source-control/use-mobile-source-control-state.ts b/mobile/src/source-control/use-mobile-source-control-state.ts index 10a7c181343..6ca585a3a4c 100644 --- a/mobile/src/source-control/use-mobile-source-control-state.ts +++ b/mobile/src/source-control/use-mobile-source-control-state.ts @@ -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(null) const [commitMessage, setCommitMessage] = useState('') const [generatingMessage, setGeneratingMessage] = useState(false) - const [showPrSheet, setShowPrSheet] = useState(false) const [showBranchPicker, setShowBranchPicker] = useState(false) const [localBranches, setLocalBranches] = useState(null) const [createdPrUrl, setCreatedPrUrl] = useState(null) const [createdPrWarning, setCreatedPrWarning] = useState(null) - const [prPrefill, setPrPrefill] = useState(null) const [discardTarget, setDiscardTarget] = useState(null) const [showActionSheet, setShowActionSheet] = useState(false) const [actionError, setActionError] = useState(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,