diff --git a/ui/desktop/src/components/ActionPopover.tsx b/ui/desktop/src/components/ActionPopover.tsx index 3515cd6dd3..dae1674667 100644 --- a/ui/desktop/src/components/ActionPopover.tsx +++ b/ui/desktop/src/components/ActionPopover.tsx @@ -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(null); const listRef = useRef(null); - const [customCommands, setCustomCommands] = useState([]); + const [allCommands, setAllCommands] = useState([]); - // 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'] || ; }; - // 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 = {}; + 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<
{action.label}
- - Custom + + {action.isCustom ? 'Custom' : 'Built-in'}
@@ -254,8 +296,8 @@ const ActionPopover = forwardRef<
{query ? `No commands match "${query}"` - : customCommands.length === 0 - ? 'No custom commands found' + : allCommands.length === 0 + ? 'No commands found' : 'No starred commands found' }
@@ -271,7 +313,7 @@ const ActionPopover = forwardRef< Create Command - ) : !query && customCommands.length === 0 ? ( + ) : !query && allCommands.length === 0 ? ( onCreateCommand ? ( ) : ( -
Create custom commands in Settings → Chat
+
Create commands in Settings → Chat
) ) : (
Star commands to see them here when you type /
diff --git a/ui/desktop/src/components/RichChatInput.tsx b/ui/desktop/src/components/RichChatInput.tsx index 03845a26ac..6b96085a68 100644 --- a/ui/desktop/src/components/RichChatInput.tsx +++ b/ui/desktop/src/components/RichChatInput.tsx @@ -36,28 +36,52 @@ const getCustomCommandIcon = (iconName?: string) => { return iconMap[iconName || 'Zap'] || ; }; -// 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 = {}; + 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 = {}; - - 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 { diff --git a/ui/desktop/src/components/settings/CustomCommandsSettings.tsx b/ui/desktop/src/components/settings/CustomCommandsSettings.tsx index 415bffa28d..44b4cc99d1 100644 --- a/ui/desktop/src/components/settings/CustomCommandsSettings.tsx +++ b/ui/desktop/src/components/settings/CustomCommandsSettings.tsx @@ -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 = () => { - const [commands, setCommands] = useState([]); + const [userCommands, setUserCommands] = useState([]); + const [builtInCommands, setBuiltInCommands] = useState(BUILT_IN_COMMANDS); const [categories] = useState(DEFAULT_CATEGORIES); const [isModalOpen, setIsModalOpen] = useState(false); const [editingCommand, setEditingCommand] = useState(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 = () 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 = {}; + 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 = () }; 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 = () 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 = () {/* Commands List - Row Style */} {filteredCommands.map(command => (
-
-
+
+
{ICON_MAP[command.icon as keyof typeof ICON_MAP] || ICON_MAP.Zap}
-

/{command.name}

+
+

/{command.name}

+

{command.description}

-
+
@@ -203,7 +262,8 @@ export const CustomCommandsSettings: React.FC = () 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' : ''}`} > @@ -214,10 +274,20 @@ export const CustomCommandsSettings: React.FC = () 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' : ''}`} > - + + + {/* Built-in pill overlay */} + {command.isBuiltIn && ( +
+ + Built-in + +
+ )}
diff --git a/ui/desktop/src/types/customCommands.ts b/ui/desktop/src/types/customCommands.ts index 8c9242ab10..99ff632620 100644 --- a/ui/desktop/src/types/customCommands.ts +++ b/ui/desktop/src/types/customCommands.ts @@ -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, + }, +];