From 20f34ab9d1806fd79681ebfa2eb86e1caf1ff703 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sat, 2 May 2026 18:12:37 +0200 Subject: [PATCH] fix: preserve ai panel delete shortcuts --- .../AiPanelChrome.performance.test.tsx | 55 +++++++++++++++++++ src/components/AiPanelChrome.tsx | 18 +++--- src/components/WikilinkChatInput.test.tsx | 20 +++++++ src/components/inlineWikilinkKeydown.ts | 1 + 4 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 src/components/AiPanelChrome.performance.test.tsx 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()