Agent profiles, be able to select model on task creation

This commit is contained in:
AndyMik90
2025-12-16 18:05:51 +01:00
parent a891225b4e
commit d735c5c303
10 changed files with 312 additions and 20 deletions
+1
View File
@@ -355,6 +355,7 @@ export default function App() {
<ProgressCircle value={25} size="sm" />
<ProgressCircle value={50} size="md" />
<ProgressCircle value={75} size="lg" />
<ProgressCircle value={100} size="lg" color="var(--color-semantic-success)" />
</div>
</Card>
+6 -8
View File
@@ -48,9 +48,8 @@ export interface AgentAPI {
// Roadmap Operations
getRoadmap: (projectId: string) => Promise<IPCResult<Roadmap | null>>;
saveRoadmap: (projectId: string, roadmap: Roadmap) => Promise<IPCResult>;
generateRoadmap: (projectId: string) => void;
refreshRoadmap: (projectId: string) => void;
updateFeatureStatus: (
generateRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void;
refreshRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void; updateFeatureStatus: (
projectId: string,
featureId: string,
status: RoadmapFeatureStatus
@@ -177,11 +176,10 @@ export const createAgentAPI = (): AgentAPI => ({
saveRoadmap: (projectId: string, roadmap: Roadmap): Promise<IPCResult> =>
ipcRenderer.invoke(IPC_CHANNELS.ROADMAP_SAVE, projectId, roadmap),
generateRoadmap: (projectId: string): void =>
ipcRenderer.send(IPC_CHANNELS.ROADMAP_GENERATE, projectId),
refreshRoadmap: (projectId: string): void =>
ipcRenderer.send(IPC_CHANNELS.ROADMAP_REFRESH, projectId),
generateRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean): void =>
ipcRenderer.send(IPC_CHANNELS.ROADMAP_GENERATE, projectId, enableCompetitorAnalysis),
refreshRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean): void =>
ipcRenderer.send(IPC_CHANNELS.ROADMAP_REFRESH, projectId, enableCompetitorAnalysis),
updateFeatureStatus: (
projectId: string,
+4
View File
@@ -28,6 +28,7 @@ import { Insights } from './components/Insights';
import { GitHubIssues } from './components/GitHubIssues';
import { Changelog } from './components/Changelog';
import { Worktrees } from './components/Worktrees';
import { AgentProfiles } from './components/AgentProfiles';
import { WelcomeScreen } from './components/WelcomeScreen';
import { RateLimitModal } from './components/RateLimitModal';
import { SDKRateLimitModal } from './components/SDKRateLimitModal';
@@ -326,6 +327,9 @@ export function App() {
{activeView === 'worktrees' && selectedProjectId && (
<Worktrees projectId={selectedProjectId} />
)}
{activeView === 'agent-profiles' && (
<AgentProfiles />
)}
{activeView === 'agent-tools' && (
<div className="flex h-full items-center justify-center">
<div className="text-center">
@@ -0,0 +1,141 @@
import { Brain, Scale, Zap, Check } from 'lucide-react';
import { cn } from '../lib/utils';
import { DEFAULT_AGENT_PROFILES, AVAILABLE_MODELS, THINKING_LEVELS } from '../../shared/constants';
import { useSettingsStore, saveSettings } from '../stores/settings-store';
import type { AgentProfile } from '../../shared/types/settings';
/**
* Icon mapping for agent profile icons
*/
const iconMap: Record<string, React.ElementType> = {
Brain,
Scale,
Zap
};
/**
* Agent Profiles view component
* Displays preset agent profiles for quick model/thinking level configuration
*/
export function AgentProfiles() {
const settings = useSettingsStore((state) => state.settings);
const selectedProfileId = settings.selectedAgentProfile || 'balanced';
const handleSelectProfile = async (profileId: string) => {
await saveSettings({ selectedAgentProfile: profileId });
};
/**
* Get human-readable model label
*/
const getModelLabel = (modelValue: string): string => {
const model = AVAILABLE_MODELS.find((m) => m.value === modelValue);
return model?.label || modelValue;
};
/**
* Get human-readable thinking level label
*/
const getThinkingLabel = (thinkingValue: string): string => {
const level = THINKING_LEVELS.find((l) => l.value === thinkingValue);
return level?.label || thinkingValue;
};
/**
* Render a single profile card
*/
const renderProfileCard = (profile: AgentProfile) => {
const isSelected = selectedProfileId === profile.id;
const Icon = iconMap[profile.icon || 'Brain'] || Brain;
return (
<button
key={profile.id}
onClick={() => handleSelectProfile(profile.id)}
className={cn(
'relative w-full rounded-xl border p-6 text-left transition-all duration-200',
'hover:border-primary/50 hover:shadow-md',
isSelected
? 'border-primary bg-primary/5 shadow-sm'
: 'border-border bg-card'
)}
>
{/* Selected indicator */}
{isSelected && (
<div className="absolute right-4 top-4 flex h-6 w-6 items-center justify-center rounded-full bg-primary">
<Check className="h-4 w-4 text-primary-foreground" />
</div>
)}
{/* Profile content */}
<div className="flex items-start gap-4">
<div
className={cn(
'flex h-12 w-12 items-center justify-center rounded-lg',
isSelected ? 'bg-primary/10' : 'bg-muted'
)}
>
<Icon
className={cn(
'h-6 w-6',
isSelected ? 'text-primary' : 'text-muted-foreground'
)}
/>
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-foreground">{profile.name}</h3>
<p className="mt-1 text-sm text-muted-foreground">
{profile.description}
</p>
{/* Model and thinking level badges */}
<div className="mt-4 flex flex-wrap gap-2">
<span className="inline-flex items-center rounded-md bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
{getModelLabel(profile.model)}
</span>
<span className="inline-flex items-center rounded-md bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
{getThinkingLabel(profile.thinkingLevel)} Thinking
</span>
</div>
</div>
</div>
</button>
);
};
return (
<div className="h-full flex flex-col overflow-hidden">
{/* Header */}
<div className="shrink-0 border-b border-border bg-background px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-foreground">Agent Profiles</h1>
<p className="mt-1 text-sm text-muted-foreground">
Select a preset configuration for model and thinking level
</p>
</div>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-auto p-6">
<div className="max-w-2xl mx-auto space-y-4">
{/* Description */}
<div className="rounded-lg bg-muted/50 p-4 mb-6">
<p className="text-sm text-muted-foreground">
Agent profiles provide preset configurations for Claude model and thinking level.
When you create a new task, these settings will be used as defaults. You can always
override them in the task creation wizard.
</p>
</div>
{/* Profile cards */}
<div className="space-y-3">
{DEFAULT_AGENT_PROFILES.map(renderProfileCard)}
</div>
</div>
</div>
</div>
);
}
@@ -18,7 +18,8 @@ import {
FileText,
Sparkles,
GitBranch,
HelpCircle
HelpCircle,
UserCog
} from 'lucide-react';
import { Button } from './ui/button';
import { ScrollArea } from './ui/scroll-area';
@@ -57,7 +58,7 @@ import { AddProjectModal } from './AddProjectModal';
import { RateLimitIndicator } from './RateLimitIndicator';
import type { Project, AutoBuildVersionInfo } from '../../shared/types';
export type SidebarView = 'kanban' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'github-issues' | 'changelog' | 'insights' | 'worktrees' | 'agent-tools';
export type SidebarView = 'kanban' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'github-issues' | 'changelog' | 'insights' | 'worktrees' | 'agent-tools' | 'agent-profiles';
interface SidebarProps {
onSettingsClick: () => void;
@@ -85,7 +86,8 @@ const projectNavItems: NavItem[] = [
const toolsNavItems: NavItem[] = [
{ id: 'github-issues', label: 'GitHub Issues', icon: Github, shortcut: 'G' },
{ id: 'worktrees', label: 'Worktrees', icon: GitBranch, shortcut: 'W' }
{ id: 'worktrees', label: 'Worktrees', icon: GitBranch, shortcut: 'W' },
{ id: 'agent-profiles', label: 'Agent Profiles', icon: UserCog, shortcut: 'P' }
];
export function Sidebar({
@@ -30,15 +30,19 @@ import {
} from './ImageUpload';
import { createTask, saveDraft, loadDraft, clearDraft, isDraftEmpty } from '../stores/task-store';
import { cn } from '../lib/utils';
import type { TaskCategory, TaskPriority, TaskComplexity, TaskImpact, TaskMetadata, ImageAttachment, TaskDraft } from '../../shared/types';
import type { TaskCategory, TaskPriority, TaskComplexity, TaskImpact, TaskMetadata, ImageAttachment, TaskDraft, ModelType, ThinkingLevel } from '../../shared/types';
import {
TASK_CATEGORY_LABELS,
TASK_PRIORITY_LABELS,
TASK_COMPLEXITY_LABELS,
TASK_IMPACT_LABELS,
MAX_IMAGES_PER_TASK,
ALLOWED_IMAGE_TYPES_DISPLAY
ALLOWED_IMAGE_TYPES_DISPLAY,
DEFAULT_AGENT_PROFILES,
AVAILABLE_MODELS,
THINKING_LEVELS
} from '../../shared/constants';
import { useSettingsStore } from '../stores/settings-store';
interface TaskCreationWizardProps {
projectId: string;
@@ -51,6 +55,12 @@ export function TaskCreationWizard({
open,
onOpenChange
}: TaskCreationWizardProps) {
// Get selected agent profile from settings
const { settings } = useSettingsStore();
const selectedProfile = DEFAULT_AGENT_PROFILES.find(
p => p.id === settings.selectedAgentProfile
) || DEFAULT_AGENT_PROFILES.find(p => p.id === 'balanced')!;
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [isCreating, setIsCreating] = useState(false);
@@ -64,6 +74,10 @@ export function TaskCreationWizard({
const [complexity, setComplexity] = useState<TaskComplexity | ''>('');
const [impact, setImpact] = useState<TaskImpact | ''>('');
// Model configuration (initialized from selected agent profile)
const [model, setModel] = useState<ModelType | ''>(selectedProfile.model);
const [thinkingLevel, setThinkingLevel] = useState<ThinkingLevel | ''>(selectedProfile.thinkingLevel);
// Image attachments
const [images, setImages] = useState<ImageAttachment[]>([]);
@@ -77,7 +91,7 @@ export function TaskCreationWizard({
// Ref for the textarea to handle paste events
const descriptionRef = useRef<HTMLTextAreaElement>(null);
// Load draft when dialog opens
// Load draft when dialog opens, or initialize from selected profile
useEffect(() => {
if (open && projectId) {
const draft = loadDraft(projectId);
@@ -88,6 +102,9 @@ export function TaskCreationWizard({
setPriority(draft.priority);
setComplexity(draft.complexity);
setImpact(draft.impact);
// Load model/thinkingLevel from draft if present, otherwise use profile defaults
setModel(draft.model || selectedProfile.model);
setThinkingLevel(draft.thinkingLevel || selectedProfile.thinkingLevel);
setImages(draft.images);
setRequireReviewBeforeCoding(draft.requireReviewBeforeCoding ?? false);
setIsDraftRestored(true);
@@ -99,9 +116,13 @@ export function TaskCreationWizard({
if (draft.images.length > 0) {
setShowImages(true);
}
} else {
// No draft - initialize model/thinkingLevel from selected profile
setModel(selectedProfile.model);
setThinkingLevel(selectedProfile.thinkingLevel);
}
}
}, [open, projectId]);
}, [open, projectId, selectedProfile.model, selectedProfile.thinkingLevel]);
/**
* Get current form state as a draft
@@ -114,10 +135,12 @@ export function TaskCreationWizard({
priority,
complexity,
impact,
model,
thinkingLevel,
images,
requireReviewBeforeCoding,
savedAt: new Date()
}), [projectId, title, description, category, priority, complexity, impact, images, requireReviewBeforeCoding]);
}), [projectId, title, description, category, priority, complexity, impact, model, thinkingLevel, images, requireReviewBeforeCoding]);
/**
* Handle paste event for screenshot support
@@ -218,6 +241,8 @@ export function TaskCreationWizard({
if (priority) metadata.priority = priority;
if (complexity) metadata.complexity = complexity;
if (impact) metadata.impact = impact;
if (model) metadata.model = model;
if (thinkingLevel) metadata.thinkingLevel = thinkingLevel;
if (images.length > 0) metadata.attachedImages = images;
if (requireReviewBeforeCoding) metadata.requireReviewBeforeCoding = true;
@@ -246,6 +271,9 @@ export function TaskCreationWizard({
setPriority('');
setComplexity('');
setImpact('');
// Reset model/thinkingLevel to selected profile defaults
setModel(selectedProfile.model);
setThinkingLevel(selectedProfile.thinkingLevel);
setImages([]);
setRequireReviewBeforeCoding(false);
setError(null);
@@ -351,6 +379,58 @@ export function TaskCreationWizard({
</p>
</div>
{/* Model Selection */}
<div className="space-y-2">
<Label htmlFor="model" className="text-sm font-medium text-foreground">
Model
</Label>
<Select
value={model}
onValueChange={(value) => setModel(value as ModelType)}
disabled={isCreating}
>
<SelectTrigger id="model" className="h-9">
<SelectValue placeholder="Select model" />
</SelectTrigger>
<SelectContent>
{AVAILABLE_MODELS.map((m) => (
<SelectItem key={m.value} value={m.value}>
{m.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
The Claude model to use for this task. Defaults to your selected agent profile.
</p>
</div>
{/* Thinking Level Selection */}
<div className="space-y-2">
<Label htmlFor="thinking-level" className="text-sm font-medium text-foreground">
Thinking Level
</Label>
<Select
value={thinkingLevel}
onValueChange={(value) => setThinkingLevel(value as ThinkingLevel)}
disabled={isCreating}
>
<SelectTrigger id="thinking-level" className="h-9">
<SelectValue placeholder="Select thinking level" />
</SelectTrigger>
<SelectContent>
{THINKING_LEVELS.map((level) => (
<SelectItem key={level.value} value={level.value}>
{level.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Extended thinking depth for complex reasoning. Higher levels use more tokens but provide deeper analysis.
</p>
</div>
{/* Paste Success Indicator */}
{pasteSuccess && (
<div className="flex items-center gap-2 text-sm text-success animate-in fade-in slide-in-from-top-1 duration-200">
+42 -1
View File
@@ -2,6 +2,8 @@
* Shared constants for Auto Claude UI
*/
import type { AgentProfile } from './types/settings';
// Task status columns in Kanban board order
export const TASK_STATUS_COLUMNS = [
'backlog',
@@ -99,7 +101,9 @@ export const DEFAULT_APP_SETTINGS = {
},
// Global API keys (used as defaults for all projects)
globalClaudeOAuthToken: undefined as string | undefined,
globalOpenAIApiKey: undefined as string | undefined
globalOpenAIApiKey: undefined as string | undefined,
// Selected agent profile - defaults to 'balanced' for good speed/quality balance
selectedAgentProfile: 'balanced'
};
// Default project settings
@@ -421,6 +425,43 @@ export const AVAILABLE_MODELS = [
{ value: 'haiku', label: 'Claude Haiku 3.5' }
] as const;
// Thinking levels for Claude model (budget token allocation)
export const THINKING_LEVELS = [
{ value: 'none', label: 'None', description: 'No extended thinking' },
{ value: 'low', label: 'Low', description: 'Brief consideration' },
{ value: 'medium', label: 'Medium', description: 'Moderate analysis' },
{ value: 'high', label: 'High', description: 'Deep thinking' },
{ value: 'ultrathink', label: 'Ultra Think', description: 'Maximum reasoning depth' }
] as const;
// Default agent profiles for preset model/thinking configurations
export const DEFAULT_AGENT_PROFILES: AgentProfile[] = [
{
id: 'complex',
name: 'Complex Tasks',
description: 'For intricate, multi-step implementations requiring deep analysis',
model: 'opus',
thinkingLevel: 'ultrathink',
icon: 'Brain'
},
{
id: 'balanced',
name: 'Balanced',
description: 'Good balance of speed and quality for most tasks',
model: 'sonnet',
thinkingLevel: 'medium',
icon: 'Scale'
},
{
id: 'quick',
name: 'Quick Edits',
description: 'Fast iterations for simple changes and quick fixes',
model: 'haiku',
thinkingLevel: 'low',
icon: 'Zap'
}
];
// Memory backends
export const MEMORY_BACKENDS = [
{ value: 'file', label: 'File-based (default)' },
+2 -3
View File
@@ -221,9 +221,8 @@ export interface ElectronAPI {
// Roadmap operations
getRoadmap: (projectId: string) => Promise<IPCResult<Roadmap | null>>;
saveRoadmap: (projectId: string, roadmap: Roadmap) => Promise<IPCResult>;
generateRoadmap: (projectId: string) => void;
refreshRoadmap: (projectId: string) => void;
updateFeatureStatus: (
generateRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void;
refreshRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void; updateFeatureStatus: (
projectId: string,
featureId: string,
status: RoadmapFeatureStatus
@@ -4,6 +4,19 @@
import type { NotificationSettings } from './project';
// Thinking level for Claude model (budget token allocation)
export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'ultrathink';
// Agent profile for preset model/thinking configurations
export interface AgentProfile {
id: string;
name: string;
description: string;
model: 'haiku' | 'sonnet' | 'opus';
thinkingLevel: ThinkingLevel;
icon?: string; // Lucide icon name
}
export interface AppSettings {
theme: 'light' | 'dark' | 'system';
defaultModel: string;
@@ -18,6 +31,8 @@ export interface AppSettings {
globalOpenAIApiKey?: string;
// Onboarding wizard completion state
onboardingCompleted?: boolean;
// Selected agent profile for preset model/thinking configurations
selectedAgentProfile?: string;
}
// Auto-Claude Source Environment Configuration (for auto-claude repo .env)
+11
View File
@@ -2,6 +2,8 @@
* Task-related types
*/
import type { ThinkingLevel } from './settings';
export type TaskStatus = 'backlog' | 'in_progress' | 'ai_review' | 'human_review' | 'done';
// Reason why a task is in human_review status
@@ -125,6 +127,8 @@ export interface TaskDraft {
priority: TaskPriority | '';
complexity: TaskComplexity | '';
impact: TaskImpact | '';
model: ModelType | '';
thinkingLevel: ThinkingLevel | '';
images: ImageAttachment[];
requireReviewBeforeCoding?: boolean;
savedAt: Date;
@@ -134,6 +138,9 @@ export interface TaskDraft {
export type TaskComplexity = 'trivial' | 'small' | 'medium' | 'large' | 'complex';
export type TaskImpact = 'low' | 'medium' | 'high' | 'critical';
export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent';
// Re-export ThinkingLevel (defined in settings.ts) for convenience
export type { ThinkingLevel };
export type ModelType = 'haiku' | 'sonnet' | 'opus';
export type TaskCategory =
| 'feature'
| 'bug_fix'
@@ -188,6 +195,10 @@ export interface TaskMetadata {
// Review settings
requireReviewBeforeCoding?: boolean; // Require human review of spec/plan before coding starts
// Agent configuration (from agent profile or manual selection)
model?: ModelType; // Claude model to use (haiku, sonnet, opus)
thinkingLevel?: ThinkingLevel; // Thinking budget level (none, low, medium, high, ultrathink)
// Archive status
archivedAt?: string; // ISO date when task was archived
archivedInVersion?: string; // Version in which task was archived (from changelog)