diff --git a/apps/x/apps/renderer/src/components/ai-elements/prompt-input.tsx b/apps/x/apps/renderer/src/components/ai-elements/prompt-input.tsx index abd348da..5ba67292 100644 --- a/apps/x/apps/renderer/src/components/ai-elements/prompt-input.tsx +++ b/apps/x/apps/renderer/src/components/ai-elements/prompt-input.tsx @@ -1026,6 +1026,46 @@ export const PromptInputTextarea = ({ form?.requestSubmit(); } + // Handle backspace to delete entire mention at once + if (e.key === "Backspace" && controller) { + const textarea = e.currentTarget; + const cursorPos = textarea.selectionStart; + const selectionEnd = textarea.selectionEnd; + + // Only handle if no text is selected (cursor is at a single position) + if (cursorPos === selectionEnd) { + // Check if cursor is right after a mention + for (const label of mentionLabels) { + const mentionText = `@${label}`; + const startPos = cursorPos - mentionText.length; + if (startPos >= 0) { + const textBefore = currentValue.substring(startPos, cursorPos); + if (textBefore === mentionText) { + // Check if it's at word boundary (start of string or preceded by whitespace) + if (startPos === 0 || /\s/.test(currentValue[startPos - 1])) { + e.preventDefault(); + const newText = currentValue.substring(0, startPos) + currentValue.substring(cursorPos); + controller.textInput.setInput(newText); + // Remove the mention from state + if (mentionsCtx) { + const mentionToRemove = mentionsCtx.mentions.find(m => m.displayName === label); + if (mentionToRemove) { + mentionsCtx.removeMention(mentionToRemove.id); + } + } + // Set cursor position after React updates + setTimeout(() => { + textarea.selectionStart = startPos; + textarea.selectionEnd = startPos; + }, 0); + return; + } + } + } + } + } + } + // Remove last attachment when Backspace is pressed and textarea is empty if ( e.key === "Backspace" && @@ -1094,7 +1134,7 @@ export const PromptInputTextarea = ({ segment.highlighted ? ( {segment.text} diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx index c6c00374..1f059503 100644 --- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx +++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx @@ -294,7 +294,7 @@ export function ChatSidebar({ } } - const handleKeyDown = (e: React.KeyboardEvent) => { + const handleKeyDown = (e: React.KeyboardEvent) => { // If mention popover is open, let it handle navigation keys if (activeMention && ['ArrowDown', 'ArrowUp', 'Tab', 'Escape'].includes(e.key)) { return @@ -311,6 +311,41 @@ export function ChatSidebar({ handleSubmit() } } + + // Handle backspace to delete entire mention at once + if (e.key === 'Backspace') { + const textarea = e.currentTarget + const cursorPos = textarea.selectionStart + const selectionEnd = textarea.selectionEnd + + // Only handle if no text is selected (cursor is at a single position) + if (cursorPos !== selectionEnd) return + + // Check if cursor is right after a mention + for (const label of mentionLabels) { + const mentionText = `@${label}` + const startPos = cursorPos - mentionText.length + if (startPos >= 0) { + const textBefore = message.substring(startPos, cursorPos) + if (textBefore === mentionText) { + // Check if it's at word boundary (start of string or preceded by whitespace) + if (startPos === 0 || /\s/.test(message[startPos - 1])) { + e.preventDefault() + const newText = message.substring(0, startPos) + message.substring(cursorPos) + onMessageChange(newText) + // Remove the mention from state + setMentions(prev => prev.filter(m => m.displayName !== label)) + // Set cursor position after React updates + setTimeout(() => { + textarea.selectionStart = startPos + textarea.selectionEnd = startPos + }, 0) + return + } + } + } + } + } } const renderConversationItem = (item: ConversationItem) => {