Surface detailed commit failure summaries in Create PR flow

Instead of showing a generic commit failure message during the Create PR
intent flow, leverage `summarizeCommitFailure` to extract the underlying
reason (such as pre-commit hook or lint failures) and present it clearly.

Also introduce `commitErrorsRef` to reliably track and retrieve the most
up-to-date commit errors across renders without stale closure issues.
This commit is contained in:
Jinjing
2026-07-04 20:08:54 -07:00
parent 5ba4e2454d
commit 6309c98328
8 changed files with 82 additions and 19 deletions
@@ -254,6 +254,7 @@ import {
createPrIntentCurrentTargetConflictsWithToken,
createPrIntentGitStatusMatchesToken,
createPrIntentRunTokenMatches,
getCreatePrIntentCommitFailureNoticeMessage,
getCreatePrIntentStagePaths,
resolveCreatePrIntentReviewBase,
resolveCreatePrIntentRemoteStep,
@@ -1027,6 +1028,7 @@ function SourceControlInner(): React.JSX.Element {
loadSessionCommitDrafts()
)
const commitDraftsRef = useRef<CommitDraftsByWorktree>(commitDrafts)
const commitErrorsRef = useRef<Record<string, string | null>>({})
const [commitErrors, setCommitErrors] = useState<Record<string, string | null>>({})
const [remoteActionErrors, setRemoteActionErrors] = useState<
Record<string, SourceControlActionError | null>
@@ -1155,6 +1157,13 @@ function SourceControlInner(): React.JSX.Element {
},
[]
)
const setCommitErrorForWorktree = useCallback(
(worktreeId: string, message: string | null): void => {
commitErrorsRef.current = { ...commitErrorsRef.current, [worktreeId]: message }
setCommitErrors((prev) => ({ ...prev, [worktreeId]: message }))
},
[]
)
const isFolder = activeRepo ? isFolderRepo(activeRepo) : false
const worktreePath = activeWorktree?.path ?? null
@@ -1917,6 +1926,7 @@ function SourceControlInner(): React.JSX.Element {
return changed ? next : prev
}
updateCommitDrafts((prev) => pruneRecord(prev))
commitErrorsRef.current = pruneRecord(commitErrorsRef.current)
setCommitErrors((prev) => pruneRecord(prev))
setRemoteActionErrors((prev) => pruneRecord(prev))
setCommitInFlightByWorktree((prev) => pruneRecord(prev))
@@ -2037,7 +2047,7 @@ function SourceControlInner(): React.JSX.Element {
commitInFlightRef.current[target.worktreeId] = true
setCommitInFlightByWorktree((prev) => ({ ...prev, [target.worktreeId]: true }))
setCommitErrors((prev) => ({ ...prev, [target.worktreeId]: null }))
setCommitErrorForWorktree(target.worktreeId, null)
try {
const commitResult = await commitRuntimeGit(
{
@@ -2050,10 +2060,7 @@ function SourceControlInner(): React.JSX.Element {
message
)
if (!commitResult.success) {
setCommitErrors((prev) => ({
...prev,
[target.worktreeId]: commitResult.error ?? 'Commit failed'
}))
setCommitErrorForWorktree(target.worktreeId, commitResult.error ?? 'Commit failed')
return false
}
@@ -2071,7 +2078,7 @@ function SourceControlInner(): React.JSX.Element {
}
return writeCommitDraftForWorktree(prev, target.worktreeId, '')
})
setCommitErrors((prev) => ({ ...prev, [target.worktreeId]: null }))
setCommitErrorForWorktree(target.worktreeId, null)
if (!options?.target) {
void refreshActiveGitStatusAfterMutation()
}
@@ -2103,10 +2110,10 @@ function SourceControlInner(): React.JSX.Element {
}
return true
} catch (error) {
setCommitErrors((prev) => ({
...prev,
[target.worktreeId]: error instanceof Error ? error.message : 'Commit failed'
}))
setCommitErrorForWorktree(
target.worktreeId,
error instanceof Error ? error.message : 'Commit failed'
)
return false
} finally {
setCommitInFlightByWorktree((prev) => ({ ...prev, [target.worktreeId]: false }))
@@ -2122,6 +2129,7 @@ function SourceControlInner(): React.JSX.Element {
compareBaseRef,
grouped.staged.length,
refreshActiveGitStatusAfterMutation,
setCommitErrorForWorktree,
updateCommitDrafts,
unresolvedConflicts.length,
worktreePath
@@ -3822,12 +3830,21 @@ function SourceControlInner(): React.JSX.Element {
if (abortIfStale()) {
return
}
const commitFailure = commitErrorsRef.current[token.worktreeId] ?? null
setCreatePrIntentNoticeForWorktree(token.worktreeId, {
tone: 'destructive',
message: translate(
'auto.components.right.sidebar.SourceControl.createPrIntentCommitFailed',
'Could not commit changes. Fix the issue, then retry Create PR.'
)
message: getCreatePrIntentCommitFailureNoticeMessage(commitFailure, {
fallback: translate(
'auto.components.right.sidebar.SourceControl.createPrIntentCommitFailed',
'Could not commit changes. Fix the issue, then retry Create PR.'
),
withSummary: (summary) =>
translate(
'auto.components.right.sidebar.SourceControl.createPrIntentCommitBlockedSummary',
'Commit blocked: {{value0}} Fix the issue, then retry Create PR.',
{ value0: summary }
)
})
})
return
}
@@ -5,6 +5,7 @@ import {
createPrIntentCurrentTargetConflictsWithToken,
createPrIntentGitStatusMatchesToken,
createPrIntentRunTokenMatches,
getCreatePrIntentCommitFailureNoticeMessage,
getCreatePrIntentStagePaths,
resolveCreatePrIntentReviewBase,
resolveCreatePrIntentRemoteStep
@@ -241,4 +242,23 @@ describe('source-control Create PR intent flow helpers', () => {
})
).toBe('blocked')
})
it('surfaces the commit failure summary in the Create PR intent notice', () => {
expect(
getCreatePrIntentCommitFailureNoticeMessage(
'husky - pre-commit hook\neslint found 2 errors\nfull output'
)
).toBe('Commit blocked: Lint failed during commit. Fix the issue, then retry Create PR.')
expect(getCreatePrIntentCommitFailureNoticeMessage(null)).toBe(
'Could not commit changes. Fix the issue, then retry Create PR.'
)
expect(
getCreatePrIntentCommitFailureNoticeMessage('pre-commit hook failed', {
fallback: 'fallback',
withSummary: (summary) => `localized ${summary}`
})
).toBe('localized Pre-commit hook failed.')
})
})
@@ -5,6 +5,7 @@ import {
normalizeHostedReviewHeadRef
} from '../../../../shared/hosted-review-refs'
import type { GitStatusEntry, GitUpstreamStatus } from '../../../../shared/types'
import { summarizeCommitFailure } from './commit-failure-summary'
import { getStageAllPaths } from './discard-all-sequence'
export type CreatePrIntentRemoteStep = 'publish' | 'push' | 'force_push' | 'blocked' | 'none'
@@ -145,3 +146,23 @@ export function resolveCreatePrIntentRemoteStep({
return 'none'
}
export function getCreatePrIntentCommitFailureNoticeMessage(
commitError: string | null | undefined,
copy: {
fallback: string
withSummary: (summary: string) => string
} = {
fallback: 'Could not commit changes. Fix the issue, then retry Create PR.',
withSummary: (summary: string) =>
`Commit blocked: ${summary} Fix the issue, then retry Create PR.`
}
): string {
const summary = commitError ? summarizeCommitFailure(commitError) : null
if (!summary) {
return copy.fallback
}
return copy.withSummary(summary)
}
+2 -1
View File
@@ -9177,7 +9177,8 @@
"b715ef615b": "{{value0}} commits ahead of {{value1}}",
"c1a8f3e204": "1 commit behind {{value0}}",
"d2b9g4f315": "{{value0}} commits behind {{value1}}",
"e9c2a5d416": "Even with {{value0}}"
"e9c2a5d416": "Even with {{value0}}",
"createPrIntentCommitBlockedSummary": "Commit blocked: {{value0}} Fix the issue, then retry Create PR."
},
"SourceControlAgentActionDialog": {
"8e856842d1": "Could not start the selected agent.",
+2 -1
View File
@@ -9177,7 +9177,8 @@
"b715ef615b": "{{value0}} commits por delante de {{value1}}",
"c1a8f3e204": "1 commit por detrás de {{value0}}",
"d2b9g4f315": "{{value0}} commits por detrás de {{value1}}",
"e9c2a5d416": "A la par con {{value0}}"
"e9c2a5d416": "A la par con {{value0}}",
"createPrIntentCommitBlockedSummary": "Commit blocked: {{value0}} Fix the issue, then retry Create PR."
},
"SourceControlAgentActionDialog": {
"8e856842d1": "No se pudo iniciar el agente seleccionado.",
+2 -1
View File
@@ -9177,7 +9177,8 @@
"b715ef615b": "{{value1}}より{{value0}}commits 進んでいます",
"c1a8f3e204": "{{value0}}より1commit 遅れています",
"d2b9g4f315": "{{value1}}より{{value0}}commits 遅れています",
"e9c2a5d416": "{{value0}}と同じ状態"
"e9c2a5d416": "{{value0}}と同じ状態",
"createPrIntentCommitBlockedSummary": "Commit blocked: {{value0}} Fix the issue, then retry Create PR."
},
"SourceControlAgentActionDialog": {
"8e856842d1": "選択した agent を開始できませんでした。",
+2 -1
View File
@@ -9177,7 +9177,8 @@
"b715ef615b": "{{value1}}보다 {{value0}} commits 앞서 있음",
"c1a8f3e204": "{{value0}}보다 1 commit 뒤처짐",
"d2b9g4f315": "{{value1}}보다 {{value0}} commits 뒤처짐",
"e9c2a5d416": "{{value0}}와 동일한 상태"
"e9c2a5d416": "{{value0}}와 동일한 상태",
"createPrIntentCommitBlockedSummary": "Commit blocked: {{value0}} Fix the issue, then retry Create PR."
},
"SourceControlAgentActionDialog": {
"8e856842d1": "선택한 agent를 시작할 수 없습니다.",
+2 -1
View File
@@ -9177,7 +9177,8 @@
"b715ef615b": "领先 {{value1}} {{value0}} 个提交",
"c1a8f3e204": "落后 {{value0}} 1 个提交",
"d2b9g4f315": "落后 {{value1}} {{value0}} 个提交",
"e9c2a5d416": "与 {{value0}} 同步"
"e9c2a5d416": "与 {{value0}} 同步",
"createPrIntentCommitBlockedSummary": "Commit blocked: {{value0}} Fix the issue, then retry Create PR."
},
"SourceControlAgentActionDialog": {
"8e856842d1": "无法启动选定的智能体。",