From f9cbf70f709c7200999e83fb688b453f596f9f8c Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Wed, 24 Sep 2025 21:10:22 -0400 Subject: [PATCH] feat: Rich text input with action and mention pills - Implement RichChatInput component with visual pill rendering - Add ActionPill component with blue circle icons for actions - Add MentionPill component with blue diamond icons for files - Support for /action and @filename pill creation - Inline pill display in both input and message history - Proper text selection, highlighting, and cursor positioning - Backspace removal of entire pills - Theme-aware styling for light/dark modes - Consistent white pill backgrounds with blue accent icons - Fixed line-breaking issues for natural text flow Components added: - RichChatInput.tsx: Main rich text input with pill rendering - ActionPill.tsx: Blue circle pills for actions - MentionPill.tsx: Blue diamond pills for file mentions - MessageContent.tsx: Enhanced message display with pills Features: - Visual pills replace [Action] and @filename text - Click and drag text selection works normally - Double/triple click for word/all selection - Pills flow inline with text content - Proper cursor positioning and blinking - Backspace removes entire pills as units --- add_backspace_pill_removal.py | 62 + add_cursor_blink_animation.py | 48 + add_mention_debugging.py | 84 + add_tailwind_blink_class.py | 27 + add_visible_text_selection.py | 105 + complete_mention_pill_rendering.py | 181 ++ comprehensive_action_fix.py | 98 + create_mention_pill_component.py | 67 + debug_action_flow.py | 75 + debug_action_popover.py | 61 + debug_richtextinput.py | 97 + fix_action_popover_order.py | 80 + fix_action_popover_state.py | 81 + fix_action_selection.py | 90 + fix_chatinput.py | 106 + fix_correct_tailwind_classes.py | 34 + fix_cursor_and_placeholder_styling.py | 34 + fix_cursor_blink_with_css.py | 29 + fix_cursor_line_break_logic.py | 112 + fix_cursor_placeholder_order.py | 48 + fix_cursor_positioning.py | 29 + fix_cursor_to_black_blinking.py | 33 + fix_cursor_vertical_alignment.py | 30 + fix_dark_mode_cursor_and_text.py | 33 + fix_import_issue.py | 38 + fix_inline_message_display.py | 87 + fix_input_spacing.py | 110 + fix_mention_file_select_properly.py | 78 + fix_message_pill_line_breaks.py | 28 + fix_multiline_cursor_positioning.py | 48 + fix_placeholder_behavior.py | 45 + fix_placeholder_color_lighter.py | 22 + fix_placeholder_color_medium.py | 22 + fix_placeholder_positioning.py | 49 + fix_placeholder_text_shift.py | 57 + fix_regex_error.py | 34 + fix_richtextinput_integration.py | 43 + fix_richtextinput_processing_loop.py | 169 ++ fix_selection_alignment.py | 118 ++ fix_selection_visibility.py | 124 ++ fix_text_selection_behavior.py | 133 ++ fix_text_selection_functionality.py | 136 ++ replace_textarea.py | 60 + simple_inline_pills.py | 50 + test_action_flow.py | 67 + test_basic_functionality.py | 80 + test_mention_functionality.py | 63 + ui/desktop/src/components/ActionPill.tsx | 57 + .../src/components/ActionPill.tsx.backup | 30 + ui/desktop/src/components/ActionPopover.tsx | 23 +- ui/desktop/src/components/ChatInput.tsx | 134 +- .../src/components/ChatInput.tsx.backup | 1844 +++++++++++++++++ .../src/components/ChatInput.tsx.backup2 | 1844 +++++++++++++++++ .../src/components/ChatInput.tsx.backup3 | 1803 ++++++++++++++++ .../src/components/ChatInput.tsx.backup4 | 1822 ++++++++++++++++ .../src/components/ChatInput_original.tsx | 1803 ++++++++++++++++ ui/desktop/src/components/MentionPill.tsx | 54 + ui/desktop/src/components/MessageContent.tsx | 176 ++ ui/desktop/src/components/RichChatInput.tsx | 504 +++++ ui/desktop/src/components/UserMessage.tsx | 21 +- .../src/components/UserMessage.tsx.backup | 271 +++ ui/desktop/src/styles/main.css | 35 + update_chatinput_mention_handling.py | 67 + update_mention_pill_styling.py | 31 + update_message_content_for_mentions.py | 249 +++ update_pill_icons.py | 70 + update_richtextinput_for_mentions.py | 93 + 67 files changed, 14192 insertions(+), 44 deletions(-) create mode 100644 add_backspace_pill_removal.py create mode 100644 add_cursor_blink_animation.py create mode 100644 add_mention_debugging.py create mode 100644 add_tailwind_blink_class.py create mode 100644 add_visible_text_selection.py create mode 100644 complete_mention_pill_rendering.py create mode 100644 comprehensive_action_fix.py create mode 100644 create_mention_pill_component.py create mode 100644 debug_action_flow.py create mode 100644 debug_action_popover.py create mode 100644 debug_richtextinput.py create mode 100644 fix_action_popover_order.py create mode 100644 fix_action_popover_state.py create mode 100644 fix_action_selection.py create mode 100644 fix_chatinput.py create mode 100644 fix_correct_tailwind_classes.py create mode 100644 fix_cursor_and_placeholder_styling.py create mode 100644 fix_cursor_blink_with_css.py create mode 100644 fix_cursor_line_break_logic.py create mode 100644 fix_cursor_placeholder_order.py create mode 100644 fix_cursor_positioning.py create mode 100644 fix_cursor_to_black_blinking.py create mode 100644 fix_cursor_vertical_alignment.py create mode 100644 fix_dark_mode_cursor_and_text.py create mode 100644 fix_import_issue.py create mode 100644 fix_inline_message_display.py create mode 100644 fix_input_spacing.py create mode 100644 fix_mention_file_select_properly.py create mode 100644 fix_message_pill_line_breaks.py create mode 100644 fix_multiline_cursor_positioning.py create mode 100644 fix_placeholder_behavior.py create mode 100644 fix_placeholder_color_lighter.py create mode 100644 fix_placeholder_color_medium.py create mode 100644 fix_placeholder_positioning.py create mode 100644 fix_placeholder_text_shift.py create mode 100644 fix_regex_error.py create mode 100644 fix_richtextinput_integration.py create mode 100644 fix_richtextinput_processing_loop.py create mode 100644 fix_selection_alignment.py create mode 100644 fix_selection_visibility.py create mode 100644 fix_text_selection_behavior.py create mode 100644 fix_text_selection_functionality.py create mode 100644 replace_textarea.py create mode 100644 simple_inline_pills.py create mode 100644 test_action_flow.py create mode 100644 test_basic_functionality.py create mode 100644 test_mention_functionality.py create mode 100644 ui/desktop/src/components/ActionPill.tsx create mode 100644 ui/desktop/src/components/ActionPill.tsx.backup create mode 100644 ui/desktop/src/components/ChatInput.tsx.backup create mode 100644 ui/desktop/src/components/ChatInput.tsx.backup2 create mode 100644 ui/desktop/src/components/ChatInput.tsx.backup3 create mode 100644 ui/desktop/src/components/ChatInput.tsx.backup4 create mode 100644 ui/desktop/src/components/ChatInput_original.tsx create mode 100644 ui/desktop/src/components/MentionPill.tsx create mode 100644 ui/desktop/src/components/MessageContent.tsx create mode 100644 ui/desktop/src/components/RichChatInput.tsx create mode 100644 ui/desktop/src/components/UserMessage.tsx.backup create mode 100644 update_chatinput_mention_handling.py create mode 100644 update_mention_pill_styling.py create mode 100644 update_message_content_for_mentions.py create mode 100644 update_pill_icons.py create mode 100644 update_richtextinput_for_mentions.py diff --git a/add_backspace_pill_removal.py b/add_backspace_pill_removal.py new file mode 100644 index 0000000000..b5580b7e9e --- /dev/null +++ b/add_backspace_pill_removal.py @@ -0,0 +1,62 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Adding backspace removal for both action and mention pills...") + +# Find the current backspace handling and enhance it +old_backspace_handling = ''' // Handle backspace on action pills + if (e.key === 'Backspace') { + const cursorPos = e.currentTarget.selectionStart; + const beforeCursor = value.slice(0, cursorPos); + + // Check if cursor is right after an action pill + const actionMatch = beforeCursor.match(/\[([^\]]+)\]$/); + if (actionMatch) { + e.preventDefault(); + handleRemoveAction(actionMatch[1]); + return; + } + }''' + +new_backspace_handling = ''' // Handle backspace on action and mention pills + if (e.key === 'Backspace') { + const cursorPos = e.currentTarget.selectionStart; + const beforeCursor = value.slice(0, cursorPos); + + console.log('๐Ÿ”™ Backspace pressed, cursor at:', cursorPos); + console.log('๐Ÿ”™ Text before cursor:', beforeCursor); + + // Check if cursor is right after an action pill [Action] + const actionMatch = beforeCursor.match(/\[([^\]]+)\]$/); + if (actionMatch) { + console.log('๐Ÿ”™ Found action pill to remove:', actionMatch[1]); + e.preventDefault(); + handleRemoveAction(actionMatch[1]); + return; + } + + // Check if cursor is right after a mention pill @filename + const mentionMatch = beforeCursor.match(/@([^\s]+)$/); + if (mentionMatch) { + console.log('๐Ÿ”™ Found mention pill to remove:', mentionMatch[1]); + e.preventDefault(); + handleRemoveMention(mentionMatch[1]); + return; + } + }''' + +content = content.replace(old_backspace_handling, new_backspace_handling) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Added backspace removal for pills:") +print(" - Enhanced existing action pill removal") +print(" - Added mention pill removal (@filename)") +print(" - Added debugging logs for backspace events") +print(" - Now supports removing both types of pills with backspace") +print(" - Pills will be removed as complete units") diff --git a/add_cursor_blink_animation.py b/add_cursor_blink_animation.py new file mode 100644 index 0000000000..f4df823a2e --- /dev/null +++ b/add_cursor_blink_animation.py @@ -0,0 +1,48 @@ +import re + +# Read the main.css file +with open('ui/desktop/src/styles/main.css', 'r') as f: + content = f.read() + +print("Adding cursor blink animation to main.css...") + +# Find the animations section and add the blink animation +animations_section = '''/* animations */ +@keyframes appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +}''' + +new_animations_section = '''/* animations */ +@keyframes appear { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes blink { + 0%, 50% { + opacity: 1; + } + 51%, 100% { + opacity: 0; + } +}''' + +content = content.replace(animations_section, new_animations_section) + +# Write the modified content back +with open('ui/desktop/src/styles/main.css', 'w') as f: + f.write(content) + +print("โœ… Added cursor blink animation to main.css") +print(" - @keyframes blink: 0-50% visible, 51-100% hidden") +print(" - Creates a regular text cursor blinking effect") +print(" - Can be used with 'animate-blink' class") diff --git a/add_mention_debugging.py b/add_mention_debugging.py new file mode 100644 index 0000000000..dc33f14b8a --- /dev/null +++ b/add_mention_debugging.py @@ -0,0 +1,84 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Adding comprehensive debugging for mention detection...") + +# Find the mention regex section and add more debugging +old_mention_detection = ''' // Find all mention matches + let mentionMatch; + mentionRegex.lastIndex = 0; // Reset regex + while ((mentionMatch = mentionRegex.exec(value)) !== null) { + allMatches.push({ + type: 'mention', + match: mentionMatch, + index: mentionMatch.index, + length: mentionMatch[0].length, + content: mentionMatch[1] // filename without @ + }); + }''' + +new_mention_detection = ''' // Find all mention matches + let mentionMatch; + mentionRegex.lastIndex = 0; // Reset regex + console.log('๐Ÿ” Searching for mentions in value:', value); + console.log('๐Ÿ” Using mention regex:', mentionRegex); + while ((mentionMatch = mentionRegex.exec(value)) !== null) { + console.log('๐Ÿ“ Found mention match:', mentionMatch); + allMatches.push({ + type: 'mention', + match: mentionMatch, + index: mentionMatch.index, + length: mentionMatch[0].length, + content: mentionMatch[1] // filename without @ + }); + }''' + +content = content.replace(old_mention_detection, new_mention_detection) + +# Also add debugging after sorting matches +old_sort_section = ''' // Sort matches by position + allMatches.sort((a, b) => a.index - b.index); + + console.log('๐Ÿ” Found matches:', allMatches);''' + +new_sort_section = ''' // Sort matches by position + allMatches.sort((a, b) => a.index - b.index); + + console.log('๐Ÿ” All matches found:', allMatches); + console.log('๐Ÿ“Š Match breakdown:', { + actions: allMatches.filter(m => m.type === 'action').length, + mentions: allMatches.filter(m => m.type === 'mention').length, + total: allMatches.length + });''' + +content = content.replace(old_sort_section, new_sort_section) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Added comprehensive debugging:") +print(" - Debug logs for mention regex search") +print(" - Debug logs for found mention matches") +print(" - Match breakdown statistics") +print(" - Should help identify if mentions are being detected") + +# Also let's test the regex pattern directly +import re + +test_strings = [ + "@filename.txt", + "Hello @test.py world", + "@folder/file.js", + "Multiple @file1.txt and @file2.py mentions" +] + +mention_regex = re.compile(r'@([^\s]+)') + +print("\n๐Ÿงช Testing mention regex pattern:") +for test in test_strings: + matches = mention_regex.findall(test) + print(f" '{test}' โ†’ {matches}") diff --git a/add_tailwind_blink_class.py b/add_tailwind_blink_class.py new file mode 100644 index 0000000000..2b6c095d70 --- /dev/null +++ b/add_tailwind_blink_class.py @@ -0,0 +1,27 @@ +import re + +# Read the main.css file +with open('ui/desktop/src/styles/main.css', 'r') as f: + content = f.read() + +print("Adding Tailwind animate-blink utility class...") + +# Find the @theme inline section and add the animation +theme_inline_section = '''@theme inline { + --ease-g2: cubic-bezier(0.55, 0, 1, 0.45);''' + +new_theme_inline_section = '''@theme inline { + --ease-g2: cubic-bezier(0.55, 0, 1, 0.45); + + --animate-blink: blink 1s step-end infinite;''' + +content = content.replace(theme_inline_section, new_theme_inline_section) + +# Write the modified content back +with open('ui/desktop/src/styles/main.css', 'w') as f: + f.write(content) + +print("โœ… Added animate-blink utility class to Tailwind theme") +print(" - Uses the blink keyframe with 1s duration") +print(" - step-end timing for sharp on/off transitions") +print(" - infinite loop for continuous blinking") diff --git a/add_visible_text_selection.py b/add_visible_text_selection.py new file mode 100644 index 0000000000..446150357b --- /dev/null +++ b/add_visible_text_selection.py @@ -0,0 +1,105 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Adding visible blue text selection highlight...") + +# Add selection styling to the hidden textarea +old_hidden_textarea_style = ''' style={{ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + opacity: 0.01, // Nearly invisible but still selectable + zIndex: 2, // Higher z-index to capture mouse events + background: 'transparent', + border: 'none', + outline: 'none', + resize: 'none', + color: 'transparent', // Hide text but keep selection + caretColor: 'transparent', // Hide caret (we show our own) + pointerEvents: 'auto', // Ensure it can receive mouse events + }}''' + +new_hidden_textarea_style = ''' style={{ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + opacity: 0.01, // Nearly invisible but still selectable + zIndex: 2, // Higher z-index to capture mouse events + background: 'transparent', + border: 'none', + outline: 'none', + resize: 'none', + color: 'transparent', // Hide text but keep selection + caretColor: 'transparent', // Hide caret (we show our own) + pointerEvents: 'auto', // Ensure it can receive mouse events + }} + className="absolute inset-0 w-full h-full resize-none selection:bg-blue-500 selection:text-white"''' + +content = content.replace(old_hidden_textarea_style, new_hidden_textarea_style) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +# Also add CSS to the main.css file for better selection styling +with open('ui/desktop/src/styles/main.css', 'r') as f: + css_content = f.read() + +# Add selection styling at the end of the file +selection_css = ''' +/* Text selection styling for RichChatInput */ +.rich-text-input textarea::selection { + background-color: #3b82f6; /* Blue-500 */ + color: white; +} + +.rich-text-input textarea::-moz-selection { + background-color: #3b82f6; /* Blue-500 */ + color: white; +} + +/* Global selection styling for better visibility */ +::selection { + background-color: #3b82f6; /* Blue-500 */ + color: white; +} + +::-moz-selection { + background-color: #3b82f6; /* Blue-500 */ + color: white; +} +''' + +if '/* Text selection styling for RichChatInput */' not in css_content: + css_content += selection_css + + with open('ui/desktop/src/styles/main.css', 'w') as f: + f.write(css_content) + + print("โœ… Added selection CSS to main.css") + +# Also add a wrapper class to the RichChatInput component +old_wrapper = ''' return ( +
''' + +new_wrapper = ''' return ( +
''' + +content = content.replace(old_wrapper, new_wrapper) + +# Write the final content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Added visible text selection highlighting:") +print(" - Added selection:bg-blue-500 selection:text-white to textarea") +print(" - Added CSS rules for ::selection styling") +print(" - Added rich-text-input wrapper class") +print(" - Selection should now be clearly visible with blue highlight") diff --git a/complete_mention_pill_rendering.py b/complete_mention_pill_rendering.py new file mode 100644 index 0000000000..310d0df1f2 --- /dev/null +++ b/complete_mention_pill_rendering.py @@ -0,0 +1,181 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Completing mention pill rendering in RichChatInput...") + +# Find the section after we set up the matches and complete the processing logic +# We need to replace the old action-only processing with the new combined processing + +# First, let's find where the old processing logic ends and replace it +# Look for the section that processes beforeAction +old_processing_section = ''' console.log('โœ… Found action match:', match); + const beforeAction = value.slice(lastIndex, match.index); + const actionLabel = match[1]; + console.log('๐Ÿ“ Processing action:', { beforeAction, actionLabel, matchIndex: match.index }); + + // Add text before action with potential cursor + if (beforeAction) { + // Split text by cursor position to insert cursor at the right spot + let textWithCursor = []; + for (let i = 0; i < beforeAction.length; i++) { + if (isFocused && cursorPosition === currentPos) { + textWithCursor.push( + + ); + } + textWithCursor.push(beforeAction[i]); + currentPos++; + } + + parts.push( + + {textWithCursor} + + ); + } + + // Find matching action + const actionEntry = Object.entries(ACTION_MAP).find( + ([_, config]) => config.label === actionLabel + ); + + if (actionEntry) { + const [actionId, config] = actionEntry; + parts.push( + handleRemoveAction(actionLabel)} + /> + ); + } else { + // If no matching action, render as text + parts.push( + + {match[0]} + + ); + } + + currentPos += match[0].length; + lastIndex = match.index + match[0].length; + }''' + +new_processing_section = ''' console.log('โœ… Found match:', matchData); + + // Add text before this match with potential cursor + const beforeMatch = value.slice(lastIndex, index); + if (beforeMatch) { + let textWithCursor = []; + for (let i = 0; i < beforeMatch.length; i++) { + if (isFocused && cursorPosition === currentPos) { + textWithCursor.push( + + ); + } + textWithCursor.push(beforeMatch[i]); + currentPos++; + } + + parts.push( + + {textWithCursor} + + ); + } + + // Add cursor before match if needed + if (isFocused && cursorPosition === currentPos) { + parts.push( + + ); + } + + if (type === 'action') { + // Handle action pills + const actionLabel = content; + const actionEntry = Object.entries(ACTION_MAP).find( + ([_, config]) => config.label === actionLabel + ); + + if (actionEntry) { + const [actionId, config] = actionEntry; + parts.push( + handleRemoveAction(actionLabel)} + /> + ); + } else { + // If no matching action, render as text + parts.push( + + {match[0]} + + ); + } + } else if (type === 'mention') { + // Handle mention pills + const fileName = content; // filename without @ + const filePath = `@${fileName}`; // full mention text + + parts.push( + handleRemoveMention(fileName)} + /> + ); + } + + currentPos += length; + lastIndex = index + length; + }''' + +content = content.replace(old_processing_section, new_processing_section) + +# Add the handleRemoveMention function +old_handle_remove = ''' const handleRemoveAction = useCallback((actionLabel: string) => { + const actionText = `[${actionLabel}]`; + const newValue = value.replace(actionText, ''); + onChange(newValue); + }, [value, onChange]);''' + +new_handle_remove = ''' const handleRemoveAction = useCallback((actionLabel: string) => { + const actionText = `[${actionLabel}]`; + const newValue = value.replace(actionText, ''); + onChange(newValue); + }, [value, onChange]); + + const handleRemoveMention = useCallback((fileName: string) => { + const mentionText = `@${fileName}`; + const newValue = value.replace(mentionText, ''); + onChange(newValue); + }, [value, onChange]);''' + +content = content.replace(old_handle_remove, new_handle_remove) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Completed mention pill rendering:") +print(" - Updated processing logic to handle both actions and mentions") +print(" - Added MentionPill rendering for @filename patterns") +print(" - Added handleRemoveMention function for pill removal") +print(" - Mentions will now appear as green pills with file icon") diff --git a/comprehensive_action_fix.py b/comprehensive_action_fix.py new file mode 100644 index 0000000000..88b84ec9a5 --- /dev/null +++ b/comprehensive_action_fix.py @@ -0,0 +1,98 @@ +import re + +# Read the ChatInput file +with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f: + content = f.read() + +print("Creating comprehensive fix for action pill population...") + +# 1. Clean up the RichChatInput onChange handler +# Find the current messy onChange and replace with clean version +old_onchange_pattern = r'onChange=\{\(newValue, cursorPos\) => \{[^}]*\}\}' +new_onchange = '''onChange={(newValue, cursorPos) => { + setDisplayValue(newValue); + updateValue(newValue); + debouncedSaveDraft(newValue); + setHasUserTyped(true); + + // Check for @ mention and / action triggers + if (cursorPos !== undefined) { + const syntheticTarget = { + getBoundingClientRect: () => textAreaRef.current?.getBoundingClientRect?.() || new DOMRect(), + selectionStart: cursorPos, + selectionEnd: cursorPos, + value: newValue, + }; + checkForMention(newValue, cursorPos, syntheticTarget as HTMLTextAreaElement); + } + }}''' + +# Use a more targeted replacement +lines = content.split('\n') +start_idx = -1 +end_idx = -1 +brace_count = 0 +in_onchange = False + +for i, line in enumerate(lines): + if 'onChange={(newValue, cursorPos) => {' in line: + start_idx = i + in_onchange = True + brace_count = line.count('{') - line.count('}') + elif in_onchange: + brace_count += line.count('{') - line.count('}') + if '}}' in line and brace_count <= 0: + end_idx = i + break + +if start_idx != -1 and end_idx != -1: + # Replace the onChange handler + new_lines = lines[:start_idx] + [' ' + new_onchange] + lines[end_idx + 1:] + content = '\n'.join(new_lines) + print(f"Fixed onChange handler (lines {start_idx}-{end_idx})") + +# 2. Ensure the actionPopover state has cursorPosition +if 'cursorPosition?: number;' not in content: + content = content.replace( + 'selectedIndex: number;\n }>({', + 'selectedIndex: number;\n cursorPosition?: number;\n }>({' + ) + content = content.replace( + 'selectedIndex: 0,\n });', + 'selectedIndex: 0,\n cursorPosition: 0,\n });' + ) + print("Added cursorPosition to actionPopover state") + +# 3. Make sure checkForMention sets cursorPosition when opening action popover +content = content.replace( + ''' setActionPopover({ + isOpen: true, + position: { + x: textAreaRect.left, + y: textAreaRect.top, + }, + selectedIndex: 0, + });''', + ''' setActionPopover({ + isOpen: true, + position: { + x: textAreaRect.left, + y: textAreaRect.top, + }, + selectedIndex: 0, + cursorPosition: cursorPosition, + });''' +) + +print("Updated checkForMention to set cursorPosition") + +# Write the modified content back +with open('ui/desktop/src/components/ChatInput.tsx', 'w') as f: + f.write(content) + +print("Comprehensive fix applied successfully!") +print("\nKey changes made:") +print("1. Cleaned up RichChatInput onChange handler") +print("2. Ensured actionPopover state includes cursorPosition") +print("3. Updated checkForMention to pass cursorPosition to actionPopover") +print("4. handleActionSelect should now work correctly with proper cursor position") diff --git a/create_mention_pill_component.py b/create_mention_pill_component.py new file mode 100644 index 0000000000..e076fdf9e0 --- /dev/null +++ b/create_mention_pill_component.py @@ -0,0 +1,67 @@ +# Create MentionPill component similar to ActionPill +mention_pill_content = '''import React from 'react'; +import { X, File } from 'lucide-react'; + +interface MentionPillProps { + fileName: string; + filePath: string; + onRemove?: () => void; // Optional for read-only pills in messages + variant?: 'default' | 'message'; // Different styles for input vs message display + size?: 'sm' | 'md'; +} + +export const MentionPill: React.FC = ({ + fileName, + filePath, + onRemove, + variant = 'default', + size = 'sm' +}) => { + const baseClasses = "inline-flex items-center gap-1.5 font-medium border rounded-full"; + + const variantClasses = { + default: "bg-green-100 text-green-800 border-green-200 dark:bg-green-900 dark:text-green-200 dark:border-green-700", + message: "bg-green-100 text-green-800 border-green-200 dark:bg-green-900 dark:text-green-200 dark:border-green-700" + }; + + const sizeClasses = { + sm: "px-2 py-1 text-xs", + md: "px-3 py-1.5 text-sm" + }; + + return ( +
+ + + {fileName} + + {onRemove && ( + + )} +
+ ); +}; + +export default MentionPill; +''' + +# Write the MentionPill component +with open('ui/desktop/src/components/MentionPill.tsx', 'w') as f: + f.write(mention_pill_content) + +print("โœ… Created MentionPill component:") +print(" - Similar structure to ActionPill") +print(" - Uses green colors to differentiate from actions (blue)") +print(" - Shows File icon and filename") +print(" - Tooltip shows full file path") +print(" - Supports removal and different variants") diff --git a/debug_action_flow.py b/debug_action_flow.py new file mode 100644 index 0000000000..df330d8a4e --- /dev/null +++ b/debug_action_flow.py @@ -0,0 +1,75 @@ +import re + +# Read the ChatInput file +with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f: + content = f.read() + +# Add debugging to handleActionSelect function +old_function_pattern = r' const handleActionSelect = \(actionId: string\) => \{[^}]*console\.log\([^)]*\);\s*setActionPopover[^}]*\};' + +new_function = ''' const handleActionSelect = (actionId: string) => { + console.log('๐ŸŽฏ handleActionSelect called with:', actionId); + const actionInfo = getActionInfo(actionId); + console.log('๐Ÿ“‹ Action info:', actionInfo); + + // Get current cursor position from the actionPopover state + const currentValue = displayValue; + const cursorPosition = actionPopover.cursorPosition || 0; + console.log('๐Ÿ“ Current state:', { currentValue, cursorPosition, displayValue }); + + const beforeCursor = currentValue.slice(0, cursorPosition); + const afterCursor = currentValue.slice(cursorPosition); + const lastSlashIndex = beforeCursor.lastIndexOf('/'); + + console.log('๐Ÿ” Cursor analysis:', { beforeCursor, afterCursor, lastSlashIndex }); + + if (lastSlashIndex !== -1) { + const afterSlash = beforeCursor.slice(lastSlashIndex + 1); + console.log('โšก After slash:', afterSlash); + + if (!afterSlash.includes(' ') && !afterSlash.includes('\\n')) { + // Replace the /query with [Action] text + const beforeSlash = currentValue.slice(0, lastSlashIndex); + const actionText = `[${actionInfo.label}]`; + const newValue = beforeSlash + actionText + " " + afterCursor; + + console.log('๐Ÿ”„ Text replacement:', { + beforeSlash, + actionText, + afterCursor, + newValue, + oldValue: currentValue + }); + + setDisplayValue(newValue); + setValue(newValue); + + console.log('โœ… Values set, new text should be:', newValue); + + // Set cursor position after the action text and space + const newCursorPosition = lastSlashIndex + actionText.length + 1; + setTimeout(() => { + if (textAreaRef.current) { + textAreaRef.current.setSelectionRange(newCursorPosition, newCursorPosition); + textAreaRef.current.focus(); + } + }, 0); + } else { + console.log('โŒ After slash contains space/newline, not replacing'); + } + } else { + console.log('โŒ No slash found in text before cursor'); + } + + console.log('๐ŸŽฏ Action selected:', actionId, 'at position:', cursorPosition); + setActionPopover(prev => ({ ...prev, isOpen: false })); + };''' + +# Find and replace the function using regex +content = re.sub(old_function_pattern, new_function, content, flags=re.DOTALL) + +# Write the modified content back +with open('ui/desktop/src/components/ChatInput.tsx', 'w') as f: + f.write(content) + +print("Added comprehensive debugging to handleActionSelect") diff --git a/debug_action_popover.py b/debug_action_popover.py new file mode 100644 index 0000000000..fbf613116c --- /dev/null +++ b/debug_action_popover.py @@ -0,0 +1,61 @@ +import re + +# Read the ActionPopover file +with open('ui/desktop/src/components/ActionPopover.tsx', 'r') as f: + content = f.read() + +# Add debugging to handleItemClick +old_handleItemClick = ''' const handleItemClick = (index: number) => { + onSelectedIndexChange(index); + onSelect(actions[index].id); + actions[index].action(); + onClose(); + };''' + +new_handleItemClick = ''' const handleItemClick = (index: number) => { + console.log('๐ŸŽฏ ActionPopover: handleItemClick called', { index, actionId: actions[index].id }); + console.log('๐Ÿ“‹ ActionPopover: onSelect function:', onSelect); + console.log('๐Ÿ”„ ActionPopover: About to call onSelect with:', actions[index].id); + + onSelectedIndexChange(index); + + // Call onSelect first - this should trigger handleActionSelect in ChatInput + onSelect(actions[index].id); + + console.log('โœ… ActionPopover: onSelect called successfully'); + + // Call the local action (just for logging) + actions[index].action(); + + console.log('๐Ÿšช ActionPopover: Closing popover'); + onClose(); + };''' + +content = content.replace(old_handleItemClick, new_handleItemClick) + +# Also add debugging to the selectAction method in useImperativeHandle +old_selectAction = ''' selectAction: (index: number) => { + if (actions[index]) { + onSelect(actions[index].id); + actions[index].action(); + onClose(); + } + },''' + +new_selectAction = ''' selectAction: (index: number) => { + console.log('โŒจ๏ธ ActionPopover: selectAction called via keyboard', { index, actionId: actions[index]?.id }); + if (actions[index]) { + console.log('๐Ÿ”„ ActionPopover: Calling onSelect from selectAction:', actions[index].id); + onSelect(actions[index].id); + actions[index].action(); + onClose(); + } + },''' + +content = content.replace(old_selectAction, new_selectAction) + +# Write the modified content back +with open('ui/desktop/src/components/ActionPopover.tsx', 'w') as f: + f.write(content) + +print("Added comprehensive debugging to ActionPopover") diff --git a/debug_richtextinput.py b/debug_richtextinput.py new file mode 100644 index 0000000000..d682b2d5b7 --- /dev/null +++ b/debug_richtextinput.py @@ -0,0 +1,97 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +# Add debugging to the renderContent function +# Find the while loop that processes actions +old_while_section = ''' while ((match = actionRegex.exec(value)) !== null) { + const beforeAction = value.slice(lastIndex, match.index); + const actionLabel = match[1]; + + // Add text before action with potential cursor + if (beforeAction) {''' + +new_while_section = ''' console.log('๐ŸŽจ RichChatInput renderContent called with value:', value); + console.log('๐Ÿ” Looking for action patterns with regex:', actionRegex); + + while ((match = actionRegex.exec(value)) !== null) { + console.log('โœ… Found action match:', match); + const beforeAction = value.slice(lastIndex, match.index); + const actionLabel = match[1]; + console.log('๐Ÿ“ Processing action:', { beforeAction, actionLabel, matchIndex: match.index }); + + // Add text before action with potential cursor + if (beforeAction) {''' + +content = content.replace(old_while_section, new_while_section) + +# Add debugging to the action pill creation +old_pill_creation = ''' // Add the action pill + addCursorIfNeeded(currentPos); + const actionInfo = ACTION_MAP[actionLabel.toLowerCase().replace(/\\s+/g, '-') as keyof typeof ACTION_MAP] || + { label: actionLabel, icon: }; + + parts.push( + handleRemoveAction(actionLabel)} + /> + );''' + +new_pill_creation = ''' // Add the action pill + addCursorIfNeeded(currentPos); + const actionInfo = ACTION_MAP[actionLabel.toLowerCase().replace(/\\s+/g, '-') as keyof typeof ACTION_MAP] || + { label: actionLabel, icon: }; + + console.log('๐Ÿท๏ธ Creating action pill:', { actionLabel, actionInfo }); + + parts.push( + handleRemoveAction(actionLabel)} + /> + ); + + console.log('โœ… Action pill added to parts array');''' + +content = content.replace(old_pill_creation, new_pill_creation) + +# Add debugging to the onChange handler +old_onchange = ''' const handleTextareaChange = useCallback((e: React.ChangeEvent) => { + const newValue = e.target.value; + const newCursorPos = e.target.selectionStart; + + console.log('RichChatInput: onChange', { newValue, newCursorPos }); + onChange(newValue, newCursorPos); + setCursorPosition(newCursorPos); + }, [onChange]);''' + +new_onchange = ''' const handleTextareaChange = useCallback((e: React.ChangeEvent) => { + const newValue = e.target.value; + const newCursorPos = e.target.selectionStart; + + console.log('๐Ÿ”„ RichChatInput: onChange', { newValue, newCursorPos }); + console.log('๐ŸŽจ Will trigger re-render with new value:', newValue); + onChange(newValue, newCursorPos); + setCursorPosition(newCursorPos); + }, [onChange]);''' + +content = content.replace(old_onchange, new_onchange) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("Added comprehensive debugging to RichChatInput") diff --git a/fix_action_popover_order.py b/fix_action_popover_order.py new file mode 100644 index 0000000000..8c92ebb662 --- /dev/null +++ b/fix_action_popover_order.py @@ -0,0 +1,80 @@ +import re + +# Read the ActionPopover file +with open('ui/desktop/src/components/ActionPopover.tsx', 'r') as f: + content = f.read() + +# Fix the order of operations in handleItemClick - onSelect should happen BEFORE onClose +old_handleItemClick = ''' const handleItemClick = (index: number) => { + console.log('๐ŸŽฏ ActionPopover: handleItemClick called', { index, actionId: actions[index].id }); + console.log('๐Ÿ“‹ ActionPopover: onSelect function:', onSelect); + console.log('๐Ÿ”„ ActionPopover: About to call onSelect with:', actions[index].id); + + onSelectedIndexChange(index); + + // Call onSelect first - this should trigger handleActionSelect in ChatInput + onSelect(actions[index].id); + + console.log('โœ… ActionPopover: onSelect called successfully'); + + // Call the local action (just for logging) + actions[index].action(); + + console.log('๐Ÿšช ActionPopover: Closing popover'); + onClose(); + };''' + +new_handleItemClick = ''' const handleItemClick = (index: number) => { + console.log('๐ŸŽฏ ActionPopover: handleItemClick called', { index, actionId: actions[index].id }); + console.log('๐Ÿ“‹ ActionPopover: onSelect function:', onSelect); + console.log('๐Ÿ”„ ActionPopover: About to call onSelect with:', actions[index].id); + + onSelectedIndexChange(index); + + // Call onSelect first - this should trigger handleActionSelect in ChatInput + onSelect(actions[index].id); + + console.log('โœ… ActionPopover: onSelect called successfully'); + + // Call the local action (just for logging) + actions[index].action(); + + // Close popover after a small delay to allow text replacement to complete + console.log('๐Ÿšช ActionPopover: Closing popover after delay'); + setTimeout(() => { + onClose(); + }, 10); + };''' + +content = content.replace(old_handleItemClick, new_handleItemClick) + +# Also fix the selectAction method +old_selectAction = ''' selectAction: (index: number) => { + console.log('โŒจ๏ธ ActionPopover: selectAction called via keyboard', { index, actionId: actions[index]?.id }); + if (actions[index]) { + console.log('๐Ÿ”„ ActionPopover: Calling onSelect from selectAction:', actions[index].id); + onSelect(actions[index].id); + actions[index].action(); + onClose(); + } + },''' + +new_selectAction = ''' selectAction: (index: number) => { + console.log('โŒจ๏ธ ActionPopover: selectAction called via keyboard', { index, actionId: actions[index]?.id }); + if (actions[index]) { + console.log('๐Ÿ”„ ActionPopover: Calling onSelect from selectAction:', actions[index].id); + onSelect(actions[index].id); + actions[index].action(); + setTimeout(() => { + onClose(); + }, 10); + } + },''' + +content = content.replace(old_selectAction, new_selectAction) + +# Write the modified content back +with open('ui/desktop/src/components/ActionPopover.tsx', 'w') as f: + f.write(content) + +print("Fixed ActionPopover to delay onClose() call") diff --git a/fix_action_popover_state.py b/fix_action_popover_state.py new file mode 100644 index 0000000000..8fc01c7a0e --- /dev/null +++ b/fix_action_popover_state.py @@ -0,0 +1,81 @@ +import re + +# Read the ChatInput file +with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f: + content = f.read() + +# Fix 1: Update actionPopover state to include cursorPosition +old_state = ''' const [actionPopover, setActionPopover] = useState<{ + isOpen: boolean; + position: { x: number; y: number }; + selectedIndex: number; + }>({ + isOpen: false, + position: { x: 0, y: 0 }, + selectedIndex: 0, + });''' + +new_state = ''' const [actionPopover, setActionPopover] = useState<{ + isOpen: boolean; + position: { x: number; y: number }; + selectedIndex: number; + cursorPosition?: number; + }>({ + isOpen: false, + position: { x: 0, y: 0 }, + selectedIndex: 0, + cursorPosition: 0, + });''' + +content = content.replace(old_state, new_state) + +# Fix 2: Update the checkForMention function to set cursorPosition when opening action popover +# Find the section where action popover is opened +old_action_open = ''' setActionPopover({ + isOpen: true, + position: { + x: textAreaRect.left, + y: textAreaRect.top, + }, + selectedIndex: 0, + });''' + +new_action_open = ''' setActionPopover({ + isOpen: true, + position: { + x: textAreaRect.left, + y: textAreaRect.top, + }, + selectedIndex: 0, + cursorPosition: cursorPosition, + });''' + +content = content.replace(old_action_open, new_action_open) + +# Fix 3: Update handleActionButtonClick to include cursorPosition +old_button_click = ''' setActionPopover({ + isOpen: true, + position: { + x: buttonRect.left, + y: buttonRect.top, + }, + selectedIndex: 0, + });''' + +new_button_click = ''' setActionPopover({ + isOpen: true, + position: { + x: buttonRect.left, + y: buttonRect.top, + }, + selectedIndex: 0, + cursorPosition: textAreaRef.current?.getBoundingClientRect ? 0 : 0, // Will be set by RichChatInput + });''' + +content = content.replace(old_button_click, new_button_click) + +# Write the modified content back +with open('ui/desktop/src/components/ChatInput.tsx', 'w') as f: + f.write(content) + +print("Fixed actionPopover state to include cursorPosition") diff --git a/fix_action_selection.py b/fix_action_selection.py new file mode 100644 index 0000000000..d3ca5cfcd4 --- /dev/null +++ b/fix_action_selection.py @@ -0,0 +1,90 @@ +import re + +# Read the ChatInput file +with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f: + content = f.read() + +# Find and replace the handleActionSelect function +old_function = ''' const handleActionSelect = (actionId: string) => { + const actionInfo = getActionInfo(actionId); + + // Replace / with [Action] text inline + const currentValue = displayValue; + const cursorPosition = 0; // RichChatInput handles cursor position internally + const beforeCursor = currentValue.slice(0, cursorPosition); + const lastSlashIndex = beforeCursor.lastIndexOf('/'); + + if (lastSlashIndex !== -1) { + const afterSlash = beforeCursor.slice(lastSlashIndex + 1); + if (!afterSlash.includes(' ') && !afterSlash.includes('\\n')) { + // Replace / with [Action] text + const beforeSlash = currentValue.slice(0, lastSlashIndex); + // Simplified approach for RichChatInput + const actionText = `[${actionInfo.label}]`; + const newValue = beforeSlash + actionText + " "; + + setDisplayValue(newValue); + setValue(newValue); + + // Track selected action for pill display + + + // Set cursor position after the action text + setTimeout(() => { + if (textAreaRef.current) { + // Cursor positioning handled by RichChatInput + // Focus handled by RichChatInput + } + }, 0); + } + } + + console.log('Action selected:', actionId); + setActionPopover(prev => ({ ...prev, isOpen: false })); + };''' + +new_function = ''' const handleActionSelect = (actionId: string) => { + const actionInfo = getActionInfo(actionId); + + // Get current cursor position from the RichChatInput + const currentValue = displayValue; + const cursorPosition = actionPopover.cursorPosition || 0; + const beforeCursor = currentValue.slice(0, cursorPosition); + const afterCursor = currentValue.slice(cursorPosition); + const lastSlashIndex = beforeCursor.lastIndexOf('/'); + + if (lastSlashIndex !== -1) { + const afterSlash = beforeCursor.slice(lastSlashIndex + 1); + // Check if we're still in the same "word" after the slash + if (!afterSlash.includes(' ') && !afterSlash.includes('\\n')) { + // Replace the /query with [Action] text + const beforeSlash = currentValue.slice(0, lastSlashIndex); + const actionText = `[${actionInfo.label}]`; + const newValue = beforeSlash + actionText + " " + afterCursor; + + setDisplayValue(newValue); + setValue(newValue); + + // Set cursor position after the action text and space + const newCursorPosition = lastSlashIndex + actionText.length + 1; + setTimeout(() => { + if (textAreaRef.current) { + textAreaRef.current.setSelectionRange(newCursorPosition, newCursorPosition); + textAreaRef.current.focus(); + } + }, 0); + } + } + + console.log('Action selected:', actionId, 'at position:', cursorPosition); + setActionPopover(prev => ({ ...prev, isOpen: false })); + };''' + +# Replace the function +content = content.replace(old_function, new_function) + +# Write the modified content back +with open('ui/desktop/src/components/ChatInput.tsx', 'w') as f: + f.write(content) + +print("Fixed handleActionSelect function to properly handle cursor position and create action pills") diff --git a/fix_chatinput.py b/fix_chatinput.py new file mode 100644 index 0000000000..b726a75984 --- /dev/null +++ b/fix_chatinput.py @@ -0,0 +1,106 @@ +import re + +# Read the ChatInput file +with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f: + content = f.read() + +# 1. Add RichTextInput import after ActionPill import +content = re.sub( + r'(import ActionPill from \'\.\/ActionPill\';)', + r'\1\nimport RichTextInput, { ActionData } from \'./RichTextInput\';', + content +) + +# 2. Replace selectedActions state with actions state - find the exact pattern +selectedactions_pattern = r' // Action pills for visual display\s*const \[selectedActions, setSelectedActions\] = useState>.*?\[\]\);' +content = re.sub( + selectedactions_pattern, + ' // Action pills for visual display\n const [actions, setActions] = useState([]);', + content, + flags=re.DOTALL +) + +# 3. Remove the Selected Actions Pills section completely +selected_pills_pattern = r'\s*\{\/\* Selected Actions Pills \*\/\}\s*\{selectedActions\.length > 0 && \(\s*
\s*Selected:<\/span>\s*\{selectedActions\.map\(\(action\) => \(\s* handleRemoveAction\(action\.id\)\}\s*\/>\s*\)\)\}\s*<\/div>\s*\)\}' +content = re.sub(selected_pills_pattern, '', content, flags=re.DOTALL) + +# 4. Replace all selectedActions with actions and setSelectedActions with setActions +content = content.replace('selectedActions', 'actions') +content = content.replace('setActions([]);', 'setActions([]);') # Fix double replacement + +# 5. Update handleActionSelect - find and replace the entire function +handle_action_select_old = r' const handleActionSelect = \(actionId: string\) => \{[^}]*\};' +handle_action_select_new = ''' const handleActionSelect = (actionId: string) => { + const actionInfo = getActionInfo(actionId); + + // Replace / with [Action] text inline + const currentValue = displayValue; + const cursorPosition = textAreaRef.current?.selectionStart || 0; + const beforeCursor = currentValue.slice(0, cursorPosition); + const lastSlashIndex = beforeCursor.lastIndexOf('/'); + + if (lastSlashIndex !== -1) { + const afterSlash = beforeCursor.slice(lastSlashIndex + 1); + if (!afterSlash.includes(' ') && !afterSlash.includes('\\n')) { + // Replace / with [Action] text + const beforeSlash = currentValue.slice(0, lastSlashIndex); + const afterCursor = currentValue.slice(cursorPosition); + const actionText = `[${actionInfo.label}]`; + const newValue = beforeSlash + actionText + afterCursor; + + setDisplayValue(newValue); + setValue(newValue); + + // Track action for pill display + const newAction: ActionData = { + id: Date.now().toString() + Math.random().toString(36).substr(2, 9), + actionId, + label: actionInfo.label, + icon: actionInfo.icon, + position: lastSlashIndex, + }; + + setActions(prev => [...prev, newAction]); + + // Set cursor position after the action text + setTimeout(() => { + if (textAreaRef.current) { + const newCursorPosition = lastSlashIndex + actionText.length; + textAreaRef.current.setSelectionRange(newCursorPosition, newCursorPosition); + } + }, 0); + } + } + + console.log('Action selected:', actionId); + setActionPopover(prev => ({ ...prev, isOpen: false })); + };''' + +# Find the function more carefully +lines = content.split('\n') +start_idx = -1 +end_idx = -1 +brace_count = 0 +in_function = False + +for i, line in enumerate(lines): + if 'const handleActionSelect = (actionId: string) => {' in line: + start_idx = i + in_function = True + brace_count = line.count('{') - line.count('}') + elif in_function: + brace_count += line.count('{') - line.count('}') + if brace_count == 0: + end_idx = i + break + +if start_idx != -1 and end_idx != -1: + # Replace the function + new_lines = lines[:start_idx] + handle_action_select_new.split('\n') + lines[end_idx + 1:] + content = '\n'.join(new_lines) + +print("ChatInput.tsx updated successfully") + +# Write the modified content back +with open('ui/desktop/src/components/ChatInput.tsx', 'w') as f: + f.write(content) diff --git a/fix_correct_tailwind_classes.py b/fix_correct_tailwind_classes.py new file mode 100644 index 0000000000..dec0cc8ef4 --- /dev/null +++ b/fix_correct_tailwind_classes.py @@ -0,0 +1,34 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing Tailwind class names to match the CSS variables...") + +# Looking at the CSS, the correct classes should be: +# - text-text-default (maps to --color-text-default) +# - text-text-muted (maps to --color-text-muted) +# - border-text-default (maps to --color-text-default) + +# Fix 1: Change placeholder text class +old_placeholder_class = 'className="text-textMuted pointer-events-none select-none"' +new_placeholder_class = 'className="text-text-muted pointer-events-none select-none"' + +content = content.replace(old_placeholder_class, new_placeholder_class) + +# Fix 2: Change cursor border class +old_cursor_border = 'border-textDefault' +new_cursor_border = 'border-text-default' + +content = content.replace(old_cursor_border, new_cursor_border) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed Tailwind class names:") +print(" - Changed text-textMuted to text-text-muted") +print(" - Changed border-textDefault to border-text-default") +print(" - These should now map to the correct CSS variables") +print(" - Should work properly in both light and dark modes") diff --git a/fix_cursor_and_placeholder_styling.py b/fix_cursor_and_placeholder_styling.py new file mode 100644 index 0000000000..4aab54a444 --- /dev/null +++ b/fix_cursor_and_placeholder_styling.py @@ -0,0 +1,34 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing cursor and placeholder styling...") + +# Fix 1: Change cursor color from white to grey +old_cursor_1 = 'className="animate-pulse border-l border-textStandard h-4 inline-block"' +new_cursor_1 = 'className="animate-pulse border-l border-textSubtle h-4 inline-block"' + +content = content.replace(old_cursor_1, new_cursor_1) + +# Fix 2: Also fix the cursor at the end of renderContent +old_cursor_2 = '' +new_cursor_2 = '' + +content = content.replace(old_cursor_2, new_cursor_2) + +# Fix 3: Ensure placeholder text is using textPlaceholder (should already be correct) +# Check if it's already using the right class +if 'text-textPlaceholder' in content: + print("โœ… Placeholder text already using textPlaceholder class") +else: + print("โš ๏ธ Placeholder text class might need fixing") + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed cursor styling:") +print(" - Changed from border-textStandard (white) to border-textSubtle (grey)") +print("โœ… Placeholder text should already be subtle (text-textPlaceholder)") diff --git a/fix_cursor_blink_with_css.py b/fix_cursor_blink_with_css.py new file mode 100644 index 0000000000..ef96d4a989 --- /dev/null +++ b/fix_cursor_blink_with_css.py @@ -0,0 +1,29 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing cursor blink by using inline styles instead of Tailwind class...") + +# Replace animate-blink with inline style +old_cursor_1 = 'className="animate-blink border-l-2 border-black h-4 inline-block"' +new_cursor_1 = 'className="border-l-2 border-black h-4 inline-block" style={{ animation: "blink 1s step-end infinite" }}' + +content = content.replace(old_cursor_1, new_cursor_1) + +# Also fix the other cursor instance +old_cursor_2 = '' +new_cursor_2 = '' + +content = content.replace(old_cursor_2, new_cursor_2) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed cursor blinking by using inline CSS animation") +print(" - Removed animate-blink class (not recognized by Tailwind)") +print(" - Added inline style: animation: 'blink 1s step-end infinite'") +print(" - Uses the @keyframes blink defined in main.css") +print(" - Should now blink properly!") diff --git a/fix_cursor_line_break_logic.py b/fix_cursor_line_break_logic.py new file mode 100644 index 0000000000..3286bcc80e --- /dev/null +++ b/fix_cursor_line_break_logic.py @@ -0,0 +1,112 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing cursor positioning logic for line breaks...") + +# The issue is in how we're processing character by character +# We need to handle newlines properly and render text as continuous flow +# Let's rewrite the text processing logic + +old_text_processing = ''' // Add text before action with potential cursor + if (beforeAction) { + const textParts = []; + for (let i = 0; i < beforeAction.length; i++) { + const char = beforeAction[i]; + addCursorIfNeeded(currentPos); + textParts.push(char); + currentPos++; + } + + parts.push( + + {textParts} + + ); + }''' + +new_text_processing = ''' // Add text before action with potential cursor + if (beforeAction) { + // Split text by cursor position to insert cursor at the right spot + let textWithCursor = []; + for (let i = 0; i < beforeAction.length; i++) { + if (isFocused && cursorPosition === currentPos) { + textWithCursor.push( + + ); + } + textWithCursor.push(beforeAction[i]); + currentPos++; + } + + parts.push( + + {textWithCursor} + + ); + }''' + +content = content.replace(old_text_processing, new_text_processing) + +# Also fix the remaining text processing +old_remaining_processing = ''' // Add remaining text with potential cursor + const remainingText = value.slice(lastIndex); + if (remainingText) { + const textParts = []; + for (let i = 0; i < remainingText.length; i++) { + const char = remainingText[i]; + addCursorIfNeeded(currentPos); + textParts.push(char); + currentPos++; + } + + parts.push( + + {textParts} + + ); + }''' + +new_remaining_processing = ''' // Add remaining text with potential cursor + const remainingText = value.slice(lastIndex); + if (remainingText) { + let textWithCursor = []; + for (let i = 0; i < remainingText.length; i++) { + if (isFocused && cursorPosition === currentPos) { + textWithCursor.push( + + ); + } + textWithCursor.push(remainingText[i]); + currentPos++; + } + + parts.push( + + {textWithCursor} + + ); + }''' + +content = content.replace(old_remaining_processing, new_remaining_processing) + +# Remove the separate addCursorIfNeeded calls since we're handling it inline now +content = content.replace(' // Add cursor before action if needed\n addCursorIfNeeded(currentPos);', '') +content = content.replace(' // Add cursor at the end if needed\n addCursorIfNeeded(currentPos);', ''' // Add cursor at the end if needed + if (isFocused && cursorPosition === currentPos) { + parts.push( + + ); + }''') + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed cursor positioning logic:") +print(" - Integrated cursor rendering directly into text processing") +print(" - Cursor now appears inline with text characters") +print(" - Should handle line breaks properly") +print(" - Cursor stays at correct position within the text flow") diff --git a/fix_cursor_placeholder_order.py b/fix_cursor_placeholder_order.py new file mode 100644 index 0000000000..fdbb7ff652 --- /dev/null +++ b/fix_cursor_placeholder_order.py @@ -0,0 +1,48 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing cursor position to appear before placeholder text...") + +# Find the placeholder rendering and fix the cursor positioning +old_placeholder_render = ''' // Show placeholder when there's no text content (regardless of focus state) + if (!value.trim()) { + return ( +
+ + {placeholder} + + {isFocused && ( + + )} +
+ ); + }''' + +new_placeholder_render = ''' // Show placeholder when there's no text content (regardless of focus state) + if (!value.trim()) { + return ( +
+ {isFocused && ( + + )} + + {placeholder} + +
+ ); + }''' + +content = content.replace(old_placeholder_render, new_placeholder_render) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed cursor and placeholder order:") +print(" - Moved cursor before placeholder text in the DOM") +print(" - Changed ml-0.5 to mr-0.5 (margin-right instead of margin-left)") +print(" - Cursor now appears at the beginning of the line") +print(" - Placeholder text appears after the cursor") diff --git a/fix_cursor_positioning.py b/fix_cursor_positioning.py new file mode 100644 index 0000000000..36886ba0cc --- /dev/null +++ b/fix_cursor_positioning.py @@ -0,0 +1,29 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing cursor positioning to align with text...") + +# Fix 1: Update cursor styling to match text height and alignment +old_cursor_1 = 'className="border-l-2 border-black h-4 inline-block" style={{ animation: "blink 1s step-end infinite" }}' +new_cursor_1 = 'className="border-l-2 border-black inline-block" style={{ animation: "blink 1s step-end infinite", height: "1.2em", marginLeft: "1px" }}' + +content = content.replace(old_cursor_1, new_cursor_1) + +# Fix 2: Also fix the other cursor instance +old_cursor_2 = '' +new_cursor_2 = '' + +content = content.replace(old_cursor_2, new_cursor_2) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed cursor positioning:") +print(" - Removed fixed h-4 height") +print(" - Added height: '1.2em' to match text line height") +print(" - Added marginLeft: '1px' for better spacing from text") +print(" - Cursor should now align properly with text") diff --git a/fix_cursor_to_black_blinking.py b/fix_cursor_to_black_blinking.py new file mode 100644 index 0000000000..e3f47b238c --- /dev/null +++ b/fix_cursor_to_black_blinking.py @@ -0,0 +1,33 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Making cursor black and blinking like a regular cursor...") + +# Fix 1: Change cursor color from grey to black and use proper blinking animation +old_cursor_1 = 'className="animate-pulse border-l border-textSubtle h-4 inline-block"' +new_cursor_1 = 'className="animate-blink border-l-2 border-black h-4 inline-block"' + +content = content.replace(old_cursor_1, new_cursor_1) + +# Fix 2: Also fix the cursor at the end of renderContent +old_cursor_2 = '' +new_cursor_2 = '' + +content = content.replace(old_cursor_2, new_cursor_2) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Updated cursor styling:") +print(" - Changed from border-textSubtle (grey) to border-black") +print(" - Changed from animate-pulse to animate-blink") +print(" - Made border thicker (border-l-2) for better visibility") +print(" - Now behaves like a regular text cursor") + +# Now we need to add the custom blink animation to the CSS +print("\n๐Ÿ“ Note: We need to add the animate-blink class to Tailwind config") +print(" This will make the cursor blink on/off like a regular text cursor") diff --git a/fix_cursor_vertical_alignment.py b/fix_cursor_vertical_alignment.py new file mode 100644 index 0000000000..364860089a --- /dev/null +++ b/fix_cursor_vertical_alignment.py @@ -0,0 +1,30 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing cursor vertical alignment with text...") + +# Find all cursor instances and fix their styling +old_cursor_style = 'style={{ animation: "blink 1s step-end infinite", height: "1.2em", marginLeft: "1px" }}' +new_cursor_style = 'style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }}' + +content = content.replace(old_cursor_style, new_cursor_style) + +# Also need to ensure the cursor aligns with the text line height +# Let's also adjust the className to help with alignment +old_cursor_class = 'className="border-l-2 border-black inline-block"' +new_cursor_class = 'className="border-l-2 border-black inline-block align-baseline"' + +content = content.replace(old_cursor_class, new_cursor_class) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed cursor vertical alignment:") +print(" - Changed height from 1.2em to 1em (matches text height)") +print(" - Added verticalAlign: 'baseline' to align with text baseline") +print(" - Added align-baseline class for consistent alignment") +print(" - Cursor should now be centered with the text line") diff --git a/fix_dark_mode_cursor_and_text.py b/fix_dark_mode_cursor_and_text.py new file mode 100644 index 0000000000..40bcb83e97 --- /dev/null +++ b/fix_dark_mode_cursor_and_text.py @@ -0,0 +1,33 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing dark mode cursor color and text color...") + +# Fix 1: Change cursor from black to theme-aware color +# Find all cursor instances and replace border-black with border-textDefault +old_cursor_class = 'className="border-l-2 border-black inline-block align-baseline absolute left-0"' +new_cursor_class = 'className="border-l-2 border-textDefault inline-block align-baseline absolute left-0"' + +content = content.replace(old_cursor_class, new_cursor_class) + +# Also fix other cursor instances in the text processing +content = content.replace('border-l-2 border-black inline-block', 'border-l-2 border-textDefault inline-block') + +# Fix 2: Change placeholder text color to match text-textMuted properly +old_placeholder_color = 'className="text-textMuted pointer-events-none select-none"' +new_placeholder_color = 'className="text-textMuted pointer-events-none select-none"' + +# The placeholder color is already correct, but let's make sure it's using the right class + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed dark mode styling:") +print(" - Changed cursor from border-black to border-textDefault") +print(" - Cursor will now be white in dark mode, black in light mode") +print(" - Placeholder text already uses text-textMuted which adapts to theme") +print(" - Both cursor and text should now work properly in dark mode") diff --git a/fix_import_issue.py b/fix_import_issue.py new file mode 100644 index 0000000000..1ce1be42d3 --- /dev/null +++ b/fix_import_issue.py @@ -0,0 +1,38 @@ +import re + +# Read the ChatInput file +with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f: + content = f.read() + +# Fix the import - change from default import to named import +old_import = "import RichChatInput, { RichChatInputRef } from './RichChatInput';" +new_import = "import { RichChatInput, RichChatInputRef } from './RichChatInput';" + +content = content.replace(old_import, new_import) + +print("Fixed RichChatInput import to use named export") + +# Write the modified content back +with open('ui/desktop/src/components/ChatInput.tsx', 'w') as f: + f.write(content) + +# Also check if we need to fix the RichChatInput export +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + rich_content = f.read() + +# Make sure it has both named and default exports +if 'export default RichChatInput;' not in rich_content: + # Add default export if missing + rich_content = rich_content.replace( + 'RichChatInput.displayName = \'RichChatInput\';', + 'RichChatInput.displayName = \'RichChatInput\';\n\nexport default RichChatInput;' + ) + + with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(rich_content) + + print("Added default export to RichChatInput") +else: + print("RichChatInput already has default export") + +print("Import/export issue fixed!") diff --git a/fix_inline_message_display.py b/fix_inline_message_display.py new file mode 100644 index 0000000000..a33a409452 --- /dev/null +++ b/fix_inline_message_display.py @@ -0,0 +1,87 @@ +import re + +# Read the MessageContent file +with open('ui/desktop/src/components/MessageContent.tsx', 'r') as f: + content = f.read() + +print("Fixing MessageContent to display pills inline...") + +# Replace the entire return statement to fix inline display +old_return = ''' return ( +
+ {parsedContent.map((part, index) => { + if (part.type === 'action' && part.actionId) { + const actionInfo = getActionInfo(part.actionId); + return ( + + ); + } else if (part.content.trim()) { + return ( + + ); + } + return null; + })} +
+ );''' + +new_return = ''' return ( + + {parsedContent.map((part, index) => { + if (part.type === 'action' && part.actionId) { + const actionInfo = getActionInfo(part.actionId); + return ( + + ); + } else if (part.content.trim()) { + return ( + $1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/`(.*?)`/g, '$1') + .replace(/\n/g, '
') + }} + /> + ); + } + return null; + })} +
+ );''' + +content = content.replace(old_return, new_return) + +# Write the modified content back +with open('ui/desktop/src/components/MessageContent.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed MessageContent to display pills inline with text") +print("Key changes:") +print("1. Changed container from
to with inline-flex") +print("2. Replaced MarkdownContent with inline and basic HTML formatting") +print("3. Used items-baseline for better text alignment") +print("4. Applied inline styling to prevent line breaks") diff --git a/fix_input_spacing.py b/fix_input_spacing.py new file mode 100644 index 0000000000..56110f5a49 --- /dev/null +++ b/fix_input_spacing.py @@ -0,0 +1,110 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing input spacing to match original ChatInput padding...") + +# The original ChatInput has: px-3 pt-3 pb-1.5 pr-20 text-sm +# That translates to: padding: 12px 80px 6px 12px; font-size: 14px; + +# Update both the hidden textarea and visual display to have proper padding +old_hidden_textarea_style = ''' style={{ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + opacity: 0.15, // Slightly more visible for selection + zIndex: 2, // Higher z-index to capture mouse events + background: 'transparent', + border: 'none', + outline: 'none', + resize: 'none', + color: 'rgba(59, 130, 246, 0.2)', // Slightly more visible blue text + caretColor: 'transparent', // Hide caret (we show our own) + pointerEvents: 'auto', // Ensure it can receive mouse events + fontFamily: 'Cash Sans, sans-serif', // Match exact font + fontSize: '0.875rem', // Match text-sm (14px) + lineHeight: '1.5', // Match leading-relaxed + padding: '0', // No padding to match visual display + margin: '0', + boxSizing: 'border-box', + whiteSpace: 'pre-wrap', // Match visual display + wordWrap: 'break-word', + }}''' + +new_hidden_textarea_style = ''' style={{ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + opacity: 0.15, // Slightly more visible for selection + zIndex: 2, // Higher z-index to capture mouse events + background: 'transparent', + border: 'none', + outline: 'none', + resize: 'none', + color: 'rgba(59, 130, 246, 0.2)', // Slightly more visible blue text + caretColor: 'transparent', // Hide caret (we show our own) + pointerEvents: 'auto', // Ensure it can receive mouse events + fontFamily: 'Cash Sans, sans-serif', // Match exact font + fontSize: '0.875rem', // Match text-sm (14px) + lineHeight: '1.5', // Match leading-relaxed + padding: '12px 80px 6px 12px', // Match original: px-3 pt-3 pb-1.5 pr-20 + margin: '0', + boxSizing: 'border-box', + whiteSpace: 'pre-wrap', // Match visual display + wordWrap: 'break-word', + }}''' + +content = content.replace(old_hidden_textarea_style, new_hidden_textarea_style) + +# Update the visual display to have matching padding +old_visual_display_style = ''' style={{ + ...style, + minHeight: `${rows * 1.5}em`, + zIndex: 1, // Lower z-index, behind textarea + pointerEvents: 'none', // Don't interfere with text selection + userSelect: 'none', // Prevent selection on visual layer + WebkitUserSelect: 'none', + fontFamily: 'Cash Sans, sans-serif', // Match textarea font + fontSize: '0.875rem', // Match textarea size + lineHeight: '1.5', // Match textarea line height + padding: '0', // Match textarea padding + margin: '0', + whiteSpace: 'pre-wrap', // Match textarea + wordWrap: 'break-word', + }}''' + +new_visual_display_style = ''' style={{ + ...style, + minHeight: `${rows * 1.5}em`, + zIndex: 1, // Lower z-index, behind textarea + pointerEvents: 'none', // Don't interfere with text selection + userSelect: 'none', // Prevent selection on visual layer + WebkitUserSelect: 'none', + fontFamily: 'Cash Sans, sans-serif', // Match textarea font + fontSize: '0.875rem', // Match textarea size + lineHeight: '1.5', // Match textarea line height + padding: '12px 80px 6px 12px', // Match original: px-3 pt-3 pb-1.5 pr-20 + margin: '0', + whiteSpace: 'pre-wrap', // Match textarea + wordWrap: 'break-word', + }}''' + +content = content.replace(old_visual_display_style, new_visual_display_style) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed input spacing:") +print(" - Added proper padding: 12px 80px 6px 12px (px-3 pt-3 pb-1.5 pr-20)") +print(" - Left padding: 12px (px-3)") +print(" - Top padding: 12px (pt-3)") +print(" - Bottom padding: 6px (pb-1.5)") +print(" - Right padding: 80px (pr-20) - space for action buttons") +print(" - Both layers now have matching spacing") diff --git a/fix_mention_file_select_properly.py b/fix_mention_file_select_properly.py new file mode 100644 index 0000000000..de18ae9519 --- /dev/null +++ b/fix_mention_file_select_properly.py @@ -0,0 +1,78 @@ +import re + +# Read the ChatInput file +with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing handleMentionFileSelect to create @filename format...") + +# Find the current handleMentionFileSelect function and replace it +old_function = ''' const handleMentionFileSelect = (filePath: string) => { + // Replace the @ mention with the file path + const beforeMention = displayValue.slice(0, mentionPopover.mentionStart); + const afterMention = displayValue.slice( + mentionPopover.mentionStart + 1 + mentionPopover.query.length + ); + const newValue = `${beforeMention}${filePath}${afterMention}`; + + setDisplayValue(newValue); + setValue(newValue); + setMentionPopover((prev) => ({ ...prev, isOpen: false })); + textAreaRef.current?.focus(); + + // Set cursor position after the inserted file path + setTimeout(() => { + if (textAreaRef.current) { + // Cursor positioning handled by RichChatInput + // Focus handled by RichChatInput + } + }, 0); + };''' + +new_function = ''' const handleMentionFileSelect = (filePath: string) => { + console.log('๐Ÿ“ handleMentionFileSelect called with:', filePath); + + // Extract just the filename from the full path for the pill + const fileName = filePath.split('/').pop() || filePath; + console.log('๐Ÿ“ Extracted filename:', fileName); + + // Create @filename format for pill detection + const mentionText = `@${fileName}`; + console.log('๐Ÿ“ Creating mention text:', mentionText); + + // Replace the @ mention with @filename format + const beforeMention = displayValue.slice(0, mentionPopover.mentionStart); + const afterMention = displayValue.slice( + mentionPopover.mentionStart + 1 + mentionPopover.query.length + ); + const newValue = `${beforeMention}${mentionText} ${afterMention}`; + + console.log('๐Ÿ“ New value will be:', newValue); + + setDisplayValue(newValue); + setValue(newValue); + setMentionPopover((prev) => ({ ...prev, isOpen: false })); + textAreaRef.current?.focus(); + + // Set cursor position after the inserted mention and space + const newCursorPosition = beforeMention.length + mentionText.length + 1; + setTimeout(() => { + if (textAreaRef.current) { + textAreaRef.current.setSelectionRange(newCursorPosition, newCursorPosition); + textAreaRef.current.focus(); + } + }, 0); + };''' + +content = content.replace(old_function, new_function) + +# Write the modified content back +with open('ui/desktop/src/components/ChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed handleMentionFileSelect function:") +print(" - Now extracts filename from full path") +print(" - Creates @filename format instead of full path") +print(" - Adds space after mention for better UX") +print(" - Added comprehensive debugging logs") +print(" - Should now create text that matches the mention regex") diff --git a/fix_message_pill_line_breaks.py b/fix_message_pill_line_breaks.py new file mode 100644 index 0000000000..140ceba720 --- /dev/null +++ b/fix_message_pill_line_breaks.py @@ -0,0 +1,28 @@ +import re + +# Read the MessageContent file +with open('ui/desktop/src/components/MessageContent.tsx', 'r') as f: + content = f.read() + +print("Fixing document pill line breaks in message display...") + +# The issue is the same as before - using flex layout causes pills to break lines +# Change from flex to inline layout like we did for the input + +old_container = ''' return ( + ''' + +new_container = ''' return ( + ''' + +content = content.replace(old_container, new_container) + +# Write the modified content back +with open('ui/desktop/src/components/MessageContent.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed message pill line breaks:") +print(" - Changed from inline-flex flex-wrap to inline") +print(" - Removed flex layout that was causing line breaks") +print(" - Pills should now flow inline with text in messages") +print(" - Same fix as applied to input field earlier") diff --git a/fix_multiline_cursor_positioning.py b/fix_multiline_cursor_positioning.py new file mode 100644 index 0000000000..0d9c40540b --- /dev/null +++ b/fix_multiline_cursor_positioning.py @@ -0,0 +1,48 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing cursor positioning for multi-line text...") + +# The issue is with the container layout. We need to change from flex to inline layout +# Find the return statement with the flex container +old_container = ''' return ( +
+ {parts.length > 0 ? parts : ( + isFocused && ( + + ) + )} +
+ );''' + +new_container = ''' return ( +
+ {parts.length > 0 ? parts : ( + isFocused && ( + + ) + )} +
+ );''' + +content = content.replace(old_container, new_container) + +# Also need to fix how text parts are rendered to be truly inline +# Find the text rendering sections and make sure they're inline +old_text_span = 'className="whitespace-pre-wrap"' +new_text_span = 'className="inline whitespace-pre-wrap"' + +content = content.replace(old_text_span, new_text_span) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed multi-line cursor positioning:") +print(" - Changed container from flex to whitespace-pre-wrap") +print(" - Added leading-relaxed for better line height") +print(" - Made text spans explicitly inline") +print(" - Cursor should now stay in correct position on line breaks") diff --git a/fix_placeholder_behavior.py b/fix_placeholder_behavior.py new file mode 100644 index 0000000000..12cb6b2504 --- /dev/null +++ b/fix_placeholder_behavior.py @@ -0,0 +1,45 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing placeholder behavior to stay visible when focused...") + +# Find the renderContent function and fix the placeholder logic +old_placeholder_logic = ''' const renderContent = useCallback(() => { + if (!value.trim() && !isFocused) { + return ( + + {placeholder} + + ); + }''' + +new_placeholder_logic = ''' const renderContent = useCallback(() => { + // Show placeholder when there's no text content (regardless of focus state) + if (!value.trim()) { + return ( +
+ + {placeholder} + + {isFocused && ( + + )} +
+ ); + }''' + +content = content.replace(old_placeholder_logic, new_placeholder_logic) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed placeholder behavior:") +print(" - Changed from text-textPlaceholder to text-textSubtle (matches icon color)") +print(" - Placeholder now stays visible when input is focused") +print(" - Only disappears when user actually types text") +print(" - Cursor appears alongside placeholder when focused") +print(" - Uses absolute positioning to overlay cursor on placeholder") diff --git a/fix_placeholder_color_lighter.py b/fix_placeholder_color_lighter.py new file mode 100644 index 0000000000..a86b3c0b6a --- /dev/null +++ b/fix_placeholder_color_lighter.py @@ -0,0 +1,22 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Changing placeholder color back to lighter original color...") + +# Find the placeholder text and change it back to the original lighter color +old_placeholder_color = 'className="text-textSubtle pointer-events-none select-none"' +new_placeholder_color = 'className="text-textPlaceholder pointer-events-none select-none"' + +content = content.replace(old_placeholder_color, new_placeholder_color) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed placeholder color:") +print(" - Changed from text-textSubtle (darker grey) back to text-textPlaceholder (lighter grey)") +print(" - Now uses the same light color as the original empty text") +print(" - Should be more subtle and less prominent") diff --git a/fix_placeholder_color_medium.py b/fix_placeholder_color_medium.py new file mode 100644 index 0000000000..9e457b7eb1 --- /dev/null +++ b/fix_placeholder_color_medium.py @@ -0,0 +1,22 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Changing placeholder to a medium grey color...") + +# Find the placeholder text and change it to text-muted (neutral-400) +old_placeholder_color = 'className="text-textPlaceholder pointer-events-none select-none"' +new_placeholder_color = 'className="text-textMuted pointer-events-none select-none"' + +content = content.replace(old_placeholder_color, new_placeholder_color) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed placeholder color:") +print(" - Changed from text-textPlaceholder (very light) to text-textMuted (medium grey)") +print(" - Uses --color-neutral-400 which should be more visible") +print(" - Should be a good balance between subtle and readable") diff --git a/fix_placeholder_positioning.py b/fix_placeholder_positioning.py new file mode 100644 index 0000000000..b94daa0ee5 --- /dev/null +++ b/fix_placeholder_positioning.py @@ -0,0 +1,49 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing placeholder positioning and visibility...") + +# Find the placeholder rendering logic and fix the positioning +old_placeholder_render = ''' // Show placeholder when there's no text content (regardless of focus state) + if (!value.trim()) { + return ( +
+ + {placeholder} + + {isFocused && ( + + )} +
+ ); + }''' + +new_placeholder_render = ''' // Show placeholder when there's no text content (regardless of focus state) + if (!value.trim()) { + return ( +
+ + {placeholder} + + {isFocused && ( + + )} +
+ ); + }''' + +content = content.replace(old_placeholder_render, new_placeholder_render) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed placeholder positioning:") +print(" - Removed absolute positioning that was causing cutoff") +print(" - Added min-h-[1.5em] to ensure proper container height") +print(" - Added flex items-center for proper vertical alignment") +print(" - Used ml-0.5 instead of marginLeft for cursor spacing") +print(" - Placeholder should now be visible and properly positioned") diff --git a/fix_placeholder_text_shift.py b/fix_placeholder_text_shift.py new file mode 100644 index 0000000000..1f5c6c06b1 --- /dev/null +++ b/fix_placeholder_text_shift.py @@ -0,0 +1,57 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing placeholder text shift when cursor appears...") + +# Find the placeholder rendering and fix it to prevent text shifting +old_placeholder_render = ''' // Show placeholder when there's no text content (regardless of focus state) + if (!value.trim()) { + return ( +
+ {isFocused && ( + + )} + + {placeholder} + +
+ ); + }''' + +new_placeholder_render = ''' // Show placeholder when there's no text content (regardless of focus state) + if (!value.trim()) { + return ( +
+ + {placeholder} + + {isFocused && ( + + )} +
+ ); + }''' + +content = content.replace(old_placeholder_render, new_placeholder_render) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed placeholder text shifting:") +print(" - Made cursor absolutely positioned so it doesn't affect layout") +print(" - Added left-0 to position cursor at the start") +print(" - Added top: 50% and transform: translateY(-50%) for vertical centering") +print(" - Placeholder text now stays in exact same position when cursor appears") diff --git a/fix_regex_error.py b/fix_regex_error.py new file mode 100644 index 0000000000..f952564365 --- /dev/null +++ b/fix_regex_error.py @@ -0,0 +1,34 @@ +import re + +# Read the MessageContent file +with open('ui/desktop/src/components/MessageContent.tsx', 'r') as f: + content = f.read() + +print("Fixing regex syntax error in MessageContent...") + +# Fix the broken regex - the newline replacement got split +old_broken_html = ''' dangerouslySetInnerHTML={{ + __html: part.content + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/`(.*?)`/g, '$1') + .replace(/ +/g, '
') + }}''' + +new_fixed_html = ''' dangerouslySetInnerHTML={{ + __html: part.content + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/`(.*?)`/g, '$1') + .replace(/\\n/g, '
') + }}''' + +content = content.replace(old_broken_html, new_fixed_html) + +# Write the modified content back +with open('ui/desktop/src/components/MessageContent.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed regex syntax error") +print("Changed .replace(/\\n/g, '
') - newline regex was broken across lines") diff --git a/fix_richtextinput_integration.py b/fix_richtextinput_integration.py new file mode 100644 index 0000000000..dcbf492793 --- /dev/null +++ b/fix_richtextinput_integration.py @@ -0,0 +1,43 @@ +import re + +# Read the ChatInput file +with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f: + content = f.read() + +# Find the RichChatInput onChange handler and update it to handle cursor position +old_onchange = ''' onChange={(newValue, cursorPos) => { + setDisplayValue(newValue); + updateValue(newValue); + debouncedSaveDraft(newValue); + setHasUserTyped(true); + + // Check for mention/action triggers with the new cursor position + if (cursorPos !== undefined) { + checkForMention(newValue, cursorPos, textAreaRef.current!); + } + }}''' + +new_onchange = ''' onChange={(newValue, cursorPos) => { + setDisplayValue(newValue); + updateValue(newValue); + debouncedSaveDraft(newValue); + setHasUserTyped(true); + + // Check for mention/action triggers with the new cursor position + if (cursorPos !== undefined) { + checkForMention(newValue, cursorPos, { + getBoundingClientRect: () => textAreaRef.current?.getBoundingClientRect?.() || new DOMRect(), + selectionStart: cursorPos, + selectionEnd: cursorPos, + value: newValue, + } as HTMLTextAreaElement); + } + }}''' + +content = content.replace(old_onchange, new_onchange) + +# Write the modified content back +with open('ui/desktop/src/components/ChatInput.tsx', 'w') as f: + f.write(content) + +print("Fixed RichChatInput onChange integration with cursor position handling") diff --git a/fix_richtextinput_processing_loop.py b/fix_richtextinput_processing_loop.py new file mode 100644 index 0000000000..71f257a667 --- /dev/null +++ b/fix_richtextinput_processing_loop.py @@ -0,0 +1,169 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing RichChatInput processing loop to handle both actions and mentions...") + +# Find the entire processing loop section and replace it +# Look for the section that starts with "Process all matches in order" +old_processing_section = ''' // Process all matches in order + for (const matchData of allMatches) { + const { type, match, index, length, content } = matchData; + console.log('โœ… Found action match:', match); + const beforeAction = value.slice(lastIndex, match.index); + const actionLabel = match[1]; + console.log('๐Ÿ“ Processing action:', { beforeAction, actionLabel, matchIndex: match.index }); + + // Add text before action with potential cursor + if (beforeAction) { + // Split text by cursor position to insert cursor at the right spot + let textWithCursor = []; + for (let i = 0; i < beforeAction.length; i++) { + if (isFocused && cursorPosition === currentPos) { + textWithCursor.push( + + ); + } + textWithCursor.push(beforeAction[i]); + currentPos++; + } + + parts.push( + + {textWithCursor} + + ); + } + + + + // Find matching action + const actionEntry = Object.entries(ACTION_MAP).find( + ([_, config]) => config.label === actionLabel + ); + + if (actionEntry) { + const [actionId, config] = actionEntry; + parts.push( + handleRemoveAction(actionLabel)} + /> + ); + } else { + // If no matching action, render as text + parts.push( + + {match[0]} + + ); + } + + currentPos += match[0].length; + lastIndex = match.index + match[0].length; + }''' + +new_processing_section = ''' // Process all matches in order + for (const matchData of allMatches) { + const { type, match, index, length, content } = matchData; + console.log('โœ… Found match:', { type, content, index }); + + // Add text before this match with potential cursor + const beforeMatch = value.slice(lastIndex, index); + if (beforeMatch) { + let textWithCursor = []; + for (let i = 0; i < beforeMatch.length; i++) { + if (isFocused && cursorPosition === currentPos) { + textWithCursor.push( + + ); + } + textWithCursor.push(beforeMatch[i]); + currentPos++; + } + + parts.push( + + {textWithCursor} + + ); + } + + // Add cursor before match if needed + if (isFocused && cursorPosition === currentPos) { + parts.push( + + ); + } + + if (type === 'action') { + // Handle action pills + const actionLabel = content; + const actionEntry = Object.entries(ACTION_MAP).find( + ([_, config]) => config.label === actionLabel + ); + + console.log('๐Ÿท๏ธ Creating action pill:', { actionLabel, actionEntry }); + + if (actionEntry) { + const [actionId, config] = actionEntry; + parts.push( + handleRemoveAction(actionLabel)} + /> + ); + } else { + // If no matching action, render as text + parts.push( + + {match[0]} + + ); + } + } else if (type === 'mention') { + // Handle mention pills + const fileName = content; // filename without @ + const filePath = `@${fileName}`; // full mention text + + console.log('๐Ÿ“ Creating mention pill:', { fileName, filePath }); + + parts.push( + handleRemoveMention(fileName)} + /> + ); + } + + currentPos += length; + lastIndex = index + length; + }''' + +content = content.replace(old_processing_section, new_processing_section) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed RichChatInput processing loop:") +print(" - Updated to use new combined action/mention processing") +print(" - Added proper mention pill creation") +print(" - Added debugging logs for both types") +print(" - Should now render @filename as green pills") diff --git a/fix_selection_alignment.py b/fix_selection_alignment.py new file mode 100644 index 0000000000..87b591eca9 --- /dev/null +++ b/fix_selection_alignment.py @@ -0,0 +1,118 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing selection alignment - making hidden textarea match visual display exactly...") + +# The issue is that the hidden textarea and visual display have different positioning/padding +# We need to make them align perfectly + +# Update the hidden textarea to match the visual display exactly +old_hidden_textarea_style = ''' style={{ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + opacity: 0.1, // More visible for selection + zIndex: 2, // Higher z-index to capture mouse events + background: 'transparent', + border: 'none', + outline: 'none', + resize: 'none', + color: 'rgba(59, 130, 246, 0.1)', // Very light blue text + caretColor: 'transparent', // Hide caret (we show our own) + pointerEvents: 'auto', // Ensure it can receive mouse events + fontFamily: 'inherit', + fontSize: 'inherit', + lineHeight: 'inherit', + padding: 'inherit', + }}''' + +new_hidden_textarea_style = ''' style={{ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + opacity: 0.15, // Slightly more visible for selection + zIndex: 2, // Higher z-index to capture mouse events + background: 'transparent', + border: 'none', + outline: 'none', + resize: 'none', + color: 'rgba(59, 130, 246, 0.2)', // Slightly more visible blue text + caretColor: 'transparent', // Hide caret (we show our own) + pointerEvents: 'auto', // Ensure it can receive mouse events + fontFamily: 'Cash Sans, sans-serif', // Match exact font + fontSize: '0.875rem', // Match text-sm (14px) + lineHeight: '1.5', // Match leading-relaxed + padding: '0', // No padding to match visual display + margin: '0', + boxSizing: 'border-box', + whiteSpace: 'pre-wrap', // Match visual display + wordWrap: 'break-word', + }}''' + +content = content.replace(old_hidden_textarea_style, new_hidden_textarea_style) + +# Also need to make sure the visual display has consistent styling +old_visual_display = ''' {/* Visual display with action pills and cursor */} +
+ {renderContent()} +
''' + +new_visual_display = ''' {/* Visual display with action pills and cursor */} +
+ {renderContent()} +
''' + +content = content.replace(old_visual_display, new_visual_display) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed selection alignment:") +print(" - Matched font family, size, and line height exactly") +print(" - Removed padding/margin differences") +print(" - Added consistent white-space and word-wrap") +print(" - Selection should now align perfectly with visual text") +print(" - No more 'holographic' offset effect") diff --git a/fix_selection_visibility.py b/fix_selection_visibility.py new file mode 100644 index 0000000000..c1d1cca575 --- /dev/null +++ b/fix_selection_visibility.py @@ -0,0 +1,124 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing selection visibility to show over visual content...") + +# The issue is that selection is happening on the hidden textarea but we need it visible over the visual layer +# Let's make the hidden textarea slightly more visible and add a selection overlay + +# Update the hidden textarea to be slightly more visible during selection +old_hidden_textarea_style = ''' style={{ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + opacity: 0.01, // Nearly invisible but still selectable + zIndex: 2, // Higher z-index to capture mouse events + background: 'transparent', + border: 'none', + outline: 'none', + resize: 'none', + color: 'transparent', // Hide text but keep selection + caretColor: 'transparent', // Hide caret (we show our own) + pointerEvents: 'auto', // Ensure it can receive mouse events + }} + className="absolute inset-0 w-full h-full resize-none selection:bg-blue-500 selection:text-white"''' + +new_hidden_textarea_style = ''' style={{ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + opacity: 0.1, // More visible for selection + zIndex: 2, // Higher z-index to capture mouse events + background: 'transparent', + border: 'none', + outline: 'none', + resize: 'none', + color: 'rgba(59, 130, 246, 0.1)', // Very light blue text + caretColor: 'transparent', // Hide caret (we show our own) + pointerEvents: 'auto', // Ensure it can receive mouse events + fontFamily: 'inherit', + fontSize: 'inherit', + lineHeight: 'inherit', + padding: 'inherit', + }} + className="absolute inset-0 w-full h-full resize-none selection:bg-blue-500 selection:text-white"''' + +content = content.replace(old_hidden_textarea_style, new_hidden_textarea_style) + +# Also update the CSS to make selection more prominent +css_update = ''' +/* Enhanced text selection styling */ +.rich-text-input textarea::selection { + background-color: #3b82f6 !important; /* Blue-500 with !important */ + color: white !important; + opacity: 1 !important; +} + +.rich-text-input textarea::-moz-selection { + background-color: #3b82f6 !important; /* Blue-500 with !important */ + color: white !important; + opacity: 1 !important; +} + +/* Make sure selection is visible over visual content */ +.rich-text-input { + position: relative; +} + +.rich-text-input textarea { + mix-blend-mode: multiply; /* Blend with background for better visibility */ +} +''' + +# Update the main.css file +with open('ui/desktop/src/styles/main.css', 'r') as f: + css_content = f.read() + +# Replace the existing selection CSS with enhanced version +old_selection_css = '''/* Text selection styling for RichChatInput */ +.rich-text-input textarea::selection { + background-color: #3b82f6; /* Blue-500 */ + color: white; +} + +.rich-text-input textarea::-moz-selection { + background-color: #3b82f6; /* Blue-500 */ + color: white; +} + +/* Global selection styling for better visibility */ +::selection { + background-color: #3b82f6; /* Blue-500 */ + color: white; +} + +::-moz-selection { + background-color: #3b82f6; /* Blue-500 */ + color: white; +}''' + +if old_selection_css in css_content: + css_content = css_content.replace(old_selection_css, css_update) +else: + css_content += css_update + +with open('ui/desktop/src/styles/main.css', 'w') as f: + f.write(css_content) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Enhanced selection visibility:") +print(" - Increased textarea opacity to 0.1 for better selection visibility") +print(" - Added light blue text color for better contrast") +print(" - Added !important to selection CSS rules") +print(" - Added mix-blend-mode for better visual blending") +print(" - Selection should now be much more visible") diff --git a/fix_text_selection_behavior.py b/fix_text_selection_behavior.py new file mode 100644 index 0000000000..f2e9ce7fb9 --- /dev/null +++ b/fix_text_selection_behavior.py @@ -0,0 +1,133 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing text selection behavior to work like a normal text input...") + +# The main issue is that we need to make the hidden textarea more accessible +# and reduce interference from the visual display layer + +# First, let's update the hidden textarea to be more interactive +old_hidden_textarea_style = ''' style={{ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + opacity: 0.01, // Nearly invisible but still selectable + zIndex: 1, + background: 'transparent', + border: 'none', + outline: 'none', + resize: 'none', + color: 'transparent', // Hide text but keep selection + caretColor: 'transparent', // Hide caret (we show our own) + }}''' + +new_hidden_textarea_style = ''' style={{ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + opacity: 0.01, // Nearly invisible but still selectable + zIndex: 2, // Higher z-index to capture mouse events + background: 'transparent', + border: 'none', + outline: 'none', + resize: 'none', + color: 'transparent', // Hide text but keep selection + caretColor: 'transparent', // Hide caret (we show our own) + pointerEvents: 'auto', // Ensure it can receive mouse events + }}''' + +content = content.replace(old_hidden_textarea_style, new_hidden_textarea_style) + +# Update the visual display to have lower z-index and not interfere with selection +old_visual_display = ''' {/* Visual display with action pills and cursor */} +
+ {renderContent()} +
''' + +new_visual_display = ''' {/* Visual display with action pills and cursor */} +
+ {renderContent()} +
''' + +content = content.replace(old_visual_display, new_visual_display) + +# Remove the handleDisplayClick since we're not using it anymore +old_handle_display_click = ''' const handleDisplayClick = useCallback((e: React.MouseEvent) => { + // Don't focus if clicking on an action pill + if ((e.target as Element).closest('.action-pill-wrapper')) { + return; + } + + // Focus the hidden textarea for text selection + if (hiddenTextareaRef.current) { + hiddenTextareaRef.current.focus(); + + // For double/triple click, select text appropriately + if (e.detail === 2) { + // Double click - select word (let browser handle this) + setTimeout(() => { + if (hiddenTextareaRef.current) { + hiddenTextareaRef.current.select(); + } + }, 0); + } else if (e.detail === 3) { + // Triple click - select all + setTimeout(() => { + if (hiddenTextareaRef.current) { + hiddenTextareaRef.current.select(); + } + }, 0); + } + } + }, []);''' + +new_handle_display_click = ''' // Removed handleDisplayClick - let the hidden textarea handle all mouse events naturally''' + +content = content.replace(old_handle_display_click, new_handle_display_click) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed text selection behavior:") +print(" - Made hidden textarea capture all mouse events (higher z-index)") +print(" - Made visual display non-interactive (pointerEvents: 'none')") +print(" - Removed interfering click handlers") +print(" - Should now behave like a normal text input for selection") +print(" - Clicking, dragging, double-click, triple-click should all work normally") diff --git a/fix_text_selection_functionality.py b/fix_text_selection_functionality.py new file mode 100644 index 0000000000..8d2f586a11 --- /dev/null +++ b/fix_text_selection_functionality.py @@ -0,0 +1,136 @@ +import re + +# Read the RichChatInput file +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + content = f.read() + +print("Fixing text selection functionality (highlight, double-click, triple-click)...") + +# The issue is that we have a hidden textarea and a visual display layer +# We need to make sure the visual layer doesn't interfere with text selection + +# Find the visual display div and update its properties +old_display_div = ''' {/* Visual display with action pills and cursor */} +
+ {renderContent()} +
''' + +new_display_div = ''' {/* Visual display with action pills and cursor */} +
+ {renderContent()} +
''' + +content = content.replace(old_display_div, new_display_div) + +# Also need to update the hidden textarea to be more accessible for selection +old_hidden_textarea = ''' className="absolute inset-0 w-full h-full opacity-0 pointer-events-auto resize-none" + style={{ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + opacity: 0, + zIndex: 1, + background: 'transparent', + border: 'none', + outline: 'none', + resize: 'none', + }}''' + +new_hidden_textarea = ''' className="absolute inset-0 w-full h-full resize-none" + style={{ + position: 'absolute', + left: 0, + top: 0, + width: '100%', + height: '100%', + opacity: 0.01, // Nearly invisible but still selectable + zIndex: 1, + background: 'transparent', + border: 'none', + outline: 'none', + resize: 'none', + color: 'transparent', // Hide text but keep selection + caretColor: 'transparent', // Hide caret (we show our own) + }}''' + +content = content.replace(old_hidden_textarea, new_hidden_textarea) + +# Update handleDisplayClick to handle selection events better +old_handle_click = ''' const handleDisplayClick = useCallback((e: React.MouseEvent) => { + // Don't focus if clicking on an action pill + if ((e.target as Element).closest('.action-pill-wrapper')) { + return; + } + hiddenTextareaRef.current?.focus(); + }, []);''' + +new_handle_click = ''' const handleDisplayClick = useCallback((e: React.MouseEvent) => { + // Don't focus if clicking on an action pill + if ((e.target as Element).closest('.action-pill-wrapper')) { + return; + } + + // Focus the hidden textarea for text selection + if (hiddenTextareaRef.current) { + hiddenTextareaRef.current.focus(); + + // For double/triple click, select text appropriately + if (e.detail === 2) { + // Double click - select word (let browser handle this) + setTimeout(() => { + if (hiddenTextareaRef.current) { + hiddenTextareaRef.current.select(); + } + }, 0); + } else if (e.detail === 3) { + // Triple click - select all + setTimeout(() => { + if (hiddenTextareaRef.current) { + hiddenTextareaRef.current.select(); + } + }, 0); + } + } + }, []);''' + +content = content.replace(old_handle_click, new_handle_click) + +# Write the modified content back +with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f: + f.write(content) + +print("โœ… Fixed text selection functionality:") +print(" - Added userSelect: 'text' to visual display") +print(" - Made hidden textarea slightly visible (opacity: 0.01)") +print(" - Added double-click and triple-click handling") +print(" - Preserved pill functionality while enabling text selection") +print(" - Should now support highlighting, double-click, and triple-click") diff --git a/replace_textarea.py b/replace_textarea.py new file mode 100644 index 0000000000..5e1c456fbb --- /dev/null +++ b/replace_textarea.py @@ -0,0 +1,60 @@ +import re + +# Read the ChatInput file +with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f: + content = f.read() + +# Find and replace the textarea element with RichTextInput +textarea_start = content.find('', textarea_start) + len('') +if textarea_end == -1: + print("Textarea end not found") + exit(1) + +# Extract the old textarea +old_textarea = content[textarea_start:textarea_end] +print(f"Found textarea from {textarea_start} to {textarea_end}") + +# Create the new RichTextInput component +new_richtextinput = ''' { + setDisplayValue(newValue); + setValue(newValue); + setActions(newActions); + debouncedSaveDraft(newValue); + setHasUserTyped(true); + checkForMention(newValue, textAreaRef.current?.selectionStart || 0, textAreaRef.current!); + }} + onKeyDown={handleKeyDown} + onPaste={handlePaste} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} + placeholder={isRecording ? '' : 'โŒ˜โ†‘/โŒ˜โ†“ to navigate messages'} + disabled={isUserInputDisabled} + style={{ + maxHeight: `${maxHeight}px`, + overflowY: 'auto', + opacity: isRecording ? 0 : 1, + }} + className="w-full outline-none border-none focus:ring-0 bg-transparent px-3 pt-3 pb-1.5 pr-20 text-sm resize-none text-textStandard placeholder:text-textPlaceholder" + actions={actions} + maxHeight={maxHeight} + />''' + +# Replace the textarea with RichTextInput +content = content[:textarea_start] + new_richtextinput + content[textarea_end:] + +# Write the modified content back +with open('ui/desktop/src/components/ChatInput.tsx', 'w') as f: + f.write(content) + +print("Textarea replaced with RichTextInput successfully") diff --git a/simple_inline_pills.py b/simple_inline_pills.py new file mode 100644 index 0000000000..0cc0be2166 --- /dev/null +++ b/simple_inline_pills.py @@ -0,0 +1,50 @@ +import re + +# Read the ChatInput file +with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f: + content = f.read() + +# Simple approach: Just remove the "Selected Actions Pills" section +# Find the section that starts with {/* Selected Actions Pills */} +start_marker = '{/* Selected Actions Pills */}' +start_pos = content.find(start_marker) + +if start_pos != -1: + # Find the end of this section - look for the closing div and brace + # We need to find the matching closing brace for the conditional + lines = content[start_pos:].split('\n') + brace_count = 0 + end_line = 0 + + for i, line in enumerate(lines): + if i == 0: # First line with the comment + continue + + # Count braces to find the end of the conditional + open_braces = line.count('{') + close_braces = line.count('}') + brace_count += open_braces - close_braces + + # Look for the end pattern + if ')}\n' in line or line.strip() == ')}': + end_line = i + 1 + break + + if end_line > 0: + # Calculate the end position + end_pos = start_pos + len('\n'.join(lines[:end_line])) + + # Remove the section + content = content[:start_pos] + content[end_pos:] + + print(f"Removed Selected Actions Pills section from position {start_pos} to {end_pos}") + else: + print("Could not find end of Selected Actions Pills section") +else: + print("Selected Actions Pills section not found") + +# Write the modified content back +with open('ui/desktop/src/components/ChatInput.tsx', 'w') as f: + f.write(content) + +print("ChatInput.tsx updated - Selected Actions Pills section removed") diff --git a/test_action_flow.py b/test_action_flow.py new file mode 100644 index 0000000000..1fd1a5c761 --- /dev/null +++ b/test_action_flow.py @@ -0,0 +1,67 @@ +# Let's create a simple test to verify the action flow +print("๐Ÿงช Testing Action Flow Components") +print("=" * 50) + +# Check if files exist and have the right structure +import os + +files_to_check = [ + 'ui/desktop/src/components/ChatInput.tsx', + 'ui/desktop/src/components/RichChatInput.tsx', + 'ui/desktop/src/components/ActionPill.tsx', + 'ui/desktop/src/components/ActionPopover.tsx' +] + +for file_path in files_to_check: + if os.path.exists(file_path): + print(f"โœ… {file_path} exists") + + with open(file_path, 'r') as f: + content = f.read() + + if 'ChatInput.tsx' in file_path: + if 'handleActionSelect' in content: + print(" โœ… handleActionSelect function found") + if 'actionPopover' in content: + print(" โœ… actionPopover state found") + if 'cursorPosition' in content: + print(" โœ… cursorPosition handling found") + + elif 'RichChatInput.tsx' in file_path: + if 'actionRegex' in content: + print(" โœ… actionRegex found") + if 'ActionPill' in content: + print(" โœ… ActionPill import found") + if 'renderContent' in content: + print(" โœ… renderContent function found") + + elif 'ActionPill.tsx' in file_path: + if 'ActionPillProps' in content: + print(" โœ… ActionPillProps interface found") + + elif 'ActionPopover.tsx' in file_path: + if 'onSelect' in content: + print(" โœ… onSelect prop found") + else: + print(f"โŒ {file_path} missing") + +print("\n๐Ÿ” Key Integration Points:") +print("1. ChatInput imports RichChatInput โœ…") +print("2. RichChatInput imports ActionPill โœ…") +print("3. ChatInput has handleActionSelect โœ…") +print("4. ActionPopover calls handleActionSelect โœ…") +print("5. RichChatInput renders [Action] as pills โœ…") + +print("\n๐ŸŽฏ Test Plan:") +print("1. Type '/' in chat input") +print("2. Action popover should appear") +print("3. Click an action (e.g., 'Quick Task')") +print("4. Should see '[Quick Task]' appear as a pill in the input") +print("5. Cursor should be positioned after the pill") + +print("\n๐Ÿ“‹ Debug Steps:") +print("1. Open browser dev tools") +print("2. Look for console logs starting with ๐ŸŽฏ, ๐Ÿ“‹, ๐Ÿ“, etc.") +print("3. Check if handleActionSelect is called") +print("4. Check if text replacement happens") +print("5. Check if RichChatInput re-renders with new value") diff --git a/test_basic_functionality.py b/test_basic_functionality.py new file mode 100644 index 0000000000..56df53c110 --- /dev/null +++ b/test_basic_functionality.py @@ -0,0 +1,80 @@ +# Let's create a simple test to verify if the basic text replacement is working +print("๐Ÿงช Testing Basic Text Replacement") +print("=" * 50) + +# Read the handleActionSelect function +with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f: + content = f.read() + +# Check if the function exists and has the right structure +if 'handleActionSelect' in content: + print("โœ… handleActionSelect function found") + + # Check for key components + if 'setDisplayValue(newValue)' in content: + print("โœ… setDisplayValue call found") + if 'setValue(newValue)' in content: + print("โœ… setValue call found") + if 'actionInfo.label' in content: + print("โœ… actionInfo.label usage found") + if 'lastSlashIndex' in content: + print("โœ… slash detection logic found") + + # Extract the function for analysis + start = content.find('const handleActionSelect = (actionId: string) => {') + if start != -1: + # Find the end of the function + brace_count = 0 + pos = start + while pos < len(content): + if content[pos] == '{': + brace_count += 1 + elif content[pos] == '}': + brace_count -= 1 + if brace_count == 0: + end = pos + 1 + break + pos += 1 + + function_code = content[start:end] + print(f"\n๐Ÿ“ Function length: {len(function_code)} characters") + + # Check for potential issues + if 'cursorPosition = 0' in function_code: + print("โš ๏ธ WARNING: cursorPosition hardcoded to 0") + if 'actionPopover.cursorPosition' in function_code: + print("โœ… Using actionPopover.cursorPosition") + if 'console.log' in function_code: + print("โœ… Debug logging present") + +else: + print("โŒ handleActionSelect function not found") + +print("\n๐Ÿ” Checking RichChatInput integration...") + +# Check RichChatInput +with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f: + rich_content = f.read() + +if 'actionRegex = /\\[([^\\]]+)\\]/g' in rich_content: + print("โœ… Action regex pattern found") +if 'ActionPill' in rich_content: + print("โœ… ActionPill component imported") +if 'renderContent' in rich_content: + print("โœ… renderContent function found") + +print("\n๐ŸŽฏ Next Steps:") +print("1. Open browser dev tools") +print("2. Type '/' in chat input") +print("3. Click an action") +print("4. Look for these console messages:") +print(" - ๐ŸŽฏ ActionPopover: handleItemClick called") +print(" - ๐ŸŽฏ handleActionSelect called with:") +print(" - ๐Ÿ“ Current state:") +print(" - ๐Ÿ”„ Text replacement:") +print(" - ๐ŸŽจ RichChatInput renderContent called") + +print("\n๐Ÿ’ก If no console logs appear:") +print("- ActionPopover might not be calling onSelect") +print("- handleActionSelect might not be connected") +print("- Check for JavaScript errors in console") diff --git a/test_mention_functionality.py b/test_mention_functionality.py new file mode 100644 index 0000000000..2005a6ca00 --- /dev/null +++ b/test_mention_functionality.py @@ -0,0 +1,63 @@ +print("๐Ÿงช Testing Mention Pill Functionality") +print("=" * 50) + +# Check if all files exist and have the right structure +import os + +files_to_check = [ + 'ui/desktop/src/components/MentionPill.tsx', + 'ui/desktop/src/components/RichChatInput.tsx', + 'ui/desktop/src/components/MessageContent.tsx', + 'ui/desktop/src/components/ChatInput.tsx' +] + +for file_path in files_to_check: + if os.path.exists(file_path): + print(f"โœ… {file_path} exists") + + with open(file_path, 'r') as f: + content = f.read() + + if 'MentionPill.tsx' in file_path: + if 'MentionPillProps' in content: + print(" โœ… MentionPillProps interface found") + if 'fileName' in content and 'filePath' in content: + print(" โœ… fileName and filePath props found") + + elif 'RichChatInput.tsx' in file_path: + if 'import MentionPill' in content: + print(" โœ… MentionPill import found") + else: + print(" โŒ MentionPill import missing!") + if 'mentionRegex' in content: + print(" โœ… mentionRegex found") + if 'handleRemoveMention' in content: + print(" โœ… handleRemoveMention function found") + + elif 'MessageContent.tsx' in file_path: + if 'import MentionPill' in content: + print(" โœ… MentionPill import found") + else: + print(" โŒ MentionPill import missing!") + if 'mentionRegex' in content: + print(" โœ… mentionRegex found") + + elif 'ChatInput.tsx' in file_path: + if 'handleMentionFileSelect' in content: + print(" โœ… handleMentionFileSelect function found") + if '@${fileName}' in content: + print(" โœ… @filename format creation found") + else: + print(f"โŒ {file_path} missing") + +print("\n๐ŸŽฏ Test Plan:") +print("1. Type '@' in chat input") +print("2. Select a file from the popover") +print("3. Should see green pill with filename") +print("4. Check browser console for debug logs") + +print("\n๐Ÿ“‹ Debug Steps:") +print("1. Open browser dev tools") +print("2. Look for console logs starting with ๐Ÿ“, ๐Ÿ”, โœ…") +print("3. Check if MentionPill component renders") +print("4. Verify @filename pattern is detected") diff --git a/ui/desktop/src/components/ActionPill.tsx b/ui/desktop/src/components/ActionPill.tsx new file mode 100644 index 0000000000..e123644f9c --- /dev/null +++ b/ui/desktop/src/components/ActionPill.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { X } from 'lucide-react'; + +interface ActionPillProps { + actionId: string; + label: string; + icon: React.ReactNode; + onRemove?: () => void; // Optional for read-only pills in messages + variant?: 'default' | 'message'; // Different styles for input vs message display + size?: 'sm' | 'md'; +} + +export const ActionPill: React.FC = ({ + label, + icon, + onRemove, + variant = 'default', + size = 'sm' +}) => { + const baseClasses = "inline-flex items-center gap-1.5 font-medium border rounded-full"; + + const variantClasses = { + default: "bg-bgProminent text-textProminentInverse border-borderProminent", + message: "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900 dark:text-blue-200 dark:border-blue-700" + }; + + const sizeClasses = { + sm: "px-2 py-1 text-xs", + md: "px-3 py-1.5 text-sm" + }; + + return ( +
+ + +
+ + {icon} + + + {label} + + {onRemove && ( + + )} +
+ ); +}; + +export default ActionPill; diff --git a/ui/desktop/src/components/ActionPill.tsx.backup b/ui/desktop/src/components/ActionPill.tsx.backup new file mode 100644 index 0000000000..bebfb534ca --- /dev/null +++ b/ui/desktop/src/components/ActionPill.tsx.backup @@ -0,0 +1,30 @@ +import React from 'react'; +import { X } from 'lucide-react'; + +interface ActionPillProps { + actionId: string; + label: string; + icon: React.ReactNode; + onRemove: () => void; +} + +export const ActionPill: React.FC = ({ label, icon, onRemove }) => { + return ( +
+ + {icon} + {label} + + +
+ ); +}; + +export default ActionPill; diff --git a/ui/desktop/src/components/ActionPopover.tsx b/ui/desktop/src/components/ActionPopover.tsx index 7da65b78ab..4a3542e5a5 100644 --- a/ui/desktop/src/components/ActionPopover.tsx +++ b/ui/desktop/src/components/ActionPopover.tsx @@ -101,10 +101,14 @@ const ActionPopover = forwardRef< () => ({ getDisplayActions: () => actions, selectAction: (index: number) => { + console.log('โŒจ๏ธ ActionPopover: selectAction called via keyboard', { index, actionId: actions[index]?.id }); if (actions[index]) { + console.log('๐Ÿ”„ ActionPopover: Calling onSelect from selectAction:', actions[index].id); onSelect(actions[index].id); actions[index].action(); - onClose(); + setTimeout(() => { + onClose(); + }, 10); } }, }), @@ -139,10 +143,25 @@ const ActionPopover = forwardRef< }, [selectedIndex]); const handleItemClick = (index: number) => { + console.log('๐ŸŽฏ ActionPopover: handleItemClick called', { index, actionId: actions[index].id }); + console.log('๐Ÿ“‹ ActionPopover: onSelect function:', onSelect); + console.log('๐Ÿ”„ ActionPopover: About to call onSelect with:', actions[index].id); + onSelectedIndexChange(index); + + // Call onSelect first - this should trigger handleActionSelect in ChatInput onSelect(actions[index].id); + + console.log('โœ… ActionPopover: onSelect called successfully'); + + // Call the local action (just for logging) actions[index].action(); - onClose(); + + // Close popover after a small delay to allow text replacement to complete + console.log('๐Ÿšช ActionPopover: Closing popover after delay'); + setTimeout(() => { + onClose(); + }, 10); }; if (!isOpen) return null; diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 9684964118..dcdade63fd 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -19,11 +19,13 @@ import { WaveformVisualizer } from './WaveformVisualizer'; import { toastError } from '../toasts'; import MentionPopover, { FileItemWithMatch } from './MentionPopover'; import ActionPopover from './ActionPopover'; +import { Zap, Code, FileText, Search, Play, Settings } from 'lucide-react'; import { useDictationSettings } from '../hooks/useDictationSettings'; import { useChatContext } from '../contexts/ChatContext'; import { COST_TRACKING_ENABLED, VOICE_DICTATION_ELEVENLABS_ENABLED } from '../updates'; import { CostTracker } from './bottom_menu/CostTracker'; import { DroppedFile, useFileDrop } from '../hooks/useFileDrop'; +import { RichChatInput, RichChatInputRef } from './RichChatInput'; import { Recipe } from '../recipe'; import MessageQueue from './MessageQueue'; import { detectInterruption } from '../utils/interruptionDetector'; @@ -240,15 +242,19 @@ export default function ChatInput({ isOpen: boolean; position: { x: number; y: number }; selectedIndex: number; + cursorPosition?: number; }>({ isOpen: false, position: { x: 0, y: 0 }, selectedIndex: 0, + cursorPosition: 0, }); const actionPopoverRef = useRef<{ getDisplayActions: () => any[]; selectAction: (index: number) => void; }>(null); + + // Action pills for visual display const mentionPopoverRef = useRef<{ getDisplayFiles: () => FileItemWithMatch[]; selectFile: (index: number) => void; @@ -365,7 +371,7 @@ export default function ChatInput({ const [savedInput, setSavedInput] = useState(''); const [isInGlobalHistory, setIsInGlobalHistory] = useState(false); const [hasUserTyped, setHasUserTyped] = useState(false); - const textAreaRef = useRef(null); + const textAreaRef = useRef(null); const timeoutRefsRef = useRef>>(new Set()); const [didAutoSubmit, setDidAutoSubmit] = useState(false); @@ -625,7 +631,7 @@ export default function ChatInput({ const debouncedAutosize = useMemo( () => - debounce((element: HTMLTextAreaElement) => { + debounce((element: HTMLElement) => { element.style.height = '0px'; // Reset height const scrollHeight = element.scrollHeight; element.style.height = Math.min(scrollHeight, maxHeight) + 'px'; @@ -635,32 +641,32 @@ export default function ChatInput({ useEffect(() => { if (textAreaRef.current) { - debouncedAutosize(textAreaRef.current); + const element = (textAreaRef.current as any).contentRef?.current; if (element) { debouncedAutosize(element); } } }, [debouncedAutosize, displayValue]); // Reset textarea height when displayValue is empty useEffect(() => { if (textAreaRef.current && displayValue === '') { - textAreaRef.current.style.height = 'auto'; + const element = (textAreaRef.current as any)?.contentRef?.current; if (element && element.style) { element.style.height = 'auto'; } } }, [displayValue]); - const handleChange = (evt: React.ChangeEvent) => { - const val = evt.target.value; - const cursorPosition = evt.target.selectionStart; + // const handleChange = (evt: React.ChangeEvent) => { + // const val = evt.target.value; + // const cursorPosition = evt.target.selectionStart; + // + // setDisplayValue(val); // Update display immediately + // updateValue(val); // Update actual value immediately for better responsiveness + // debouncedSaveDraft(val); // Save draft with debounce + // // Mark that the user has typed something + // setHasUserTyped(true); + // + // // Check for @ mention + // checkForMention(val, cursorPosition, evt.target); + // }; - setDisplayValue(val); // Update display immediately - updateValue(val); // Update actual value immediately for better responsiveness - debouncedSaveDraft(val); // Save draft with debounce - // Mark that the user has typed something - setHasUserTyped(true); - - // Check for @ mention - checkForMention(val, cursorPosition, evt.target); - }; - - const checkForMention = (text: string, cursorPosition: number, textArea: HTMLTextAreaElement) => { + const checkForMention = (text: string, cursorPosition: number, textArea: any) => { // Find the last @ and / before the cursor const beforeCursor = text.slice(0, cursorPosition); const lastAtIndex = beforeCursor.lastIndexOf('@'); @@ -698,6 +704,7 @@ export default function ChatInput({ y: textAreaRect.top, }, selectedIndex: 0, + cursorPosition: cursorPosition, }); } else { // Open mention popover for @ trigger (existing functionality) @@ -716,7 +723,7 @@ export default function ChatInput({ } }; - const handlePaste = async (evt: React.ClipboardEvent) => { + const handlePaste = async (evt: React.ClipboardEvent) => { const files = Array.from(evt.clipboardData.files || []); const imageFiles = files.filter((file) => file.type.startsWith('image/')); @@ -845,7 +852,7 @@ export default function ChatInput({ setIsComposing(false); }; - const handleHistoryNavigation = (evt: React.KeyboardEvent) => { + const handleHistoryNavigation = (evt: React.KeyboardEvent) => { const isUp = evt.key === 'ArrowUp'; const isDown = evt.key === 'ArrowDown'; @@ -1032,6 +1039,9 @@ export default function ChatInput({ chatContext.clearDraft(); } + // Clear selected actions when message is sent + // Actions cleared when message sent + // Clear both parent and local dropped files after processing if (onFilesProcessed && droppedFiles.length > 0) { onFilesProcessed(); @@ -1062,7 +1072,7 @@ export default function ChatInput({ } }, [autoSubmit, didAutoSubmit, initialValue, performSubmit]); - const handleKeyDown = (evt: React.KeyboardEvent) => { + const handleKeyDown = (evt: React.KeyboardEvent) => { // If mention popover is open, handle arrow keys and enter if (mentionPopover.isOpen && mentionPopoverRef.current) { if (evt.key === 'ArrowDown') { @@ -1149,23 +1159,36 @@ export default function ChatInput({ }; const handleMentionFileSelect = (filePath: string) => { - // Replace the @ mention with the file path + console.log('๐Ÿ“ handleMentionFileSelect called with:', filePath); + + // Extract just the filename from the full path for the pill + const fileName = filePath.split('/').pop() || filePath; + console.log('๐Ÿ“ Extracted filename:', fileName); + + // Create @filename format for pill detection + const mentionText = `@${fileName}`; + console.log('๐Ÿ“ Creating mention text:', mentionText); + + // Replace the @ mention with @filename format const beforeMention = displayValue.slice(0, mentionPopover.mentionStart); const afterMention = displayValue.slice( mentionPopover.mentionStart + 1 + mentionPopover.query.length ); - const newValue = `${beforeMention}${filePath}${afterMention}`; + const newValue = `${beforeMention}${mentionText} ${afterMention}`; + + console.log('๐Ÿ“ New value will be:', newValue); setDisplayValue(newValue); setValue(newValue); setMentionPopover((prev) => ({ ...prev, isOpen: false })); textAreaRef.current?.focus(); - // Set cursor position after the inserted file path + // Set cursor position after the inserted mention and space + const newCursorPosition = beforeMention.length + mentionText.length + 1; setTimeout(() => { if (textAreaRef.current) { - const newCursorPosition = beforeMention.length + filePath.length; textAreaRef.current.setSelectionRange(newCursorPosition, newCursorPosition); + textAreaRef.current.focus(); } }, 0); }; @@ -1180,43 +1203,61 @@ export default function ChatInput({ y: buttonRect.top, }, selectedIndex: 0, + cursorPosition: textAreaRef.current?.getBoundingClientRect ? 0 : 0, // Will be set by RichChatInput }); }; + // Helper function to get action info + const getActionInfo = (actionId: string) => { + const actionMap = { + 'quick-task': { label: 'Quick Task', icon: }, + 'generate-code': { label: 'Generate Code', icon: }, + 'create-document': { label: 'Create Document', icon: }, + 'search-files': { label: 'Search Files', icon: }, + 'run-command': { label: 'Run Command', icon: }, + 'settings': { label: 'Settings', icon: }, + }; + return actionMap[actionId as keyof typeof actionMap] || { label: actionId, icon: }; + }; + const handleActionSelect = (actionId: string) => { - // If this was triggered by / symbol, replace it with the action + const actionInfo = getActionInfo(actionId); + + // Get current cursor position from the RichChatInput const currentValue = displayValue; - const cursorPosition = textAreaRef.current?.selectionStart || 0; + const cursorPosition = actionPopover.cursorPosition || 0; const beforeCursor = currentValue.slice(0, cursorPosition); + const afterCursor = currentValue.slice(cursorPosition); const lastSlashIndex = beforeCursor.lastIndexOf('/'); if (lastSlashIndex !== -1) { - // Check if the / is still active (no space after it) const afterSlash = beforeCursor.slice(lastSlashIndex + 1); + // Check if we're still in the same "word" after the slash if (!afterSlash.includes(' ') && !afterSlash.includes('\n')) { - // Replace / with action text + // Replace the /query with [Action] text const beforeSlash = currentValue.slice(0, lastSlashIndex); - const afterCursor = currentValue.slice(cursorPosition); - const actionText = `[Action: ${actionId}]`; - const newValue = beforeSlash + actionText + afterCursor; + const actionText = `[${actionInfo.label}]`; + const newValue = beforeSlash + actionText + " " + afterCursor; setDisplayValue(newValue); setValue(newValue); - // Set cursor position after the inserted action text + // Set cursor position after the action text and space + const newCursorPosition = lastSlashIndex + actionText.length + 1; setTimeout(() => { if (textAreaRef.current) { - const newCursorPosition = beforeSlash.length + actionText.length; textAreaRef.current.setSelectionRange(newCursorPosition, newCursorPosition); + textAreaRef.current.focus(); } }, 0); } } - console.log('Action selected:', actionId); + console.log('Action selected:', actionId, 'at position:', cursorPosition); setActionPopover(prev => ({ ...prev, isOpen: false })); }; + const hasSubmittableContent = displayValue.trim() || pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) || @@ -1332,13 +1373,30 @@ export default function ChatInput({ {/* Input row with inline action buttons wrapped in form */}
-