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