From d477d272c52480269dfef53fa2f834c2164ef789 Mon Sep 17 00:00:00 2001 From: 777genius Date: Wed, 27 May 2026 21:54:18 +0300 Subject: [PATCH] fix(textarea): stabilize inline interaction overlays --- .../components/ui/ChipInteractionLayer.tsx | 74 +++++++++++++---- .../ui/MentionInteractionLayer.test.tsx | 82 +++++++++++++++++++ .../components/ui/MentionInteractionLayer.tsx | 43 +++++++++- .../components/ui/MentionableTextarea.tsx | 28 +++---- .../ui/SlashCommandInteractionLayer.tsx | 41 ++++++++-- .../ui/TaskReferenceInteractionLayer.tsx | 46 ++++++++++- .../components/ui/UrlInteractionLayer.tsx | 36 +++++++- 7 files changed, 302 insertions(+), 48 deletions(-) create mode 100644 src/renderer/components/ui/MentionInteractionLayer.test.tsx diff --git a/src/renderer/components/ui/ChipInteractionLayer.tsx b/src/renderer/components/ui/ChipInteractionLayer.tsx index 521c1365..173ed897 100644 --- a/src/renderer/components/ui/ChipInteractionLayer.tsx +++ b/src/renderer/components/ui/ChipInteractionLayer.tsx @@ -171,6 +171,34 @@ interface ChipInteractionLayerProps { onRemove: (chipId: string) => void; } +function areChipsEquivalent(a: InlineChip, b: InlineChip): boolean { + return ( + a.id === b.id && + a.filePath === b.filePath && + a.fileName === b.fileName && + a.fromLine === b.fromLine && + a.toLine === b.toLine && + a.codeText === b.codeText && + a.displayPath === b.displayPath && + a.isFolder === b.isFolder + ); +} + +function areChipPositionsEquivalent(current: ChipPosition[], next: ChipPosition[]): boolean { + if (current.length !== next.length) return false; + + return current.every((position, index) => { + const nextPosition = next[index]; + return ( + position.top === nextPosition.top && + position.left === nextPosition.left && + position.width === nextPosition.width && + position.height === nextPosition.height && + areChipsEquivalent(position.chip, nextPosition.chip) + ); + }); +} + export const ChipInteractionLayer = ({ chips, value, @@ -179,18 +207,25 @@ export const ChipInteractionLayer = ({ onRemove, }: ChipInteractionLayerProps): React.JSX.Element | null => { const [positions, setPositions] = React.useState([]); + const positionsRef = React.useRef([]); const revealFileInEditor = useStore((s) => s.revealFileInEditor); const revealFolderInEditor = useStore((s) => s.revealFolderInEditor); + const commitPositions = React.useCallback((nextPositions: ChipPosition[]) => { + if (areChipPositionsEquivalent(positionsRef.current, nextPositions)) return; + positionsRef.current = nextPositions; + setPositions(nextPositions); + }, []); + React.useLayoutEffect(() => { if (chips.length === 0) { - setPositions([]); + commitPositions([]); return; } const textarea = textareaRef.current; if (!textarea) return; - setPositions(calculateChipPositions(textarea, value, chips)); - }, [chips, value, textareaRef]); + commitPositions(calculateChipPositions(textarea, value, chips)); + }, [chips, commitPositions, value, textareaRef]); if (positions.length === 0) return null; @@ -200,6 +235,14 @@ export const ChipInteractionLayer = ({ {positions.map((pos) => { const isFileChip = pos.chip.fromLine == null; const isFolderChip = pos.chip.isFolder === true; + const openChipTarget = (): void => { + if (isFolderChip) { + revealFolderInEditor(pos.chip.filePath); + } else { + revealFileInEditor(pos.chip.filePath); + } + }; + return ( @@ -211,20 +254,19 @@ export const ChipInteractionLayer = ({ width: pos.width, height: pos.height, }} - onClick={ - isFileChip - ? (e) => { - e.preventDefault(); - e.stopPropagation(); - if (isFolderChip) { - revealFolderInEditor(pos.chip.filePath); - } else { - revealFileInEditor(pos.chip.filePath); - } - } - : undefined - } > + {isFileChip ? ( +