diff --git a/src/components/AiPanelChrome.performance.test.tsx b/src/components/AiPanelChrome.performance.test.tsx
new file mode 100644
index 00000000..e83b1ac9
--- /dev/null
+++ b/src/components/AiPanelChrome.performance.test.tsx
@@ -0,0 +1,55 @@
+import { useState } from 'react'
+import { describe, expect, it, vi } from 'vitest'
+import { fireEvent, render, screen } from '@testing-library/react'
+import { AiPanelMessageHistory } from './AiPanelChrome'
+
+const aiMessageRender = vi.hoisted(() => vi.fn())
+
+vi.mock('./AiMessage', () => ({
+ AiMessage: (props: { id?: string; response: string }) => {
+ aiMessageRender(props.id)
+ return
{props.response}
+ },
+}))
+
+const noop = () => {}
+
+function HistoryRerenderHarness() {
+ const [draft, setDraft] = useState('')
+ const [messages] = useState([{
+ userMessage: 'Explain the note',
+ actions: [],
+ response: 'Here is a long answer with [[Test Note]].',
+ id: 'msg-stable',
+ }])
+
+ return (
+ <>
+
+ {draft}
+
+ >
+ )
+}
+
+describe('AiPanelChrome performance', () => {
+ it('keeps stable message history from re-rendering while composer state changes', () => {
+ render()
+ expect(screen.getByTestId('ai-message')).toHaveTextContent('Here is a long answer')
+ expect(aiMessageRender).toHaveBeenCalledTimes(1)
+ aiMessageRender.mockClear()
+
+ fireEvent.click(screen.getByRole('button', { name: 'type' }))
+
+ expect(screen.getByTestId('draft')).toHaveTextContent('typing')
+ expect(aiMessageRender).not.toHaveBeenCalled()
+ })
+})
diff --git a/src/components/AiPanelChrome.tsx b/src/components/AiPanelChrome.tsx
index 05d43511..c6284926 100644
--- a/src/components/AiPanelChrome.tsx
+++ b/src/components/AiPanelChrome.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef } from 'react'
+import { memo, useEffect, useRef } from 'react'
import { Robot, X, PaperPlaneRight, Plus, Link } from '@phosphor-icons/react'
import { AiMessage } from './AiMessage'
import { Button } from '@/components/ui/button'
@@ -164,7 +164,7 @@ function AiPanelEmptyState({
)
}
-export function AiPanelHeader({
+export const AiPanelHeader = memo(function AiPanelHeader({
agentLabel,
agentReadiness,
locale = 'en',
@@ -221,7 +221,7 @@ export function AiPanelHeader({
/>
)
-}
+})
function AiPermissionModeToggle({
value,
@@ -277,7 +277,11 @@ function AiPermissionModeToggle({
)
}
-export function AiPanelContextBar({ activeEntry, linkedCount, locale = 'en' }: AiPanelContextBarProps) {
+export const AiPanelContextBar = memo(function AiPanelContextBar({
+ activeEntry,
+ linkedCount,
+ locale = 'en',
+}: AiPanelContextBarProps) {
const t = createTranslator(locale)
return (
@@ -293,9 +297,9 @@ export function AiPanelContextBar({ activeEntry, linkedCount, locale = 'en' }: A
)}
)
-}
+})
-export function AiPanelMessageHistory({
+export const AiPanelMessageHistory = memo(function AiPanelMessageHistory({
agentLabel,
agentReadiness,
locale = 'en',
@@ -332,7 +336,7 @@ export function AiPanelMessageHistory({
)
-}
+})
export function AiPanelComposer({
entries,
diff --git a/src/components/WikilinkChatInput.test.tsx b/src/components/WikilinkChatInput.test.tsx
index e6eb8bb9..3ce8193d 100644
--- a/src/components/WikilinkChatInput.test.tsx
+++ b/src/components/WikilinkChatInput.test.tsx
@@ -437,6 +437,26 @@ describe('WikilinkChatInput', () => {
expect(screen.queryByTestId('inline-wikilink-chip')).toBeNull()
})
+ it('lets native modified delete shortcuts reach the browser editor pipeline', () => {
+ const onDraftChange = vi.fn()
+ render()
+ updateEditorText('delete the previous words')
+ onDraftChange.mockClear()
+
+ const editor = screen.getByTestId('agent-input')
+ const optionBackspace = createEvent.keyDown(editor, { key: 'Backspace', altKey: true })
+ fireEvent(editor, optionBackspace)
+ const commandBackspace = createEvent.keyDown(editor, { key: 'Backspace', metaKey: true })
+ fireEvent(editor, commandBackspace)
+ const controlDelete = createEvent.keyDown(editor, { key: 'Delete', ctrlKey: true })
+ fireEvent(editor, controlDelete)
+
+ expect(optionBackspace.defaultPrevented).toBe(false)
+ expect(commandBackspace.defaultPrevented).toBe(false)
+ expect(controlDelete.defaultPrevented).toBe(false)
+ expect(onDraftChange).not.toHaveBeenCalled()
+ })
+
it('submits serialized wikilink text and resolved references', () => {
const onSend = vi.fn()
render()
diff --git a/src/components/inlineWikilinkKeydown.ts b/src/components/inlineWikilinkKeydown.ts
index 5bf64a1c..9e7922be 100644
--- a/src/components/inlineWikilinkKeydown.ts
+++ b/src/components/inlineWikilinkKeydown.ts
@@ -61,6 +61,7 @@ function handleDeleteKeys({
onDeleteContent,
}: HandleDeleteKeysArgs): boolean {
if (isInlineWikilinkCompositionEvent(event, isComposing)) return false
+ if (event.altKey || event.ctrlKey || event.metaKey) return false
if (event.key === 'Backspace') {
event.preventDefault()