Clean up: Remove debug Python scripts and console.log statements

- Removed all temporary Python debugging scripts from root directory
- Cleaned up debug console.log statements from ChatInput.tsx and RichChatInput.tsx
- Kept only the core implementation files for the rich text input feature
This commit is contained in:
spencrmartin
2025-09-24 21:15:02 -04:00
committed by Alex Hancock
parent f9cbf70f70
commit 3674e2bd0c
53 changed files with 80 additions and 3815 deletions
-62
View File
@@ -1,62 +0,0 @@
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")
-48
View File
@@ -1,48 +0,0 @@
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")
-84
View File
@@ -1,84 +0,0 @@
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}")
-27
View File
@@ -1,27 +0,0 @@
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")
-105
View File
@@ -1,105 +0,0 @@
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 (
<div className="relative">'''
new_wrapper = ''' return (
<div className="relative rich-text-input">'''
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")
-181
View File
@@ -1,181 +0,0 @@
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(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-text-default inline-block align-baseline" style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }} />
);
}
textWithCursor.push(beforeAction[i]);
currentPos++;
}
parts.push(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{textWithCursor}
</span>
);
}
// Find matching action
const actionEntry = Object.entries(ACTION_MAP).find(
([_, config]) => config.label === actionLabel
);
if (actionEntry) {
const [actionId, config] = actionEntry;
parts.push(
<ActionPill
key={`action-${keyCounter++}`}
actionId={actionId}
label={config.label}
icon={config.icon}
variant="default"
size="sm"
onRemove={() => handleRemoveAction(actionLabel)}
/>
);
} else {
// If no matching action, render as text
parts.push(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{match[0]}
</span>
);
}
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(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-text-default inline-block align-baseline" style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }} />
);
}
textWithCursor.push(beforeMatch[i]);
currentPos++;
}
parts.push(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{textWithCursor}
</span>
);
}
// Add cursor before match if needed
if (isFocused && cursorPosition === currentPos) {
parts.push(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-text-default inline-block align-baseline" style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }} />
);
}
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(
<ActionPill
key={`action-${keyCounter++}`}
actionId={actionId}
label={config.label}
icon={config.icon}
variant="default"
size="sm"
onRemove={() => handleRemoveAction(actionLabel)}
/>
);
} else {
// If no matching action, render as text
parts.push(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{match[0]}
</span>
);
}
} else if (type === 'mention') {
// Handle mention pills
const fileName = content; // filename without @
const filePath = `@${fileName}`; // full mention text
parts.push(
<MentionPill
key={`mention-${keyCounter++}`}
fileName={fileName}
filePath={filePath}
variant="default"
size="sm"
onRemove={() => 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")
-98
View File
@@ -1,98 +0,0 @@
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")
-67
View File
@@ -1,67 +0,0 @@
# 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<MentionPillProps> = ({
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 (
<div
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`}
title={filePath} // Show full path on hover
>
<span className="flex items-center gap-1">
<File size={12} />
{fileName}
</span>
{onRemove && (
<button
type="button"
onClick={onRemove}
className="flex items-center justify-center w-4 h-4 rounded-full hover:bg-white/20 transition-colors"
aria-label={`Remove ${fileName} mention`}
>
<X size={10} />
</button>
)}
</div>
);
};
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")
-75
View File
@@ -1,75 +0,0 @@
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")
-61
View File
@@ -1,61 +0,0 @@
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")
-97
View File
@@ -1,97 +0,0 @@
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: <Zap size={12} /> };
parts.push(
<ActionPill
key={`action-${keyCounter++}`}
actionId={actionLabel.toLowerCase().replace(/\\s+/g, '-')}
label={actionLabel}
icon={actionInfo.icon}
variant="default"
size="sm"
onRemove={() => 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: <Zap size={12} /> };
console.log('🏷️ Creating action pill:', { actionLabel, actionInfo });
parts.push(
<ActionPill
key={`action-${keyCounter++}`}
actionId={actionLabel.toLowerCase().replace(/\\s+/g, '-')}
label={actionLabel}
icon={actionInfo.icon}
variant="default"
size="sm"
onRemove={() => 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<HTMLTextAreaElement>) => {
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<HTMLTextAreaElement>) => {
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")
-80
View File
@@ -1,80 +0,0 @@
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")
-81
View File
@@ -1,81 +0,0 @@
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")
-90
View File
@@ -1,90 +0,0 @@
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")
-106
View File
@@ -1,106 +0,0 @@
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<Array<\{\s*id: string;\s*actionId: string;\s*label: string;\s*icon: React\.ReactNode;\s*\}>>.*?\[\]\);'
content = re.sub(
selectedactions_pattern,
' // Action pills for visual display\n const [actions, setActions] = useState<ActionData[]>([]);',
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*<div className="flex flex-wrap items-center gap-2 px-3 pt-2 pb-1 bg-bgSubtle\/20 border-b border-borderSubtle\/50">\s*<span className="text-xs text-textSubtle font-medium">Selected:<\/span>\s*\{selectedActions\.map\(\(action\) => \(\s*<ActionPill\s*key=\{action\.id\}\s*actionId=\{action\.actionId\}\s*label=\{action\.label\}\s*icon=\{action\.icon\}\s*onRemove=\{\(\) => 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)
-34
View File
@@ -1,34 +0,0 @@
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")
-34
View File
@@ -1,34 +0,0 @@
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 = '<span className="animate-pulse border-l border-textStandard h-4 inline-block" />'
new_cursor_2 = '<span className="animate-pulse border-l border-textSubtle h-4 inline-block" />'
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)")
-29
View File
@@ -1,29 +0,0 @@
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 = '<span className="animate-blink border-l-2 border-black h-4 inline-block" />'
new_cursor_2 = '<span className="border-l-2 border-black h-4 inline-block" style={{ animation: "blink 1s step-end infinite" }} />'
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!")
-112
View File
@@ -1,112 +0,0 @@
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(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{textParts}
</span>
);
}'''
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(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-black inline-block" style={{ animation: "blink 1s step-end infinite", height: "1.2em", marginLeft: "1px" }} />
);
}
textWithCursor.push(beforeAction[i]);
currentPos++;
}
parts.push(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{textWithCursor}
</span>
);
}'''
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(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{textParts}
</span>
);
}'''
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(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-black inline-block" style={{ animation: "blink 1s step-end infinite", height: "1.2em", marginLeft: "1px" }} />
);
}
textWithCursor.push(remainingText[i]);
currentPos++;
}
parts.push(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{textWithCursor}
</span>
);
}'''
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(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-black inline-block" style={{ animation: "blink 1s step-end infinite", height: "1.2em", marginLeft: "1px" }} />
);
}''')
# 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")
-48
View File
@@ -1,48 +0,0 @@
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 (
<div className="relative min-h-[1.5em] flex items-center">
<span className="text-textSubtle pointer-events-none select-none">
{placeholder}
</span>
{isFocused && (
<span className="border-l-2 border-black inline-block align-baseline ml-0.5" style={{ animation: "blink 1s step-end infinite", height: "1em", verticalAlign: "baseline" }} />
)}
</div>
);
}'''
new_placeholder_render = ''' // Show placeholder when there's no text content (regardless of focus state)
if (!value.trim()) {
return (
<div className="relative min-h-[1.5em] flex items-center">
{isFocused && (
<span className="border-l-2 border-black inline-block align-baseline mr-0.5" style={{ animation: "blink 1s step-end infinite", height: "1em", verticalAlign: "baseline" }} />
)}
<span className="text-textSubtle pointer-events-none select-none">
{placeholder}
</span>
</div>
);
}'''
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")
-29
View File
@@ -1,29 +0,0 @@
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 = '<span className="border-l-2 border-black h-4 inline-block" style={{ animation: "blink 1s step-end infinite" }} />'
new_cursor_2 = '<span 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_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")
-33
View File
@@ -1,33 +0,0 @@
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 = '<span className="animate-pulse border-l border-textSubtle h-4 inline-block" />'
new_cursor_2 = '<span className="animate-blink border-l-2 border-black h-4 inline-block" />'
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")
-30
View File
@@ -1,30 +0,0 @@
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")
-33
View File
@@ -1,33 +0,0 @@
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")
-38
View File
@@ -1,38 +0,0 @@
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!")
-87
View File
@@ -1,87 +0,0 @@
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 (
<div className={`flex flex-wrap items-center gap-1 ${className || ''}`}>
{parsedContent.map((part, index) => {
if (part.type === 'action' && part.actionId) {
const actionInfo = getActionInfo(part.actionId);
return (
<ActionPill
key={`action-${index}`}
actionId={part.actionId}
label={part.content}
icon={actionInfo.icon}
variant="message"
size="sm"
// No onRemove for message display - pills are read-only
/>
);
} else if (part.content.trim()) {
return (
<MarkdownContent
key={`text-${index}`}
content={part.content}
className={className}
/>
);
}
return null;
})}
</div>
);'''
new_return = ''' return (
<span className={`inline-flex flex-wrap items-baseline gap-1 ${className || ''}`}>
{parsedContent.map((part, index) => {
if (part.type === 'action' && part.actionId) {
const actionInfo = getActionInfo(part.actionId);
return (
<ActionPill
key={`action-${index}`}
actionId={part.actionId}
label={part.content}
icon={actionInfo.icon}
variant="message"
size="sm"
// No onRemove for message display - pills are read-only
/>
);
} else if (part.content.trim()) {
return (
<span
key={`text-${index}`}
className={`inline ${className || ''}`}
dangerouslySetInnerHTML={{
__html: part.content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>')
.replace(/\n/g, '<br>')
}}
/>
);
}
return null;
})}
</span>
);'''
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 <div> to <span> with inline-flex")
print("2. Replaced MarkdownContent with inline <span> and basic HTML formatting")
print("3. Used items-baseline for better text alignment")
print("4. Applied inline styling to prevent line breaks")
-110
View File
@@ -1,110 +0,0 @@
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")
-78
View File
@@ -1,78 +0,0 @@
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")
-28
View File
@@ -1,28 +0,0 @@
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 (
<span className={`inline-flex flex-wrap items-baseline gap-1 ${className || ''}`}>'''
new_container = ''' return (
<span className={`inline ${className || ''}`}>'''
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")
-48
View File
@@ -1,48 +0,0 @@
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 (
<div className="flex flex-wrap items-center gap-1 min-h-[1.5em]">
{parts.length > 0 ? parts : (
isFocused && (
<span className="border-l-2 border-black inline-block" style={{ animation: "blink 1s step-end infinite", height: "1.2em", marginLeft: "1px" }} />
)
)}
</div>
);'''
new_container = ''' return (
<div className="whitespace-pre-wrap min-h-[1.5em] leading-relaxed">
{parts.length > 0 ? parts : (
isFocused && (
<span className="border-l-2 border-black inline-block" style={{ animation: "blink 1s step-end infinite", height: "1.2em", marginLeft: "1px" }} />
)
)}
</div>
);'''
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")
-45
View File
@@ -1,45 +0,0 @@
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 (
<span className="text-textPlaceholder pointer-events-none select-none">
{placeholder}
</span>
);
}'''
new_placeholder_logic = ''' const renderContent = useCallback(() => {
// Show placeholder when there's no text content (regardless of focus state)
if (!value.trim()) {
return (
<div className="relative">
<span className="text-textSubtle pointer-events-none select-none absolute">
{placeholder}
</span>
{isFocused && (
<span className="border-l-2 border-black inline-block" style={{ animation: "blink 1s step-end infinite", height: "1.2em", marginLeft: "1px" }} />
)}
</div>
);
}'''
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")
-22
View File
@@ -1,22 +0,0 @@
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")
-22
View File
@@ -1,22 +0,0 @@
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")
-49
View File
@@ -1,49 +0,0 @@
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 (
<div className="relative">
<span className="text-textSubtle pointer-events-none select-none absolute">
{placeholder}
</span>
{isFocused && (
<span className="border-l-2 border-black inline-block align-baseline" style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }} />
)}
</div>
);
}'''
new_placeholder_render = ''' // Show placeholder when there's no text content (regardless of focus state)
if (!value.trim()) {
return (
<div className="relative min-h-[1.5em] flex items-center">
<span className="text-textSubtle pointer-events-none select-none">
{placeholder}
</span>
{isFocused && (
<span className="border-l-2 border-black inline-block align-baseline ml-0.5" style={{ animation: "blink 1s step-end infinite", height: "1em", verticalAlign: "baseline" }} />
)}
</div>
);
}'''
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")
-57
View File
@@ -1,57 +0,0 @@
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 (
<div className="relative min-h-[1.5em] flex items-center">
{isFocused && (
<span className="border-l-2 border-black inline-block align-baseline mr-0.5" style={{ animation: "blink 1s step-end infinite", height: "1em", verticalAlign: "baseline" }} />
)}
<span className="text-textSubtle pointer-events-none select-none">
{placeholder}
</span>
</div>
);
}'''
new_placeholder_render = ''' // Show placeholder when there's no text content (regardless of focus state)
if (!value.trim()) {
return (
<div className="relative min-h-[1.5em] flex items-center">
<span className="text-textSubtle pointer-events-none select-none">
{placeholder}
</span>
{isFocused && (
<span
className="border-l-2 border-black inline-block align-baseline absolute left-0"
style={{
animation: "blink 1s step-end infinite",
height: "1em",
verticalAlign: "baseline",
top: "50%",
transform: "translateY(-50%)"
}}
/>
)}
</div>
);
}'''
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")
-34
View File
@@ -1,34 +0,0 @@
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, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>')
.replace(/
/g, '<br>')
}}'''
new_fixed_html = ''' dangerouslySetInnerHTML={{
__html: part.content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>')
.replace(/\\n/g, '<br>')
}}'''
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, '<br>') - newline regex was broken across lines")
-43
View File
@@ -1,43 +0,0 @@
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")
-169
View File
@@ -1,169 +0,0 @@
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(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-text-default inline-block align-baseline" style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }} />
);
}
textWithCursor.push(beforeAction[i]);
currentPos++;
}
parts.push(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{textWithCursor}
</span>
);
}
// Find matching action
const actionEntry = Object.entries(ACTION_MAP).find(
([_, config]) => config.label === actionLabel
);
if (actionEntry) {
const [actionId, config] = actionEntry;
parts.push(
<ActionPill
key={`action-${keyCounter++}`}
actionId={actionId}
label={config.label}
icon={config.icon}
variant="default"
size="sm"
onRemove={() => handleRemoveAction(actionLabel)}
/>
);
} else {
// If no matching action, render as text
parts.push(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{match[0]}
</span>
);
}
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(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-text-default inline-block align-baseline" style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }} />
);
}
textWithCursor.push(beforeMatch[i]);
currentPos++;
}
parts.push(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{textWithCursor}
</span>
);
}
// Add cursor before match if needed
if (isFocused && cursorPosition === currentPos) {
parts.push(
<span key={`cursor-${keyCounter++}`} className="border-l-2 border-text-default inline-block align-baseline" style={{ animation: "blink 1s step-end infinite", height: "1em", marginLeft: "1px", verticalAlign: "baseline" }} />
);
}
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(
<ActionPill
key={`action-${keyCounter++}`}
actionId={actionId}
label={config.label}
icon={config.icon}
variant="default"
size="sm"
onRemove={() => handleRemoveAction(actionLabel)}
/>
);
} else {
// If no matching action, render as text
parts.push(
<span key={`text-${keyCounter++}`} className="inline whitespace-pre-wrap">
{match[0]}
</span>
);
}
} 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(
<MentionPill
key={`mention-${keyCounter++}`}
fileName={fileName}
filePath={filePath}
variant="default"
size="sm"
onRemove={() => 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")
-118
View File
@@ -1,118 +0,0 @@
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 */}
<div
ref={displayRef}
className={`${className} cursor-text relative`}
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',
}}
role="textbox"
aria-multiline="true"
aria-placeholder={placeholder}
>
{renderContent()}
</div>'''
new_visual_display = ''' {/* Visual display with action pills and cursor */}
<div
ref={displayRef}
className={`${className} cursor-text relative`}
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',
}}
role="textbox"
aria-multiline="true"
aria-placeholder={placeholder}
>
{renderContent()}
</div>'''
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")
-124
View File
@@ -1,124 +0,0 @@
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")
-133
View File
@@ -1,133 +0,0 @@
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 */}
<div
ref={displayRef}
onClick={handleDisplayClick}
onDoubleClick={handleDisplayClick}
className={`${className} cursor-text relative`}
style={{
...style,
minHeight: `${rows * 1.5}em`,
zIndex: 0,
userSelect: 'text', // Allow text selection
WebkitUserSelect: 'text', // Safari support
}}
role="textbox"
aria-multiline="true"
aria-placeholder={placeholder}
>
{renderContent()}
</div>'''
new_visual_display = ''' {/* Visual display with action pills and cursor */}
<div
ref={displayRef}
className={`${className} cursor-text relative`}
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',
}}
role="textbox"
aria-multiline="true"
aria-placeholder={placeholder}
>
{renderContent()}
</div>'''
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")
-136
View File
@@ -1,136 +0,0 @@
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 */}
<div
ref={displayRef}
onClick={handleDisplayClick}
className={`${className} cursor-text relative`}
style={{
...style,
minHeight: `${rows * 1.5}em`,
zIndex: 0,
}}
role="textbox"
aria-multiline="true"
aria-placeholder={placeholder}
>
{renderContent()}
</div>'''
new_display_div = ''' {/* Visual display with action pills and cursor */}
<div
ref={displayRef}
onClick={handleDisplayClick}
onDoubleClick={handleDisplayClick}
className={`${className} cursor-text relative`}
style={{
...style,
minHeight: `${rows * 1.5}em`,
zIndex: 0,
userSelect: 'text', // Allow text selection
WebkitUserSelect: 'text', // Safari support
}}
role="textbox"
aria-multiline="true"
aria-placeholder={placeholder}
>
{renderContent()}
</div>'''
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")
+80
View File
@@ -0,0 +1,80 @@
# Rich Text Input with Action and Mention Pills
This PR introduces a sophisticated rich text input system that replaces plain text with visual pills for actions and file mentions, creating a more intuitive and modern chat interface.
## 🎯 Features
### Visual Pills
- **Action Pills**: Blue circles with white icons for `/` commands (e.g., `[Quick Task]`)
- **Mention Pills**: Blue diamonds for `@` file references (e.g., `@filename.txt`)
- **Consistent Styling**: White pill backgrounds with blue accent icons
- **Theme Support**: Adapts to light/dark modes
### User Experience
- **Natural Text Flow**: Pills display inline with text content
- **Text Selection**: Full support for click/drag, double-click, triple-click
- **Backspace Removal**: Delete entire pills as single units
- **Proper Cursor**: Black blinking cursor with correct positioning
- **Multi-line Support**: Cursor stays aligned on line breaks
### 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
## 🔧 Technical Implementation
### Architecture
- **Hidden Textarea**: Handles actual input and selection
- **Visual Display**: Renders pills and cursor overlay
- **Perfect Alignment**: Synchronized font, padding, and positioning
- **Event Handling**: Proper mouse/keyboard event delegation
### Pattern Matching
- **Actions**: `/action` → `[Action Name]` → Blue circle pill
- **Mentions**: `@filename` → Blue diamond pill
- **Mixed Content**: Supports both types inline with text
### Accessibility
- **Keyboard Navigation**: Full keyboard support maintained
- **Screen Readers**: Proper ARIA attributes and roles
- **Selection**: Standard text selection behaviors preserved
## 🎨 Visual Design
### Before
```
Hello /quick-task and @file.txt world
```
### After
```
Hello [Quick Task] and @file.txt world
↑ blue circle ↑ blue diamond
```
### Message History
Pills appear in sent messages maintaining the same visual style and inline flow.
## 🧪 Testing
Tested extensively for:
- ✅ Action pill creation via `/` command
- ✅ File mention pills via `@` command
- ✅ Text selection and highlighting
- ✅ Cursor positioning and movement
- ✅ Backspace pill removal
- ✅ Multi-line text handling
- ✅ Light/dark theme compatibility
- ✅ Message history display
## 🚀 Impact
This creates a modern chat interface similar to Slack/Discord with:
- **Improved UX**: Visual pills are easier to understand than plain text
- **Better Organization**: Clear distinction between actions and file references
- **Enhanced Workflow**: Faster recognition of commands and mentions
- **Professional Appearance**: Polished, modern interface design
The implementation maintains full backward compatibility while significantly enhancing the user experience.
-60
View File
@@ -1,60 +0,0 @@
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')
if textarea_start == -1:
print("Textarea not found")
exit(1)
# Find the end of the textarea element
textarea_end = content.find('</textarea>', textarea_start) + len('</textarea>')
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 = '''<RichTextInput
ref={textAreaRef}
value={displayValue}
onChange={(newValue, newActions) => {
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")
-50
View File
@@ -1,50 +0,0 @@
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")
-67
View File
@@ -1,67 +0,0 @@
# 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")
-80
View File
@@ -1,80 +0,0 @@
# 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")
-63
View File
@@ -1,63 +0,0 @@
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")
-67
View File
@@ -1,67 +0,0 @@
import re
# Read the ChatInput file
with open('ui/desktop/src/components/ChatInput.tsx', 'r') as f:
content = f.read()
print("Updating ChatInput to convert @filename to pills when selected...")
# Find the handleMentionFileSelect function and update it to create pill format
old_mention_select = ''' const handleMentionFileSelect = (filePath: string) => {
const beforeMention = displayValue.slice(0, mentionPopover.mentionStart);
const afterMention = displayValue.slice(
textAreaRef.current?.selectionStart || displayValue.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_mention_select = ''' const handleMentionFileSelect = (filePath: string) => {
const beforeMention = displayValue.slice(0, mentionPopover.mentionStart);
const afterMention = displayValue.slice(
textAreaRef.current?.selectionStart || displayValue.length
);
// Extract just the filename from the path for the pill
const fileName = filePath.split('/').pop() || filePath;
const mentionText = `@${fileName}`;
const newValue = `${beforeMention}${mentionText} ${afterMention}`;
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_mention_select, new_mention_select)
# Write the modified content back
with open('ui/desktop/src/components/ChatInput.tsx', 'w') as f:
f.write(content)
print("✅ Updated ChatInput mention handling:")
print(" - Modified handleMentionFileSelect to create @filename format")
print(" - Extracts filename from full path for cleaner pills")
print(" - Adds space after mention for better UX")
print(" - Sets cursor position properly after insertion")
-31
View File
@@ -1,31 +0,0 @@
import re
# Read the MentionPill file
with open('ui/desktop/src/components/MentionPill.tsx', 'r') as f:
content = f.read()
print("Updating MentionPill styling to match ActionPill (white background, same spacing)...")
# Update the variantClasses to match ActionPill styling
old_variant_classes = ''' 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"
};'''
new_variant_classes = ''' const variantClasses = {
default: "bg-bgProminent text-textProminentInverse border-borderProminent",
message: "bg-bgProminent text-textProminentInverse border-borderProminent"
};'''
content = content.replace(old_variant_classes, new_variant_classes)
# Write the modified content back
with open('ui/desktop/src/components/MentionPill.tsx', 'w') as f:
f.write(content)
print("✅ Updated MentionPill styling:")
print(" - Changed from green colors to theme-aware white background")
print(" - Now uses bg-bgProminent text-textProminentInverse border-borderProminent")
print(" - Same styling as ActionPill (white background, proper contrast)")
print(" - Maintains same spacing and padding (px-2 py-1 text-xs)")
print(" - Both pills now have consistent appearance")
-249
View File
@@ -1,249 +0,0 @@
import re
# Read the MessageContent file
with open('ui/desktop/src/components/MessageContent.tsx', 'r') as f:
content = f.read()
print("Updating MessageContent to support mention pills in sent messages...")
# Add MentionPill import
old_imports = """import React, { useMemo } from 'react';
import MarkdownContent from './MarkdownContent';
import ActionPill from './ActionPill';
import { Zap, Code, FileText, Search, Play, Settings } from 'lucide-react';"""
new_imports = """import React, { useMemo } from 'react';
import MarkdownContent from './MarkdownContent';
import ActionPill from './ActionPill';
import MentionPill from './MentionPill';
import { Zap, Code, FileText, Search, Play, Settings } from 'lucide-react';"""
content = content.replace(old_imports, new_imports)
# Update the parsedContent logic to handle both actions and mentions
old_parsing_logic = ''' const parsedContent = useMemo(() => {
// Find all [Action] patterns and replace them with pill components
const actionRegex = /\[([^\]]+)\]/g;
const parts: Array<{ type: 'text' | 'action'; content: string; actionId?: string }> = [];
let currentIndex = 0;
let match;
while ((match = actionRegex.exec(content)) !== null) {
// Add text before the action
if (match.index > currentIndex) {
parts.push({
type: 'text',
content: content.slice(currentIndex, match.index),
});
}
// Add the action
const actionLabel = match[1];
const actionId = getActionIdFromLabel(actionLabel);
parts.push({
type: 'action',
content: actionLabel,
actionId: actionId,
});
currentIndex = match.index + match[0].length;
}
// Add remaining text
if (currentIndex < content.length) {
parts.push({
type: 'text',
content: content.slice(currentIndex),
});
}
// If no actions found, return the original content as a single text part
if (parts.length === 0) {
parts.push({
type: 'text',
content: content,
});
}
return parts;
}, [content]);'''
new_parsing_logic = ''' const parsedContent = useMemo(() => {
// Find all [Action] and @mention patterns and replace them with pill components
const actionRegex = /\[([^\]]+)\]/g;
const mentionRegex = /@([^\s]+)/g;
const parts: Array<{ type: 'text' | 'action' | 'mention'; content: string; actionId?: string; fileName?: string }> = [];
// Find all matches and sort by position
const allMatches = [];
// Find all action matches
let actionMatch;
actionRegex.lastIndex = 0;
while ((actionMatch = actionRegex.exec(content)) !== null) {
allMatches.push({
type: 'action',
index: actionMatch.index,
length: actionMatch[0].length,
content: actionMatch[1]
});
}
// Find all mention matches
let mentionMatch;
mentionRegex.lastIndex = 0;
while ((mentionMatch = mentionRegex.exec(content)) !== null) {
allMatches.push({
type: 'mention',
index: mentionMatch.index,
length: mentionMatch[0].length,
content: mentionMatch[1] // filename without @
});
}
// Sort matches by position
allMatches.sort((a, b) => a.index - b.index);
let currentIndex = 0;
// Process all matches in order
for (const match of allMatches) {
// Add text before this match
if (match.index > currentIndex) {
parts.push({
type: 'text',
content: content.slice(currentIndex, match.index),
});
}
if (match.type === 'action') {
// Add the action
const actionLabel = match.content;
const actionId = getActionIdFromLabel(actionLabel);
parts.push({
type: 'action',
content: actionLabel,
actionId: actionId,
});
} else if (match.type === 'mention') {
// Add the mention
parts.push({
type: 'mention',
content: match.content, // filename without @
fileName: match.content,
});
}
currentIndex = match.index + match.length;
}
// Add remaining text
if (currentIndex < content.length) {
parts.push({
type: 'text',
content: content.slice(currentIndex),
});
}
// If no matches found, return the original content as a single text part
if (parts.length === 0) {
parts.push({
type: 'text',
content: content,
});
}
return parts;
}, [content]);'''
content = content.replace(old_parsing_logic, new_parsing_logic)
# Update the rendering logic to handle mention pills
old_rendering = ''' {parsedContent.map((part, index) => {
if (part.type === 'action' && part.actionId) {
const actionInfo = getActionInfo(part.actionId);
return (
<ActionPill
key={`action-${index}`}
actionId={part.actionId}
label={part.content}
icon={actionInfo.icon}
variant="message"
size="sm"
// No onRemove for message display - pills are read-only
/>
);
} else if (part.content.trim()) {
return (
<span
key={`text-${index}`}
className={`inline ${className || ''}`}
dangerouslySetInnerHTML={{
__html: part.content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>')
.replace(/\\n/g, '<br>')
}}
/>
);
}
return null;
})}'''
new_rendering = ''' {parsedContent.map((part, index) => {
if (part.type === 'action' && part.actionId) {
const actionInfo = getActionInfo(part.actionId);
return (
<ActionPill
key={`action-${index}`}
actionId={part.actionId}
label={part.content}
icon={actionInfo.icon}
variant="message"
size="sm"
// No onRemove for message display - pills are read-only
/>
);
} else if (part.type === 'mention' && part.fileName) {
return (
<MentionPill
key={`mention-${index}`}
fileName={part.fileName}
filePath={`@${part.fileName}`}
variant="message"
size="sm"
// No onRemove for message display - pills are read-only
/>
);
} else if (part.content.trim()) {
return (
<span
key={`text-${index}`}
className={`inline ${className || ''}`}
dangerouslySetInnerHTML={{
__html: part.content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>')
.replace(/\\n/g, '<br>')
}}
/>
);
}
return null;
})}'''
content = content.replace(old_rendering, new_rendering)
# Write the modified content back
with open('ui/desktop/src/components/MessageContent.tsx', 'w') as f:
f.write(content)
print("✅ Updated MessageContent for mention pills:")
print(" - Added MentionPill import")
print(" - Updated parsing logic to detect both [Action] and @mention patterns")
print(" - Added mention pill rendering in sent messages")
print(" - Mentions will appear as green pills in message history")
-70
View File
@@ -1,70 +0,0 @@
import re
print("Updating pill icons: blue diamond for documents, blue circle for actions...")
# First, update MentionPill to use blue diamond icon
with open('ui/desktop/src/components/MentionPill.tsx', 'r') as f:
mention_content = f.read()
# Replace the File import and icon with a blue diamond
old_mention_imports = "import { X, File } from 'lucide-react';"
new_mention_imports = "import { X, Diamond } from 'lucide-react';"
mention_content = mention_content.replace(old_mention_imports, new_mention_imports)
# Replace the File icon with Diamond icon and add blue color
old_mention_icon = ''' <span className="flex items-center gap-1">
<File size={12} />
{fileName}
</span>'''
new_mention_icon = ''' <span className="flex items-center gap-1">
<Diamond size={12} className="text-blue-500 fill-blue-500" />
{fileName}
</span>'''
mention_content = mention_content.replace(old_mention_icon, new_mention_icon)
# Write the updated MentionPill
with open('ui/desktop/src/components/MentionPill.tsx', 'w') as f:
f.write(mention_content)
print("✅ Updated MentionPill:")
print(" - Changed from File icon to Diamond icon")
print(" - Added blue color (text-blue-500 fill-blue-500)")
# Now update ActionPill to use blue circle icons
with open('ui/desktop/src/components/ActionPill.tsx', 'r') as f:
action_content = f.read()
# We need to modify how the icon is rendered to add blue color
# Find the icon rendering section
old_action_icon = ''' <span className="flex items-center gap-1">
{icon}
{label}
</span>'''
new_action_icon = ''' <span className="flex items-center gap-1">
<span className="relative">
<div className="w-3 h-3 bg-blue-500 rounded-full absolute inset-0" />
<span className="relative text-white text-[8px] flex items-center justify-center w-3 h-3">
{icon}
</span>
</span>
{label}
</span>'''
action_content = action_content.replace(old_action_icon, new_action_icon)
# Write the updated ActionPill
with open('ui/desktop/src/components/ActionPill.tsx', 'w') as f:
f.write(action_content)
print("✅ Updated ActionPill:")
print(" - Added blue circle background for icons")
print(" - Icons now appear as white symbols on blue circles")
print("\n🎯 Result:")
print(" - Document pills: Blue diamond icon + filename")
print(" - Action pills: Blue circle with white action icon + label")
print(" - Both maintain white pill background with consistent styling")
-93
View File
@@ -1,93 +0,0 @@
import re
# Read the RichChatInput file
with open('ui/desktop/src/components/RichChatInput.tsx', 'r') as f:
content = f.read()
print("Updating RichChatInput to support mention pills...")
# Add MentionPill import
old_imports = "import { ActionPill } from './ActionPill';"
new_imports = """import { ActionPill } from './ActionPill';
import MentionPill from './MentionPill';"""
content = content.replace(old_imports, new_imports)
# Update the renderContent function to handle both actions and mentions
# Find the while loop that processes actions and extend it to handle mentions too
old_regex_section = ''' const parts: React.ReactNode[] = [];
const actionRegex = /\[([^\]]+)\]/g;
let lastIndex = 0;
let match;
let keyCounter = 0;
let currentPos = 0; // Track position for cursor placement'''
new_regex_section = ''' const parts: React.ReactNode[] = [];
const actionRegex = /\[([^\]]+)\]/g;
const mentionRegex = /@([^\s]+)/g; // Match @filename patterns
let lastIndex = 0;
let match;
let keyCounter = 0;
let currentPos = 0; // Track position for cursor placement'''
content = content.replace(old_regex_section, new_regex_section)
# Now we need to process both actions and mentions in the correct order
# Find the existing while loop and replace it with a more comprehensive approach
old_while_loop_start = ''' console.log('🎨 RichChatInput renderContent called with value:', value);
console.log('🔍 Looking for action patterns with regex:', actionRegex);
while ((match = actionRegex.exec(value)) !== null) {'''
new_while_loop_start = ''' console.log('🎨 RichChatInput renderContent called with value:', value);
console.log('🔍 Looking for action and mention patterns with regex:', { actionRegex, mentionRegex });
// Find all actions and mentions, then sort by position
const allMatches = [];
// Find all action matches
let actionMatch;
actionRegex.lastIndex = 0; // Reset regex
while ((actionMatch = actionRegex.exec(value)) !== null) {
allMatches.push({
type: 'action',
match: actionMatch,
index: actionMatch.index,
length: actionMatch[0].length,
content: actionMatch[1]
});
}
// 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 @
});
}
// Sort matches by position
allMatches.sort((a, b) => a.index - b.index);
console.log('🔍 Found matches:', allMatches);
// Process all matches in order
for (const matchData of allMatches) {
const { type, match, index, length, content } = matchData;'''
content = content.replace(old_while_loop_start, new_while_loop_start)
# Write the modified content back
with open('ui/desktop/src/components/RichChatInput.tsx', 'w') as f:
f.write(content)
print("✅ Updated RichChatInput imports and regex setup:")
print(" - Added MentionPill import")
print(" - Added mentionRegex to detect @filename patterns")
print(" - Started updating processing logic to handle both actions and mentions")
print(" - Next step: Update the processing loop to render mention pills")