mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 16:02:55 +00:00
* feat(mobile): add commit failure recovery panel with AI fix action - Surfaces a "Commit failed" panel with a one-tap AI fix button when a git commit fails in the source control view or PR creation flow - Detects commit failures specifically during the committing progress step and captures staged entries and commit message for context - Extracts commit failure summary and prompt logic into `src/shared/source-control-commit-failure.ts` and PR checks prompt into `src/shared/pr-checks-fix-prompt.ts` so both desktop and mobile share the same implementations - Adds auto-find of an available Metro port starting from 8081 and extracts expo CLI bootstrap into `mobile-expo-cli.mjs` shared by `start-emulator` and a new `start-expo.mjs` wrapper * Share source-control AI prompts and simplify mobile PR actions - Extract conflict, check-fixing, and commit-failure prompt builders to shared modules for reuse by both desktop and mobile. - Configure Metro in the mobile package to watch and bundle modules from the repository-root shared directory. - Remove the desktop-style merge method picker from the mobile PR actions panel, opting to use repository defaults automatically. - Refactor mobile hosted review creation and git preparation logic into dedicated helper files.
301 lines
8.7 KiB
TypeScript
301 lines
8.7 KiB
TypeScript
import { ActivityIndicator, Pressable, ScrollView, Text, View } from 'react-native'
|
|
import { RotateCw } from 'lucide-react-native'
|
|
import { colors } from '../theme/mobile-theme'
|
|
import type { PrSidebarState } from '../session/mobile-pr-sidebar-state'
|
|
import type { ConnectionState } from '../transport/types'
|
|
import type { RpcClient } from '../transport/rpc-client'
|
|
import { useMobilePrActions, type MobilePrActions } from '../session/use-mobile-pr-actions'
|
|
import {
|
|
useMobilePrCommentActions,
|
|
type MobilePrCommentActions
|
|
} from '../session/use-mobile-pr-comment-actions'
|
|
import {
|
|
useMobilePrTitleAction,
|
|
type MobilePrTitleAction
|
|
} from '../session/use-mobile-pr-title-action'
|
|
import { useMobilePrAiTriage, type MobilePrAiTriage } from '../session/use-mobile-pr-ai-triage'
|
|
import { buildFixChecksPrompt, buildResolveConflictsPrompt } from '../session/pr-ai-triage-prompt'
|
|
import { prSidebarRenderBranch } from './mobile-pr-sidebar-presentation'
|
|
import { mobilePrSidebarStyles as styles } from './pr-sidebar/mobile-pr-sidebar-styles'
|
|
import type { MobileGitStatusResult } from '../source-control/mobile-git-status'
|
|
import { PRSidebarHeader } from './pr-sidebar/PRSidebarHeader'
|
|
import { PRConflictingFilesSection } from './pr-sidebar/PRConflictingFilesSection'
|
|
import { PRActionsSection } from './pr-sidebar/PRActionsSection'
|
|
import { PRReviewersSection } from './pr-sidebar/PRReviewersSection'
|
|
import { PRChecksSection } from './pr-sidebar/PRChecksSection'
|
|
import { PRCommentsSection } from './pr-sidebar/PRCommentsSection'
|
|
import { PrSidebarCreateEmptyState } from './pr-sidebar/PrSidebarCreateEmptyState'
|
|
|
|
type Props = {
|
|
state: PrSidebarState
|
|
onRetry: () => void
|
|
// Re-fetches authoritative PR data after a successful mutation (U3/U6) or create.
|
|
refetch: () => void
|
|
// Threaded to sections for github.* fetches + mutations.
|
|
client: RpcClient | null
|
|
connState: ConnectionState
|
|
worktreeId: string
|
|
gitBranch: string | null
|
|
gitStatus: MobileGitStatusResult | null
|
|
headSha: string | null
|
|
bottomInset?: number
|
|
}
|
|
|
|
// Mutation hooks run unconditionally here and gate internally until a PR is ready.
|
|
export function MobilePRSidebar({
|
|
state,
|
|
onRetry,
|
|
refetch,
|
|
client,
|
|
connState,
|
|
worktreeId,
|
|
gitBranch,
|
|
gitStatus,
|
|
headSha,
|
|
bottomInset = 0
|
|
}: Props) {
|
|
const branch = prSidebarRenderBranch(state)
|
|
// prNumber is 0 until ready; the hook gates on `ready` so it never fires early.
|
|
const prNumber = state.kind === 'ready' ? state.data.pr.number : 0
|
|
const prRepo =
|
|
state.kind === 'ready'
|
|
? state.data.pr.prRepo
|
|
? { owner: state.data.pr.prRepo.owner, repo: state.data.pr.prRepo.repo }
|
|
: null
|
|
: null
|
|
const actions = useMobilePrActions({
|
|
client,
|
|
connState,
|
|
worktreeId,
|
|
prNumber,
|
|
headSha,
|
|
prRepo,
|
|
refetch
|
|
})
|
|
const commentActions = useMobilePrCommentActions({
|
|
client,
|
|
connState,
|
|
worktreeId,
|
|
prNumber,
|
|
prRepo,
|
|
refetch
|
|
})
|
|
const titleAction = useMobilePrTitleAction({
|
|
client,
|
|
connState,
|
|
worktreeId,
|
|
prNumber,
|
|
prRepo,
|
|
refetch
|
|
})
|
|
const triage = useMobilePrAiTriage({ client, connState, worktreeId })
|
|
|
|
return (
|
|
<ScrollView
|
|
style={{ flex: 1 }}
|
|
contentContainerStyle={[styles.scrollContent, { paddingBottom: bottomInset }]}
|
|
keyboardShouldPersistTaps="handled"
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<PrSidebarContent
|
|
branch={branch}
|
|
state={state}
|
|
onRetry={onRetry}
|
|
refetch={refetch}
|
|
client={client}
|
|
connState={connState}
|
|
worktreeId={worktreeId}
|
|
gitBranch={gitBranch}
|
|
gitStatus={gitStatus}
|
|
actions={actions}
|
|
commentActions={commentActions}
|
|
titleAction={titleAction}
|
|
triage={triage}
|
|
/>
|
|
</ScrollView>
|
|
)
|
|
}
|
|
|
|
function PrSidebarContent({
|
|
branch,
|
|
state,
|
|
onRetry,
|
|
refetch,
|
|
client,
|
|
connState,
|
|
worktreeId,
|
|
gitBranch,
|
|
gitStatus,
|
|
actions,
|
|
commentActions,
|
|
titleAction,
|
|
triage
|
|
}: {
|
|
branch: ReturnType<typeof prSidebarRenderBranch>
|
|
state: PrSidebarState
|
|
onRetry: () => void
|
|
refetch: () => void
|
|
client: RpcClient | null
|
|
connState: ConnectionState
|
|
worktreeId: string
|
|
gitBranch: string | null
|
|
gitStatus: MobileGitStatusResult | null
|
|
actions: MobilePrActions
|
|
commentActions: MobilePrCommentActions
|
|
titleAction: MobilePrTitleAction
|
|
triage: MobilePrAiTriage
|
|
}) {
|
|
if (branch === 'loading') {
|
|
return (
|
|
<View style={styles.stateArea}>
|
|
<ActivityIndicator color={colors.textSecondary} />
|
|
<Text style={styles.stateText}>Loading pull request…</Text>
|
|
</View>
|
|
)
|
|
}
|
|
if (branch === 'error') {
|
|
const message = state.kind === 'error' ? state.message : 'Something went wrong.'
|
|
return (
|
|
<View style={styles.stateArea}>
|
|
<Text style={styles.stateText}>{message}</Text>
|
|
<Pressable
|
|
style={styles.retryButton}
|
|
onPress={onRetry}
|
|
accessibilityRole="button"
|
|
accessibilityLabel="Retry loading pull request"
|
|
>
|
|
<RotateCw size={14} color={colors.textPrimary} strokeWidth={2.2} />
|
|
<Text style={styles.retryText}>Retry</Text>
|
|
</Pressable>
|
|
</View>
|
|
)
|
|
}
|
|
if (branch === 'blocked' || actions.blocked) {
|
|
// Permanent failure (R9): explanatory, no retry-encouragement styling. A
|
|
// mutation-time block (actions.blocked) routes here even from a ready state.
|
|
const message =
|
|
actions.blocked ??
|
|
(state.kind === 'blocked'
|
|
? state.message
|
|
: 'Not permitted — your GitHub account is not connected.')
|
|
return (
|
|
<View style={styles.stateArea}>
|
|
<Text style={styles.blockedText}>{message}</Text>
|
|
</View>
|
|
)
|
|
}
|
|
if (branch === 'none') {
|
|
// GitHub repo, but the current branch has no open PR — offer to create one
|
|
// (desktop parity) rather than showing a dead-end message.
|
|
return (
|
|
<PrSidebarCreateEmptyState
|
|
client={client}
|
|
worktreeId={worktreeId}
|
|
gitBranch={gitBranch}
|
|
gitStatus={gitStatus}
|
|
connState={connState}
|
|
onCreated={refetch}
|
|
/>
|
|
)
|
|
}
|
|
if (branch === 'ready' && state.kind === 'ready') {
|
|
return (
|
|
<PrSidebarSections
|
|
data={state.data}
|
|
client={client}
|
|
worktreeId={worktreeId}
|
|
actions={actions}
|
|
commentActions={commentActions}
|
|
titleAction={titleAction}
|
|
triage={triage}
|
|
refetch={refetch}
|
|
/>
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
function PrSidebarSections({
|
|
data,
|
|
client,
|
|
worktreeId,
|
|
actions,
|
|
commentActions,
|
|
titleAction,
|
|
triage,
|
|
refetch
|
|
}: {
|
|
data: Extract<PrSidebarState, { kind: 'ready' }>['data']
|
|
client: RpcClient | null
|
|
worktreeId: string
|
|
actions: MobilePrActions
|
|
commentActions: MobilePrCommentActions
|
|
titleAction: MobilePrTitleAction
|
|
triage: MobilePrAiTriage
|
|
refetch: () => void
|
|
}) {
|
|
const pr = data.pr
|
|
// Bind the triage launchers to this PR's data; the prompt builders are pure so
|
|
// building lazily inside launch() keeps a stale capture from leaking in.
|
|
const checksTriage = {
|
|
fixChecks: () =>
|
|
void triage.launch('fix-checks', () =>
|
|
buildFixChecksPrompt({
|
|
prNumber: pr.number,
|
|
prTitle: pr.title,
|
|
prUrl: pr.url,
|
|
checks: data.checks
|
|
})
|
|
),
|
|
isBusy: triage.isBusy('fix-checks'),
|
|
error: triage.error
|
|
}
|
|
const conflictsTriage = {
|
|
resolveConflicts: () =>
|
|
void triage.launch('resolve-conflicts', () =>
|
|
buildResolveConflictsPrompt({
|
|
prNumber: pr.number,
|
|
baseRef: pr.conflictSummary?.baseRef ?? pr.baseRefName ?? null,
|
|
files: pr.conflictSummary?.files ?? []
|
|
})
|
|
),
|
|
isBusy: triage.isBusy('resolve-conflicts'),
|
|
error: triage.error
|
|
}
|
|
return (
|
|
<>
|
|
<PRSidebarHeader pr={data.pr} details={data.details} titleAction={titleAction} />
|
|
{/* Conflicting-files section mirrors desktop order: directly below the header,
|
|
before actions/checks. Renders only when the PR has merge conflicts. */}
|
|
<PRConflictingFilesSection pr={data.pr} triage={conflictsTriage} />
|
|
<PRActionsSection
|
|
pr={data.pr}
|
|
actions={actions}
|
|
client={client}
|
|
worktreeId={worktreeId}
|
|
onUnlinked={refetch}
|
|
/>
|
|
<PRReviewersSection
|
|
details={data.details}
|
|
actions={actions}
|
|
client={client}
|
|
worktreeId={worktreeId}
|
|
/>
|
|
<PRChecksSection
|
|
checks={data.checks}
|
|
client={client}
|
|
worktreeId={worktreeId}
|
|
prRepo={data.pr.prRepo ?? null}
|
|
actions={actions}
|
|
triage={checksTriage}
|
|
/>
|
|
<PRCommentsSection
|
|
details={data.details}
|
|
prState={data.pr.state}
|
|
prRepo={data.pr.prRepo ?? null}
|
|
actions={commentActions}
|
|
/>
|
|
</>
|
|
)
|
|
}
|