From 16347ab7993d98cffdd8b1be11e0cbb2b6da6d01 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:08:58 -0700 Subject: [PATCH] Handle clean local merges when hosting provider reports conflicts (#5942) When the hosting provider (GitHub) reports conflicts but a local merge simulation is clean, we now mark the conflict summary as locally clean. This state is surfaced in both the desktop and mobile sidebars with a clear explanation and a copyable set of commands to trigger a remote mergeability recalculation via an empty commit and push. --- mobile/scripts/mobile-lag-scenario.ts | 2 + .../pr-sidebar/PRConflictingFilesSection.tsx | 75 ++++++++++- .../pr-conflict-presentation.test.ts | 61 ++++++++- .../pr-sidebar/pr-conflict-presentation.ts | 16 ++- .../pr-sidebar/pr-conflict-styles.ts | 44 +++++++ src/main/github/client.test.ts | 36 ++++++ src/main/github/conflict-summary.ts | 3 +- .../checks-panel-content.test.tsx | 52 ++++++++ .../right-sidebar/checks-panel-content.tsx | 117 ++++++++++++++++-- src/renderer/src/i18n/locales/en.json | 5 + src/renderer/src/i18n/locales/es.json | 7 +- src/renderer/src/i18n/locales/ja.json | 5 + src/renderer/src/i18n/locales/ko.json | 5 + src/renderer/src/i18n/locales/zh.json | 7 +- src/shared/types.ts | 1 + 15 files changed, 413 insertions(+), 23 deletions(-) diff --git a/mobile/scripts/mobile-lag-scenario.ts b/mobile/scripts/mobile-lag-scenario.ts index e2ad63c48..b7f9862b4 100644 --- a/mobile/scripts/mobile-lag-scenario.ts +++ b/mobile/scripts/mobile-lag-scenario.ts @@ -91,6 +91,8 @@ function createMockAgent(index: number, now: number): RuntimeWorktreeAgentRow { state: index % 12 === 0 ? 'waiting' : 'working', agentType: index % 3 === 0 ? 'claude' : 'codex', prompt: `Investigate mobile lag scenario ${index + 1}`, + taskTitle: null, + displayName: null, lastAssistantMessage: index % 6 === 0 ? 'Running focused checks' : null, toolName: null, toolInput: null, diff --git a/mobile/src/components/pr-sidebar/PRConflictingFilesSection.tsx b/mobile/src/components/pr-sidebar/PRConflictingFilesSection.tsx index d4c9fce08..95c95d305 100644 --- a/mobile/src/components/pr-sidebar/PRConflictingFilesSection.tsx +++ b/mobile/src/components/pr-sidebar/PRConflictingFilesSection.tsx @@ -1,5 +1,7 @@ +import { useEffect, useRef, useState } from 'react' import { ActivityIndicator, Pressable, ScrollView, Text, View } from 'react-native' -import { FileWarning, Sparkles } from 'lucide-react-native' +import * as Clipboard from 'expo-clipboard' +import { Check, Copy, FileWarning, Sparkles } from 'lucide-react-native' import { colors } from '../../theme/mobile-theme' import type { PRInfo } from '../../../../src/shared/types' import { PRSection } from './PRSection' @@ -27,10 +29,47 @@ type Props = { // list is not yet available. Ports the desktop ConflictingFilesSection + // MergeConflictNotice into the mobile card shell. export function PRConflictingFilesSection({ pr, isRefreshing = false, triage }: Props) { + const [commandsCopied, setCommandsCopied] = useState(false) + const copiedResetTimerRef = useRef | null>(null) const conflict = resolveConflictDisplay(pr) + + useEffect(() => { + return () => { + if (copiedResetTimerRef.current) { + clearTimeout(copiedResetTimerRef.current) + } + } + }, []) + if (!conflict) { return null } + let noticeBody = 'Conflict file details are unavailable' + if (isRefreshing) { + noticeBody = 'Refreshing conflict details…' + } else if (conflict.localMergeClean) { + noticeBody = + 'GitHub reports conflicts, but local Git did not reproduce them. Refresh the PR or push the branch to recalculate mergeability.' + } + + const copyRefreshCommands = async () => { + if (!conflict.mergeabilityRefreshCommands) { + return + } + try { + await Clipboard.setStringAsync(conflict.mergeabilityRefreshCommands) + } catch { + return + } + if (copiedResetTimerRef.current) { + clearTimeout(copiedResetTimerRef.current) + } + setCommandsCopied(true) + copiedResetTimerRef.current = setTimeout(() => { + copiedResetTimerRef.current = null + setCommandsCopied(false) + }, 1500) + } return ( @@ -44,11 +83,35 @@ export function PRConflictingFilesSection({ pr, isRefreshing = false, triage }: {conflict.fileDetailsUnavailable ? ( This branch has conflicts that must be resolved - - {isRefreshing - ? 'Refreshing conflict details…' - : 'Conflict file details are unavailable'} - + {noticeBody} + {conflict.mergeabilityRefreshCommands ? ( + + + Run from this worktree + [ + styles.copyCommandButton, + pressed && styles.copyCommandButtonPressed + ]} + onPress={() => void copyRefreshCommands()} + accessibilityRole="button" + accessibilityLabel="Copy mergeability refresh commands" + > + {commandsCopied ? ( + + ) : ( + + )} + + {commandsCopied ? 'Copied' : 'Copy commands'} + + + + + {conflict.mergeabilityRefreshCommands} + + + ) : null} ) : ( diff --git a/mobile/src/components/pr-sidebar/pr-conflict-presentation.test.ts b/mobile/src/components/pr-sidebar/pr-conflict-presentation.test.ts index 7209aa617..07172a52a 100644 --- a/mobile/src/components/pr-sidebar/pr-conflict-presentation.test.ts +++ b/mobile/src/components/pr-sidebar/pr-conflict-presentation.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from 'vitest' import type { PRInfo } from '../../../../src/shared/types' -import { hasMergeConflicts, resolveConflictDisplay } from './pr-conflict-presentation' +import { + buildMergeabilityRefreshCommands, + hasMergeConflicts, + resolveConflictDisplay +} from './pr-conflict-presentation' function pr(over: Partial): PRInfo { return { @@ -24,6 +28,16 @@ describe('hasMergeConflicts', () => { }) describe('resolveConflictDisplay', () => { + it('builds safe mergeability refresh commands', () => { + expect(buildMergeabilityRefreshCommands()).toBe( + [ + 'git fetch origin', + 'git commit --allow-empty --only -m "chore: refresh PR mergeability"', + 'git push' + ].join('\n') + ) + }) + it('returns null when there are no conflicts', () => { expect(resolveConflictDisplay(pr({ mergeable: 'MERGEABLE' }))).toBeNull() expect(resolveConflictDisplay(pr({ mergeable: 'UNKNOWN' }))).toBeNull() @@ -45,7 +59,9 @@ describe('resolveConflictDisplay', () => { files: ['src/a.ts', 'src/b.ts'], commitsBehind: 3, baseCommit: 'abc1234', - fileDetailsUnavailable: false + fileDetailsUnavailable: false, + localMergeClean: false, + mergeabilityRefreshCommands: null }) }) @@ -55,7 +71,9 @@ describe('resolveConflictDisplay', () => { files: [], commitsBehind: null, baseCommit: null, - fileDetailsUnavailable: true + fileDetailsUnavailable: true, + localMergeClean: false, + mergeabilityRefreshCommands: null }) }) @@ -68,4 +86,41 @@ describe('resolveConflictDisplay', () => { ) expect(display?.fileDetailsUnavailable).toBe(true) }) + + it('flags locally clean when GitHub reports a conflict that local git does not reproduce', () => { + const display = resolveConflictDisplay( + pr({ + mergeable: 'CONFLICTING', + conflictSummary: { + baseRef: 'main', + baseCommit: 'x', + commitsBehind: 1, + files: [], + localMergeState: 'clean' + } + }) + ) + expect(display?.localMergeClean).toBe(true) + expect(display?.mergeabilityRefreshCommands).toContain('git fetch origin') + expect(display?.mergeabilityRefreshCommands).toContain('git commit --allow-empty --only') + }) + + it('does not interpolate shell-sensitive base refs into copyable commands', () => { + const display = resolveConflictDisplay( + pr({ + mergeable: 'CONFLICTING', + conflictSummary: { + baseRef: 'release/$USER;echo unsafe', + baseCommit: 'x', + commitsBehind: 1, + files: [], + localMergeState: 'clean' + } + }) + ) + + expect(display?.mergeabilityRefreshCommands).toContain('git fetch origin') + expect(display?.mergeabilityRefreshCommands).not.toContain('$USER') + expect(display?.mergeabilityRefreshCommands).not.toContain('echo unsafe') + }) }) diff --git a/mobile/src/components/pr-sidebar/pr-conflict-presentation.ts b/mobile/src/components/pr-sidebar/pr-conflict-presentation.ts index f72a3add8..0b2278065 100644 --- a/mobile/src/components/pr-sidebar/pr-conflict-presentation.ts +++ b/mobile/src/components/pr-sidebar/pr-conflict-presentation.ts @@ -14,6 +14,9 @@ export type ConflictDisplay = { // True when conflicts exist but no file list is available — desktop shows a // fallback notice instead of the file list in this case. fileDetailsUnavailable: boolean + // True when the host reports conflicts but the local merge simulation is clean. + localMergeClean: boolean + mergeabilityRefreshCommands: string | null } // Conflicts exist only when the host reports CONFLICTING. Anything else (MERGEABLE @@ -22,6 +25,14 @@ export function hasMergeConflicts(pr: Pick): boolean { return pr.mergeable === 'CONFLICTING' } +export function buildMergeabilityRefreshCommands(): string { + return [ + 'git fetch origin', + 'git commit --allow-empty --only -m "chore: refresh PR mergeability"', + 'git push' + ].join('\n') +} + // Resolve the conflict view-model, or null when there is nothing to show. Returns // a model both when files are listed AND when conflicts exist without a file list // (so the section can render the fallback notice, matching desktop). @@ -32,10 +43,13 @@ export function resolveConflictDisplay( return null } const files = pr.conflictSummary?.files ?? [] + const localMergeClean = pr.conflictSummary?.localMergeState === 'clean' return { files, commitsBehind: pr.conflictSummary?.commitsBehind ?? null, baseCommit: pr.conflictSummary?.baseCommit ?? null, - fileDetailsUnavailable: files.length === 0 + fileDetailsUnavailable: files.length === 0, + localMergeClean, + mergeabilityRefreshCommands: localMergeClean ? buildMergeabilityRefreshCommands() : null } } diff --git a/mobile/src/components/pr-sidebar/pr-conflict-styles.ts b/mobile/src/components/pr-sidebar/pr-conflict-styles.ts index 6e5ad9d35..32ce54099 100644 --- a/mobile/src/components/pr-sidebar/pr-conflict-styles.ts +++ b/mobile/src/components/pr-sidebar/pr-conflict-styles.ts @@ -56,5 +56,49 @@ export const prConflictStyles = StyleSheet.create({ color: colors.textSecondary, fontSize: 11, marginTop: spacing.xs + }, + commandBox: { + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.borderSubtle, + backgroundColor: colors.bgRaised, + borderRadius: radii.button, + marginTop: spacing.sm, + padding: spacing.sm + }, + commandHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: spacing.sm + }, + commandLabel: { + color: colors.textSecondary, + fontSize: 10, + fontWeight: '600' + }, + copyCommandButton: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.xs, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.borderSubtle, + borderRadius: radii.button, + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs + }, + copyCommandButtonPressed: { + backgroundColor: colors.borderSubtle + }, + copyCommandText: { + color: colors.textPrimary, + fontSize: 11, + fontWeight: '600' + }, + commandText: { + color: colors.textPrimary, + fontFamily: typography.monoFamily, + fontSize: 10, + lineHeight: 15, + marginTop: spacing.sm } }) diff --git a/src/main/github/client.test.ts b/src/main/github/client.test.ts index e9c8f5751..1ef156e4e 100644 --- a/src/main/github/client.test.ts +++ b/src/main/github/client.test.ts @@ -1632,6 +1632,42 @@ describe('getPRForBranch', () => { expect(pr?.conflictSummary).toBeUndefined() }) + it('marks the conflict summary as locally clean when GitHub reports dirty but merge-tree has no conflicted files', async () => { + getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' }) + ghExecFileAsyncMock.mockResolvedValueOnce({ + stdout: JSON.stringify([ + { + number: 42, + title: 'Fix PR discovery', + state: 'open', + html_url: 'https://github.com/acme/widgets/pull/42', + updated_at: '2026-06-20T22:16:43Z', + draft: false, + mergeable_state: 'dirty', + base: { ref: 'main', sha: 'base-oid' }, + head: { ref: 'feature/test', sha: 'head-oid' } + } + ]) + }) + gitExecFileAsyncMock + .mockResolvedValueOnce({ stdout: '' }) + .mockResolvedValueOnce({ stdout: 'latest-base-oid\n' }) + .mockResolvedValueOnce({ stdout: 'merge-base-oid\n' }) + .mockResolvedValueOnce({ stdout: '1\n' }) + .mockResolvedValueOnce({ stdout: 'result-tree-oid\u0000' }) + + const pr = await getPRForBranch('/repo-root', 'feature/test') + + expect(pr?.mergeable).toBe('CONFLICTING') + expect(pr?.conflictSummary).toEqual({ + baseRef: 'main', + baseCommit: 'latest-', + commitsBehind: 1, + files: [], + localMergeState: 'clean' + }) + }) + it('falls back to GitHub baseRefOid when fetching or resolving the base ref fails', async () => { getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' }) ghExecFileAsyncMock.mockResolvedValueOnce({ diff --git a/src/main/github/conflict-summary.ts b/src/main/github/conflict-summary.ts index cf62d6e3c..62a162a78 100644 --- a/src/main/github/conflict-summary.ts +++ b/src/main/github/conflict-summary.ts @@ -37,7 +37,8 @@ export async function getPRConflictSummary( baseRef: baseRefName, baseCommit: latestBaseOid.slice(0, 7), commitsBehind, - files + files, + ...(files.length === 0 ? { localMergeState: 'clean' as const } : {}) } } catch { return undefined 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 index 7b755ce9e..307c20ecf 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.test.tsx @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest' import { TooltipProvider } from '@/components/ui/tooltip' import type { PRCheckDetail, PRComment, PRInfo } from '../../../../shared/types' import { + buildMergeabilityRecalculationCommands, CheckJobLogTail, ChecksList, ConflictTriageStrip, @@ -41,6 +42,16 @@ function renderNotice(pr: PRInfo, isRefreshingConflictDetails = false): string { } describe('MergeConflictNotice', () => { + it('builds safe mergeability recalculation commands', () => { + expect(buildMergeabilityRecalculationCommands()).toBe( + [ + 'git fetch origin', + 'git commit --allow-empty --only -m "chore: refresh PR mergeability"', + 'git push' + ].join('\n') + ) + }) + it('does not claim conflict details are refreshing after the refresh has settled', () => { const markup = renderNotice(makePR()) @@ -54,6 +65,47 @@ describe('MergeConflictNotice', () => { expect(markup).toContain('Refreshing conflict details') }) + it('explains when the hosting provider reports conflicts but local git simulates a clean merge', () => { + const markup = renderNotice( + makePR({ + conflictSummary: { + baseRef: 'main', + baseCommit: 'abc1234', + commitsBehind: 1, + files: [], + localMergeState: 'clean' + } + }) + ) + + expect(markup).toContain('local Git did not reproduce them') + expect(markup).toContain('Run from this worktree') + expect(markup).toContain('hosting provider reports conflicts') + expect(markup).toContain('git fetch origin') + expect(markup).toContain('git commit --allow-empty --only') + expect(markup).toContain('git push') + expect(markup).toContain('Copy commands') + expect(markup).not.toContain('Conflict file details are unavailable') + }) + + it('does not interpolate shell-sensitive base refs into copyable commands', () => { + const markup = renderNotice( + makePR({ + conflictSummary: { + baseRef: 'release/$USER;echo unsafe', + baseCommit: 'abc1234', + commitsBehind: 1, + files: [], + localMergeState: 'clean' + } + }) + ) + + expect(markup).toContain('git fetch origin') + expect(markup).not.toContain('$USER') + expect(markup).not.toContain('echo unsafe') + }) + it('hides when the conflicting file list is available', () => { const markup = renderNotice( makePR({ diff --git a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx index ee9ae25f2..938ad6ff8 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx @@ -108,6 +108,14 @@ type ConflictReview = { conflictSummary?: PRConflictSummary } +export function buildMergeabilityRecalculationCommands(): string { + return [ + 'git fetch origin', + 'git commit --allow-empty --only -m "chore: refresh PR mergeability"', + 'git push' + ].join('\n') +} + export function ConflictingFilesSection({ pr }: { pr: ConflictReview }): React.JSX.Element | null { const files = pr.conflictSummary?.files ?? [] if (pr.mergeable !== 'CONFLICTING' || files.length === 0) { @@ -164,6 +172,23 @@ export function MergeConflictNotice({ if (pr.mergeable !== 'CONFLICTING' || (pr.conflictSummary?.files.length ?? 0) > 0) { return null } + const locallyClean = pr.conflictSummary?.localMergeState === 'clean' + let noticeBody = translate( + 'auto.components.right.sidebar.checks.panel.content.ae8a04ef17', + 'Conflict file details are unavailable' + ) + if (isRefreshingConflictDetails) { + noticeBody = translate( + 'auto.components.right.sidebar.checks.panel.content.73d0675356', + 'Refreshing conflict details…' + ) + } else if (locallyClean) { + noticeBody = translate( + 'auto.components.right.sidebar.checks.panel.content.f5bc5c4cf1', + 'The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.' + ) + } + const refreshCommands = locallyClean ? buildMergeabilityRecalculationCommands() : null return (
@@ -173,17 +198,89 @@ export function MergeConflictNotice({ 'This branch has conflicts that must be resolved' )}
-
- {isRefreshingConflictDetails - ? translate( - 'auto.components.right.sidebar.checks.panel.content.73d0675356', - 'Refreshing conflict details…' - ) - : translate( - 'auto.components.right.sidebar.checks.panel.content.ae8a04ef17', - 'Conflict file details are unavailable' - )} +
{noticeBody}
+ {refreshCommands ? : null} +
+ ) +} + +function MergeabilityRecalculationCommandBox({ + commands +}: { + commands: string +}): React.JSX.Element { + const [copied, setCopied] = useState(false) + const copiedResetTimerRef = useRef(null) + const isMountedRef = useRef(false) + + const clearCopiedResetTimer = useCallback((): void => { + if (copiedResetTimerRef.current !== null) { + window.clearTimeout(copiedResetTimerRef.current) + copiedResetTimerRef.current = null + } + }, []) + + const setCopyButtonRef = useCallback( + (node: HTMLButtonElement | null) => { + isMountedRef.current = node !== null + if (node === null) { + clearCopiedResetTimer() + } + }, + [clearCopiedResetTimer] + ) + + const copyCommands = useCallback((): void => { + void window.api.ui + .writeClipboardText(commands) + .then(() => { + if (!isMountedRef.current) { + return + } + clearCopiedResetTimer() + setCopied(true) + copiedResetTimerRef.current = window.setTimeout(() => { + copiedResetTimerRef.current = null + setCopied(false) + }, 1500) + }) + .catch(() => { + /* best-effort */ + }) + }, [clearCopiedResetTimer, commands]) + + return ( +
+
+
+ {translate( + 'auto.components.right.sidebar.checks.panel.content.5bc9bda2af', + 'Run from this worktree' + )} +
+
+
+        {commands}
+      
) } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 906c5c504..ae5e1c9e1 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -8795,6 +8795,11 @@ "066fedd446": "Failed jobs", "ae8a04ef17": "Conflict file details are unavailable", "73d0675356": "Refreshing conflict details…", + "f5bc5c4cf1": "The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.", + "5bc9bda2af": "Run from this worktree", + "e87fb3d929": "Copy mergeability refresh commands", + "1e53e45072": "Copied", + "084c516efb": "Copy commands", "5dc3af25c0": "Select comment", "d7a2f9c401": "Send unresolved {{value0}} comments", "d91f2a6c39": "Send {{value0}} queued comments", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 0a00b8288..2d2067249 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -2307,7 +2307,7 @@ "TerminalContextMenu": { "b4cdd9314e": "Borrar pantalla", "8c17d6786d": "Cerrar panel", - "copyTerminalId": "Copiar ID de terminal", + "copyTerminalId": "Copy Terminal ID", "2cf85a6a55": "Copiar ID del panel", "39809d152f": "Establecer título…", "06c2b0f043": "Igualar tamaños de paneles", @@ -8794,6 +8794,11 @@ "066fedd446": "Trabajos fallidos", "ae8a04ef17": "Los detalles del archivo de conflicto no están disponibles", "73d0675356": "Detalles refrescantes del conflicto...", + "f5bc5c4cf1": "The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.", + "5bc9bda2af": "Run from this worktree", + "e87fb3d929": "Copy mergeability refresh commands", + "1e53e45072": "Copied", + "084c516efb": "Copy commands", "5dc3af25c0": "Seleccionar comentario", "d7a2f9c401": "Send unresolved {{value0}} comments", "d91f2a6c39": "Enviar {{value0}} comentarios en cola", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index d0296f472..3ce7e9cb4 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -8794,6 +8794,11 @@ "066fedd446": "失敗したジョブ", "ae8a04ef17": "競合ファイルの詳細は利用できません", "73d0675356": "競合の詳細を更新しています…", + "f5bc5c4cf1": "The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.", + "5bc9bda2af": "Run from this worktree", + "e87fb3d929": "Copy mergeability refresh commands", + "1e53e45072": "Copied", + "084c516efb": "Copy commands", "5dc3af25c0": "コメントを選択", "d7a2f9c401": "Send unresolved {{value0}} comments", "d91f2a6c39": "キュー内の {{value0}} 件のコメントを送信", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 5c57b69e9..1d8dcac3e 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -8794,6 +8794,11 @@ "066fedd446": "실패한 작업", "ae8a04ef17": "충돌 파일 세부정보를 사용할 수 없습니다.", "73d0675356": "충돌 세부정보 새로고침 중…", + "f5bc5c4cf1": "The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.", + "5bc9bda2af": "Run from this worktree", + "e87fb3d929": "Copy mergeability refresh commands", + "1e53e45072": "Copied", + "084c516efb": "Copy commands", "5dc3af25c0": "댓글 선택", "d7a2f9c401": "해결되지 않은 댓글 {{value0}}개 보내기", "d91f2a6c39": "대기 중인 댓글 {{value0}}개 보내기", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index d32b98bee..810301a80 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -2307,7 +2307,7 @@ "TerminalContextMenu": { "b4cdd9314e": "清晰的屏幕", "8c17d6786d": "关闭窗格", - "copyTerminalId": "复制终端 ID", + "copyTerminalId": "Copy Terminal ID", "2cf85a6a55": "复制窗格 ID", "39809d152f": "设置标题...", "06c2b0f043": "均衡窗格大小", @@ -8794,6 +8794,11 @@ "066fedd446": "失败的工作", "ae8a04ef17": "冲突文件详细信息不可用", "73d0675356": "刷新冲突细节...", + "f5bc5c4cf1": "The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.", + "5bc9bda2af": "Run from this worktree", + "e87fb3d929": "Copy mergeability refresh commands", + "1e53e45072": "Copied", + "084c516efb": "Copy commands", "5dc3af25c0": "选择评论", "d7a2f9c401": "Send unresolved {{value0}} comments", "d91f2a6c39": "发送 {{value0}} 条已排队评论", diff --git a/src/shared/types.ts b/src/shared/types.ts index a3c5119e5..bb79c463b 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1067,6 +1067,7 @@ export type PRConflictSummary = { baseCommit: string commitsBehind: number files: string[] + localMergeState?: 'clean' } export type GitHubRepositoryIdentity = { owner: string; repo: string }