mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
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:
@@ -59,7 +59,7 @@ export function Sidebar() {
|
||||
} = useAppStore();
|
||||
|
||||
// Environment variable flags for hiding sidebar items
|
||||
const { hideTerminal, hideWiki, hideRunningAgents, hideContext, hideSpecEditor, hideAiProfiles } =
|
||||
const { hideTerminal, hideWiki, hideRunningAgents, hideContext, hideSpecEditor } =
|
||||
SIDEBAR_FEATURE_FLAGS;
|
||||
|
||||
// Get customizable keyboard shortcuts
|
||||
@@ -232,7 +232,6 @@ export function Sidebar() {
|
||||
hideSpecEditor,
|
||||
hideContext,
|
||||
hideTerminal,
|
||||
hideAiProfiles,
|
||||
currentProject,
|
||||
projects,
|
||||
projectHistory,
|
||||
|
||||
@@ -20,5 +20,4 @@ export const SIDEBAR_FEATURE_FLAGS = {
|
||||
hideRunningAgents: import.meta.env.VITE_HIDE_RUNNING_AGENTS === 'true',
|
||||
hideContext: import.meta.env.VITE_HIDE_CONTEXT === 'true',
|
||||
hideSpecEditor: import.meta.env.VITE_HIDE_SPEC_EDITOR === 'true',
|
||||
hideAiProfiles: import.meta.env.VITE_HIDE_AI_PROFILES === 'true',
|
||||
} as const;
|
||||
|
||||
@@ -5,11 +5,9 @@ import {
|
||||
LayoutGrid,
|
||||
Bot,
|
||||
BookOpen,
|
||||
UserCircle,
|
||||
Terminal,
|
||||
CircleDot,
|
||||
GitPullRequest,
|
||||
Zap,
|
||||
Lightbulb,
|
||||
} from 'lucide-react';
|
||||
import type { NavSection, NavItem } from '../types';
|
||||
@@ -26,7 +24,6 @@ interface UseNavigationProps {
|
||||
cycleNextProject: string;
|
||||
spec: string;
|
||||
context: string;
|
||||
profiles: string;
|
||||
board: string;
|
||||
agent: string;
|
||||
terminal: string;
|
||||
@@ -38,7 +35,6 @@ interface UseNavigationProps {
|
||||
hideSpecEditor: boolean;
|
||||
hideContext: boolean;
|
||||
hideTerminal: boolean;
|
||||
hideAiProfiles: boolean;
|
||||
currentProject: Project | null;
|
||||
projects: Project[];
|
||||
projectHistory: string[];
|
||||
@@ -57,7 +53,6 @@ export function useNavigation({
|
||||
hideSpecEditor,
|
||||
hideContext,
|
||||
hideTerminal,
|
||||
hideAiProfiles,
|
||||
currentProject,
|
||||
projects,
|
||||
projectHistory,
|
||||
@@ -114,12 +109,6 @@ export function useNavigation({
|
||||
icon: BookOpen,
|
||||
shortcut: shortcuts.context,
|
||||
},
|
||||
{
|
||||
id: 'profiles',
|
||||
label: 'AI Profiles',
|
||||
icon: UserCircle,
|
||||
shortcut: shortcuts.profiles,
|
||||
},
|
||||
];
|
||||
|
||||
// Filter out hidden items
|
||||
@@ -130,9 +119,6 @@ export function useNavigation({
|
||||
if (item.id === 'context' && hideContext) {
|
||||
return false;
|
||||
}
|
||||
if (item.id === 'profiles' && hideAiProfiles) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -201,7 +187,6 @@ export function useNavigation({
|
||||
hideSpecEditor,
|
||||
hideContext,
|
||||
hideTerminal,
|
||||
hideAiProfiles,
|
||||
hasGitHubRemote,
|
||||
unviewedValidationsCount,
|
||||
]);
|
||||
|
||||
@@ -88,7 +88,6 @@ const SHORTCUT_LABELS: Record<keyof KeyboardShortcuts, string> = {
|
||||
spec: 'Spec Editor',
|
||||
context: 'Context',
|
||||
settings: 'Settings',
|
||||
profiles: 'AI Profiles',
|
||||
terminal: 'Terminal',
|
||||
ideation: 'Ideation',
|
||||
githubIssues: 'GitHub Issues',
|
||||
@@ -102,7 +101,6 @@ const SHORTCUT_LABELS: Record<keyof KeyboardShortcuts, string> = {
|
||||
projectPicker: 'Project Picker',
|
||||
cyclePrevProject: 'Prev Project',
|
||||
cycleNextProject: 'Next Project',
|
||||
addProfile: 'Add Profile',
|
||||
splitTerminalRight: 'Split Right',
|
||||
splitTerminalDown: 'Split Down',
|
||||
closeTerminal: 'Close Terminal',
|
||||
@@ -116,7 +114,6 @@ const SHORTCUT_CATEGORIES: Record<keyof KeyboardShortcuts, 'navigation' | 'ui' |
|
||||
spec: 'navigation',
|
||||
context: 'navigation',
|
||||
settings: 'navigation',
|
||||
profiles: 'navigation',
|
||||
terminal: 'navigation',
|
||||
ideation: 'navigation',
|
||||
githubIssues: 'navigation',
|
||||
@@ -130,7 +127,6 @@ const SHORTCUT_CATEGORIES: Record<keyof KeyboardShortcuts, 'navigation' | 'ui' |
|
||||
projectPicker: 'action',
|
||||
cyclePrevProject: 'action',
|
||||
cycleNextProject: 'action',
|
||||
addProfile: 'action',
|
||||
splitTerminalRight: 'action',
|
||||
splitTerminalDown: 'action',
|
||||
closeTerminal: 'action',
|
||||
|
||||
@@ -73,8 +73,6 @@ export function BoardView() {
|
||||
maxConcurrency,
|
||||
setMaxConcurrency,
|
||||
defaultSkipTests,
|
||||
showProfilesOnly,
|
||||
aiProfiles,
|
||||
kanbanCardDetailLevel,
|
||||
setKanbanCardDetailLevel,
|
||||
boardViewMode,
|
||||
@@ -1299,8 +1297,6 @@ export function BoardView() {
|
||||
onClose={() => setShowMassEditDialog(false)}
|
||||
selectedFeatures={selectedFeatures}
|
||||
onApply={handleBulkUpdate}
|
||||
showProfilesOnly={showProfilesOnly}
|
||||
aiProfiles={aiProfiles}
|
||||
/>
|
||||
|
||||
{/* Board Background Modal */}
|
||||
@@ -1348,8 +1344,6 @@ export function BoardView() {
|
||||
defaultBranch={selectedWorktreeBranch}
|
||||
currentBranch={currentWorktreeBranch || undefined}
|
||||
isMaximized={isMaximized}
|
||||
showProfilesOnly={showProfilesOnly}
|
||||
aiProfiles={aiProfiles}
|
||||
parentFeature={spawnParentFeature}
|
||||
allFeatures={hookFeatures}
|
||||
/>
|
||||
@@ -1364,8 +1358,6 @@ export function BoardView() {
|
||||
branchCardCounts={branchCardCounts}
|
||||
currentBranch={currentWorktreeBranch || undefined}
|
||||
isMaximized={isMaximized}
|
||||
showProfilesOnly={showProfilesOnly}
|
||||
aiProfiles={aiProfiles}
|
||||
allFeatures={hookFeatures}
|
||||
/>
|
||||
|
||||
|
||||
@@ -32,24 +32,17 @@ import {
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
FeatureImage,
|
||||
AIProfile,
|
||||
PlanningMode,
|
||||
Feature,
|
||||
} from '@/store/app-store';
|
||||
import type { ReasoningEffort, PhaseModelEntry } from '@automaker/types';
|
||||
import {
|
||||
supportsReasoningEffort,
|
||||
PROVIDER_PREFIXES,
|
||||
isCursorModel,
|
||||
isClaudeModel,
|
||||
} from '@automaker/types';
|
||||
import { supportsReasoningEffort, isClaudeModel } from '@automaker/types';
|
||||
import {
|
||||
TestingTabContent,
|
||||
PrioritySelector,
|
||||
WorkModeSelector,
|
||||
PlanningModeSelect,
|
||||
AncestorContextSection,
|
||||
ProfileTypeahead,
|
||||
} from '../shared';
|
||||
import type { WorkMode } from '../shared';
|
||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||
@@ -60,7 +53,6 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
getAncestors,
|
||||
formatAncestorContextForPrompt,
|
||||
@@ -100,8 +92,6 @@ interface AddFeatureDialogProps {
|
||||
defaultBranch?: string;
|
||||
currentBranch?: string;
|
||||
isMaximized: boolean;
|
||||
showProfilesOnly: boolean;
|
||||
aiProfiles: AIProfile[];
|
||||
parentFeature?: Feature | null;
|
||||
allFeatures?: Feature[];
|
||||
}
|
||||
@@ -118,13 +108,10 @@ export function AddFeatureDialog({
|
||||
defaultBranch = 'main',
|
||||
currentBranch,
|
||||
isMaximized,
|
||||
showProfilesOnly,
|
||||
aiProfiles,
|
||||
parentFeature = null,
|
||||
allFeatures = [],
|
||||
}: AddFeatureDialogProps) {
|
||||
const isSpawnMode = !!parentFeature;
|
||||
const navigate = useNavigate();
|
||||
const [workMode, setWorkMode] = useState<WorkMode>('current');
|
||||
|
||||
// Form state
|
||||
@@ -139,7 +126,6 @@ export function AddFeatureDialog({
|
||||
const [priority, setPriority] = useState(2);
|
||||
|
||||
// Model selection state
|
||||
const [selectedProfileId, setSelectedProfileId] = useState<string | undefined>();
|
||||
const [modelEntry, setModelEntry] = useState<PhaseModelEntry>({ model: 'opus' });
|
||||
|
||||
// Check if current model supports planning mode (Claude/Anthropic only)
|
||||
@@ -163,7 +149,7 @@ export function AddFeatureDialog({
|
||||
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Get defaults from store
|
||||
const { defaultPlanningMode, defaultRequirePlanApproval, defaultAIProfileId } = useAppStore();
|
||||
const { defaultPlanningMode, defaultRequirePlanApproval } = useAppStore();
|
||||
|
||||
// Enhancement model override
|
||||
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
|
||||
@@ -177,24 +163,12 @@ export function AddFeatureDialog({
|
||||
wasOpenRef.current = open;
|
||||
|
||||
if (justOpened) {
|
||||
const defaultProfile = defaultAIProfileId
|
||||
? aiProfiles.find((p) => p.id === defaultAIProfileId)
|
||||
: null;
|
||||
|
||||
setSkipTests(defaultSkipTests);
|
||||
setBranchName(defaultBranch || '');
|
||||
setWorkMode('current');
|
||||
setPlanningMode(defaultPlanningMode);
|
||||
setRequirePlanApproval(defaultRequirePlanApproval);
|
||||
|
||||
// Set model from default profile or fallback
|
||||
if (defaultProfile) {
|
||||
setSelectedProfileId(defaultProfile.id);
|
||||
applyProfileToModel(defaultProfile);
|
||||
} else {
|
||||
setSelectedProfileId(undefined);
|
||||
setModelEntry({ model: 'opus' });
|
||||
}
|
||||
setModelEntry({ model: 'opus' });
|
||||
|
||||
// Initialize ancestors for spawn mode
|
||||
if (parentFeature) {
|
||||
@@ -212,41 +186,12 @@ export function AddFeatureDialog({
|
||||
defaultBranch,
|
||||
defaultPlanningMode,
|
||||
defaultRequirePlanApproval,
|
||||
defaultAIProfileId,
|
||||
aiProfiles,
|
||||
parentFeature,
|
||||
allFeatures,
|
||||
]);
|
||||
|
||||
const applyProfileToModel = (profile: AIProfile) => {
|
||||
if (profile.provider === 'cursor') {
|
||||
const cursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
|
||||
setModelEntry({ model: cursorModel as ModelAlias });
|
||||
} else if (profile.provider === 'codex') {
|
||||
setModelEntry({
|
||||
model: profile.codexModel || 'codex-gpt-5.2-codex',
|
||||
reasoningEffort: 'none',
|
||||
});
|
||||
} else if (profile.provider === 'opencode') {
|
||||
setModelEntry({ model: profile.opencodeModel || 'opencode/big-pickle' });
|
||||
} else {
|
||||
// Claude
|
||||
setModelEntry({
|
||||
model: profile.model || 'sonnet',
|
||||
thinkingLevel: profile.thinkingLevel || 'none',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleProfileSelect = (profile: AIProfile) => {
|
||||
setSelectedProfileId(profile.id);
|
||||
applyProfileToModel(profile);
|
||||
};
|
||||
|
||||
const handleModelChange = (entry: PhaseModelEntry) => {
|
||||
setModelEntry(entry);
|
||||
// Clear profile selection when manually changing model
|
||||
setSelectedProfileId(undefined);
|
||||
};
|
||||
|
||||
const buildFeatureData = (): FeatureData | null => {
|
||||
@@ -327,7 +272,6 @@ export function AddFeatureDialog({
|
||||
setSkipTests(defaultSkipTests);
|
||||
setBranchName('');
|
||||
setPriority(2);
|
||||
setSelectedProfileId(undefined);
|
||||
setModelEntry({ model: 'opus' });
|
||||
setWorkMode('current');
|
||||
setPlanningMode(defaultPlanningMode);
|
||||
@@ -538,31 +482,14 @@ export function AddFeatureDialog({
|
||||
<span>AI & Execution</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">Profile</Label>
|
||||
<ProfileTypeahead
|
||||
profiles={aiProfiles}
|
||||
selectedProfileId={selectedProfileId}
|
||||
onSelect={handleProfileSelect}
|
||||
placeholder="Select profile..."
|
||||
showManageLink
|
||||
onManageLinkClick={() => {
|
||||
onOpenChange(false);
|
||||
navigate({ to: '/profiles' });
|
||||
}}
|
||||
testIdPrefix="add-feature-profile"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">Model</Label>
|
||||
<PhaseModelSelector
|
||||
value={modelEntry}
|
||||
onChange={handleModelChange}
|
||||
compact
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">Model</Label>
|
||||
<PhaseModelSelector
|
||||
value={modelEntry}
|
||||
onChange={handleModelChange}
|
||||
compact
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -34,21 +34,13 @@ import {
|
||||
import { toast } from 'sonner';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { cn, modelSupportsThinking } from '@/lib/utils';
|
||||
import {
|
||||
Feature,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
AIProfile,
|
||||
useAppStore,
|
||||
PlanningMode,
|
||||
} from '@/store/app-store';
|
||||
import { Feature, ModelAlias, ThinkingLevel, useAppStore, PlanningMode } from '@/store/app-store';
|
||||
import type { ReasoningEffort, PhaseModelEntry, DescriptionHistoryEntry } from '@automaker/types';
|
||||
import {
|
||||
TestingTabContent,
|
||||
PrioritySelector,
|
||||
WorkModeSelector,
|
||||
PlanningModeSelect,
|
||||
ProfileTypeahead,
|
||||
} from '../shared';
|
||||
import type { WorkMode } from '../shared';
|
||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||
@@ -61,13 +53,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { DependencyTreeDialog } from './dependency-tree-dialog';
|
||||
import {
|
||||
isCursorModel,
|
||||
isClaudeModel,
|
||||
PROVIDER_PREFIXES,
|
||||
supportsReasoningEffort,
|
||||
} from '@automaker/types';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { isClaudeModel, supportsReasoningEffort } from '@automaker/types';
|
||||
|
||||
const logger = createLogger('EditFeatureDialog');
|
||||
|
||||
@@ -99,8 +85,6 @@ interface EditFeatureDialogProps {
|
||||
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
|
||||
currentBranch?: string;
|
||||
isMaximized: boolean;
|
||||
showProfilesOnly: boolean;
|
||||
aiProfiles: AIProfile[];
|
||||
allFeatures: Feature[];
|
||||
}
|
||||
|
||||
@@ -113,11 +97,8 @@ export function EditFeatureDialog({
|
||||
branchCardCounts,
|
||||
currentBranch,
|
||||
isMaximized,
|
||||
showProfilesOnly,
|
||||
aiProfiles,
|
||||
allFeatures,
|
||||
}: EditFeatureDialogProps) {
|
||||
const navigate = useNavigate();
|
||||
const [editingFeature, setEditingFeature] = useState<Feature | null>(feature);
|
||||
// Derive initial workMode from feature's branchName
|
||||
const [workMode, setWorkMode] = useState<WorkMode>(() => {
|
||||
@@ -140,7 +121,6 @@ export function EditFeatureDialog({
|
||||
);
|
||||
|
||||
// Model selection state
|
||||
const [selectedProfileId, setSelectedProfileId] = useState<string | undefined>();
|
||||
const [modelEntry, setModelEntry] = useState<PhaseModelEntry>(() => ({
|
||||
model: (feature?.model as ModelAlias) || 'opus',
|
||||
thinkingLevel: feature?.thinkingLevel || 'none',
|
||||
@@ -180,7 +160,6 @@ export function EditFeatureDialog({
|
||||
thinkingLevel: feature.thinkingLevel || 'none',
|
||||
reasoningEffort: feature.reasoningEffort || 'none',
|
||||
});
|
||||
setSelectedProfileId(undefined);
|
||||
} else {
|
||||
setEditFeaturePreviewMap(new Map());
|
||||
setDescriptionChangeSource(null);
|
||||
@@ -188,35 +167,8 @@ export function EditFeatureDialog({
|
||||
}
|
||||
}, [feature]);
|
||||
|
||||
const applyProfileToModel = (profile: AIProfile) => {
|
||||
if (profile.provider === 'cursor') {
|
||||
const cursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
|
||||
setModelEntry({ model: cursorModel as ModelAlias });
|
||||
} else if (profile.provider === 'codex') {
|
||||
setModelEntry({
|
||||
model: profile.codexModel || 'codex-gpt-5.2-codex',
|
||||
reasoningEffort: 'none',
|
||||
});
|
||||
} else if (profile.provider === 'opencode') {
|
||||
setModelEntry({ model: profile.opencodeModel || 'opencode/big-pickle' });
|
||||
} else {
|
||||
// Claude
|
||||
setModelEntry({
|
||||
model: profile.model || 'sonnet',
|
||||
thinkingLevel: profile.thinkingLevel || 'none',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleProfileSelect = (profile: AIProfile) => {
|
||||
setSelectedProfileId(profile.id);
|
||||
applyProfileToModel(profile);
|
||||
};
|
||||
|
||||
const handleModelChange = (entry: PhaseModelEntry) => {
|
||||
setModelEntry(entry);
|
||||
// Clear profile selection when manually changing model
|
||||
setSelectedProfileId(undefined);
|
||||
};
|
||||
|
||||
const handleUpdate = () => {
|
||||
@@ -554,31 +506,14 @@ export function EditFeatureDialog({
|
||||
<span>AI & Execution</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">Profile</Label>
|
||||
<ProfileTypeahead
|
||||
profiles={aiProfiles}
|
||||
selectedProfileId={selectedProfileId}
|
||||
onSelect={handleProfileSelect}
|
||||
placeholder="Select profile..."
|
||||
showManageLink
|
||||
onManageLinkClick={() => {
|
||||
onClose();
|
||||
navigate({ to: '/profiles' });
|
||||
}}
|
||||
testIdPrefix="edit-feature-profile"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">Model</Label>
|
||||
<PhaseModelSelector
|
||||
value={modelEntry}
|
||||
onChange={handleModelChange}
|
||||
compact
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs text-muted-foreground">Model</Label>
|
||||
<PhaseModelSelector
|
||||
value={modelEntry}
|
||||
onChange={handleModelChange}
|
||||
compact
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -12,10 +12,10 @@ import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
import { Feature, ModelAlias, ThinkingLevel, AIProfile, PlanningMode } from '@/store/app-store';
|
||||
import { ProfileSelect, TestingTabContent, PrioritySelect, PlanningModeSelect } from '../shared';
|
||||
import { Feature, ModelAlias, ThinkingLevel, PlanningMode } from '@/store/app-store';
|
||||
import { TestingTabContent, PrioritySelect, PlanningModeSelect } from '../shared';
|
||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||
import { isCursorModel, PROVIDER_PREFIXES, type PhaseModelEntry } from '@automaker/types';
|
||||
import { isCursorModel, type PhaseModelEntry } from '@automaker/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface MassEditDialogProps {
|
||||
@@ -23,8 +23,6 @@ interface MassEditDialogProps {
|
||||
onClose: () => void;
|
||||
selectedFeatures: Feature[];
|
||||
onApply: (updates: Partial<Feature>) => Promise<void>;
|
||||
showProfilesOnly: boolean;
|
||||
aiProfiles: AIProfile[];
|
||||
}
|
||||
|
||||
interface ApplyState {
|
||||
@@ -98,14 +96,7 @@ function FieldWrapper({ label, isMixed, willApply, onApplyChange, children }: Fi
|
||||
);
|
||||
}
|
||||
|
||||
export function MassEditDialog({
|
||||
open,
|
||||
onClose,
|
||||
selectedFeatures,
|
||||
onApply,
|
||||
showProfilesOnly,
|
||||
aiProfiles,
|
||||
}: MassEditDialogProps) {
|
||||
export function MassEditDialog({ open, onClose, selectedFeatures, onApply }: MassEditDialogProps) {
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
|
||||
// Track which fields to apply
|
||||
@@ -149,26 +140,6 @@ export function MassEditDialog({
|
||||
}
|
||||
}, [open, selectedFeatures]);
|
||||
|
||||
const handleModelSelect = (newModel: string) => {
|
||||
const isCursor = isCursorModel(newModel);
|
||||
setModel(newModel as ModelAlias);
|
||||
if (isCursor || !modelSupportsThinking(newModel)) {
|
||||
setThinkingLevel('none');
|
||||
}
|
||||
};
|
||||
|
||||
const handleProfileSelect = (profile: AIProfile) => {
|
||||
if (profile.provider === 'cursor') {
|
||||
const cursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
|
||||
setModel(cursorModel as ModelAlias);
|
||||
setThinkingLevel('none');
|
||||
} else {
|
||||
setModel((profile.model || 'sonnet') as ModelAlias);
|
||||
setThinkingLevel(profile.thinkingLevel || 'none');
|
||||
}
|
||||
setApplyState((prev) => ({ ...prev, model: true, thinkingLevel: true }));
|
||||
};
|
||||
|
||||
const handleApply = async () => {
|
||||
const updates: Partial<Feature> = {};
|
||||
|
||||
@@ -208,29 +179,11 @@ export function MassEditDialog({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4 pr-4 space-y-4 max-h-[60vh] overflow-y-auto">
|
||||
{/* Quick Select Profile Section */}
|
||||
{aiProfiles.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Quick Select Profile</Label>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
Selecting a profile will automatically enable model settings
|
||||
</p>
|
||||
<ProfileSelect
|
||||
profiles={aiProfiles}
|
||||
selectedModel={model}
|
||||
selectedThinkingLevel={thinkingLevel}
|
||||
selectedCursorModel={isCurrentModelCursor ? model : undefined}
|
||||
onSelect={handleProfileSelect}
|
||||
testIdPrefix="mass-edit-profile"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model Selector */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">AI Model</Label>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
Or select a specific model configuration
|
||||
Select a specific model configuration
|
||||
</p>
|
||||
<PhaseModelSelector
|
||||
value={{ model, thinkingLevel }}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -26,8 +26,7 @@ export function GitHubIssuesView() {
|
||||
const [pendingRevalidateOptions, setPendingRevalidateOptions] =
|
||||
useState<ValidateIssueOptions | null>(null);
|
||||
|
||||
const { currentProject, defaultAIProfileId, aiProfiles, getCurrentWorktree, worktreesByProject } =
|
||||
useAppStore();
|
||||
const { currentProject, getCurrentWorktree, worktreesByProject } = useAppStore();
|
||||
|
||||
// Model override for validation
|
||||
const validationModelOverride = useModelOverride({ phase: 'validationModel' });
|
||||
@@ -45,12 +44,6 @@ export function GitHubIssuesView() {
|
||||
onShowValidationDialogChange: setShowValidationDialog,
|
||||
});
|
||||
|
||||
// Get default AI profile for task creation
|
||||
const defaultProfile = useMemo(() => {
|
||||
if (!defaultAIProfileId) return null;
|
||||
return aiProfiles.find((p) => p.id === defaultAIProfileId) ?? null;
|
||||
}, [defaultAIProfileId, aiProfiles]);
|
||||
|
||||
// Get current branch from selected worktree
|
||||
const currentBranch = useMemo(() => {
|
||||
if (!currentProject?.path) return '';
|
||||
@@ -99,9 +92,6 @@ export function GitHubIssuesView() {
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
// Use profile default model
|
||||
const featureModel = defaultProfile?.model ?? 'opus';
|
||||
|
||||
const feature = {
|
||||
id: `issue-${issue.number}-${crypto.randomUUID()}`,
|
||||
title: issue.title,
|
||||
@@ -110,8 +100,8 @@ export function GitHubIssuesView() {
|
||||
status: 'backlog' as const,
|
||||
passes: false,
|
||||
priority: getFeaturePriority(validation.estimatedComplexity),
|
||||
model: featureModel,
|
||||
thinkingLevel: defaultProfile?.thinkingLevel ?? 'none',
|
||||
model: 'opus',
|
||||
thinkingLevel: 'none' as const,
|
||||
branchName: currentBranch,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
@@ -129,7 +119,7 @@ export function GitHubIssuesView() {
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to create task');
|
||||
}
|
||||
},
|
||||
[currentProject?.path, defaultProfile, currentBranch]
|
||||
[currentProject?.path, currentBranch]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { useAppStore, AIProfile } from '@/store/app-store';
|
||||
import {
|
||||
useKeyboardShortcuts,
|
||||
useKeyboardShortcutsConfig,
|
||||
KeyboardShortcut,
|
||||
} from '@/hooks/use-keyboard-shortcuts';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Sparkles } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/delete-confirm-dialog';
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCenter,
|
||||
} from '@dnd-kit/core';
|
||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { SortableProfileCard, ProfileForm, ProfilesHeader } from './profiles-view/components';
|
||||
|
||||
export function ProfilesView() {
|
||||
const {
|
||||
aiProfiles,
|
||||
addAIProfile,
|
||||
updateAIProfile,
|
||||
removeAIProfile,
|
||||
reorderAIProfiles,
|
||||
resetAIProfiles,
|
||||
} = useAppStore();
|
||||
const shortcuts = useKeyboardShortcutsConfig();
|
||||
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
const [editingProfile, setEditingProfile] = useState<AIProfile | null>(null);
|
||||
const [profileToDelete, setProfileToDelete] = useState<AIProfile | null>(null);
|
||||
|
||||
// Sensors for drag-and-drop
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 5,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Separate built-in and custom profiles
|
||||
const builtInProfiles = useMemo(() => aiProfiles.filter((p) => p.isBuiltIn), [aiProfiles]);
|
||||
const customProfiles = useMemo(() => aiProfiles.filter((p) => !p.isBuiltIn), [aiProfiles]);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = aiProfiles.findIndex((p) => p.id === active.id);
|
||||
const newIndex = aiProfiles.findIndex((p) => p.id === over.id);
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
reorderAIProfiles(oldIndex, newIndex);
|
||||
}
|
||||
}
|
||||
},
|
||||
[aiProfiles, reorderAIProfiles]
|
||||
);
|
||||
|
||||
const handleAddProfile = (profile: Omit<AIProfile, 'id'>) => {
|
||||
addAIProfile(profile);
|
||||
setShowAddDialog(false);
|
||||
toast.success('Profile created', {
|
||||
description: `Created "${profile.name}" profile`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateProfile = (profile: Omit<AIProfile, 'id'>) => {
|
||||
if (editingProfile) {
|
||||
updateAIProfile(editingProfile.id, profile);
|
||||
setEditingProfile(null);
|
||||
toast.success('Profile updated', {
|
||||
description: `Updated "${profile.name}" profile`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeleteProfile = () => {
|
||||
if (!profileToDelete) return;
|
||||
|
||||
removeAIProfile(profileToDelete.id);
|
||||
toast.success('Profile deleted', {
|
||||
description: `Deleted "${profileToDelete.name}" profile`,
|
||||
});
|
||||
setProfileToDelete(null);
|
||||
};
|
||||
|
||||
const handleResetProfiles = () => {
|
||||
resetAIProfiles();
|
||||
toast.success('Profiles refreshed', {
|
||||
description: 'Default profiles have been updated to the latest version',
|
||||
});
|
||||
};
|
||||
|
||||
// Build keyboard shortcuts for profiles view
|
||||
const profilesShortcuts: KeyboardShortcut[] = useMemo(() => {
|
||||
const shortcutsList: KeyboardShortcut[] = [];
|
||||
|
||||
// Add profile shortcut - when in profiles view
|
||||
shortcutsList.push({
|
||||
key: shortcuts.addProfile,
|
||||
action: () => setShowAddDialog(true),
|
||||
description: 'Create new profile',
|
||||
});
|
||||
|
||||
return shortcutsList;
|
||||
}, [shortcuts]);
|
||||
|
||||
// Register keyboard shortcuts for profiles view
|
||||
useKeyboardShortcuts(profilesShortcuts);
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="profiles-view">
|
||||
{/* Header Section */}
|
||||
<ProfilesHeader
|
||||
onResetProfiles={handleResetProfiles}
|
||||
onAddProfile={() => setShowAddDialog(true)}
|
||||
addProfileHotkey={shortcuts.addProfile}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
<div className="max-w-4xl mx-auto space-y-8">
|
||||
{/* Custom Profiles Section */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<h2 className="text-lg font-semibold text-foreground">Custom Profiles</h2>
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary">
|
||||
{customProfiles.length}
|
||||
</span>
|
||||
</div>
|
||||
{customProfiles.length === 0 ? (
|
||||
<div
|
||||
className="group rounded-xl border border-dashed border-border p-8 text-center transition-all duration-300 hover:border-primary hover:bg-primary/5 cursor-pointer"
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
>
|
||||
<Sparkles className="w-10 h-10 text-muted-foreground mx-auto mb-3 opacity-50 transition-all duration-300 group-hover:text-primary group-hover:opacity-100 group-hover:scale-110 group-hover:rotate-12" />
|
||||
<p className="text-muted-foreground transition-colors duration-300 group-hover:text-foreground">
|
||||
No custom profiles yet. Create one to get started!
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={customProfiles.map((p) => p.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{customProfiles.map((profile) => (
|
||||
<SortableProfileCard
|
||||
key={profile.id}
|
||||
profile={profile}
|
||||
onEdit={() => setEditingProfile(profile)}
|
||||
onDelete={() => setProfileToDelete(profile)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Built-in Profiles Section */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<h2 className="text-lg font-semibold text-foreground">Built-in Profiles</h2>
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
|
||||
{builtInProfiles.length}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Pre-configured profiles for common use cases. These cannot be edited or deleted.
|
||||
</p>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={builtInProfiles.map((p) => p.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{builtInProfiles.map((profile) => (
|
||||
<SortableProfileCard
|
||||
key={profile.id}
|
||||
profile={profile}
|
||||
onEdit={() => {}}
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Profile Dialog */}
|
||||
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||||
<DialogContent
|
||||
data-testid="add-profile-dialog"
|
||||
className="flex flex-col max-h-[calc(100vh-4rem)]"
|
||||
>
|
||||
<DialogHeader className="shrink-0">
|
||||
<DialogTitle>Create New Profile</DialogTitle>
|
||||
<DialogDescription>Define a reusable model configuration preset.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ProfileForm
|
||||
profile={{}}
|
||||
onSave={handleAddProfile}
|
||||
onCancel={() => setShowAddDialog(false)}
|
||||
isEditing={false}
|
||||
hotkeyActive={showAddDialog}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Edit Profile Dialog */}
|
||||
<Dialog open={!!editingProfile} onOpenChange={() => setEditingProfile(null)}>
|
||||
<DialogContent
|
||||
data-testid="edit-profile-dialog"
|
||||
className="flex flex-col max-h-[calc(100vh-4rem)]"
|
||||
>
|
||||
<DialogHeader className="shrink-0">
|
||||
<DialogTitle>Edit Profile</DialogTitle>
|
||||
<DialogDescription>Modify your profile settings.</DialogDescription>
|
||||
</DialogHeader>
|
||||
{editingProfile && (
|
||||
<ProfileForm
|
||||
profile={editingProfile}
|
||||
onSave={handleUpdateProfile}
|
||||
onCancel={() => setEditingProfile(null)}
|
||||
isEditing={true}
|
||||
hotkeyActive={!!editingProfile}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<DeleteConfirmDialog
|
||||
open={!!profileToDelete}
|
||||
onOpenChange={(open) => !open && setProfileToDelete(null)}
|
||||
onConfirm={confirmDeleteProfile}
|
||||
title="Delete Profile"
|
||||
description={
|
||||
profileToDelete
|
||||
? `Are you sure you want to delete "${profileToDelete.name}"? This action cannot be undone.`
|
||||
: ''
|
||||
}
|
||||
confirmText="Delete Profile"
|
||||
testId="delete-profile-confirm-dialog"
|
||||
confirmTestId="confirm-delete-profile-button"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { SortableProfileCard } from './sortable-profile-card';
|
||||
export { ProfileForm } from './profile-form';
|
||||
export { ProfilesHeader } from './profiles-header';
|
||||
@@ -1,560 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { HotkeyButton } from '@/components/ui/hotkey-button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { cn, modelSupportsThinking } from '@/lib/utils';
|
||||
import { DialogFooter } from '@/components/ui/dialog';
|
||||
import { Brain } from 'lucide-react';
|
||||
import { AnthropicIcon, CursorIcon, OpenAIIcon, OpenCodeIcon } from '@/components/ui/provider-icon';
|
||||
import { toast } from 'sonner';
|
||||
import type {
|
||||
AIProfile,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
ModelProvider,
|
||||
CursorModelId,
|
||||
CodexModelId,
|
||||
OpencodeModelId,
|
||||
} from '@automaker/types';
|
||||
import {
|
||||
CURSOR_MODEL_MAP,
|
||||
cursorModelHasThinking,
|
||||
CODEX_MODEL_MAP,
|
||||
OPENCODE_MODELS,
|
||||
DEFAULT_OPENCODE_MODEL,
|
||||
} from '@automaker/types';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { CLAUDE_MODELS, THINKING_LEVELS, ICON_OPTIONS } from '../constants';
|
||||
|
||||
interface ProfileFormProps {
|
||||
profile: Partial<AIProfile>;
|
||||
onSave: (profile: Omit<AIProfile, 'id'>) => void;
|
||||
onCancel: () => void;
|
||||
isEditing: boolean;
|
||||
hotkeyActive: boolean;
|
||||
}
|
||||
|
||||
export function ProfileForm({
|
||||
profile,
|
||||
onSave,
|
||||
onCancel,
|
||||
isEditing,
|
||||
hotkeyActive,
|
||||
}: ProfileFormProps) {
|
||||
const { enabledCursorModels } = useAppStore();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: profile.name || '',
|
||||
description: profile.description || '',
|
||||
provider: (profile.provider || 'claude') as ModelProvider,
|
||||
// Claude-specific
|
||||
model: profile.model || ('sonnet' as ModelAlias),
|
||||
thinkingLevel: profile.thinkingLevel || ('none' as ThinkingLevel),
|
||||
// Cursor-specific
|
||||
cursorModel: profile.cursorModel || ('auto' as CursorModelId),
|
||||
// Codex-specific - use a valid CodexModelId from CODEX_MODEL_MAP
|
||||
codexModel: profile.codexModel || (CODEX_MODEL_MAP.gpt52Codex as CodexModelId),
|
||||
// OpenCode-specific
|
||||
opencodeModel: profile.opencodeModel || (DEFAULT_OPENCODE_MODEL as OpencodeModelId),
|
||||
icon: profile.icon || 'Brain',
|
||||
});
|
||||
|
||||
// Sync formData with profile prop when it changes
|
||||
useEffect(() => {
|
||||
setFormData({
|
||||
name: profile.name || '',
|
||||
description: profile.description || '',
|
||||
provider: (profile.provider || 'claude') as ModelProvider,
|
||||
// Claude-specific
|
||||
model: profile.model || ('sonnet' as ModelAlias),
|
||||
thinkingLevel: profile.thinkingLevel || ('none' as ThinkingLevel),
|
||||
// Cursor-specific
|
||||
cursorModel: profile.cursorModel || ('auto' as CursorModelId),
|
||||
// Codex-specific - use a valid CodexModelId from CODEX_MODEL_MAP
|
||||
codexModel: profile.codexModel || (CODEX_MODEL_MAP.gpt52Codex as CodexModelId),
|
||||
// OpenCode-specific
|
||||
opencodeModel: profile.opencodeModel || (DEFAULT_OPENCODE_MODEL as OpencodeModelId),
|
||||
icon: profile.icon || 'Brain',
|
||||
});
|
||||
}, [profile]);
|
||||
|
||||
const supportsThinking = formData.provider === 'claude' && modelSupportsThinking(formData.model);
|
||||
|
||||
const handleProviderChange = (provider: ModelProvider) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
provider,
|
||||
// Only reset Claude fields when switching TO Claude; preserve otherwise
|
||||
model: provider === 'claude' ? 'sonnet' : formData.model,
|
||||
thinkingLevel: provider === 'claude' ? 'none' : formData.thinkingLevel,
|
||||
// Reset cursor/codex/opencode models when switching to that provider
|
||||
cursorModel: provider === 'cursor' ? 'auto' : formData.cursorModel,
|
||||
codexModel:
|
||||
provider === 'codex' ? (CODEX_MODEL_MAP.gpt52Codex as CodexModelId) : formData.codexModel,
|
||||
opencodeModel:
|
||||
provider === 'opencode'
|
||||
? (DEFAULT_OPENCODE_MODEL as OpencodeModelId)
|
||||
: formData.opencodeModel,
|
||||
});
|
||||
};
|
||||
|
||||
const handleModelChange = (model: ModelAlias) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
model,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCursorModelChange = (cursorModel: CursorModelId) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
cursorModel,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCodexModelChange = (codexModel: CodexModelId) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
codexModel,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpencodeModelChange = (opencodeModel: OpencodeModelId) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
opencodeModel,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formData.name.trim()) {
|
||||
toast.error('Please enter a profile name');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure model is always set for Claude profiles
|
||||
const validModels: ModelAlias[] = ['haiku', 'sonnet', 'opus'];
|
||||
const finalModel =
|
||||
formData.provider === 'claude'
|
||||
? validModels.includes(formData.model)
|
||||
? formData.model
|
||||
: 'sonnet'
|
||||
: undefined;
|
||||
|
||||
const baseProfile = {
|
||||
name: formData.name.trim(),
|
||||
description: formData.description.trim(),
|
||||
provider: formData.provider,
|
||||
isBuiltIn: false,
|
||||
icon: formData.icon,
|
||||
};
|
||||
|
||||
if (formData.provider === 'cursor') {
|
||||
onSave({
|
||||
...baseProfile,
|
||||
cursorModel: formData.cursorModel,
|
||||
});
|
||||
} else if (formData.provider === 'codex') {
|
||||
onSave({
|
||||
...baseProfile,
|
||||
codexModel: formData.codexModel,
|
||||
});
|
||||
} else if (formData.provider === 'opencode') {
|
||||
onSave({
|
||||
...baseProfile,
|
||||
opencodeModel: formData.opencodeModel,
|
||||
});
|
||||
} else {
|
||||
onSave({
|
||||
...baseProfile,
|
||||
model: finalModel as ModelAlias,
|
||||
thinkingLevel: supportsThinking ? formData.thinkingLevel : 'none',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-y-auto flex-1 min-h-0 space-y-4 pr-3 -mr-3 pl-1">
|
||||
{/* Name */}
|
||||
<div className="mt-2 space-y-2">
|
||||
<Label htmlFor="profile-name">Profile Name</Label>
|
||||
<Input
|
||||
id="profile-name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="e.g., Heavy Task, Quick Fix"
|
||||
data-testid="profile-name-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="profile-description">Description</Label>
|
||||
<Textarea
|
||||
id="profile-description"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="Describe when to use this profile..."
|
||||
rows={2}
|
||||
data-testid="profile-description-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Icon Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>Icon</Label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{ICON_OPTIONS.map(({ name, icon: Icon }) => (
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, icon: name })}
|
||||
className={cn(
|
||||
'w-10 h-10 rounded-lg flex items-center justify-center border transition-colors',
|
||||
formData.icon === name
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid={`icon-select-${name}`}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Provider Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>AI Provider</Label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleProviderChange('claude')}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
|
||||
formData.provider === 'claude'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid="provider-select-claude"
|
||||
>
|
||||
<AnthropicIcon className="w-4 h-4" />
|
||||
Claude
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleProviderChange('cursor')}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
|
||||
formData.provider === 'cursor'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid="provider-select-cursor"
|
||||
>
|
||||
<CursorIcon className="w-4 h-4" />
|
||||
Cursor
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleProviderChange('codex')}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
|
||||
formData.provider === 'codex'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid="provider-select-codex"
|
||||
>
|
||||
<OpenAIIcon className="w-4 h-4" />
|
||||
Codex
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleProviderChange('opencode')}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-center gap-2',
|
||||
formData.provider === 'opencode'
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid="provider-select-opencode"
|
||||
>
|
||||
<OpenCodeIcon className="w-4 h-4" />
|
||||
OpenCode
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Claude Model Selection */}
|
||||
{formData.provider === 'claude' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Brain className="w-4 h-4 text-primary" />
|
||||
Model
|
||||
</Label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{CLAUDE_MODELS.map(({ id, label }) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => handleModelChange(id)}
|
||||
className={cn(
|
||||
'flex-1 min-w-[100px] px-3 py-2 rounded-md border text-sm font-medium transition-colors',
|
||||
formData.model === id
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid={`model-select-${id}`}
|
||||
>
|
||||
{label.replace('Claude ', '')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cursor Model Selection */}
|
||||
{formData.provider === 'cursor' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<CursorIcon className="w-4 h-4 text-primary" />
|
||||
Cursor Model
|
||||
</Label>
|
||||
<div className="flex flex-col gap-2">
|
||||
{enabledCursorModels.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground p-3 border border-dashed rounded-md text-center">
|
||||
No Cursor models enabled. Enable models in Settings → AI Providers.
|
||||
</div>
|
||||
) : (
|
||||
Object.entries(CURSOR_MODEL_MAP)
|
||||
.filter(([id]) => enabledCursorModels.includes(id as CursorModelId))
|
||||
.map(([id, config]) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => handleCursorModelChange(id as CursorModelId)}
|
||||
className={cn(
|
||||
'w-full px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-between',
|
||||
formData.cursorModel === id
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid={`cursor-model-select-${id}`}
|
||||
>
|
||||
<span>{config.label}</span>
|
||||
<div className="flex gap-1">
|
||||
{config.hasThinking && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'text-xs',
|
||||
formData.cursorModel === id
|
||||
? 'border-primary-foreground/50 text-primary-foreground'
|
||||
: 'border-amber-500/50 text-amber-600 dark:text-amber-400'
|
||||
)}
|
||||
>
|
||||
Thinking
|
||||
</Badge>
|
||||
)}
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
'text-xs',
|
||||
formData.cursorModel === id && 'bg-primary-foreground/20'
|
||||
)}
|
||||
>
|
||||
Tier
|
||||
</Badge>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{formData.cursorModel && cursorModelHasThinking(formData.cursorModel) && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This model has built-in extended thinking capabilities.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Codex Model Selection */}
|
||||
{formData.provider === 'codex' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<OpenAIIcon className="w-4 h-4 text-primary" />
|
||||
Codex Model
|
||||
</Label>
|
||||
<div className="flex flex-col gap-2">
|
||||
{Object.entries(CODEX_MODEL_MAP).map(([_, modelId]) => {
|
||||
const modelConfig = {
|
||||
label: modelId,
|
||||
badge: 'Standard' as const,
|
||||
hasReasoning: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
key={modelId}
|
||||
type="button"
|
||||
onClick={() => handleCodexModelChange(modelId)}
|
||||
className={cn(
|
||||
'w-full px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-between',
|
||||
formData.codexModel === modelId
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid={`codex-model-select-${modelId}`}
|
||||
>
|
||||
<span>{modelConfig.label}</span>
|
||||
<div className="flex gap-1">
|
||||
{modelConfig.hasReasoning && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'text-xs',
|
||||
formData.codexModel === modelId
|
||||
? 'border-primary-foreground/50 text-primary-foreground'
|
||||
: 'border-amber-500/50 text-amber-600 dark:text-amber-400'
|
||||
)}
|
||||
>
|
||||
Reasoning
|
||||
</Badge>
|
||||
)}
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'text-xs',
|
||||
formData.codexModel === modelId
|
||||
? 'border-primary-foreground/50 text-primary-foreground'
|
||||
: 'border-muted-foreground/50 text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{modelConfig.badge}
|
||||
</Badge>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OpenCode Model Selection */}
|
||||
{formData.provider === 'opencode' && (
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<OpenCodeIcon className="w-4 h-4 text-primary" />
|
||||
OpenCode Model
|
||||
</Label>
|
||||
<div className="flex flex-col gap-2 max-h-[300px] overflow-y-auto">
|
||||
{OPENCODE_MODELS.map((model) => (
|
||||
<button
|
||||
key={model.id}
|
||||
type="button"
|
||||
onClick={() => handleOpencodeModelChange(model.id)}
|
||||
className={cn(
|
||||
'w-full px-3 py-2 rounded-md border text-sm font-medium transition-colors flex items-center justify-between',
|
||||
formData.opencodeModel === model.id
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid={`opencode-model-select-${model.id}`}
|
||||
>
|
||||
<div className="flex flex-col items-start gap-0.5">
|
||||
<span>{model.label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs',
|
||||
formData.opencodeModel === model.id
|
||||
? 'text-primary-foreground/70'
|
||||
: 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{model.description}
|
||||
</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'text-xs capitalize shrink-0',
|
||||
formData.opencodeModel === model.id
|
||||
? 'border-primary-foreground/50 text-primary-foreground'
|
||||
: model.tier === 'free'
|
||||
? 'border-green-500/50 text-green-600 dark:text-green-400'
|
||||
: model.tier === 'premium'
|
||||
? 'border-amber-500/50 text-amber-600 dark:text-amber-400'
|
||||
: 'border-muted-foreground/50 text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{model.tier}
|
||||
</Badge>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Claude Thinking Level */}
|
||||
{formData.provider === 'claude' && supportsThinking && (
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Brain className="w-4 h-4 text-amber-500" />
|
||||
Thinking Level
|
||||
</Label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{THINKING_LEVELS.map(({ id, label }) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFormData({ ...formData, thinkingLevel: id });
|
||||
if (id === 'ultrathink') {
|
||||
toast.warning('Ultrathink uses extensive reasoning', {
|
||||
description:
|
||||
'Best for complex architecture, migrations, or deep debugging (~$0.48/task).',
|
||||
duration: 4000,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'flex-1 min-w-[70px] px-3 py-2 rounded-md border text-sm font-medium transition-colors',
|
||||
formData.thinkingLevel === id
|
||||
? 'bg-amber-500 text-white border-amber-400'
|
||||
: 'bg-background hover:bg-accent border-border'
|
||||
)}
|
||||
data-testid={`thinking-select-${id}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Higher levels give more time to reason through complex problems.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<DialogFooter className="pt-4 border-t border-border mt-4 shrink-0">
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<HotkeyButton
|
||||
onClick={handleSubmit}
|
||||
hotkey={{ key: 'Enter', cmdCtrl: true }}
|
||||
hotkeyActive={hotkeyActive}
|
||||
data-testid="save-profile-button"
|
||||
>
|
||||
{isEditing ? 'Save Changes' : 'Create Profile'}
|
||||
</HotkeyButton>
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { HotkeyButton } from '@/components/ui/hotkey-button';
|
||||
import { UserCircle, Plus, RefreshCw } from 'lucide-react';
|
||||
|
||||
interface ProfilesHeaderProps {
|
||||
onResetProfiles: () => void;
|
||||
onAddProfile: () => void;
|
||||
addProfileHotkey: string;
|
||||
}
|
||||
|
||||
export function ProfilesHeader({
|
||||
onResetProfiles,
|
||||
onAddProfile,
|
||||
addProfileHotkey,
|
||||
}: ProfilesHeaderProps) {
|
||||
return (
|
||||
<div className="shrink-0 border-b border-border bg-glass backdrop-blur-md">
|
||||
<div className="px-8 py-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
|
||||
<UserCircle className="w-5 h-5 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">AI Profiles</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create and manage model configuration presets
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onResetProfiles}
|
||||
data-testid="refresh-profiles-button"
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Refresh Defaults
|
||||
</Button>
|
||||
<HotkeyButton
|
||||
onClick={onAddProfile}
|
||||
hotkey={addProfileHotkey}
|
||||
hotkeyActive={false}
|
||||
data-testid="add-profile-button"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Profile
|
||||
</HotkeyButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { GripVertical, Lock, Pencil, Trash2 } from 'lucide-react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import type { AIProfile } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP, profileHasThinking, getCodexModelLabel } from '@automaker/types';
|
||||
import { PROFILE_ICONS } from '../constants';
|
||||
import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
|
||||
interface SortableProfileCardProps {
|
||||
profile: AIProfile;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export function SortableProfileCard({ profile, onEdit, onDelete }: SortableProfileCardProps) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: profile.id,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
const getDefaultIcon = () => {
|
||||
if (profile.provider === 'cursor') return CursorIcon;
|
||||
if (profile.provider === 'codex') return OpenAIIcon;
|
||||
return AnthropicIcon;
|
||||
};
|
||||
|
||||
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : getDefaultIcon();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
'group relative flex items-start gap-4 p-4 rounded-xl border bg-card transition-all',
|
||||
isDragging && 'shadow-lg',
|
||||
profile.isBuiltIn
|
||||
? 'border-border/50'
|
||||
: 'border-border hover:border-primary/50 hover:shadow-sm'
|
||||
)}
|
||||
data-testid={`profile-card-${profile.id}`}
|
||||
>
|
||||
{/* Drag Handle */}
|
||||
<button
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="p-1 rounded hover:bg-accent cursor-grab active:cursor-grabbing flex-shrink-0 mt-1"
|
||||
data-testid={`profile-drag-handle-${profile.id}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label={`Reorder ${profile.name} profile`}
|
||||
>
|
||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
|
||||
{/* Icon */}
|
||||
<div className="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center bg-primary/10">
|
||||
{IconComponent && <IconComponent className="w-5 h-5 text-primary" />}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-foreground">{profile.name}</h3>
|
||||
{profile.isBuiltIn && (
|
||||
<span className="flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground">
|
||||
<Lock className="w-2.5 h-2.5" />
|
||||
Built-in
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-0.5 line-clamp-2">{profile.description}</p>
|
||||
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
||||
{/* Provider badge */}
|
||||
<span className="text-xs px-2 py-0.5 rounded-full border border-border text-muted-foreground bg-muted/50 flex items-center gap-1">
|
||||
{profile.provider === 'cursor' ? (
|
||||
<CursorIcon className="w-3 h-3" />
|
||||
) : profile.provider === 'codex' ? (
|
||||
<OpenAIIcon className="w-3 h-3" />
|
||||
) : (
|
||||
<AnthropicIcon className="w-3 h-3" />
|
||||
)}
|
||||
{profile.provider === 'cursor'
|
||||
? 'Cursor'
|
||||
: profile.provider === 'codex'
|
||||
? 'Codex'
|
||||
: 'Claude'}
|
||||
</span>
|
||||
|
||||
{/* Model badge */}
|
||||
<span className="text-xs px-2 py-0.5 rounded-full border border-primary/30 text-primary bg-primary/10">
|
||||
{profile.provider === 'cursor'
|
||||
? CURSOR_MODEL_MAP[profile.cursorModel || 'auto']?.label ||
|
||||
profile.cursorModel ||
|
||||
'auto'
|
||||
: profile.provider === 'codex'
|
||||
? getCodexModelLabel(profile.codexModel || 'codex-gpt-5.2-codex')
|
||||
: profile.model || 'sonnet'}
|
||||
</span>
|
||||
|
||||
{/* Thinking badge - works for both providers */}
|
||||
{profileHasThinking(profile) && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full border border-amber-500/30 text-amber-600 dark:text-amber-400 bg-amber-500/10">
|
||||
{profile.provider === 'cursor' ? 'Thinking' : profile.thinkingLevel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{!profile.isBuiltIn && (
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onEdit}
|
||||
className="h-8 w-8 p-0"
|
||||
data-testid={`edit-profile-${profile.id}`}
|
||||
aria-label={`Edit ${profile.name} profile`}
|
||||
>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onDelete}
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||||
data-testid={`delete-profile-${profile.id}`}
|
||||
aria-label={`Delete ${profile.name} profile`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Brain, Zap, Scale, Cpu, Rocket, Sparkles } from 'lucide-react';
|
||||
import type { ModelAlias, ThinkingLevel } from '@/store/app-store';
|
||||
import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
|
||||
// Icon mapping for profiles
|
||||
export const PROFILE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
Brain,
|
||||
Zap,
|
||||
Scale,
|
||||
Cpu,
|
||||
Rocket,
|
||||
Sparkles,
|
||||
Anthropic: AnthropicIcon,
|
||||
Cursor: CursorIcon,
|
||||
Codex: OpenAIIcon,
|
||||
};
|
||||
|
||||
// Available icons for selection
|
||||
export const ICON_OPTIONS = [
|
||||
{ name: 'Brain', icon: Brain },
|
||||
{ name: 'Zap', icon: Zap },
|
||||
{ name: 'Scale', icon: Scale },
|
||||
{ name: 'Cpu', icon: Cpu },
|
||||
{ name: 'Rocket', icon: Rocket },
|
||||
{ name: 'Sparkles', icon: Sparkles },
|
||||
{ name: 'Anthropic', icon: AnthropicIcon },
|
||||
{ name: 'Cursor', icon: CursorIcon },
|
||||
{ name: 'Codex', icon: OpenAIIcon },
|
||||
];
|
||||
|
||||
// Model options for the form
|
||||
export const CLAUDE_MODELS: { id: ModelAlias; label: string }[] = [
|
||||
{ id: 'haiku', label: 'Claude Haiku' },
|
||||
{ id: 'sonnet', label: 'Claude Sonnet' },
|
||||
{ id: 'opus', label: 'Claude Opus' },
|
||||
];
|
||||
|
||||
export const THINKING_LEVELS: { id: ThinkingLevel; label: string }[] = [
|
||||
{ id: 'none', label: 'None' },
|
||||
{ id: 'low', label: 'Low' },
|
||||
{ id: 'medium', label: 'Medium' },
|
||||
{ id: 'high', label: 'High' },
|
||||
{ id: 'ultrathink', label: 'Ultrathink' },
|
||||
];
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { ModelAlias, ModelProvider, AIProfile } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP } from '@automaker/types';
|
||||
|
||||
// Helper to determine provider from model (legacy, always returns 'claude')
|
||||
export function getProviderFromModel(model: ModelAlias): ModelProvider {
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an AI profile for completeness and correctness
|
||||
*/
|
||||
export function validateProfile(profile: Partial<AIProfile>): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Name is required
|
||||
if (!profile.name?.trim()) {
|
||||
errors.push('Profile name is required');
|
||||
}
|
||||
|
||||
// Provider must be valid
|
||||
if (!profile.provider || !['claude', 'cursor'].includes(profile.provider)) {
|
||||
errors.push('Invalid provider');
|
||||
}
|
||||
|
||||
// Claude-specific validation
|
||||
if (profile.provider === 'claude') {
|
||||
if (!profile.model) {
|
||||
errors.push('Claude model is required');
|
||||
} else if (!['haiku', 'sonnet', 'opus'].includes(profile.model)) {
|
||||
errors.push('Invalid Claude model');
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor-specific validation
|
||||
if (profile.provider === 'cursor') {
|
||||
if (profile.cursorModel && !(profile.cursorModel in CURSOR_MODEL_MAP)) {
|
||||
errors.push('Invalid Cursor model');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
@@ -42,8 +42,6 @@ export function SettingsView() {
|
||||
setSkipVerificationInAutoMode,
|
||||
useWorktrees,
|
||||
setUseWorktrees,
|
||||
showProfilesOnly,
|
||||
setShowProfilesOnly,
|
||||
muteDoneSound,
|
||||
setMuteDoneSound,
|
||||
currentProject,
|
||||
@@ -52,9 +50,6 @@ export function SettingsView() {
|
||||
setDefaultPlanningMode,
|
||||
defaultRequirePlanApproval,
|
||||
setDefaultRequirePlanApproval,
|
||||
defaultAIProfileId,
|
||||
setDefaultAIProfileId,
|
||||
aiProfiles,
|
||||
autoLoadClaudeMd,
|
||||
setAutoLoadClaudeMd,
|
||||
promptCustomization,
|
||||
@@ -151,23 +146,18 @@ export function SettingsView() {
|
||||
case 'defaults':
|
||||
return (
|
||||
<FeatureDefaultsSection
|
||||
showProfilesOnly={showProfilesOnly}
|
||||
defaultSkipTests={defaultSkipTests}
|
||||
enableDependencyBlocking={enableDependencyBlocking}
|
||||
skipVerificationInAutoMode={skipVerificationInAutoMode}
|
||||
useWorktrees={useWorktrees}
|
||||
defaultPlanningMode={defaultPlanningMode}
|
||||
defaultRequirePlanApproval={defaultRequirePlanApproval}
|
||||
defaultAIProfileId={defaultAIProfileId}
|
||||
aiProfiles={aiProfiles}
|
||||
onShowProfilesOnlyChange={setShowProfilesOnly}
|
||||
onDefaultSkipTestsChange={setDefaultSkipTests}
|
||||
onEnableDependencyBlockingChange={setEnableDependencyBlocking}
|
||||
onSkipVerificationInAutoModeChange={setSkipVerificationInAutoMode}
|
||||
onUseWorktreesChange={setUseWorktrees}
|
||||
onDefaultPlanningModeChange={setDefaultPlanningMode}
|
||||
onDefaultRequirePlanApprovalChange={setDefaultRequirePlanApproval}
|
||||
onDefaultAIProfileIdChange={setDefaultAIProfileId}
|
||||
/>
|
||||
);
|
||||
case 'account':
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
FlaskConical,
|
||||
Settings2,
|
||||
TestTube,
|
||||
GitBranch,
|
||||
AlertCircle,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
FileText,
|
||||
ScrollText,
|
||||
ShieldCheck,
|
||||
User,
|
||||
FastForward,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -22,53 +20,38 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import type { AIProfile } from '@/store/app-store';
|
||||
|
||||
type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||
|
||||
interface FeatureDefaultsSectionProps {
|
||||
showProfilesOnly: boolean;
|
||||
defaultSkipTests: boolean;
|
||||
enableDependencyBlocking: boolean;
|
||||
skipVerificationInAutoMode: boolean;
|
||||
useWorktrees: boolean;
|
||||
defaultPlanningMode: PlanningMode;
|
||||
defaultRequirePlanApproval: boolean;
|
||||
defaultAIProfileId: string | null;
|
||||
aiProfiles: AIProfile[];
|
||||
onShowProfilesOnlyChange: (value: boolean) => void;
|
||||
onDefaultSkipTestsChange: (value: boolean) => void;
|
||||
onEnableDependencyBlockingChange: (value: boolean) => void;
|
||||
onSkipVerificationInAutoModeChange: (value: boolean) => void;
|
||||
onUseWorktreesChange: (value: boolean) => void;
|
||||
onDefaultPlanningModeChange: (value: PlanningMode) => void;
|
||||
onDefaultRequirePlanApprovalChange: (value: boolean) => void;
|
||||
onDefaultAIProfileIdChange: (value: string | null) => void;
|
||||
}
|
||||
|
||||
export function FeatureDefaultsSection({
|
||||
showProfilesOnly,
|
||||
defaultSkipTests,
|
||||
enableDependencyBlocking,
|
||||
skipVerificationInAutoMode,
|
||||
useWorktrees,
|
||||
defaultPlanningMode,
|
||||
defaultRequirePlanApproval,
|
||||
defaultAIProfileId,
|
||||
aiProfiles,
|
||||
onShowProfilesOnlyChange,
|
||||
onDefaultSkipTestsChange,
|
||||
onEnableDependencyBlockingChange,
|
||||
onSkipVerificationInAutoModeChange,
|
||||
onUseWorktreesChange,
|
||||
onDefaultPlanningModeChange,
|
||||
onDefaultRequirePlanApprovalChange,
|
||||
onDefaultAIProfileIdChange,
|
||||
}: FeatureDefaultsSectionProps) {
|
||||
// Find the selected profile name for display
|
||||
const selectedProfile = defaultAIProfileId
|
||||
? aiProfiles.find((p) => p.id === defaultAIProfileId)
|
||||
: null;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -194,71 +177,6 @@ export function FeatureDefaultsSection({
|
||||
{/* Separator */}
|
||||
{defaultPlanningMode === 'skip' && <div className="border-t border-border/30" />}
|
||||
|
||||
{/* Default AI Profile */}
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
<div className="w-10 h-10 mt-0.5 rounded-xl flex items-center justify-center shrink-0 bg-brand-500/10">
|
||||
<User className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-foreground font-medium">Default AI Profile</Label>
|
||||
<Select
|
||||
value={defaultAIProfileId ?? 'none'}
|
||||
onValueChange={(v: string) => onDefaultAIProfileIdChange(v === 'none' ? null : v)}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] h-8" data-testid="default-ai-profile-select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">
|
||||
<span className="text-muted-foreground">None (pick manually)</span>
|
||||
</SelectItem>
|
||||
{aiProfiles.map((profile) => (
|
||||
<SelectItem key={profile.id} value={profile.id}>
|
||||
<span>{profile.name}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
{selectedProfile
|
||||
? `New features will use the "${selectedProfile.name}" profile (${selectedProfile.model}, ${selectedProfile.thinkingLevel} thinking).`
|
||||
: 'Pre-select an AI profile when creating new features. Choose "None" to pick manually each time.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="border-t border-border/30" />
|
||||
|
||||
{/* Profiles Only Setting */}
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
<Checkbox
|
||||
id="show-profiles-only"
|
||||
checked={showProfilesOnly}
|
||||
onCheckedChange={(checked) => onShowProfilesOnlyChange(checked === true)}
|
||||
className="mt-1"
|
||||
data-testid="show-profiles-only-checkbox"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label
|
||||
htmlFor="show-profiles-only"
|
||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||
>
|
||||
<Settings2 className="w-4 h-4 text-brand-500" />
|
||||
Show profiles only by default
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
When enabled, the Add Feature dialog will show only AI profiles and hide advanced
|
||||
model tweaking options. This creates a cleaner, less overwhelming UI.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="border-t border-border/30" />
|
||||
|
||||
{/* Automated Testing Setting */}
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
<Checkbox
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
Image,
|
||||
TestTube,
|
||||
Brain,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface WikiSection {
|
||||
@@ -236,12 +235,6 @@ export function WikiView() {
|
||||
description:
|
||||
'Each feature runs in its own git worktree for safe parallel development.',
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: 'AI Profiles',
|
||||
description:
|
||||
'Pre-configured model + thinking level combinations for different task types.',
|
||||
},
|
||||
{
|
||||
icon: Terminal,
|
||||
title: 'Integrated Terminal',
|
||||
@@ -424,10 +417,6 @@ export function WikiView() {
|
||||
file: 'views/terminal-view/',
|
||||
desc: 'Integrated terminal with splits and tabs',
|
||||
},
|
||||
{
|
||||
file: 'views/profiles-view.tsx',
|
||||
desc: 'AI profile management (model + thinking presets)',
|
||||
},
|
||||
{
|
||||
file: 'store/app-store.ts',
|
||||
desc: 'Central Zustand state management',
|
||||
@@ -528,12 +517,6 @@ export function WikiView() {
|
||||
steps
|
||||
</p>
|
||||
</li>
|
||||
<li className="text-foreground">
|
||||
<strong>Configure AI Profile</strong>
|
||||
<p className="text-muted-foreground ml-5 mt-1">
|
||||
Choose an AI profile or customize model/thinking settings per feature
|
||||
</p>
|
||||
</li>
|
||||
<li className="text-foreground">
|
||||
<strong>Start Implementation</strong>
|
||||
<p className="text-muted-foreground ml-5 mt-1">
|
||||
@@ -555,9 +538,6 @@ export function WikiView() {
|
||||
<code className="px-1 py-0.5 bg-brand-500/20 rounded">?</code> to see all)
|
||||
</li>
|
||||
<li>Enable git worktree isolation for parallel feature development</li>
|
||||
<li>
|
||||
Start with "Quick Edit" profile for simple tasks, use "Heavy Task" for complex work
|
||||
</li>
|
||||
<li>Keep your app spec up to date as your project evolves</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user