diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index 3c95dde3111..2acc9681a42 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -186,11 +186,13 @@ import type { SourceControlAiOperation } from '../../../../shared/source-control import { getCommitMessageModelDiscoveryHostKeyForScope } from '../../../../shared/commit-message-host-key' import { getRuntimeGitScope } from '@/runtime/runtime-git-client' import { getRepositorySourceControlAiSectionId } from '@/components/settings/repository-settings-targets' -import { hasExpandedCommitFailureDetails, summarizeCommitFailure } from './commit-failure-summary' import { - resolveCommitFailureDialogState, + getCommitFailureDialogWorktreeKey, + shouldShowCommitFailureDialog, + syncCommitFailureDialogState, type CommitFailureDialogState } from './commit-failure-dialog-state' +import { hasExpandedCommitFailureDetails, summarizeCommitFailure } from './commit-failure-summary' import { isSourceControlSplitOpenModifier, type SourceControlRowOpenEvent @@ -5623,24 +5625,24 @@ export function CommitArea({ : false, [commitError, commitFailureSummary] ) - const commitFailureIdentity = `${worktreeId ?? 'no-worktree'}:${commitError ?? ''}` + // Why: the details dialog is scoped to the worktree, not the exact stderr + // text, so a retried commit can refresh an open dialog with newer output. + const commitFailureWorktreeKey = getCommitFailureDialogWorktreeKey(worktreeId) const [commitFailureDialogState, setCommitFailureDialogState] = - useState({ identity: commitFailureIdentity, open: false }) - const resolvedCommitFailureDialogState = resolveCommitFailureDialogState( + useState({ + worktreeKey: commitFailureWorktreeKey, + open: false + }) + const isCommitFailureDialogOpen = shouldShowCommitFailureDialog( commitFailureDialogState, - commitFailureIdentity + commitFailureWorktreeKey, + hasCommitFailureDetails ) - if (resolvedCommitFailureDialogState !== commitFailureDialogState) { - setCommitFailureDialogState(resolvedCommitFailureDialogState) - } - const isCommitFailureDialogOpen = - resolvedCommitFailureDialogState.open && - resolvedCommitFailureDialogState.identity === commitFailureIdentity const setCommitFailureDialogOpen = useCallback( (open: boolean) => { - setCommitFailureDialogState({ identity: commitFailureIdentity, open }) + setCommitFailureDialogState({ worktreeKey: commitFailureWorktreeKey, open }) }, - [commitFailureIdentity] + [commitFailureWorktreeKey] ) const handleFixCommitFailureWithAI = useCallback( async (promptOverride?: string): Promise => { @@ -5656,6 +5658,12 @@ export function CommitArea({ setCommitFailureDialogOpen(false) }, [setCommitFailureDialogOpen]) + useEffect(() => { + setCommitFailureDialogState((current) => + syncCommitFailureDialogState(current, commitFailureWorktreeKey, hasCommitFailureDetails) + ) + }, [commitFailureWorktreeKey, hasCommitFailureDetails]) + // Why: most primary-kind labels are anchored by a directional icon so // the affirmative Commit (✓) reads distinctly from the remote-state // labels sharing this slot — Push (↑), Sync (↕), Publish (☁︎↑). Pull is @@ -5925,9 +5933,9 @@ export function CommitArea({ )} - {commitError && commitFailureSummary && ( + {commitError && commitFailureSummary && hasCommitFailureDetails && ( diff --git a/src/renderer/src/components/right-sidebar/commit-failure-dialog-state.test.ts b/src/renderer/src/components/right-sidebar/commit-failure-dialog-state.test.ts index 4e652dae075..0fe830147b8 100644 --- a/src/renderer/src/components/right-sidebar/commit-failure-dialog-state.test.ts +++ b/src/renderer/src/components/right-sidebar/commit-failure-dialog-state.test.ts @@ -1,31 +1,31 @@ import { describe, expect, it } from 'vitest' -import { resolveCommitFailureDialogState } from './commit-failure-dialog-state' +import { + shouldShowCommitFailureDialog, + syncCommitFailureDialogState, + type CommitFailureDialogState +} from './commit-failure-dialog-state' -describe('resolveCommitFailureDialogState', () => { - it('keeps the dialog state when the commit failure identity still matches', () => { - const state = { identity: 'wt-1:error-a', open: true } +describe('commit failure dialog state', () => { + it('keeps an open dialog visible when a new detailed error arrives for the same worktree', () => { + const state: CommitFailureDialogState = { worktreeKey: 'wt-1', open: true } - expect(resolveCommitFailureDialogState(state, 'wt-1:error-a')).toBe(state) + expect(syncCommitFailureDialogState(state, 'wt-1', true)).toBe(state) + expect(shouldShowCommitFailureDialog(state, 'wt-1', true)).toBe(true) }) - it('closes the dialog when the active commit failure identity changes', () => { - expect( - resolveCommitFailureDialogState({ identity: 'wt-1:error-a', open: true }, 'wt-1:') - ).toEqual({ - identity: 'wt-1:', - open: false - }) - }) - - it('does not reopen an older failure when its identity comes back later', () => { - const cleared = resolveCommitFailureDialogState( - { identity: 'wt-1:error-a', open: true }, - 'wt-1:error-b' + it('closes the dialog when the failure moves to another worktree', () => { + expect(syncCommitFailureDialogState({ worktreeKey: 'wt-1', open: true }, 'wt-2', true)).toEqual( + { + worktreeKey: 'wt-2', + open: false + } ) + }) - expect(resolveCommitFailureDialogState(cleared, 'wt-1:error-a')).toEqual({ - identity: 'wt-1:error-a', - open: false - }) + it('closes the dialog when the latest failure no longer has expanded details', () => { + const next = syncCommitFailureDialogState({ worktreeKey: 'wt-1', open: true }, 'wt-1', false) + + expect(next).toEqual({ worktreeKey: 'wt-1', open: false }) + expect(shouldShowCommitFailureDialog(next, 'wt-1', false)).toBe(false) }) }) diff --git a/src/renderer/src/components/right-sidebar/commit-failure-dialog-state.ts b/src/renderer/src/components/right-sidebar/commit-failure-dialog-state.ts index bc46a974eb7..e3ed0bd27ea 100644 --- a/src/renderer/src/components/right-sidebar/commit-failure-dialog-state.ts +++ b/src/renderer/src/components/right-sidebar/commit-failure-dialog-state.ts @@ -1,11 +1,32 @@ export type CommitFailureDialogState = { - identity: string + worktreeKey: string open: boolean } -export function resolveCommitFailureDialogState( +export function getCommitFailureDialogWorktreeKey(worktreeId: string | null | undefined): string { + return worktreeId ?? 'no-worktree' +} + +export function shouldShowCommitFailureDialog( state: CommitFailureDialogState, - currentIdentity: string + worktreeKey: string, + hasDetails: boolean +): boolean { + return hasDetails && state.open && state.worktreeKey === worktreeKey +} + +export function syncCommitFailureDialogState( + state: CommitFailureDialogState, + worktreeKey: string, + hasDetails: boolean ): CommitFailureDialogState { - return state.identity === currentIdentity ? state : { identity: currentIdentity, open: false } + if (state.worktreeKey === worktreeKey && hasDetails) { + return state + } + + if (state.worktreeKey === worktreeKey && !state.open) { + return state + } + + return { worktreeKey, open: false } } diff --git a/src/renderer/src/components/right-sidebar/commit-failure-summary.test.ts b/src/renderer/src/components/right-sidebar/commit-failure-summary.test.ts index 90cc911cb47..d07eedd387a 100644 --- a/src/renderer/src/components/right-sidebar/commit-failure-summary.test.ts +++ b/src/renderer/src/components/right-sidebar/commit-failure-summary.test.ts @@ -19,6 +19,13 @@ describe('commit failure summary', () => { ) }) + it('does not treat generic non-lint error counts as lint failures', () => { + expect(summarizeCommitFailure('tsc --noEmit\nFound 5 errors in 3 files.')).toBe('tsc --noEmit') + expect(summarizeCommitFailure('pre-commit hook failed\ntsc found 5 errors')).toBe( + 'Pre-commit hook failed.' + ) + }) + it('falls back to the first meaningful line for generic failures', () => { expect(summarizeCommitFailure('\n fatal: unable to auto-detect email address\nmore')).toBe( 'fatal: unable to auto-detect email address' diff --git a/src/renderer/src/components/right-sidebar/commit-failure-summary.ts b/src/renderer/src/components/right-sidebar/commit-failure-summary.ts index 8597c643ce5..d3ce7bb64c9 100644 --- a/src/renderer/src/components/right-sidebar/commit-failure-summary.ts +++ b/src/renderer/src/components/right-sidebar/commit-failure-summary.ts @@ -11,8 +11,7 @@ const CONTROL_PATTERN = const LOW_SIGNAL_LINE_PATTERN = /^(?:npm\s+(?:warn|warning)\b.*(?:env|config)|npm\s+notice\b|husky\s+-\s+deprecated\b)/i const HOOK_PATTERN = /\b(?:pre-commit|precommit|husky|lint-staged)\b/i -const LINT_PATTERN = - /\b(?:eslint|oxlint|lint-staged|lint)\b|(?:found|found:)\s+\d+\s+errors?\b|\b\d+\s+errors?\b/i +const LINT_PATTERN = /\b(?:eslint|oxlint|lint-staged|lint)\b/i function normalizeCommitFailure(raw: string): string { return raw.replace(ANSI_PATTERN, '').replace(/\r\n?/g, '\n').replace(CONTROL_PATTERN, '').trim()