fix(combined-diff): re-resolve sections by key to handle list rebuilds

The section list can rebuild while a write is pending (due to rebase, file changes, etc.); re-resolve by key instead of stale index to apply updates to the correct section.

- Convert skipped conflicts message to structured i18n plural forms
- Add oldPath field to git status signature for rename tracking
This commit is contained in:
Jinjing
2026-08-30 15:12:02 -07:00
parent 5afcc85085
commit 39b50adaeb
8 changed files with 33 additions and 18 deletions
@@ -10,6 +10,7 @@ export function buildCombinedGitStatusSignature(
return JSON.stringify(
matching.map((entry) => ({
path: entry.path,
oldPath: entry.oldPath ?? null,
area: entry.area,
status: entry.status,
added: entry.added ?? null,
@@ -79,12 +79,13 @@ export function CombinedDiffSkippedConflictNotice({
)}
</div>
<div className="mt-1 text-muted-foreground">
{skippedConflicts.length}{' '}
{translate('auto.components.editor.CombinedDiffViewer.689b99f8ad', 'unresolved conflict')}
{skippedConflicts.length === 1 ? '' : 's'}{' '}
{translate(
'auto.components.editor.CombinedDiffViewer.39e73e7181',
'were excluded from this diff view.'
'auto.components.editor.CombinedDiffViewer.skippedConflictsExcluded',
'{{count}} unresolved conflicts were excluded from this diff view.',
{
count: skippedConflicts.length,
defaultValue_one: '{{count}} unresolved conflict was excluded from this diff view.'
}
)}
</div>
<div className="mt-2 flex items-center gap-2">
@@ -163,6 +163,7 @@ export function useCombinedDiffSectionActions({
return
}
const sectionKey = section.key
const content = modifiedEditor?.getValue() ?? section.modifiedContent
const absolutePath = joinPath(file.filePath, section.path)
try {
@@ -183,10 +184,16 @@ export function useCombinedDiffSectionActions({
absolutePath,
content
)
setSectionHeights((prev) => removeDiffSectionMeasuredHeight(prev, index))
// Why: the section list can be rebuilt while the write is pending, so re-resolve
// by key — the captured index may now point at a different file.
const savedIndex = sectionsRef.current.findIndex((s) => s.key === sectionKey)
if (savedIndex === -1) {
return
}
setSectionHeights((prev) => removeDiffSectionMeasuredHeight(prev, savedIndex))
setSections((prev) =>
prev.map((s, i) => {
if (i !== index) {
prev.map((s) => {
if (s.key !== sectionKey) {
return s
}
@@ -226,6 +233,7 @@ export function useCombinedDiffSectionActions({
file.runtimeEnvironmentId,
file.worktreeId,
sections,
sectionsRef,
setSectionHeights,
setSections
]
+3 -2
View File
@@ -14233,8 +14233,9 @@
"7e7ca60816": "changed files",
"b6c3b84476": "Show file tree",
"39f8007549": "Review conflicts",
"39e73e7181": "were excluded from this diff view.",
"689b99f8ad": "unresolved conflict",
"skippedConflictsExcluded": "{{count}} unresolved conflicts were excluded from this diff view.",
"skippedConflictsExcluded_one": "{{count}} unresolved conflict was excluded from this diff view.",
"skippedConflictsExcluded_other": "{{count}} unresolved conflicts were excluded from this diff view.",
"820ec01f24": "Conflicted files are reviewed separately",
"fd8892b120": "No changes to display",
"eb5f40e49c": "This diff view excludes unresolved conflicts because the normal two-way diff pipeline is not conflict-safe.",
+3 -2
View File
@@ -12767,8 +12767,9 @@
"7e7ca60816": "archivos cambiados",
"b6c3b84476": "Mostrar árbol de archivos",
"39f8007549": "Revisar conflictos",
"39e73e7181": "se excluyeron de esta vista de diff.",
"689b99f8ad": "conflicto no resuelto",
"skippedConflictsExcluded": "Se excluyeron {{count}} conflictos no resueltos de esta vista de diff.",
"skippedConflictsExcluded_one": "Se excluyó {{count}} conflicto no resuelto de esta vista de diff.",
"skippedConflictsExcluded_other": "Se excluyeron {{count}} conflictos no resueltos de esta vista de diff.",
"820ec01f24": "Los archivos en conflicto se revisan por separado",
"fd8892b120": "No hay cambios para mostrar",
"eb5f40e49c": "Esta vista de diff excluye los conflictos sin resolver porque el pipeline normal de diff de dos vías no es seguro para conflictos.",
+3 -2
View File
@@ -12767,8 +12767,9 @@
"7e7ca60816": "変更されたファイル",
"b6c3b84476": "ファイルツリーを表示",
"39f8007549": "競合をレビューする",
"39e73e7181": "はこの差分ビューから除外されました。",
"689b99f8ad": "未解決の競合",
"skippedConflictsExcluded": "未解決の競合 {{count}} 件がこの差分ビューから除外されました。",
"skippedConflictsExcluded_one": "未解決の競合 {{count}} 件がこの差分ビューから除外されました。",
"skippedConflictsExcluded_other": "未解決の競合 {{count}} 件がこの差分ビューから除外されました。",
"820ec01f24": "競合したファイルは個別にレビューされます",
"fd8892b120": "表示に変更はありません",
"eb5f40e49c": "通常の双方向 diff パイプラインは競合に対して安全ではないため、この diff ビューでは未解決の競合が除外されます。",
+3 -2
View File
@@ -12799,8 +12799,9 @@
"7e7ca60816": "변경된 파일",
"b6c3b84476": "파일 트리 표시",
"39f8007549": "충돌 리뷰",
"39e73e7181": "이 차이점 보기에서 제외되었습니다.",
"689b99f8ad": "미해결 충돌",
"skippedConflictsExcluded": "미해결 충돌 {{count}}개가 이 차이점 보기에서 제외되었습니다.",
"skippedConflictsExcluded_one": "미해결 충돌 {{count}}개가 이 차이점 보기에서 제외되었습니다.",
"skippedConflictsExcluded_other": "미해결 충돌 {{count}}개가 이 차이점 보기에서 제외되었습니다.",
"820ec01f24": "충돌하는 파일은 별도로 리뷰됩니다.",
"fd8892b120": "표시할 변경사항이 없습니다.",
"eb5f40e49c": "일반적인 양방향 diff 파이프라인은 충돌로부터 안전하지 않기 때문에 이 diff 보기는 해결되지 않은 충돌을 제외합니다.",
+3 -2
View File
@@ -12810,8 +12810,9 @@
"7e7ca60816": "更改的文件",
"b6c3b84476": "显示文件树",
"39f8007549": "评审冲突",
"39e73e7181": "被排除在此差异视图之外。",
"689b99f8ad": "未解决的冲突",
"skippedConflictsExcluded": "{{count}} 个未解决的冲突已被排除在此差异视图之外。",
"skippedConflictsExcluded_one": "{{count}} 个未解决的冲突已被排除在此差异视图之外。",
"skippedConflictsExcluded_other": "{{count}} 个未解决的冲突已被排除在此差异视图之外。",
"820ec01f24": "冲突文件单独评审",
"fd8892b120": "没有显示任何变化",
"eb5f40e49c": "此差异视图排除了未解决的冲突,因为普通的双向差异管道不是冲突安全的。",