diff --git a/src/renderer/src/components/GitHubItemDialog.tsx b/src/renderer/src/components/GitHubItemDialog.tsx index 6f449118a..b8cd117d6 100644 --- a/src/renderer/src/components/GitHubItemDialog.tsx +++ b/src/renderer/src/components/GitHubItemDialog.tsx @@ -50,7 +50,7 @@ import CommentMarkdown from '@/components/sidebar/CommentMarkdown' import { detectLanguage } from '@/lib/language-detect' import { cn } from '@/lib/utils' import { buildDiffTree, type DiffTreeNode } from '@/components/pr-diff-tree' -import { CHECK_COLOR, CHECK_ICON } from '@/components/right-sidebar/checks-helpers' +import { CHECK_COLOR, CHECK_ICON } from '@/components/right-sidebar/checks-panel-content' import { filterPRCommentsByAudience, getPRCommentAudienceCounts, diff --git a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx index 1e644af9a..b401307d2 100644 --- a/src/renderer/src/components/right-sidebar/ChecksPanel.tsx +++ b/src/renderer/src/components/right-sidebar/ChecksPanel.tsx @@ -14,7 +14,7 @@ import { MergeConflictNotice, ChecksList, PRCommentsList -} from './checks-helpers' +} from './checks-panel-content' import { ENTRY_REFRESH_GRACE_MS, shouldEntryRefresh } from './checks-entry-refresh' import type { PRInfo, PRCheckDetail, PRComment } from '../../../../shared/types' @@ -43,6 +43,7 @@ export default function ChecksPanel(): React.JSX.Element { const [commentsLoading, setCommentsLoading] = useState(false) const [emptyRefreshing, setEmptyRefreshing] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) + const [conflictDetailsRefreshing, setConflictDetailsRefreshing] = useState(false) const [editingTitle, setEditingTitle] = useState(false) const [titleDraft, setTitleDraft] = useState('') const [titleSaving, setTitleSaving] = useState(false) @@ -67,6 +68,7 @@ export default function ChecksPanel(): React.JSX.Element { setTitleSaving(false) setIsRefreshing(false) setEmptyRefreshing(false) + setConflictDetailsRefreshing(false) conflictSummaryRefreshKeyRef.current = null } @@ -108,6 +110,7 @@ export default function ChecksPanel(): React.JSX.Element { useEffect(() => { if (!repo || isFolder || !branch || !pr || pr.mergeable !== 'CONFLICTING') { conflictSummaryRefreshKeyRef.current = null + setConflictDetailsRefreshing(false) return } @@ -121,7 +124,17 @@ export default function ChecksPanel(): React.JSX.Element { // them so we don't keep rendering cached branch summaries or empty file // lists from an older payload. conflictSummaryRefreshKeyRef.current = refreshKey - void fetchPRForBranch(repo.path, branch, { force: true, linkedPRNumber: linkedPR }) + setConflictDetailsRefreshing(true) + void fetchPRForBranch(repo.path, branch, { force: true, linkedPRNumber: linkedPR }).finally( + () => { + // Why: fetchPRForBranch updates the PR cache before resolving, which + // can rerun this effect. Only the current refresh key may clear the + // spinner so stale requests don't race newer worktrees/branches. + if (conflictSummaryRefreshKeyRef.current === refreshKey) { + setConflictDetailsRefreshing(false) + } + } + ) }, [repo, isFolder, branch, pr, linkedPR, fetchPRForBranch]) // Fetch checks via cached store method @@ -580,7 +593,10 @@ export default function ChecksPanel(): React.JSX.Element { - + {/* Why: when the PR has merge conflicts and no checks have been fetched, showing "No checks configured" is misleading — checks may exist but simply cannot run until conflicts are resolved. Hide the empty state. */} diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index 0af146ae5..7047bf1e8 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -92,7 +92,7 @@ import { requestEditorSaveQuiesce } from '@/components/editor/editor-autosave' import { getConnectionId } from '@/lib/connection-context' -import { PullRequestIcon } from './checks-helpers' +import { PullRequestIcon } from './checks-panel-content' import type { DiffComment, GitBranchChangeEntry, diff --git a/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx b/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx new file mode 100644 index 000000000..1d8017fbf --- /dev/null +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it } from 'vitest' +import type { PRInfo } from '../../../../shared/types' +import { MergeConflictNotice } from './checks-panel-content' + +function makePR(overrides: Partial = {}): PRInfo { + return { + number: 42, + title: 'Conflicting PR', + state: 'open', + url: 'https://github.com/acme/widgets/pull/42', + checksStatus: 'pending', + updatedAt: '2026-05-14T00:00:00Z', + mergeable: 'CONFLICTING', + ...overrides + } +} + +function renderNotice(pr: PRInfo, isRefreshingConflictDetails = false): string { + return renderToStaticMarkup( + React.createElement(MergeConflictNotice, { pr, isRefreshingConflictDetails }) + ) +} + +describe('MergeConflictNotice', () => { + it('does not claim conflict details are refreshing after the refresh has settled', () => { + const markup = renderNotice(makePR()) + + expect(markup).toContain('Conflict file details are unavailable') + expect(markup).not.toContain('Refreshing conflict details') + }) + + it('shows refreshing copy while conflict details are actively refreshing', () => { + const markup = renderNotice(makePR(), true) + + expect(markup).toContain('Refreshing conflict details') + }) + + it('hides when the conflicting file list is available', () => { + const markup = renderNotice( + makePR({ + conflictSummary: { + baseRef: 'main', + baseCommit: 'abc1234', + commitsBehind: 2, + files: ['src/conflict.ts'] + } + }) + ) + + expect(markup).toBe('') + }) +}) diff --git a/src/renderer/src/components/right-sidebar/checks-helpers.tsx b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx similarity index 98% rename from src/renderer/src/components/right-sidebar/checks-helpers.tsx rename to src/renderer/src/components/right-sidebar/checks-panel-content.tsx index b907dd7df..8dce02260 100644 --- a/src/renderer/src/components/right-sidebar/checks-helpers.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx @@ -101,7 +101,13 @@ export function ConflictingFilesSection({ pr }: { pr: PRInfo }): React.JSX.Eleme } /** Fallback shown when GitHub reports merge conflicts but no file list is available yet. */ -export function MergeConflictNotice({ pr }: { pr: PRInfo }): React.JSX.Element | null { +export function MergeConflictNotice({ + pr, + isRefreshingConflictDetails +}: { + pr: PRInfo + isRefreshingConflictDetails: boolean +}): React.JSX.Element | null { if (pr.mergeable !== 'CONFLICTING' || (pr.conflictSummary?.files.length ?? 0) > 0) { return null } @@ -111,7 +117,11 @@ export function MergeConflictNotice({ pr }: { pr: PRInfo }): React.JSX.Element |
This branch has conflicts that must be resolved
-
Refreshing conflict details…
+
+ {isRefreshingConflictDetails + ? 'Refreshing conflict details…' + : 'Conflict file details are unavailable'} +
) }