mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 16:02:24 +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.
159 lines
4.6 KiB
TypeScript
159 lines
4.6 KiB
TypeScript
import { useCallback, type MutableRefObject } from 'react'
|
|
import { triggerError, triggerSuccess } from '../platform/haptics'
|
|
import type { LoadStatusOptions } from './mobile-source-control-screen-state'
|
|
import type {
|
|
MobileCommitFailureRecovery,
|
|
RecordMobileCommitFailure
|
|
} from './mobile-commit-failure-recovery'
|
|
|
|
type GitStep = { method: string; params?: Record<string, unknown> }
|
|
type SendGitRequest = <T>(method: string, params?: Record<string, unknown>) => Promise<T>
|
|
type RunGitWorkflow = (
|
|
actionId: string,
|
|
runner: () => Promise<void>,
|
|
options?: { clearCommitMessage?: boolean }
|
|
) => Promise<boolean>
|
|
|
|
type Params = {
|
|
commitMessage: string
|
|
stagedEntries: MobileCommitFailureRecovery['stagedEntries']
|
|
sendGitRequest: SendGitRequest
|
|
sendCommitRequest: (message: string) => Promise<unknown>
|
|
runGitSyncSteps: () => Promise<void>
|
|
runGitWorkflow: RunGitWorkflow
|
|
loadStatus: (options?: LoadStatusOptions) => Promise<boolean>
|
|
mountedRef: MutableRefObject<boolean>
|
|
busyActionRef: MutableRefObject<string | null>
|
|
setBusyAction: (next: string | null) => void
|
|
setActionError: (next: string | null) => void
|
|
setCommitMessage: (next: string) => void
|
|
recordCommitFailure: RecordMobileCommitFailure
|
|
}
|
|
|
|
// Commit + commit-then-action runners. Split from the main runners hook to keep
|
|
// each file under the line limit; behavior is unchanged from the original.
|
|
export function useMobileSourceControlCommitRunners(params: Params) {
|
|
const {
|
|
commitMessage,
|
|
stagedEntries,
|
|
sendGitRequest,
|
|
sendCommitRequest,
|
|
runGitSyncSteps,
|
|
runGitWorkflow,
|
|
loadStatus,
|
|
mountedRef,
|
|
busyActionRef,
|
|
setBusyAction,
|
|
setActionError,
|
|
setCommitMessage,
|
|
recordCommitFailure
|
|
} = params
|
|
|
|
const commit = useCallback(async () => {
|
|
const message = commitMessage.trim()
|
|
if (!message) {
|
|
return false
|
|
}
|
|
return await runGitWorkflow(
|
|
'commit',
|
|
async () => {
|
|
try {
|
|
await sendCommitRequest(message)
|
|
} catch (err) {
|
|
recordCommitFailure({
|
|
error: err instanceof Error ? err.message : 'Commit failed',
|
|
commitMessage: message,
|
|
stagedEntries
|
|
})
|
|
throw err
|
|
}
|
|
},
|
|
{ clearCommitMessage: true }
|
|
)
|
|
}, [commitMessage, recordCommitFailure, runGitWorkflow, sendCommitRequest, stagedEntries])
|
|
|
|
const runCommitFollowUps = useCallback(
|
|
async (actionId: string, afterCommit: () => Promise<void>) => {
|
|
const message = commitMessage.trim()
|
|
if (!message) {
|
|
return false
|
|
}
|
|
if (busyActionRef.current) {
|
|
return false
|
|
}
|
|
busyActionRef.current = actionId
|
|
setBusyAction(actionId)
|
|
setActionError(null)
|
|
recordCommitFailure(null)
|
|
let didCommit = false
|
|
try {
|
|
await sendCommitRequest(message)
|
|
didCommit = true
|
|
await afterCommit()
|
|
if (!mountedRef.current) {
|
|
return false
|
|
}
|
|
setCommitMessage('')
|
|
triggerSuccess()
|
|
await loadStatus({ preserveReadyOnFailure: true, force: true })
|
|
return true
|
|
} catch (err) {
|
|
if (!mountedRef.current) {
|
|
return false
|
|
}
|
|
triggerError()
|
|
const errorMessage = err instanceof Error ? err.message : 'Source control action failed'
|
|
if (!didCommit) {
|
|
recordCommitFailure({ error: errorMessage, commitMessage: message, stagedEntries })
|
|
}
|
|
if (didCommit) {
|
|
setCommitMessage('')
|
|
await loadStatus({
|
|
preserveReadyOnFailure: true,
|
|
clearActionErrorOnSuccess: false,
|
|
force: true
|
|
})
|
|
}
|
|
setActionError(errorMessage)
|
|
return false
|
|
} finally {
|
|
if (busyActionRef.current === actionId) {
|
|
busyActionRef.current = null
|
|
if (mountedRef.current) {
|
|
setBusyAction(null)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
[
|
|
busyActionRef,
|
|
commitMessage,
|
|
loadStatus,
|
|
mountedRef,
|
|
recordCommitFailure,
|
|
sendCommitRequest,
|
|
setActionError,
|
|
setBusyAction,
|
|
setCommitMessage,
|
|
stagedEntries
|
|
]
|
|
)
|
|
|
|
const runCommitSequence = useCallback(
|
|
async (actionId: string, afterCommit: GitStep[]) => {
|
|
return await runCommitFollowUps(actionId, async () => {
|
|
for (const step of afterCommit) {
|
|
await sendGitRequest<unknown>(step.method, step.params)
|
|
}
|
|
})
|
|
},
|
|
[runCommitFollowUps, sendGitRequest]
|
|
)
|
|
|
|
const runCommitSyncSequence = useCallback(async () => {
|
|
return await runCommitFollowUps('commit-sync', runGitSyncSteps)
|
|
}, [runCommitFollowUps, runGitSyncSteps])
|
|
|
|
return { commit, runCommitSequence, runCommitSyncSequence }
|
|
}
|