Files
goose/fix_richtextinput_processing_loop.py
T
spencrmartin f9cbf70f70 feat: Rich text input with action and mention pills
- Implement RichChatInput component with visual pill rendering
- Add ActionPill component with blue circle icons for actions
- Add MentionPill component with blue diamond icons for files
- Support for /action and @filename pill creation
- Inline pill display in both input and message history
- Proper text selection, highlighting, and cursor positioning
- Backspace removal of entire pills
- Theme-aware styling for light/dark modes
- Consistent white pill backgrounds with blue accent icons
- Fixed line-breaking issues for natural text flow

Components added:
- RichChatInput.tsx: Main rich text input with pill rendering
- ActionPill.tsx: Blue circle pills for actions
- MentionPill.tsx: Blue diamond pills for file mentions
- MessageContent.tsx: Enhanced message display with pills

Features:
- Visual pills replace [Action] and @filename text
- Click and drag text selection works normally
- Double/triple click for word/all selection
- Pills flow inline with text content
- Proper cursor positioning and blinking
- Backspace removes entire pills as units
2025-10-23 09:51:44 -04:00

170 lines
6.0 KiB
Python

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")