refactor: remove AI profile functionality and related components

- Deleted the AI profile management feature, including all associated views, hooks, and types.
- Updated settings and navigation components to remove references to AI profiles.
- Adjusted local storage and settings synchronization logic to reflect the removal of AI profiles.
- Cleaned up tests and utility functions that were dependent on the AI profile feature.

These changes streamline the application by eliminating unused functionality, improving maintainability and reducing complexity.
This commit is contained in:
webdevcody
2026-01-09 19:21:30 -05:00
parent 3f2707404c
commit fc20dd5ad4
40 changed files with 38 additions and 3140 deletions

View File

@@ -2,9 +2,6 @@ export * from './model-constants';
export * from './model-selector';
export * from './thinking-level-selector';
export * from './reasoning-effort-selector';
export * from './profile-quick-select';
export * from './profile-select';
export * from './profile-typeahead';
export * from './testing-tab-content';
export * from './priority-selector';
export * from './priority-select';

View File

@@ -1,155 +0,0 @@
import { Label } from '@/components/ui/label';
import { Brain, UserCircle, Terminal } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { ModelAlias, ThinkingLevel, AIProfile, CursorModelId } from '@automaker/types';
import {
CURSOR_MODEL_MAP,
profileHasThinking,
PROVIDER_PREFIXES,
getCodexModelLabel,
} from '@automaker/types';
import { PROFILE_ICONS } from './model-constants';
/**
* Get display string for a profile's model configuration
*/
function getProfileModelDisplay(profile: AIProfile): string {
if (profile.provider === 'cursor') {
const cursorModel = profile.cursorModel || 'auto';
const modelConfig = CURSOR_MODEL_MAP[cursorModel];
return modelConfig?.label || cursorModel;
}
if (profile.provider === 'codex') {
return getCodexModelLabel(profile.codexModel || 'codex-gpt-5.2-codex');
}
// Claude
return profile.model || 'sonnet';
}
/**
* Get display string for a profile's thinking configuration
*/
function getProfileThinkingDisplay(profile: AIProfile): string | null {
if (profile.provider === 'cursor') {
// For Cursor, thinking is embedded in the model
return profileHasThinking(profile) ? 'thinking' : null;
}
if (profile.provider === 'codex') {
// For Codex, thinking is embedded in the model
return profileHasThinking(profile) ? 'thinking' : null;
}
// Claude
return profile.thinkingLevel && profile.thinkingLevel !== 'none' ? profile.thinkingLevel : null;
}
interface ProfileQuickSelectProps {
profiles: AIProfile[];
selectedModel: ModelAlias | CursorModelId;
selectedThinkingLevel: ThinkingLevel;
selectedCursorModel?: string; // For detecting cursor profile selection
onSelect: (profile: AIProfile) => void; // Changed to pass full profile
testIdPrefix?: string;
showManageLink?: boolean;
onManageLinkClick?: () => void;
}
export function ProfileQuickSelect({
profiles,
selectedModel,
selectedThinkingLevel,
selectedCursorModel,
onSelect,
testIdPrefix = 'profile-quick-select',
showManageLink = false,
onManageLinkClick,
}: ProfileQuickSelectProps) {
// Show both Claude and Cursor profiles
const allProfiles = profiles;
if (allProfiles.length === 0) {
return null;
}
// Check if a profile is selected
const isProfileSelected = (profile: AIProfile): boolean => {
if (profile.provider === 'cursor') {
// For cursor profiles, check if cursor model matches
const profileCursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
return selectedCursorModel === profileCursorModel;
}
// For Claude profiles
return selectedModel === profile.model && selectedThinkingLevel === profile.thinkingLevel;
};
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<UserCircle className="w-4 h-4 text-brand-500" />
Quick Select Profile
</Label>
<span className="text-[11px] px-2 py-0.5 rounded-full border border-brand-500/40 text-brand-500">
Presets
</span>
</div>
<div className="grid grid-cols-2 gap-2">
{allProfiles.slice(0, 6).map((profile) => {
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain;
const isSelected = isProfileSelected(profile);
const isCursorProfile = profile.provider === 'cursor';
return (
<button
key={profile.id}
type="button"
onClick={() => onSelect(profile)}
className={cn(
'flex items-center gap-2 p-2 rounded-lg border text-left transition-all',
isSelected
? 'bg-brand-500/10 border-brand-500 text-foreground'
: 'bg-background hover:bg-accent border-input'
)}
data-testid={`${testIdPrefix}-${profile.id}`}
>
<div
className={cn(
'w-7 h-7 rounded flex items-center justify-center shrink-0',
isCursorProfile ? 'bg-amber-500/10' : 'bg-primary/10'
)}
>
{isCursorProfile ? (
<Terminal className="w-4 h-4 text-amber-500" />
) : (
IconComponent && <IconComponent className="w-4 h-4 text-primary" />
)}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{profile.name}</p>
<p className="text-[10px] text-muted-foreground truncate">
{getProfileModelDisplay(profile)}
{getProfileThinkingDisplay(profile) && ` + ${getProfileThinkingDisplay(profile)}`}
</p>
</div>
</button>
);
})}
</div>
<p className="text-xs text-muted-foreground">
Or customize below.
{showManageLink && onManageLinkClick && (
<>
{' '}
Manage profiles in{' '}
<button
type="button"
onClick={onManageLinkClick}
className="text-brand-500 hover:underline"
>
AI Profiles
</button>
</>
)}
</p>
</div>
);
}

View File

@@ -1,187 +0,0 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Brain, Terminal } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { ModelAlias, ThinkingLevel, AIProfile, CursorModelId } from '@automaker/types';
import {
CURSOR_MODEL_MAP,
profileHasThinking,
PROVIDER_PREFIXES,
getCodexModelLabel,
} from '@automaker/types';
import { PROFILE_ICONS } from './model-constants';
/**
* Get display string for a profile's model configuration
*/
function getProfileModelDisplay(profile: AIProfile): string {
if (profile.provider === 'cursor') {
const cursorModel = profile.cursorModel || 'auto';
const modelConfig = CURSOR_MODEL_MAP[cursorModel];
return modelConfig?.label || cursorModel;
}
if (profile.provider === 'codex') {
return getCodexModelLabel(profile.codexModel || 'codex-gpt-5.2-codex');
}
// Claude
return profile.model || 'sonnet';
}
/**
* Get display string for a profile's thinking configuration
*/
function getProfileThinkingDisplay(profile: AIProfile): string | null {
if (profile.provider === 'cursor') {
// For Cursor, thinking is embedded in the model
return profileHasThinking(profile) ? 'thinking' : null;
}
if (profile.provider === 'codex') {
// For Codex, thinking is embedded in the model
return profileHasThinking(profile) ? 'thinking' : null;
}
// Claude
return profile.thinkingLevel && profile.thinkingLevel !== 'none' ? profile.thinkingLevel : null;
}
interface ProfileSelectProps {
profiles: AIProfile[];
selectedModel: ModelAlias | CursorModelId;
selectedThinkingLevel: ThinkingLevel;
selectedCursorModel?: string; // For detecting cursor profile selection
onSelect: (profile: AIProfile) => void;
testIdPrefix?: string;
className?: string;
disabled?: boolean;
}
/**
* ProfileSelect - Compact dropdown selector for AI profiles
*
* A lightweight alternative to ProfileQuickSelect for contexts where
* space is limited (e.g., mass edit, bulk operations).
*
* Shows icon + profile name in dropdown, with model details below.
*
* @example
* ```tsx
* <ProfileSelect
* profiles={aiProfiles}
* selectedModel={model}
* selectedThinkingLevel={thinkingLevel}
* selectedCursorModel={isCurrentModelCursor ? model : undefined}
* onSelect={handleProfileSelect}
* testIdPrefix="mass-edit-profile"
* />
* ```
*/
export function ProfileSelect({
profiles,
selectedModel,
selectedThinkingLevel,
selectedCursorModel,
onSelect,
testIdPrefix = 'profile-select',
className,
disabled = false,
}: ProfileSelectProps) {
if (profiles.length === 0) {
return null;
}
// Check if a profile is selected
const isProfileSelected = (profile: AIProfile): boolean => {
if (profile.provider === 'cursor') {
// For cursor profiles, check if cursor model matches
const profileCursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
return selectedCursorModel === profileCursorModel;
}
// For Claude profiles
return selectedModel === profile.model && selectedThinkingLevel === profile.thinkingLevel;
};
const selectedProfile = profiles.find(isProfileSelected);
return (
<div className={cn('space-y-2', className)}>
<Select
value={selectedProfile?.id || 'none'}
onValueChange={(value: string) => {
if (value !== 'none') {
const profile = profiles.find((p) => p.id === value);
if (profile) {
onSelect(profile);
}
}
}}
disabled={disabled}
>
<SelectTrigger className="h-9" data-testid={`${testIdPrefix}-select-trigger`}>
<SelectValue>
{selectedProfile ? (
<div className="flex items-center gap-2">
{selectedProfile.provider === 'cursor' ? (
<Terminal className="h-4 w-4 text-amber-500" />
) : (
(() => {
const IconComponent = selectedProfile.icon
? PROFILE_ICONS[selectedProfile.icon]
: Brain;
return <IconComponent className="h-4 w-4 text-primary" />;
})()
)}
<span>{selectedProfile.name}</span>
</div>
) : (
<span className="text-muted-foreground">Select a profile...</span>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="none" className="text-muted-foreground">
No profile selected
</SelectItem>
{profiles.map((profile) => {
const isCursorProfile = profile.provider === 'cursor';
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain;
return (
<SelectItem
key={profile.id}
value={profile.id}
data-testid={`${testIdPrefix}-option-${profile.id}`}
>
<div className="flex items-center gap-2">
{isCursorProfile ? (
<Terminal className="h-3.5 w-3.5 text-amber-500" />
) : (
<IconComponent className="h-3.5 w-3.5 text-primary" />
)}
<div className="flex flex-col">
<span className="text-sm">{profile.name}</span>
<span className="text-[10px] text-muted-foreground">
{getProfileModelDisplay(profile)}
{getProfileThinkingDisplay(profile) &&
` + ${getProfileThinkingDisplay(profile)}`}
</span>
</div>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
{selectedProfile && (
<p className="text-xs text-muted-foreground">
{getProfileModelDisplay(selectedProfile)}
{getProfileThinkingDisplay(selectedProfile) &&
` + ${getProfileThinkingDisplay(selectedProfile)}`}
</p>
)}
</div>
);
}

View File

@@ -1,237 +0,0 @@
import * as React from 'react';
import { Check, ChevronsUpDown, UserCircle, Settings2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Badge } from '@/components/ui/badge';
import type { AIProfile } from '@automaker/types';
import { CURSOR_MODEL_MAP, profileHasThinking, getCodexModelLabel } from '@automaker/types';
import { PROVIDER_ICON_COMPONENTS } from '@/components/ui/provider-icon';
/**
* Get display string for a profile's model configuration
*/
function getProfileModelDisplay(profile: AIProfile): string {
if (profile.provider === 'cursor') {
const cursorModel = profile.cursorModel || 'auto';
const modelConfig = CURSOR_MODEL_MAP[cursorModel];
return modelConfig?.label || cursorModel;
}
if (profile.provider === 'codex') {
return getCodexModelLabel(profile.codexModel || 'codex-gpt-5.2-codex');
}
if (profile.provider === 'opencode') {
// Extract a short label from the opencode model
const modelId = profile.opencodeModel || '';
if (modelId.includes('/')) {
const parts = modelId.split('/');
return parts[parts.length - 1].split('.')[0] || modelId;
}
return modelId;
}
// Claude
return profile.model || 'sonnet';
}
/**
* Get display string for a profile's thinking configuration
*/
function getProfileThinkingDisplay(profile: AIProfile): string | null {
if (profile.provider === 'cursor' || profile.provider === 'codex') {
return profileHasThinking(profile) ? 'thinking' : null;
}
// Claude
return profile.thinkingLevel && profile.thinkingLevel !== 'none' ? profile.thinkingLevel : null;
}
interface ProfileTypeaheadProps {
profiles: AIProfile[];
selectedProfileId?: string;
onSelect: (profile: AIProfile) => void;
placeholder?: string;
className?: string;
disabled?: boolean;
showManageLink?: boolean;
onManageLinkClick?: () => void;
testIdPrefix?: string;
}
export function ProfileTypeahead({
profiles,
selectedProfileId,
onSelect,
placeholder = 'Select profile...',
className,
disabled = false,
showManageLink = false,
onManageLinkClick,
testIdPrefix = 'profile-typeahead',
}: ProfileTypeaheadProps) {
const [open, setOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState('');
const [triggerWidth, setTriggerWidth] = React.useState<number>(0);
const triggerRef = React.useRef<HTMLButtonElement>(null);
const selectedProfile = React.useMemo(
() => profiles.find((p) => p.id === selectedProfileId),
[profiles, selectedProfileId]
);
// Update trigger width when component mounts or value changes
React.useEffect(() => {
if (triggerRef.current) {
const updateWidth = () => {
setTriggerWidth(triggerRef.current?.offsetWidth || 0);
};
updateWidth();
const resizeObserver = new ResizeObserver(updateWidth);
resizeObserver.observe(triggerRef.current);
return () => {
resizeObserver.disconnect();
};
}
}, [selectedProfileId]);
// Filter profiles based on input
const filteredProfiles = React.useMemo(() => {
if (!inputValue) return profiles;
const lower = inputValue.toLowerCase();
return profiles.filter(
(p) =>
p.name.toLowerCase().includes(lower) ||
p.description?.toLowerCase().includes(lower) ||
p.provider.toLowerCase().includes(lower)
);
}, [profiles, inputValue]);
const handleSelect = (profile: AIProfile) => {
onSelect(profile);
setInputValue('');
setOpen(false);
};
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
ref={triggerRef}
variant="outline"
role="combobox"
aria-expanded={open}
disabled={disabled}
className={cn('w-full justify-between h-9', className)}
data-testid={`${testIdPrefix}-trigger`}
>
<span className="flex items-center gap-2 truncate">
{selectedProfile ? (
<>
{(() => {
const ProviderIcon = PROVIDER_ICON_COMPONENTS[selectedProfile.provider];
return ProviderIcon ? (
<ProviderIcon className="w-4 h-4 shrink-0 text-muted-foreground" />
) : (
<UserCircle className="w-4 h-4 shrink-0 text-muted-foreground" />
);
})()}
<span className="truncate">{selectedProfile.name}</span>
</>
) : (
<>
<UserCircle className="w-4 h-4 shrink-0 text-muted-foreground" />
<span className="text-muted-foreground">{placeholder}</span>
</>
)}
</span>
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: Math.max(triggerWidth, 280) }}
data-testid={`${testIdPrefix}-content`}
>
<Command shouldFilter={false}>
<CommandInput
placeholder="Search profiles..."
className="h-9"
value={inputValue}
onValueChange={setInputValue}
/>
<CommandList className="max-h-[300px]">
<CommandEmpty>No profile found.</CommandEmpty>
<CommandGroup>
{filteredProfiles.map((profile) => {
const ProviderIcon = PROVIDER_ICON_COMPONENTS[profile.provider];
const isSelected = profile.id === selectedProfileId;
const modelDisplay = getProfileModelDisplay(profile);
const thinkingDisplay = getProfileThinkingDisplay(profile);
return (
<CommandItem
key={profile.id}
value={profile.id}
onSelect={() => handleSelect(profile)}
className="flex items-center gap-2 py-2"
data-testid={`${testIdPrefix}-option-${profile.id}`}
>
<div className="flex items-center gap-2 flex-1 min-w-0">
{ProviderIcon ? (
<ProviderIcon className="w-4 h-4 shrink-0 text-muted-foreground" />
) : (
<UserCircle className="w-4 h-4 shrink-0 text-muted-foreground" />
)}
<div className="flex flex-col min-w-0 flex-1">
<span className="text-sm font-medium truncate">{profile.name}</span>
<span className="text-xs text-muted-foreground truncate">
{modelDisplay}
{thinkingDisplay && (
<span className="text-amber-500"> + {thinkingDisplay}</span>
)}
</span>
</div>
</div>
<div className="flex items-center gap-1 shrink-0">
{profile.isBuiltIn && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
Built-in
</Badge>
)}
<Check className={cn('h-4 w-4', isSelected ? 'opacity-100' : 'opacity-0')} />
</div>
</CommandItem>
);
})}
</CommandGroup>
{showManageLink && onManageLinkClick && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => {
setOpen(false);
onManageLinkClick();
}}
className="text-muted-foreground"
data-testid={`${testIdPrefix}-manage-link`}
>
<Settings2 className="w-4 h-4 mr-2" />
Manage AI Profiles
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}