fix(ai): remount composer after native input

This commit is contained in:
lucaronin
2026-05-05 02:43:49 +02:00
parent 982d6476d7
commit be87c3864c
5 changed files with 63 additions and 10 deletions

View File

@@ -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(() => {

View File

@@ -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() {

View File

@@ -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

View File

@@ -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(<Controlled onDraftChange={onDraftChange} />)
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(<Controlled />)
updateEditorText('edit my [[alp')

View File

@@ -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')