Abort Create PR intent on failed detail generation instead of silently c

- Previously a swallowed generation error would fall through and
  auto-submit a PR with blank/template fields; now failures, cancels,
  and success are resolved explicitly and surfaced to the user.
- Adds resolveCreatePrIntentGeneratedFields/resolveCreatePrIntentGenerationErrorDetail
  helpers with test coverage, plus a new localized failure notice
  string across all locales.
This commit is contained in:
Jinjing
2026-07-16 16:42:49 -07:00
parent dd0f4c39c8
commit 67019d33c0
8 changed files with 153 additions and 16 deletions
@@ -258,6 +258,8 @@ import {
createPrIntentRunTokenMatches,
getCreatePrIntentCommitFailureNoticeMessage,
getCreatePrIntentStagePaths,
resolveCreatePrIntentGeneratedFields,
resolveCreatePrIntentGenerationErrorDetail,
resolveCreatePrIntentReviewBase,
resolveCreatePrIntentRemoteStep,
type CreatePrIntentRunToken
@@ -3523,13 +3525,26 @@ function SourceControlInner(): React.JSX.Element {
)
})
const target = getCreatePrIntentOperationTarget(token)
// Why: the intent auto-submits, so a swallowed failure would ship a
// blank-template PR; abort and surface the error instead.
const surfaceCreatePrIntentDetailGenerationFailure = (errorDetail: string): void => {
setCreatePrIntentNoticeForWorktree(token.worktreeId, {
tone: 'destructive',
message: translate(
'auto.components.right.sidebar.SourceControl.createPrIntentDetailGenerationFailed',
'Couldn’t generate {{value0}} details: {{value1}} Retry Create PR.',
{ value0: hostedReviewCreateCopy.reviewLabel, value1: errorDetail }
)
})
}
try {
const generated = await generateRuntimePullRequestFields(target, {
...fields,
provider: eligibility.provider,
useTemplate: resolvedPrCreationDefaults.useTemplate
})
if (generated.branchChangedByPreparation) {
const resolution = resolveCreatePrIntentGeneratedFields({ generated, fallback: fields })
if (resolution.outcome === 'branchChanged') {
setCreatePrIntentNoticeForWorktree(token.worktreeId, {
tone: 'muted',
message: translate(
@@ -3539,18 +3554,20 @@ function SourceControlInner(): React.JSX.Element {
})
return false
}
if (generated.success) {
fields = {
// Why: Create PR intent auto-submits; generated details should
// not retarget the review without user confirmation.
base: fields.base,
title: generated.fields.title.trim() || fields.title,
body: generated.fields.body,
draft: generated.fields.draft
}
if (resolution.outcome === 'applied') {
fields = resolution.fields
} else if (resolution.outcome === 'canceled') {
setCreatePrIntentNoticeForWorktree(token.worktreeId, null)
return false
} else {
surfaceCreatePrIntentDetailGenerationFailure(resolution.errorDetail)
return false
}
} catch (error) {
console.warn('[SourceControl] Create PR intent detail generation failed', error)
surfaceCreatePrIntentDetailGenerationFailure(
resolveCreatePrIntentGenerationErrorDetail(error)
)
return false
}
}
@@ -7,6 +7,8 @@ import {
createPrIntentRunTokenMatches,
getCreatePrIntentCommitFailureNoticeMessage,
getCreatePrIntentStagePaths,
resolveCreatePrIntentGeneratedFields,
resolveCreatePrIntentGenerationErrorDetail,
resolveCreatePrIntentReviewBase,
resolveCreatePrIntentRemoteStep
} from './source-control-create-pr-intent-flow'
@@ -278,4 +280,65 @@ describe('source-control Create PR intent flow helpers', () => {
})
).toBe('localized Pre-commit hook failed.')
})
it('resolves generated PR details without auto-submitting a blank template on failure', () => {
const fallback = { base: 'main', title: 'fallback title', body: 'fallback body', draft: false }
expect(
resolveCreatePrIntentGeneratedFields({
generated: { success: true, branchChangedByPreparation: true },
fallback
})
).toEqual({ outcome: 'branchChanged' })
expect(
resolveCreatePrIntentGeneratedFields({
generated: {
success: true,
fields: { title: ' Generated title ', body: 'Generated body', draft: true }
},
fallback
})
).toEqual({
outcome: 'applied',
fields: { base: 'main', title: 'Generated title', body: 'Generated body', draft: true }
})
// Why: a blank generated title must not overwrite the seeded fallback title.
expect(
resolveCreatePrIntentGeneratedFields({
generated: { success: true, fields: { title: ' ', body: 'Generated body', draft: true } },
fallback
})
).toEqual({
outcome: 'applied',
fields: { base: 'main', title: 'fallback title', body: 'Generated body', draft: true }
})
expect(
resolveCreatePrIntentGeneratedFields({
generated: { success: false, canceled: true },
fallback
})
).toEqual({ outcome: 'canceled' })
expect(
resolveCreatePrIntentGeneratedFields({
generated: { success: false, error: 'Model request timed out' },
fallback
})
).toEqual({ outcome: 'failed', errorDetail: 'Model request timed out' })
expect(
resolveCreatePrIntentGeneratedFields({ generated: { success: false }, fallback })
).toEqual({ outcome: 'failed', errorDetail: '' })
})
it('stringifies both Error and non-Error thrown values for the failure notice', () => {
expect(resolveCreatePrIntentGenerationErrorDetail(new Error('boom'))).toBe('boom')
expect(resolveCreatePrIntentGenerationErrorDetail('raw string throw')).toBe('raw string throw')
expect(resolveCreatePrIntentGenerationErrorDetail({ code: 'ECONNRESET' })).toBe(
'[object Object]'
)
})
})
@@ -152,6 +152,58 @@ export function resolveCreatePrIntentRemoteStep({
return 'none'
}
export type CreatePrIntentGeneratedFields = {
base: string
title: string
body: string
draft: boolean
}
export type CreatePrIntentGeneratedFieldsResolution =
| { outcome: 'branchChanged' }
| { outcome: 'canceled' }
| { outcome: 'failed'; errorDetail: string }
| { outcome: 'applied'; fields: CreatePrIntentGeneratedFields }
export function resolveCreatePrIntentGeneratedFields({
generated,
fallback
}: {
generated: {
success: boolean
error?: string
canceled?: boolean
branchChangedByPreparation?: boolean
fields?: { title: string; body: string; draft: boolean }
}
fallback: CreatePrIntentGeneratedFields
}): CreatePrIntentGeneratedFieldsResolution {
if (generated.branchChangedByPreparation) {
return { outcome: 'branchChanged' }
}
if (generated.success && generated.fields) {
return {
outcome: 'applied',
fields: {
// Why: Create PR intent auto-submits; generated details should
// not retarget the review without user confirmation.
base: fallback.base,
title: generated.fields.title.trim() || fallback.title,
body: generated.fields.body,
draft: generated.fields.draft
}
}
}
if (generated.canceled) {
return { outcome: 'canceled' }
}
return { outcome: 'failed', errorDetail: generated.error ?? '' }
}
export function resolveCreatePrIntentGenerationErrorDetail(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
export function getCreatePrIntentCommitFailureNoticeMessage(
commitError: string | null | undefined,
copy: {
+2 -1
View File
@@ -9644,7 +9644,8 @@
"a9bf7c171a": "Push Failed",
"834cb3f23d": "Fix with AI",
"783a808870": "Close"
}
},
"createPrIntentDetailGenerationFailed": "Couldn’t generate {{value0}} details: {{value1}} Retry Create PR."
},
"SourceControlAgentActionDialog": {
"8e856842d1": "Could not start the selected agent.",
+2 -1
View File
@@ -9644,7 +9644,8 @@
"a9bf7c171a": "Push fallido",
"834cb3f23d": "Corregir con AI",
"783a808870": "Cerrar"
}
},
"createPrIntentDetailGenerationFailed": "Couldn’t generate {{value0}} details: {{value1}} Retry Create PR."
},
"SourceControlAgentActionDialog": {
"8e856842d1": "No se pudo iniciar el agente seleccionado.",
+2 -1
View File
@@ -9644,7 +9644,8 @@
"a9bf7c171a": "プッシュ失敗",
"834cb3f23d": "AIで修正",
"783a808870": "閉じる"
}
},
"createPrIntentDetailGenerationFailed": "Couldn’t generate {{value0}} details: {{value1}} Retry Create PR."
},
"SourceControlAgentActionDialog": {
"8e856842d1": "選択した agent を開始できませんでした。",
+2 -1
View File
@@ -9644,7 +9644,8 @@
"a9bf7c171a": "푸시 실패",
"834cb3f23d": "AI로 수정",
"783a808870": "닫기"
}
},
"createPrIntentDetailGenerationFailed": "Couldn’t generate {{value0}} details: {{value1}} Retry Create PR."
},
"SourceControlAgentActionDialog": {
"8e856842d1": "선택한 agent를 시작할 수 없습니다.",
+2 -1
View File
@@ -9644,7 +9644,8 @@
"a9bf7c171a": "推送失败",
"834cb3f23d": "使用 AI 修复",
"783a808870": "关闭"
}
},
"createPrIntentDetailGenerationFailed": "Couldn’t generate {{value0}} details: {{value1}} Retry Create PR."
},
"SourceControlAgentActionDialog": {
"8e856842d1": "无法启动选定的智能体。",