fix: preserve ai panel delete shortcuts
This commit is contained in:
55
src/components/AiPanelChrome.performance.test.tsx
Normal file
55
src/components/AiPanelChrome.performance.test.tsx
Normal file
@@ -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 <div data-testid="ai-message">{props.response}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
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 (
|
||||
<>
|
||||
<button type="button" onClick={() => setDraft('typing')}>type</button>
|
||||
<span data-testid="draft">{draft}</span>
|
||||
<AiPanelMessageHistory
|
||||
agentLabel="Claude Code"
|
||||
agentReadiness="ready"
|
||||
messages={messages}
|
||||
isActive={false}
|
||||
onOpenNote={noop}
|
||||
onNavigateWikilink={noop}
|
||||
hasContext
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
describe('AiPanelChrome performance', () => {
|
||||
it('keeps stable message history from re-rendering while composer state changes', () => {
|
||||
render(<HistoryRerenderHarness />)
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -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({
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export function AiPanelMessageHistory({
|
||||
export const AiPanelMessageHistory = memo(function AiPanelMessageHistory({
|
||||
agentLabel,
|
||||
agentReadiness,
|
||||
locale = 'en',
|
||||
@@ -332,7 +336,7 @@ export function AiPanelMessageHistory({
|
||||
<div ref={endRef} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export function AiPanelComposer({
|
||||
entries,
|
||||
|
||||
@@ -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(<Controlled onDraftChange={onDraftChange} />)
|
||||
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(<Controlled onSend={onSend} />)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user