diff --git a/src/renderer/src/components/editor/useRichMarkdownReviewController.submit-ownership.test.tsx b/src/renderer/src/components/editor/useRichMarkdownReviewController.submit-ownership.test.tsx
new file mode 100644
index 00000000000..56108fec22f
--- /dev/null
+++ b/src/renderer/src/components/editor/useRichMarkdownReviewController.submit-ownership.test.tsx
@@ -0,0 +1,161 @@
+// @vitest-environment happy-dom
+import { act, cleanup, renderHook } from '@testing-library/react'
+import { Editor } from '@tiptap/react'
+import StarterKit from '@tiptap/starter-kit'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import type { DiffComment } from '../../../../shared/diff-comment-types'
+import {
+ createRichMarkdownAnnotationHighlightExtension,
+ richMarkdownAnnotationHighlightPluginKey
+} from './rich-markdown-annotation-highlight'
+import type { RichMarkdownAnnotationTarget } from './rich-markdown-review-annotations'
+import { useRichMarkdownReviewController } from './useRichMarkdownReviewController'
+
+vi.mock('@/i18n/i18n', () => ({
+ translate: (_key: string, fallback: string) => fallback
+}))
+
+const editors: Editor[] = []
+const target: RichMarkdownAnnotationTarget = {
+ from: 1,
+ to: 4,
+ lineNumber: 1,
+ selectedText: 'abc',
+ top: 0,
+ buttonTop: 0,
+ buttonLeft: 0
+}
+const saved: DiffComment = {
+ id: 'saved-note',
+ createdAt: 1,
+ worktreeId: 'folder-owner',
+ filePath: 'notes.md',
+ source: 'markdown',
+ lineNumber: 1,
+ selectedText: 'abc',
+ body: 'Review abc',
+ side: 'modified'
+}
+
+function createEditor(): Editor {
+ const editor = new Editor({
+ element: document.createElement('div'),
+ extensions: [StarterKit, createRichMarkdownAnnotationHighlightExtension()],
+ content: '
abc xyz
'
+ })
+ editors.push(editor)
+ return editor
+}
+
+function highlightXyz(editor: Editor): void {
+ editor.view.dispatch(
+ editor.state.tr.setMeta(richMarkdownAnnotationHighlightPluginKey, {
+ activeRange: { from: 5, to: 8 },
+ noteRanges: [{ from: 5, to: 8 }]
+ })
+ )
+}
+
+function highlightedText(editor: Editor): string[] {
+ return [...editor.view.dom.querySelectorAll('.rich-markdown-annotation-selection')].map(
+ (element) => element.textContent ?? ''
+ )
+}
+
+function mountReview() {
+ const editor = createEditor()
+ const editorRef: { current: Editor | null } = { current: editor }
+ const rootRef: { current: HTMLDivElement | null } = { current: document.createElement('div') }
+ const pending = Promise.withResolvers()
+ const addDiffComment = vi.fn((_input: Omit) => pending.promise)
+ let comments: DiffComment[] = []
+ const hook = renderHook(() =>
+ useRichMarkdownReviewController({
+ addDiffComment,
+ allDiffComments: comments,
+ content: 'abc xyz',
+ editorRef,
+ filePath: '/workspace/notes.md',
+ markdownAnnotationsEnabled: true,
+ markdownReviewContent: 'abc xyz',
+ markdownSourceLineOffset: 0,
+ rootRef,
+ scrollContainerRef: { current: document.createElement('div') },
+ worktreeId: 'folder-owner',
+ worktreeRoot: '/workspace'
+ })
+ )
+ act(() => hook.result.current.setAnnotationPopover(target))
+ return {
+ ...hook,
+ editor,
+ pending,
+ addDiffComment,
+ updateComments(next: DiffComment[]) {
+ comments = next
+ hook.rerender()
+ }
+ }
+}
+
+afterEach(() => {
+ cleanup()
+ for (const editor of editors.splice(0)) {
+ editor.destroy()
+ }
+})
+
+describe('review submission owner', () => {
+ it('leaves a newer draft open when an older submission completes', async () => {
+ const review = mountReview()
+ const submission = review.result.current.submitAnnotation('Review abc')
+ const nextTarget = { ...target, from: 5, to: 8, selectedText: 'xyz' }
+ act(() => review.result.current.setAnnotationPopover(nextTarget))
+ await act(async () => {
+ review.pending.resolve(saved)
+ await submission
+ })
+ expect(review.result.current.annotationPopover).toEqual(nextTarget)
+ })
+
+ it('keeps highlights for comments that arrive while persistence is pending', async () => {
+ const review = mountReview()
+ const submission = review.result.current.submitAnnotation('Review abc')
+ act(() => {
+ review.updateComments([{ ...saved, id: 'other-note', selectedText: 'xyz' }])
+ highlightXyz(review.editor)
+ })
+ await act(async () => {
+ review.pending.resolve(saved)
+ await submission
+ })
+ expect(highlightedText(review.editor)).toEqual(['abc', 'xyz'])
+ })
+
+ it('closes the current draft and highlights the saved note on success', async () => {
+ const review = mountReview()
+ const submission = review.result.current.submitAnnotation('Review abc')
+ await act(async () => {
+ review.pending.resolve(saved)
+ await submission
+ })
+ expect(review.result.current.annotationPopover).toBeNull()
+ expect(highlightedText(review.editor)).toEqual(['abc'])
+ })
+
+ it('preserves a failed draft for retry', async () => {
+ const failure = mountReview()
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {})
+ try {
+ const failedSubmission = failure.result.current.submitAnnotation('Review abc')
+ await act(async () => {
+ failure.pending.resolve(null)
+ await failedSubmission
+ })
+ expect(failure.result.current.annotationPopover).toEqual(target)
+ expect(highlightedText(failure.editor)).toEqual([])
+ } finally {
+ error.mockRestore()
+ }
+ })
+})
diff --git a/src/renderer/src/components/editor/useRichMarkdownReviewController.ts b/src/renderer/src/components/editor/useRichMarkdownReviewController.ts
index 29d53249d0e..fcf17cce3e2 100644
--- a/src/renderer/src/components/editor/useRichMarkdownReviewController.ts
+++ b/src/renderer/src/components/editor/useRichMarkdownReviewController.ts
@@ -163,6 +163,7 @@ export function useRichMarkdownReviewController({
if (!annotationPopover || sourceRelativePath === null) {
return
}
+ const submittedEditor = editorRef.current
const result = await addDiffComment({
worktreeId,
filePath: sourceRelativePath,
@@ -180,10 +181,20 @@ export function useRichMarkdownReviewController({
console.error('Failed to add markdown comment — draft preserved')
return
}
+ if (
+ !submittedEditor ||
+ submittedEditor.isDestroyed ||
+ annotationPopoverRef.current !== annotationPopover
+ ) {
+ return
+ }
+ const currentComments = markdownCommentsRef.current
updateRichMarkdownAnnotationHighlightsAfterSubmit({
annotationPopover,
- comments: [...markdownComments, result],
- editor: editorRef.current,
+ comments: currentComments.some((comment) => comment.id === result.id)
+ ? currentComments
+ : [...currentComments, result],
+ editor: submittedEditor,
markdownSourceLineOffset
})
setAnnotationPopover(null)
@@ -195,7 +206,6 @@ export function useRichMarkdownReviewController({
annotationPopover,
clearAnnotationHighlight,
editorRef,
- markdownComments,
markdownSourceLineOffset,
sourceRelativePath,
worktreeId