diff --git a/src/renderer/src/components/diff-comments/DiffCommentDraftCard.test.tsx b/src/renderer/src/components/diff-comments/DiffCommentDraftCard.test.tsx index 463f5fda720..47bd6b52df3 100644 --- a/src/renderer/src/components/diff-comments/DiffCommentDraftCard.test.tsx +++ b/src/renderer/src/components/diff-comments/DiffCommentDraftCard.test.tsx @@ -28,7 +28,9 @@ describe('DiffCommentDraftCard', () => { expect(view.getByText('Line 42')).toBeDefined() expect(view.queryByText('You')).toBeNull() - expect(view.getByPlaceholderText('Add note for the AI')).toBeDefined() + const textarea = view.getByPlaceholderText('Add note for the AI') + expect(textarea).toBeDefined() + expect(document.activeElement).toBe(textarea) expect(view.getByRole('button', { name: 'Add note' })).toBeDefined() expect(view.getByRole('button', { name: 'Cancel' })).toBeDefined() }) diff --git a/src/renderer/src/components/diff-comments/DiffCommentDraftCard.tsx b/src/renderer/src/components/diff-comments/DiffCommentDraftCard.tsx index 3aefe3ad590..18d5205acf9 100644 --- a/src/renderer/src/components/diff-comments/DiffCommentDraftCard.tsx +++ b/src/renderer/src/components/diff-comments/DiffCommentDraftCard.tsx @@ -71,6 +71,16 @@ export function DiffCommentDraftCard({ textarea?.focus() }, []) + useEffect(() => { + const focusTextarea = (): void => textareaRef.current?.focus() + focusTextarea() + if (typeof requestAnimationFrame !== 'function') { + return + } + const frame = requestAnimationFrame(focusTextarea) + return () => cancelAnimationFrame(frame) + }, []) + useLayoutEffect(() => { const card = cardRef.current if (!card || !onContentResizeRef.current) { diff --git a/src/renderer/src/components/diff-comments/diff-comment-draft-zone.ts b/src/renderer/src/components/diff-comments/diff-comment-draft-zone.ts index de0a3745198..8366282cefa 100644 --- a/src/renderer/src/components/diff-comments/diff-comment-draft-zone.ts +++ b/src/renderer/src/components/diff-comments/diff-comment-draft-zone.ts @@ -75,7 +75,7 @@ export function useDiffCommentDraftZone({ const openDraft = useCallback( (draft: DiffCommentDraft, initialBody = ''): void => { - if (!editor || !onCreateCommentRef.current || !canOpenDraft) { + if (!editor || !editor.getModel() || !onCreateCommentRef.current || !canOpenDraft) { return } @@ -88,12 +88,24 @@ export function useDiffCommentDraftZone({ const disposeDomMouseDownStopper = installDiffCommentZoneMouseDownStopper(dom) const disposeMarginMouseDownStopper = installDiffCommentZoneMouseDownStopper(marginDom) const root = createRoot(dom) + let didFocus = false const delegate: monacoEditor.IViewZone = { afterLineNumber: draft.lineNumber, heightInPx: DRAFT_ZONE_DEFAULT_HEIGHT, domNode: dom, marginDomNode: marginDom, - suppressMouseDown: false + suppressMouseDown: false, + onDomNodeTop: () => { + if (didFocus) { + return + } + const textarea = dom.querySelector('textarea') + if (!textarea) { + return + } + didFocus = true + textarea.focus() + } } const zoneId = accessor.addZone(delegate) const entry: DraftZoneEntry = { @@ -177,7 +189,7 @@ export function useDiffCommentDraftZone({ } pendingDraftRef.current = { draft: current.draft, body: current.body } disposeDraftZone() - if (!editor || !onCreateCommentRef.current || !canOpenDraft) { + if (!editor || !editor.getModel() || !onCreateCommentRef.current || !canOpenDraft) { return } reanchorFrameRef.current = requestAnimationFrame(() => { @@ -190,6 +202,29 @@ export function useDiffCommentDraftZone({ }) }, [canOpenDraft, disposeDraftZone, editor, monacoModelIdentity, openDraft]) + // A combined-diff model refresh can briefly clear the editor ref before the replacement mounts. + // Re-anchor any carried draft when that replacement becomes available. + useEffect(() => { + if (!editor || !canOpenDraft || !pendingDraftRef.current) { + return + } + const pending = pendingDraftRef.current + pendingDraftRef.current = null + reanchorFrameRef.current = requestAnimationFrame(() => { + reanchorFrameRef.current = null + openDraft(pending.draft, pending.body) + }) + return () => { + if (reanchorFrameRef.current !== null) { + cancelAnimationFrame(reanchorFrameRef.current) + reanchorFrameRef.current = null + } + if (!draftZoneRef.current) { + pendingDraftRef.current = pending + } + } + }, [canOpenDraft, editor, openDraft]) + useEffect(() => { if (!editor) { return @@ -199,7 +234,6 @@ export function useDiffCommentDraftZone({ cancelAnimationFrame(reanchorFrameRef.current) reanchorFrameRef.current = null } - pendingDraftRef.current = null disposeDraftZone(false) } }, [disposeDraftZone, editor]) diff --git a/src/renderer/src/components/editor/diff-section-comment-submit.test.ts b/src/renderer/src/components/editor/diff-section-comment-submit.test.ts new file mode 100644 index 00000000000..0332839c134 --- /dev/null +++ b/src/renderer/src/components/editor/diff-section-comment-submit.test.ts @@ -0,0 +1,40 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { toast } from 'sonner' +import { submitDiffSectionComment } from './diff-section-comment-submit' +import type { DiffSection } from './diff-section-types' + +vi.mock('sonner', () => ({ toast: { error: vi.fn() } })) + +const section: DiffSection = { + key: 'section-1', + path: 'src/example.ts', + status: 'modified', + originalContent: 'before', + modifiedContent: 'after', + collapsed: false, + loading: false, + dirty: false, + diffResult: null, + largeDiffRenderLimit: null +} + +describe('submitDiffSectionComment', () => { + beforeEach(() => { + vi.mocked(toast.error).mockClear() + }) + + it('surfaces persistence failures while keeping the draft open', async () => { + const addDiffComment = vi.fn().mockResolvedValue(null) + + const result = await submitDiffSectionComment({ + addDiffComment, + body: 'Needs revision', + target: { lineNumber: 4 }, + section, + worktreeId: 'worktree-1' + }) + + expect(result).toBe(false) + expect(toast.error).toHaveBeenCalledWith('Failed to save comment') + }) +}) diff --git a/tests/e2e/diff-note-draft.spec.ts b/tests/e2e/diff-note-draft.spec.ts index 3d913d27e9e..270971d11f9 100644 --- a/tests/e2e/diff-note-draft.spec.ts +++ b/tests/e2e/diff-note-draft.spec.ts @@ -73,6 +73,9 @@ test.describe('Diff note draft', () => { await expect(draftCard).toBeVisible({ timeout: 15_000 }) await expect(draftCard).toContainText('Line 6') await expect(draftCard).not.toContainText('You') + await expect + .poll(() => textarea.evaluate((element) => document.activeElement === element)) + .toBe(true) await expect(orcaPage.locator('.orca-diff-comment-draft-margin')).toBeVisible() await expect .poll(