feat: improve built-in command styling with consistent disabled state and overlay pill

- Update built-in command badges to use grey styling matching ActionPopover
- Add blur effect to disabled edit/copy/delete buttons for built-in commands
- Position built-in pill as overlay on top of disabled action buttons
- Center align action buttons within their container space
- Add 8px left spacing to built-in pill overlay for better visual balance
This commit is contained in:
spencrmartin
2025-09-29 10:06:11 -04:00
parent 8dde6822af
commit c8249d62fe
4 changed files with 366 additions and 83 deletions
+76 -34
View File
@@ -6,7 +6,7 @@ import React, {
useState,
} from 'react';
import { Zap, FileText, Code, Settings, Search, Play, Hash, Plus } from 'lucide-react';
import { CustomCommand } from '../types/customCommands';
import { CustomCommand, BUILT_IN_COMMANDS } from '../types/customCommands';
import { Button } from './ui/button';
interface ActionItem {
@@ -36,28 +36,47 @@ const ActionPopover = forwardRef<
>(({ isOpen, onClose, onSelect, position, selectedIndex, onSelectedIndexChange, query = '', onCreateCommand }, ref) => {
const popoverRef = useRef<HTMLDivElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const [customCommands, setCustomCommands] = useState<CustomCommand[]>([]);
const [allCommands, setAllCommands] = useState<CustomCommand[]>([]);
// Load custom commands on mount
// Load both built-in and user commands on mount
useEffect(() => {
const loadCustomCommands = () => {
const loadAllCommands = () => {
try {
const stored = localStorage.getItem('goose-custom-commands');
if (stored) {
const parsed = JSON.parse(stored);
setCustomCommands(parsed.map((cmd: any) => ({
...cmd,
createdAt: new Date(cmd.createdAt),
updatedAt: new Date(cmd.updatedAt)
})));
// Load user commands
const userStored = localStorage.getItem('goose-custom-commands');
let userCommands: CustomCommand[] = [];
if (userStored) {
const parsed = JSON.parse(userStored);
userCommands = parsed
.filter((cmd: any) => !cmd.isBuiltIn) // Only user commands
.map((cmd: any) => ({
...cmd,
createdAt: new Date(cmd.createdAt),
updatedAt: new Date(cmd.updatedAt)
}));
}
// Load built-in command favorites/usage
const builtInStored = localStorage.getItem('goose-builtin-commands');
let builtInCommands = [...BUILT_IN_COMMANDS];
if (builtInStored) {
const builtInData = JSON.parse(builtInStored);
builtInCommands = BUILT_IN_COMMANDS.map(cmd => ({
...cmd,
isFavorite: builtInData[cmd.id]?.isFavorite || false,
usageCount: builtInData[cmd.id]?.usageCount || 0,
}));
}
// Combine all commands
setAllCommands([...builtInCommands, ...userCommands]);
} catch (error) {
console.error('Failed to load custom commands:', error);
console.error('Failed to load commands:', error);
}
};
if (isOpen) {
loadCustomCommands();
loadAllCommands();
}
}, [isOpen]);
@@ -75,27 +94,46 @@ const ActionPopover = forwardRef<
return iconMap[iconName || 'Zap'] || <Zap size={16} />;
};
// Convert custom commands to action items
const customActions: ActionItem[] = customCommands.map(cmd => ({
// Convert all commands to action items
const allActions: ActionItem[] = allCommands.map(cmd => ({
id: cmd.id,
label: cmd.label,
description: cmd.description,
icon: getCustomCommandIcon(cmd.icon),
isCustom: true,
isCustom: !cmd.isBuiltIn, // Built-in commands are not "custom"
prompt: cmd.prompt,
action: () => {
console.log('Custom command action triggered:', cmd.name);
// Increment usage count
const updatedCommands = customCommands.map(c =>
c.id === cmd.id ? { ...c, usageCount: c.usageCount + 1 } : c
);
localStorage.setItem('goose-custom-commands', JSON.stringify(updatedCommands));
console.log('Command action triggered:', cmd.name);
// Increment usage count for both built-in and user commands
if (cmd.isBuiltIn) {
// Update built-in command usage
const builtInStored = localStorage.getItem('goose-builtin-commands');
let builtInData: Record<string, { isFavorite: boolean; usageCount: number }> = {};
if (builtInStored) {
builtInData = JSON.parse(builtInStored);
}
builtInData[cmd.id] = {
isFavorite: builtInData[cmd.id]?.isFavorite || cmd.isFavorite,
usageCount: (builtInData[cmd.id]?.usageCount || cmd.usageCount) + 1,
};
localStorage.setItem('goose-builtin-commands', JSON.stringify(builtInData));
} else {
// Update user command usage
const userStored = localStorage.getItem('goose-custom-commands');
if (userStored) {
const userCommands = JSON.parse(userStored);
const updatedCommands = userCommands.map((c: any) =>
c.id === cmd.id ? { ...c, usageCount: c.usageCount + 1 } : c
);
localStorage.setItem('goose-custom-commands', JSON.stringify(updatedCommands));
}
}
},
}));
// Filter custom commands based on query
const filteredActions = customActions.filter(action => {
const cmd = customCommands.find(c => c.id === action.id);
// Filter commands based on query
const filteredActions = allActions.filter(action => {
const cmd = allCommands.find(c => c.id === action.id);
// If no query, show only starred commands
if (!query) {
@@ -113,8 +151,8 @@ const ActionPopover = forwardRef<
// Sort actions: favorites first, then by usage count, then alphabetically
const sortedActions = filteredActions.sort((a, b) => {
const cmdA = customCommands.find(c => c.id === a.id);
const cmdB = customCommands.find(c => c.id === b.id);
const cmdA = allCommands.find(c => c.id === a.id);
const cmdB = allCommands.find(c => c.id === b.id);
if (cmdA?.isFavorite && !cmdB?.isFavorite) return -1;
if (!cmdA?.isFavorite && cmdB?.isFavorite) return 1;
@@ -239,8 +277,12 @@ const ActionPopover = forwardRef<
<div className="text-sm font-medium text-textStandard">
{action.label}
</div>
<span className="text-xs px-1.5 py-0.5 bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300 rounded-full font-medium">
Custom
<span className={`text-xs px-1.5 py-0.5 rounded-full font-medium ${
action.isCustom
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'
}`}>
{action.isCustom ? 'Custom' : 'Built-in'}
</span>
</div>
<div className="text-xs text-textSubtle">
@@ -254,8 +296,8 @@ const ActionPopover = forwardRef<
<div className="text-sm mb-2">
{query
? `No commands match "${query}"`
: customCommands.length === 0
? 'No custom commands found'
: allCommands.length === 0
? 'No commands found'
: 'No starred commands found'
}
</div>
@@ -271,7 +313,7 @@ const ActionPopover = forwardRef<
<Plus size={14} />
Create Command
</Button>
) : !query && customCommands.length === 0 ? (
) : !query && allCommands.length === 0 ? (
onCreateCommand ? (
<Button
onClick={() => {
@@ -285,7 +327,7 @@ const ActionPopover = forwardRef<
Create Command
</Button>
) : (
<div className="text-xs">Create custom commands in Settings Chat</div>
<div className="text-xs">Create commands in Settings Chat</div>
)
) : (
<div className="text-xs">Star commands to see them here when you type /</div>
+38 -14
View File
@@ -36,28 +36,52 @@ const getCustomCommandIcon = (iconName?: string) => {
return iconMap[iconName || 'Zap'] || <Zap size={12} />;
};
// Dynamic action mapping that loads from localStorage
// Import built-in commands
import { BUILT_IN_COMMANDS } from '../types/customCommands';
// Dynamic action mapping that loads both built-in and user commands
const getActionMap = () => {
const actionMap: Record<string, { label: string; icon: React.ReactNode }> = {};
try {
// Add built-in commands
const builtInStored = localStorage.getItem('goose-builtin-commands');
let builtInCommands = [...BUILT_IN_COMMANDS];
if (builtInStored) {
const builtInData = JSON.parse(builtInStored);
builtInCommands = BUILT_IN_COMMANDS.map(cmd => ({
...cmd,
isFavorite: builtInData[cmd.id]?.isFavorite || false,
usageCount: builtInData[cmd.id]?.usageCount || 0,
}));
}
builtInCommands.forEach((cmd: any) => {
actionMap[cmd.id] = {
label: cmd.label,
icon: getCustomCommandIcon(cmd.icon),
};
});
// Add user commands
const stored = localStorage.getItem('goose-custom-commands');
if (stored) {
const commands = JSON.parse(stored);
const actionMap: Record<string, { label: string; icon: React.ReactNode }> = {};
commands.forEach((cmd: any) => {
actionMap[cmd.id] = {
label: cmd.label,
icon: getCustomCommandIcon(cmd.icon),
};
});
return actionMap;
commands
.filter((cmd: any) => !cmd.isBuiltIn) // Only user commands
.forEach((cmd: any) => {
actionMap[cmd.id] = {
label: cmd.label,
icon: getCustomCommandIcon(cmd.icon),
};
});
}
return actionMap;
} catch (error) {
console.error('Error loading custom commands for action map:', error);
console.error('Error loading commands for action map:', error);
return {};
}
return {};
};
export interface RichChatInputRef {
@@ -6,7 +6,8 @@ import { AddCustomCommandModal } from '../AddCustomCommandModal';
import {
CustomCommand,
CustomCommandCategory,
DEFAULT_CATEGORIES
DEFAULT_CATEGORIES,
BUILT_IN_COMMANDS
} from '../../types/customCommands';
interface CustomCommandsSettingsProps {
@@ -21,12 +22,16 @@ const ICON_MAP = {
};
export const CustomCommandsSettings: React.FC<CustomCommandsSettingsProps> = () => {
const [commands, setCommands] = useState<CustomCommand[]>([]);
const [userCommands, setUserCommands] = useState<CustomCommand[]>([]);
const [builtInCommands, setBuiltInCommands] = useState<CustomCommand[]>(BUILT_IN_COMMANDS);
const [categories] = useState<CustomCommandCategory[]>(DEFAULT_CATEGORIES);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingCommand, setEditingCommand] = useState<CustomCommand | null>(null);
const [searchQuery, setSearchQuery] = useState('');
// Combine built-in and user commands
const allCommands = [...builtInCommands, ...userCommands];
// Load commands from storage on mount
useEffect(() => {
loadCommands();
@@ -34,53 +39,86 @@ export const CustomCommandsSettings: React.FC<CustomCommandsSettingsProps> = ()
const loadCommands = async () => {
try {
// TODO: Load from actual storage (localStorage, config API, etc.)
// Load user commands from localStorage
const stored = localStorage.getItem('goose-custom-commands');
if (stored) {
const parsed = JSON.parse(stored);
setCommands(parsed.map((cmd: any) => ({
const userCmds = parsed
.filter((cmd: any) => !cmd.isBuiltIn) // Only user commands
.map((cmd: any) => ({
...cmd,
createdAt: new Date(cmd.createdAt),
updatedAt: new Date(cmd.updatedAt)
}));
setUserCommands(userCmds);
}
// Load built-in command favorites/usage from localStorage
const builtInStored = localStorage.getItem('goose-builtin-commands');
if (builtInStored) {
const builtInData = JSON.parse(builtInStored);
const updatedBuiltIns = BUILT_IN_COMMANDS.map(cmd => ({
...cmd,
createdAt: new Date(cmd.createdAt),
updatedAt: new Date(cmd.updatedAt)
})));
} else {
// Load some example commands for demo
setCommands(getExampleCommands());
isFavorite: builtInData[cmd.id]?.isFavorite || false,
usageCount: builtInData[cmd.id]?.usageCount || 0,
}));
setBuiltInCommands(updatedBuiltIns);
}
} catch (error) {
console.error('Failed to load custom commands:', error);
}
};
const saveCommands = async (updatedCommands: CustomCommand[]) => {
const saveUserCommands = async (updatedUserCommands: CustomCommand[]) => {
try {
localStorage.setItem('goose-custom-commands', JSON.stringify(updatedCommands));
setCommands(updatedCommands);
localStorage.setItem('goose-custom-commands', JSON.stringify(updatedUserCommands));
setUserCommands(updatedUserCommands);
} catch (error) {
console.error('Failed to save custom commands:', error);
console.error('Failed to save user commands:', error);
}
};
const saveBuiltInCommandData = async (updatedBuiltInCommands: CustomCommand[]) => {
try {
// Only save favorites and usage count for built-in commands
const builtInData: Record<string, { isFavorite: boolean; usageCount: number }> = {};
updatedBuiltInCommands.forEach(cmd => {
builtInData[cmd.id] = {
isFavorite: cmd.isFavorite,
usageCount: cmd.usageCount,
};
});
localStorage.setItem('goose-builtin-commands', JSON.stringify(builtInData));
setBuiltInCommands(updatedBuiltInCommands);
} catch (error) {
console.error('Failed to save built-in command data:', error);
}
};
const handleModalSave = (command: CustomCommand) => {
const now = new Date();
let updatedCommands: CustomCommand[];
let updatedUserCommands: CustomCommand[];
if (editingCommand) {
// Update existing command
updatedCommands = commands.map(cmd =>
if (editingCommand && !editingCommand.isBuiltIn) {
// Update existing user command
updatedUserCommands = userCommands.map(cmd =>
cmd.id === editingCommand.id
? { ...command, id: editingCommand.id, createdAt: editingCommand.createdAt, updatedAt: now }
: cmd
);
} else {
// Create new command
updatedCommands = [...commands, { ...command, createdAt: now, updatedAt: now }];
// Create new user command
updatedUserCommands = [...userCommands, { ...command, createdAt: now, updatedAt: now }];
}
saveCommands(updatedCommands);
saveUserCommands(updatedUserCommands);
};
const handleEdit = (command: CustomCommand) => {
// Built-in commands cannot be edited
if (command.isBuiltIn) {
return;
}
setEditingCommand(command);
setIsModalOpen(true);
};
@@ -96,17 +134,34 @@ export const CustomCommandsSettings: React.FC<CustomCommandsSettingsProps> = ()
};
const handleDelete = (commandId: string) => {
// Built-in commands cannot be deleted
const command = allCommands.find(cmd => cmd.id === commandId);
if (command?.isBuiltIn) {
return;
}
if (confirm('Are you sure you want to delete this command?')) {
const updatedCommands = commands.filter(cmd => cmd.id !== commandId);
saveCommands(updatedCommands);
const updatedUserCommands = userCommands.filter(cmd => cmd.id !== commandId);
saveUserCommands(updatedUserCommands);
}
};
const handleToggleFavorite = (commandId: string) => {
const updatedCommands = commands.map(cmd =>
// Handle built-in commands separately
const builtInCommand = builtInCommands.find(cmd => cmd.id === commandId);
if (builtInCommand) {
const updatedBuiltInCommands = builtInCommands.map(cmd =>
cmd.id === commandId ? { ...cmd, isFavorite: !cmd.isFavorite } : cmd
);
saveBuiltInCommandData(updatedBuiltInCommands);
return;
}
// Handle user commands
const updatedUserCommands = userCommands.map(cmd =>
cmd.id === commandId ? { ...cmd, isFavorite: !cmd.isFavorite } : cmd
);
saveCommands(updatedCommands);
saveUserCommands(updatedUserCommands);
};
const handleDuplicate = (command: CustomCommand) => {
@@ -118,11 +173,12 @@ export const CustomCommandsSettings: React.FC<CustomCommandsSettingsProps> = ()
createdAt: new Date(),
updatedAt: new Date(),
usageCount: 0,
isBuiltIn: false, // Duplicates are always user commands
};
saveCommands([...commands, duplicatedCommand]);
saveUserCommands([...userCommands, duplicatedCommand]);
};
const filteredCommands = commands.filter(cmd => {
const filteredCommands = allCommands.filter(cmd => {
const matchesSearch = !searchQuery ||
cmd.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
cmd.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -158,18 +214,20 @@ export const CustomCommandsSettings: React.FC<CustomCommandsSettingsProps> = ()
{/* Commands List - Row Style */}
{filteredCommands.map(command => (
<div key={command.id} className="group hover:cursor-pointer text-sm">
<div className="flex items-center justify-between text-text-default py-2 px-2 bg-background-default hover:bg-background-muted rounded-lg transition-all">
<div className="flex items-center gap-3">
<div className="flex items-center text-text-default py-2 px-2 bg-background-default hover:bg-background-muted rounded-lg transition-all relative">
<div className="flex items-center gap-3 flex-1">
<div className="flex items-center justify-center text-text-muted">
{ICON_MAP[command.icon as keyof typeof ICON_MAP] || ICON_MAP.Zap}
</div>
<div>
<h3 className="text-text-default font-medium">/{command.name}</h3>
<div className="flex items-center gap-2">
<h3 className="text-text-default font-medium">/{command.name}</h3>
</div>
<p className="text-text-muted text-xs mt-[2px]">{command.description}</p>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center justify-center gap-2 relative w-40">
<Button
variant="ghost"
size="sm"
@@ -192,7 +250,8 @@ export const CustomCommandsSettings: React.FC<CustomCommandsSettingsProps> = ()
e.stopPropagation();
handleEdit(command);
}}
className="p-1 h-6 w-6"
disabled={command.isBuiltIn}
className={`p-1 h-6 w-6 ${command.isBuiltIn ? 'opacity-50 cursor-not-allowed blur-sm' : ''}`}
>
<Edit size={12} className="text-text-muted hover:text-text-default" />
</Button>
@@ -203,7 +262,8 @@ export const CustomCommandsSettings: React.FC<CustomCommandsSettingsProps> = ()
e.stopPropagation();
handleDuplicate(command);
}}
className="p-1 h-6 w-6"
disabled={command.isBuiltIn}
className={`p-1 h-6 w-6 ${command.isBuiltIn ? 'opacity-50 cursor-not-allowed blur-sm' : ''}`}
>
<Copy size={12} className="text-text-muted hover:text-text-default" />
</Button>
@@ -214,10 +274,20 @@ export const CustomCommandsSettings: React.FC<CustomCommandsSettingsProps> = ()
e.stopPropagation();
handleDelete(command.id);
}}
className="p-1 h-6 w-6 text-red-600 hover:text-red-700"
disabled={command.isBuiltIn}
className={`p-1 h-6 w-6 ${command.isBuiltIn ? 'opacity-50 cursor-not-allowed blur-sm' : ''}`}
>
<Trash2 size={12} />
<Trash2 size={12} className={command.isBuiltIn ? 'text-text-muted' : 'text-red-600 hover:text-red-700'} />
</Button>
{/* Built-in pill overlay */}
{command.isBuiltIn && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none pl-2">
<span className="text-xs px-2 py-1 bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400 rounded-full font-medium shadow-sm border border-gray-200 dark:border-gray-700">
Built-in
</span>
</div>
)}
</div>
</div>
</div>
+147
View File
@@ -11,6 +11,7 @@ export interface CustomCommand {
updatedAt: Date;
usageCount: number; // Track how often it's used
isFavorite: boolean; // User can mark favorites
isBuiltIn?: boolean; // Built-in commands cannot be deleted, only favorited
}
export interface CustomCommandVariable {
@@ -127,3 +128,149 @@ export const COMMON_VARIABLES: CustomCommandVariable[] = [
required: false,
}
];
// Built-in commands that cannot be deleted, only favorited
export const BUILT_IN_COMMANDS: CustomCommand[] = [
{
id: 'builtin_explain',
name: 'explain',
label: 'Explain Code',
description: 'Provide detailed explanation of code or concepts',
prompt: `Please explain the provided code or concept in detail. Include:
1. What it does (high-level purpose)
2. How it works (step-by-step breakdown)
3. Key concepts and patterns used
4. Dependencies and requirements
5. Potential use cases
6. Any notable design decisions
Make the explanation accessible and comprehensive.`,
icon: 'Search',
category: 'development',
variables: [],
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
usageCount: 0,
isFavorite: false,
isBuiltIn: true,
},
{
id: 'builtin_review',
name: 'review',
label: 'Code Review',
description: 'Perform thorough code review with suggestions',
prompt: `Please perform a comprehensive code review. Focus on:
1. Code quality and readability
2. Performance optimizations
3. Security considerations
4. Best practices and conventions
5. Potential bugs or issues
6. Suggestions for improvement
Provide specific, actionable feedback with examples where appropriate.`,
icon: 'Search',
category: 'development',
variables: [],
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
usageCount: 0,
isFavorite: false,
isBuiltIn: true,
},
{
id: 'builtin_document',
name: 'document',
label: 'Create Documentation',
description: 'Generate comprehensive documentation',
prompt: `Please create comprehensive documentation for the provided code or project. Include:
1. Overview and purpose
2. Installation/setup instructions
3. Usage examples
4. API reference (if applicable)
5. Configuration options
6. Troubleshooting guide
Make the documentation clear, well-structured, and suitable for both beginners and experienced users.`,
icon: 'FileText',
category: 'documentation',
variables: [],
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
usageCount: 0,
isFavorite: false,
isBuiltIn: true,
},
{
id: 'builtin_optimize',
name: 'optimize',
label: 'Optimize Code',
description: 'Suggest optimizations for better performance',
prompt: `Please analyze the provided code and suggest optimizations for better performance, efficiency, and maintainability. Consider:
1. Algorithm efficiency and time complexity
2. Memory usage optimization
3. Code structure and organization
4. Best practices for the specific language/framework
5. Potential refactoring opportunities
6. Performance bottlenecks
Provide specific, actionable recommendations with code examples where helpful.`,
icon: 'Zap',
category: 'development',
variables: [],
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
usageCount: 0,
isFavorite: false,
isBuiltIn: true,
},
{
id: 'builtin_test',
name: 'test',
label: 'Generate Tests',
description: 'Create comprehensive unit tests',
prompt: `Please generate comprehensive unit tests for the provided code. Include:
1. Test cases for normal/expected behavior
2. Edge cases and boundary conditions
3. Error handling and exception cases
4. Mock objects where appropriate
5. Test setup and teardown if needed
6. Clear, descriptive test names
Use the appropriate testing framework for the language and follow testing best practices.`,
icon: 'Code',
category: 'development',
variables: [],
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
usageCount: 0,
isFavorite: false,
isBuiltIn: true,
},
{
id: 'builtin_summarize',
name: 'summarize',
label: 'Summarize Content',
description: 'Create concise summary of content',
prompt: `Please create a concise summary of the provided content. Include:
1. Main points and key takeaways
2. Important details and findings
3. Conclusions or recommendations
4. Action items (if applicable)
Keep the summary clear, well-organized, and focused on the most important information.`,
icon: 'FileText',
category: 'general',
variables: [],
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-01'),
usageCount: 0,
isFavorite: false,
isBuiltIn: true,
},
];