From be87c3864c66699e44b894b5fe8100ec76e55d93 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Tue, 5 May 2026 02:43:49 +0200 Subject: [PATCH] fix(ai): remount composer after native input --- src/App.test.tsx | 4 +-- src/components/CommandPalette.test.tsx | 2 +- src/components/InlineWikilinkInput.tsx | 18 +++++++++-- src/components/WikilinkChatInput.test.tsx | 31 ++++++++++++++++--- .../inline-wikilink-editor-regression.spec.ts | 18 +++++++++++ 5 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index 188d1344..68ec4754 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -779,8 +779,6 @@ describe('App', () => { fireEvent.keyDown(window, { key: 'l', code: 'KeyL', metaKey: true, shiftKey: true }) const input = await screen.findByTestId('agent-input') - input.textContent = 'Summarize the active vault' - fireEvent.input(input) fireEvent.click(screen.getByTestId('agent-send')) await act(async () => { @@ -805,6 +803,8 @@ describe('App', () => { expect(input).toHaveAttribute('aria-placeholder', 'Ask Codex') }) + input.textContent = 'Summarize the active vault' + fireEvent.input(input) fireEvent.click(screen.getByTestId('agent-send')) await waitFor(() => { diff --git a/src/components/CommandPalette.test.tsx b/src/components/CommandPalette.test.tsx index e2614c05..a3ff11cb 100644 --- a/src/components/CommandPalette.test.tsx +++ b/src/components/CommandPalette.test.tsx @@ -114,7 +114,7 @@ function updateAiInput(text: string) { editor.textContent = text setSelection(editor, text.length) fireEvent.input(editor) - return editor + return screen.queryByTestId('command-palette-ai-input') ?? editor } function resetNativeDropState() { diff --git a/src/components/InlineWikilinkInput.tsx b/src/components/InlineWikilinkInput.tsx index 28cad2e2..6c487510 100644 --- a/src/components/InlineWikilinkInput.tsx +++ b/src/components/InlineWikilinkInput.tsx @@ -191,7 +191,6 @@ export function InlineWikilinkInput({ setSelectionRange, setCombinedRef, syncSelectionRange, - commitValueFromEditor, focusSelectionRange, } = useInlineWikilinkSelection({ value, @@ -396,7 +395,20 @@ export function InlineWikilinkInput({ } } - commitValueFromEditor() + if (!editor) return + + const nextValue = normalizeInlineWikilinkValue(serializeInlineNode(editor)) + const nextSelection = readSelectionRange(editor) + const clampedSelection: InlineSelectionRange = { + start: Math.min(nextSelection.start, nextValue.length), + end: Math.min(nextSelection.end, nextValue.length), + } + + const shouldRestoreFocus = document.activeElement === editor + pendingFocusAfterRemountRef.current = shouldRestoreFocus ? clampedSelection : null + onChange(nextValue) + setSelectionRange(clampedSelection) + forceRender((current) => current + 1) } const flushPendingCompositionInput = () => { if (isComposingRef.current || !pendingCompositionInputRef.current) return @@ -431,6 +443,8 @@ export function InlineWikilinkInput({ queueMicrotask(flushPendingCompositionInput) } const handleInput = () => { + if (disabled) return + if (isComposingRef.current) { pendingCompositionInputRef.current = true return diff --git a/src/components/WikilinkChatInput.test.tsx b/src/components/WikilinkChatInput.test.tsx index 3ce8193d..b437935d 100644 --- a/src/components/WikilinkChatInput.test.tsx +++ b/src/components/WikilinkChatInput.test.tsx @@ -416,16 +416,37 @@ describe('WikilinkChatInput', () => { fireEvent.input(editor) expect(onDraftChange).toHaveBeenLastCalledWith(portugueseText) - const cjkKey = createEvent.keyDown(editor, { key: '你' }) - fireEvent(editor, cjkKey) + const updatedEditor = screen.getByTestId('agent-input') + const cjkKey = createEvent.keyDown(updatedEditor, { key: '你' }) + fireEvent(updatedEditor, cjkKey) expect(cjkKey.defaultPrevented).toBe(false) - editor.textContent = `${portugueseText}你` - setSelection(editor, portugueseText.length + 1) - fireEvent.input(editor) + updatedEditor.textContent = `${portugueseText}你` + setSelection(updatedEditor, portugueseText.length + 1) + fireEvent.input(updatedEditor) expect(onDraftChange).toHaveBeenLastCalledWith(`${portugueseText}你`) }) + it('remounts after native DOM input so React does not diff a mutated contentEditable tree', async () => { + const onDraftChange = vi.fn() + render() + const initialEditor = screen.getByTestId('agent-input') as HTMLDivElement + initialEditor.focus() + + initialEditor.textContent = 'follow up after edit' + setSelection(initialEditor, 'follow up after edit'.length) + fireEvent.input(initialEditor) + + await waitFor(() => { + expect(onDraftChange).toHaveBeenLastCalledWith('follow up after edit') + }) + + const remountedEditor = screen.getByTestId('agent-input') as HTMLDivElement + expect(remountedEditor).not.toBe(initialEditor) + expect(remountedEditor.textContent).toBe('follow up after edit') + expect(document.activeElement).toBe(remountedEditor) + }) + it('deletes an inline chip with a single Backspace', () => { render() updateEditorText('edit my [[alp') diff --git a/tests/smoke/inline-wikilink-editor-regression.spec.ts b/tests/smoke/inline-wikilink-editor-regression.spec.ts index 194000ad..564e7f64 100644 --- a/tests/smoke/inline-wikilink-editor-regression.spec.ts +++ b/tests/smoke/inline-wikilink-editor-regression.spec.ts @@ -35,6 +35,24 @@ test.describe('Inline wikilink editor regression', () => { await expectNoPageErrors(pageErrors) }) + test('keeps follow-up typing stable after editing the active note body', async ({ page }) => { + const pageErrors = trackPageErrors(page) + const noteBlock = page.locator('.bn-block-content').nth(1) + const editor = page.getByTestId('agent-input') + const noteMarker = ` follow-up guard ${Date.now()}` + + await expect(noteBlock).toBeVisible({ timeout: 5_000 }) + await noteBlock.click() + await page.keyboard.type(noteMarker) + await expect(page.locator('.bn-editor')).toContainText(noteMarker.trim()) + + await editor.click() + await page.keyboard.type('follow up after note edit') + + await expectNormalizedEditorText(editor, 'follow up after note edit') + await expectNoPageErrors(pageErrors) + }) + test('keeps select-all cut scoped to the AI panel chat input', async ({ page }) => { const pageErrors = trackPageErrors(page) const editor = page.getByTestId('agent-input')