mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
Merge branch 'feature/176536627888-implement-profile-view-and-in-the-sideba' into feat/worktree-system
This commit is contained in:
@@ -72,9 +72,9 @@
|
||||
"description": "When agent finish work the cards is moved either to waiting approval or into verified one But mostly its include some type of summary at the end i want you to modify our prompts and ui so when its in both states we can see the feature summary of what was done / modified instead of relying on going to code editor to see what got changed etc.",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-09T22:09:13.684Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"imagePaths": [],
|
||||
"startedAt": "2025-12-09T22:09:13.684Z",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
@@ -84,9 +84,9 @@
|
||||
"description": "When running new feature in skip automated testing once its got finished its moved to waiting approval for us to manual test it / follow up prompt. Once we are satisfied we can click commit button so ai agent can commit it work this is only hapening in this scenerio because if we have unchecked the skip automated testing its do it automaticly and commit already. But the issue is when its going to commit we move it to in progress state where we can use stop button and if user use that button its moved to backlog column and. that kinda break what we are doing becase we have no longer even abbility to move it back to waiting approval or to run commit button / follow up again so if user use manual one and stop the commit i want it to be again moved back to waiting approval state / column",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-09T22:31:41.946Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"imagePaths": [],
|
||||
"startedAt": "2025-12-09T22:31:41.946Z",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
@@ -102,9 +102,9 @@
|
||||
"agent execute task with correct model "
|
||||
],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-09T23:07:37.223Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": false,
|
||||
"imagePaths": [],
|
||||
"startedAt": "2025-12-09T23:07:37.223Z",
|
||||
"summary": "Added model selection (Haiku/Sonnet/Opus) and thinking level (None/Low/Medium/High) controls to feature creation and edit dialogs. Modified: app-store.ts (added AgentModel and ThinkingLevel types), board-view.tsx (UI controls), feature-executor.js (dynamic model/thinking config), feature-loader.js (field persistence). Agent now executes with user-selected model and extended thinking settings.",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
@@ -115,9 +115,9 @@
|
||||
"description": "I want you to refactor the add new feature modal there are to many settings going on and its hard / annoyig to navigate lets split the settings in modal into tabs \nprompt icon - prompt and category\ngear icon - model and thinking ( here i would also like to split somehow the claude with thinking and codex that dont use it )\ntest icon - skip automated testing and verification steps\n",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-10T02:17:18.943Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"imagePaths": [],
|
||||
"startedAt": "2025-12-10T02:17:18.943Z",
|
||||
"summary": "Made model selection buttons compact. Removed descriptions and badges from cards, now shows short model names (Haiku, Sonnet, Opus, Max, Codex, Mini) in horizontal row. Full description available on hover. Modified: board-view.tsx (renderModelOptions function).",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "high"
|
||||
@@ -128,7 +128,7 @@
|
||||
"description": "Make the add new feature modal widther ",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-10T02:25:21.328Z",
|
||||
"skipTests": true,
|
||||
"imagePaths": [
|
||||
{
|
||||
"id": "img-1765333063064-qygrbjul4",
|
||||
@@ -137,7 +137,7 @@
|
||||
"mimeType": "image/png"
|
||||
}
|
||||
],
|
||||
"skipTests": true,
|
||||
"startedAt": "2025-12-10T02:25:21.328Z",
|
||||
"summary": "Increased dialog max-width from max-w-md/max-w-lg to max-w-2xl. Modified: app/src/components/ui/dialog.tsx. This makes the add new feature modal and all other dialogs wider (from 448-512px to 672px) for better content display.",
|
||||
"model": "haiku",
|
||||
"thinkingLevel": "none"
|
||||
@@ -148,9 +148,9 @@
|
||||
"description": "For example i got haiku model running or codex one but we can still see opus 4.5 check if it not hardcoded and fix it to use proper model name that was used in this task",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-10T02:40:43.706Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"imagePaths": [],
|
||||
"startedAt": "2025-12-10T02:40:43.706Z",
|
||||
"summary": "Kanban cards now render the agent info model badge using feature.model so the displayed model matches the one selected for the task.",
|
||||
"model": "gpt-5.1-codex",
|
||||
"thinkingLevel": "none"
|
||||
@@ -161,7 +161,7 @@
|
||||
"description": "describe the attached image do not change code",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-10T02:02:54.785Z",
|
||||
"skipTests": true,
|
||||
"imagePaths": [
|
||||
{
|
||||
"id": "img-1765331797511-v4ssc1hha",
|
||||
@@ -170,7 +170,7 @@
|
||||
"mimeType": "image/png"
|
||||
}
|
||||
],
|
||||
"skipTests": true,
|
||||
"startedAt": "2025-12-10T02:02:54.785Z",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
@@ -180,7 +180,7 @@
|
||||
"description": "Add claude and codex to the left sidebar of settings so its will scroll to thoes sections as well",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-10T09:32:31.638Z",
|
||||
"skipTests": true,
|
||||
"imagePaths": [
|
||||
{
|
||||
"id": "img-1765358823366-6vchdhwsj",
|
||||
@@ -189,7 +189,7 @@
|
||||
"mimeType": "image/png"
|
||||
}
|
||||
],
|
||||
"skipTests": true,
|
||||
"startedAt": "2025-12-10T09:32:31.638Z",
|
||||
"model": "sonnet",
|
||||
"thinkingLevel": "none"
|
||||
},
|
||||
@@ -199,9 +199,9 @@
|
||||
"description": "When u write new feature for ai agent and attacht context images and change tab to choose diff model and go back to prompt tab the image preview break and im not sure if it even saved properly in state to be later attached check it out for me",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-10T09:59:02.988Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"imagePaths": [],
|
||||
"startedAt": "2025-12-10T09:59:02.988Z",
|
||||
"summary": "Fixed image preview breaking when switching tabs in Add Feature modal. Added previewMap/onPreviewMapChange props to DescriptionImageDropZone component to lift preview state up to parent. Modified: description-image-dropzone.tsx (added parent-controlled state support), board-view.tsx (added newFeaturePreviewMap and followUpPreviewMap state, wired up to DescriptionImageDropZone). Image paths were already stored correctly in state - only the preview thumbnails (base64) were lost on tab switch due to component unmounting.",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "high"
|
||||
@@ -212,7 +212,7 @@
|
||||
"description": "Take a look at waiting aproval column in kanban board and fix the card that render in it u can see in attached images that they text is overlaping check other columns how we have them",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-10T10:46:42.494Z",
|
||||
"skipTests": true,
|
||||
"imagePaths": [
|
||||
{
|
||||
"id": "img-1765363296205-e4cwlj2j8",
|
||||
@@ -233,7 +233,7 @@
|
||||
"mimeType": "image/png"
|
||||
}
|
||||
],
|
||||
"skipTests": true,
|
||||
"startedAt": "2025-12-10T10:46:42.494Z",
|
||||
"model": "sonnet",
|
||||
"thinkingLevel": "low"
|
||||
},
|
||||
@@ -243,9 +243,9 @@
|
||||
"description": "I want to have some abbility when executing a task on project to have some type of rewing / checkpoint system so if the changes made by agent in the project dont satisfy me / break something i can click in the ui to revert them. The best way for it would be to implement github worktress so when spin up new task claude take a look at it generate new branch that fit task issue and make it as gihub worktree then we would create a a new folder in project .automaker/worktree with branch name and clone of repo so agent can freely work one something like that ",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-10T11:11:06.115Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"imagePaths": [],
|
||||
"startedAt": "2025-12-10T11:11:06.115Z",
|
||||
"summary": "Implemented Git Worktree Checkpoint/Revert System. Created: worktree-manager.js service. Modified: auto-mode-service.js (worktree integration, revert/merge methods), feature-loader.js (worktree tracking), main.js (IPC handlers), preload.js (API exposure), app-store.ts (Feature type), electron.d.ts (types), electron.ts (mock API), kanban-card.tsx (branch badge, revert/merge buttons), board-view.tsx (handlers). Features: isolated git branches per feature, branch badge on cards, revert changes button, merge to main button, file diff APIs.",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "ultrathink"
|
||||
@@ -256,9 +256,9 @@
|
||||
"description": "When a agent is workig on task or when its in waiting approval column its would be nice to have some type of git diff panel and see what files got changed as well as reusing our custom themes we have in settings for the editor view of it take a look at codebase and create implementation for it",
|
||||
"steps": [],
|
||||
"status": "verified",
|
||||
"startedAt": "2025-12-10T11:16:54.069Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"imagePaths": [],
|
||||
"startedAt": "2025-12-10T11:16:54.069Z",
|
||||
"summary": "Added git diff panel for in-progress and waiting approval features. Created GitDiffPanel component with themed syntax highlighting. Modified: git-diff-panel.tsx (new), agent-output-modal.tsx, worktree-manager.js, auto-mode-service.js, main.js, preload.js, electron.d.ts. The panel shows changed files with +/- stats and expandable unified diff view using CSS theme variables.",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "ultrathink"
|
||||
@@ -269,29 +269,11 @@
|
||||
"description": "Implement profile view and in the sidebar the profile view would allow user to defined different ai provider profiels like heavy-task would be claude opus model with ultrathink or debugging would be codex max. This will give user flexibillity in our model tab to quickly use own defined profiles preset of models.",
|
||||
"steps": [],
|
||||
"status": "waiting_approval",
|
||||
"startedAt": "2025-12-10T11:31:20.842Z",
|
||||
"imagePaths": [],
|
||||
"skipTests": true,
|
||||
"imagePaths": [],
|
||||
"startedAt": "2025-12-10T11:31:20.842Z",
|
||||
"summary": "Implemented AI Profiles feature for managing model configuration presets. Created: profiles-view.tsx. Modified: app-store.ts (added AIProfile type, state, and CRUD actions), sidebar.tsx (added profiles nav item), page.tsx (added profiles view routing), board-view.tsx (added Quick Select Profile section in Add/Edit Feature dialogs). Features: 5 built-in profiles (Heavy Task, Balanced, Quick Edit, Codex Power, Codex Fast), custom profile CRUD, drag-and-drop reordering, quick profile selection in feature dialogs.",
|
||||
"model": "opus",
|
||||
"thinkingLevel": "high"
|
||||
},
|
||||
{
|
||||
"id": "feature-1765367758697-ihrkgxiaq",
|
||||
"category": "Kanban",
|
||||
"description": "So we added ai profiles add a default option to setting view that would determinate to show only profiles fir a new feautre modal in modal tab instead of showing profiles + custom selection this functionallity will clean up model prompt when someone will rely on own profiles ",
|
||||
"steps": [],
|
||||
"status": "waiting_approval",
|
||||
"startedAt": "2025-12-10T11:56:01.001Z",
|
||||
"imagePaths": [
|
||||
{
|
||||
"id": "img-1765367746901-1n1jqfkej",
|
||||
"path": "/Users/shirone/Library/Application Support/automaker/images/1765367746897-ymoyjh19i_SCR-20251210-lpki.png",
|
||||
"filename": "SCR-20251210-lpki.png",
|
||||
"mimeType": "image/png"
|
||||
}
|
||||
],
|
||||
"skipTests": true,
|
||||
"model": "sonnet",
|
||||
"thinkingLevel": "medium"
|
||||
}
|
||||
]
|
||||
@@ -10,6 +10,7 @@ import { SettingsView } from "@/components/views/settings-view";
|
||||
import { AgentToolsView } from "@/components/views/agent-tools-view";
|
||||
import { InterviewView } from "@/components/views/interview-view";
|
||||
import { ContextView } from "@/components/views/context-view";
|
||||
import { ProfilesView } from "@/components/views/profiles-view";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI, isElectron } from "@/lib/electron";
|
||||
|
||||
@@ -109,6 +110,8 @@ export default function Home() {
|
||||
return <InterviewView />;
|
||||
case "context":
|
||||
return <ContextView />;
|
||||
case "profiles":
|
||||
return <ProfilesView />;
|
||||
default:
|
||||
return <WelcomeView />;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
GripVertical,
|
||||
Trash2,
|
||||
Undo2,
|
||||
UserCircle,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -357,6 +358,11 @@ export function Sidebar() {
|
||||
icon: Wrench,
|
||||
shortcut: NAV_SHORTCUTS.tools,
|
||||
},
|
||||
{
|
||||
id: "profiles",
|
||||
label: "AI Profiles",
|
||||
icon: UserCircle,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
FeatureImagePath,
|
||||
AgentModel,
|
||||
ThinkingLevel,
|
||||
AIProfile,
|
||||
} from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { cn, modelSupportsThinking } from "@/lib/utils";
|
||||
@@ -71,6 +72,11 @@ import {
|
||||
Brain,
|
||||
Zap,
|
||||
Settings2,
|
||||
Scale,
|
||||
Cpu,
|
||||
Rocket,
|
||||
Sparkles,
|
||||
UserCircle,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
@@ -155,6 +161,16 @@ const CODEX_MODELS: ModelOption[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// Profile icon mapping
|
||||
const PROFILE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
Brain,
|
||||
Zap,
|
||||
Scale,
|
||||
Cpu,
|
||||
Rocket,
|
||||
Sparkles,
|
||||
};
|
||||
|
||||
export function BoardView() {
|
||||
const {
|
||||
currentProject,
|
||||
@@ -168,6 +184,7 @@ export function BoardView() {
|
||||
maxConcurrency,
|
||||
setMaxConcurrency,
|
||||
defaultSkipTests,
|
||||
aiProfiles,
|
||||
} = useAppStore();
|
||||
const [activeFeature, setActiveFeature] = useState<Feature | null>(null);
|
||||
const [editingFeature, setEditingFeature] = useState<Feature | null>(null);
|
||||
@@ -1675,6 +1692,89 @@ export function BoardView() {
|
||||
|
||||
{/* Model Tab */}
|
||||
<TabsContent value="model" className="space-y-4 overflow-y-auto">
|
||||
{/* Quick Select Profile Section */}
|
||||
{aiProfiles.length > 0 && (
|
||||
<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">
|
||||
{aiProfiles.slice(0, 6).map((profile) => {
|
||||
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain;
|
||||
const isCodex = profile.provider === "codex";
|
||||
const isSelected = newFeature.model === profile.model &&
|
||||
newFeature.thinkingLevel === profile.thinkingLevel;
|
||||
return (
|
||||
<button
|
||||
key={profile.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model: profile.model,
|
||||
thinkingLevel: profile.thinkingLevel,
|
||||
});
|
||||
if (profile.thinkingLevel === "ultrathink") {
|
||||
toast.warning("Ultrathink Selected", {
|
||||
description: "Ultrathink uses extensive reasoning (45-180s, ~$0.48/task).",
|
||||
duration: 4000,
|
||||
});
|
||||
}
|
||||
}}
|
||||
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={`profile-quick-select-${profile.id}`}
|
||||
>
|
||||
<div className={cn(
|
||||
"w-7 h-7 rounded flex items-center justify-center flex-shrink-0",
|
||||
isCodex ? "bg-emerald-500/10" : "bg-primary/10"
|
||||
)}>
|
||||
{IconComponent && (
|
||||
<IconComponent className={cn(
|
||||
"w-4 h-4",
|
||||
isCodex ? "text-emerald-500" : "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">
|
||||
{profile.model}{profile.thinkingLevel !== "none" && ` + ${profile.thinkingLevel}`}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Or customize below. Manage profiles in{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowAddDialog(false);
|
||||
useAppStore.getState().setCurrentView("profiles");
|
||||
}}
|
||||
className="text-brand-500 hover:underline"
|
||||
>
|
||||
AI Profiles
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Separator */}
|
||||
{aiProfiles.length > 0 && <div className="border-t border-border" />}
|
||||
|
||||
{/* Claude Models Section */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -1918,6 +2018,79 @@ export function BoardView() {
|
||||
|
||||
{/* Model Tab */}
|
||||
<TabsContent value="model" className="space-y-4 overflow-y-auto">
|
||||
{/* Quick Select Profile Section */}
|
||||
{aiProfiles.length > 0 && (
|
||||
<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">
|
||||
{aiProfiles.slice(0, 6).map((profile) => {
|
||||
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain;
|
||||
const isCodex = profile.provider === "codex";
|
||||
const isSelected = editingFeature.model === profile.model &&
|
||||
editingFeature.thinkingLevel === profile.thinkingLevel;
|
||||
return (
|
||||
<button
|
||||
key={profile.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model: profile.model,
|
||||
thinkingLevel: profile.thinkingLevel,
|
||||
});
|
||||
if (profile.thinkingLevel === "ultrathink") {
|
||||
toast.warning("Ultrathink Selected", {
|
||||
description: "Ultrathink uses extensive reasoning (45-180s, ~$0.48/task).",
|
||||
duration: 4000,
|
||||
});
|
||||
}
|
||||
}}
|
||||
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={`edit-profile-quick-select-${profile.id}`}
|
||||
>
|
||||
<div className={cn(
|
||||
"w-7 h-7 rounded flex items-center justify-center flex-shrink-0",
|
||||
isCodex ? "bg-emerald-500/10" : "bg-primary/10"
|
||||
)}>
|
||||
{IconComponent && (
|
||||
<IconComponent className={cn(
|
||||
"w-4 h-4",
|
||||
isCodex ? "text-emerald-500" : "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">
|
||||
{profile.model}{profile.thinkingLevel !== "none" && ` + ${profile.thinkingLevel}`}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Or customize below.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Separator */}
|
||||
{aiProfiles.length > 0 && <div className="border-t border-border" />}
|
||||
|
||||
{/* Claude Models Section */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
655
app/src/components/views/profiles-view.tsx
Normal file
655
app/src/components/views/profiles-view.tsx
Normal file
@@ -0,0 +1,655 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { useAppStore, AIProfile, AgentModel, ThinkingLevel, ModelProvider } from "@/store/app-store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn, modelSupportsThinking } from "@/lib/utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
UserCircle,
|
||||
Plus,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Brain,
|
||||
Zap,
|
||||
Scale,
|
||||
Cpu,
|
||||
Rocket,
|
||||
Sparkles,
|
||||
GripVertical,
|
||||
Lock,
|
||||
Check,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCenter,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
// Icon mapping for profiles
|
||||
const PROFILE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
Brain,
|
||||
Zap,
|
||||
Scale,
|
||||
Cpu,
|
||||
Rocket,
|
||||
Sparkles,
|
||||
};
|
||||
|
||||
// Available icons for selection
|
||||
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 },
|
||||
];
|
||||
|
||||
// Model options for the form
|
||||
const CLAUDE_MODELS: { id: AgentModel; label: string }[] = [
|
||||
{ id: "haiku", label: "Claude Haiku" },
|
||||
{ id: "sonnet", label: "Claude Sonnet" },
|
||||
{ id: "opus", label: "Claude Opus" },
|
||||
];
|
||||
|
||||
const CODEX_MODELS: { id: AgentModel; label: string }[] = [
|
||||
{ id: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.1-codex", label: "GPT-5.1 Codex" },
|
||||
{ id: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex Mini" },
|
||||
{ id: "gpt-5.1", label: "GPT-5.1" },
|
||||
];
|
||||
|
||||
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" },
|
||||
];
|
||||
|
||||
// Helper to determine provider from model
|
||||
function getProviderFromModel(model: AgentModel): ModelProvider {
|
||||
if (model.startsWith("gpt")) {
|
||||
return "codex";
|
||||
}
|
||||
return "claude";
|
||||
}
|
||||
|
||||
// Sortable Profile Card Component
|
||||
function SortableProfileCard({
|
||||
profile,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: {
|
||||
profile: AIProfile;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
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 IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain;
|
||||
const isCodex = profile.provider === "codex";
|
||||
|
||||
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()}
|
||||
>
|
||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center",
|
||||
isCodex ? "bg-emerald-500/10" : "bg-primary/10"
|
||||
)}
|
||||
>
|
||||
{IconComponent && (
|
||||
<IconComponent
|
||||
className={cn(
|
||||
"w-5 h-5",
|
||||
isCodex ? "text-emerald-500" : "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">
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs px-2 py-0.5 rounded-full border",
|
||||
isCodex
|
||||
? "border-emerald-500/30 text-emerald-600 dark:text-emerald-400 bg-emerald-500/10"
|
||||
: "border-primary/30 text-primary bg-primary/10"
|
||||
)}
|
||||
>
|
||||
{profile.model}
|
||||
</span>
|
||||
{profile.thinkingLevel !== "none" && (
|
||||
<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.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}`}
|
||||
>
|
||||
<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}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Profile Form Component
|
||||
function ProfileForm({
|
||||
profile,
|
||||
onSave,
|
||||
onCancel,
|
||||
isEditing,
|
||||
}: {
|
||||
profile: Partial<AIProfile>;
|
||||
onSave: (profile: Omit<AIProfile, "id">) => void;
|
||||
onCancel: () => void;
|
||||
isEditing: boolean;
|
||||
}) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: profile.name || "",
|
||||
description: profile.description || "",
|
||||
model: profile.model || ("opus" as AgentModel),
|
||||
thinkingLevel: profile.thinkingLevel || ("none" as ThinkingLevel),
|
||||
icon: profile.icon || "Brain",
|
||||
});
|
||||
|
||||
const provider = getProviderFromModel(formData.model);
|
||||
const supportsThinking = modelSupportsThinking(formData.model);
|
||||
|
||||
const handleModelChange = (model: AgentModel) => {
|
||||
const newProvider = getProviderFromModel(model);
|
||||
setFormData({
|
||||
...formData,
|
||||
model,
|
||||
// Reset thinking level when switching to Codex (doesn't support thinking)
|
||||
thinkingLevel: newProvider === "codex" ? "none" : formData.thinkingLevel,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formData.name.trim()) {
|
||||
toast.error("Please enter a profile name");
|
||||
return;
|
||||
}
|
||||
|
||||
onSave({
|
||||
name: formData.name.trim(),
|
||||
description: formData.description.trim(),
|
||||
model: formData.model,
|
||||
thinkingLevel: supportsThinking ? formData.thinkingLevel : "none",
|
||||
provider,
|
||||
isBuiltIn: false,
|
||||
icon: formData.icon,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Name */}
|
||||
<div className="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-input"
|
||||
)}
|
||||
data-testid={`icon-select-${name}`}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Selection - Claude */}
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Brain className="w-4 h-4 text-primary" />
|
||||
Claude Models
|
||||
</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-input"
|
||||
)}
|
||||
data-testid={`model-select-${id}`}
|
||||
>
|
||||
{label.replace("Claude ", "")}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Selection - Codex */}
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Zap className="w-4 h-4 text-emerald-500" />
|
||||
Codex Models
|
||||
</Label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{CODEX_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-emerald-600 text-white border-emerald-500"
|
||||
: "bg-background hover:bg-accent border-input"
|
||||
)}
|
||||
data-testid={`model-select-${id}`}
|
||||
>
|
||||
{label.replace("GPT-5.1 ", "").replace("Codex ", "")}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thinking Level - Only for Claude models */}
|
||||
{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-input"
|
||||
)}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<DialogFooter className="pt-4">
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} data-testid="save-profile-button">
|
||||
{isEditing ? "Save Changes" : "Create Profile"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProfilesView() {
|
||||
const { aiProfiles, addAIProfile, updateAIProfile, removeAIProfile, reorderAIProfiles } =
|
||||
useAppStore();
|
||||
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
const [editingProfile, setEditingProfile] = 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 handleDeleteProfile = (profile: AIProfile) => {
|
||||
if (profile.isBuiltIn) return;
|
||||
|
||||
removeAIProfile(profile.id);
|
||||
toast.success("Profile deleted", {
|
||||
description: `Deleted "${profile.name}" profile`,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-1 flex flex-col overflow-hidden content-bg"
|
||||
data-testid="profiles-view"
|
||||
>
|
||||
{/* Header Section */}
|
||||
<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>
|
||||
<Button onClick={() => setShowAddDialog(true)} data-testid="add-profile-button">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Profile
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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="rounded-xl border border-dashed border-border p-8 text-center">
|
||||
<Sparkles className="w-10 h-10 text-muted-foreground mx-auto mb-3 opacity-50" />
|
||||
<p className="text-muted-foreground">
|
||||
No custom profiles yet. Create one to get started!
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-4"
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Profile
|
||||
</Button>
|
||||
</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={() => handleDeleteProfile(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>
|
||||
<div className="space-y-3">
|
||||
{builtInProfiles.map((profile) => (
|
||||
<SortableProfileCard
|
||||
key={profile.id}
|
||||
profile={profile}
|
||||
onEdit={() => {}}
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Profile Dialog */}
|
||||
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||||
<DialogContent data-testid="add-profile-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Profile</DialogTitle>
|
||||
<DialogDescription>
|
||||
Define a reusable model configuration preset.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ProfileForm
|
||||
profile={{}}
|
||||
onSave={handleAddProfile}
|
||||
onCancel={() => setShowAddDialog(false)}
|
||||
isEditing={false}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Edit Profile Dialog */}
|
||||
<Dialog
|
||||
open={!!editingProfile}
|
||||
onOpenChange={() => setEditingProfile(null)}
|
||||
>
|
||||
<DialogContent data-testid="edit-profile-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Profile</DialogTitle>
|
||||
<DialogDescription>Modify your profile settings.</DialogDescription>
|
||||
</DialogHeader>
|
||||
{editingProfile && (
|
||||
<ProfileForm
|
||||
profile={editingProfile}
|
||||
onSave={handleUpdateProfile}
|
||||
onCancel={() => setEditingProfile(null)}
|
||||
isEditing={true}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,8 @@ export type ViewMode =
|
||||
| "settings"
|
||||
| "tools"
|
||||
| "interview"
|
||||
| "context";
|
||||
| "context"
|
||||
| "profiles";
|
||||
|
||||
export type ThemeMode =
|
||||
| "light"
|
||||
@@ -94,6 +95,18 @@ export type ModelProvider = "claude" | "codex";
|
||||
// Thinking level (budget_tokens) options
|
||||
export type ThinkingLevel = "none" | "low" | "medium" | "high" | "ultrathink";
|
||||
|
||||
// AI Provider Profile - user-defined presets for model configurations
|
||||
export interface AIProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
model: AgentModel;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
provider: ModelProvider;
|
||||
isBuiltIn: boolean; // Built-in profiles cannot be deleted
|
||||
icon?: string; // Optional icon name from lucide
|
||||
}
|
||||
|
||||
export interface Feature {
|
||||
id: string;
|
||||
category: string;
|
||||
@@ -154,6 +167,9 @@ export interface AppState {
|
||||
|
||||
// Feature Default Settings
|
||||
defaultSkipTests: boolean; // Default value for skip tests when creating new features
|
||||
|
||||
// AI Profiles
|
||||
aiProfiles: AIProfile[];
|
||||
}
|
||||
|
||||
export interface AutoModeActivity {
|
||||
@@ -239,10 +255,70 @@ export interface AppActions {
|
||||
// Feature Default Settings actions
|
||||
setDefaultSkipTests: (skip: boolean) => void;
|
||||
|
||||
// AI Profile actions
|
||||
addAIProfile: (profile: Omit<AIProfile, "id">) => void;
|
||||
updateAIProfile: (id: string, updates: Partial<AIProfile>) => void;
|
||||
removeAIProfile: (id: string) => void;
|
||||
reorderAIProfiles: (oldIndex: number, newIndex: number) => void;
|
||||
|
||||
// Reset
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// Default built-in AI profiles
|
||||
const DEFAULT_AI_PROFILES: AIProfile[] = [
|
||||
{
|
||||
id: "profile-heavy-task",
|
||||
name: "Heavy Task",
|
||||
description: "Claude Opus with Ultrathink for complex architecture, migrations, or deep debugging.",
|
||||
model: "opus",
|
||||
thinkingLevel: "ultrathink",
|
||||
provider: "claude",
|
||||
isBuiltIn: true,
|
||||
icon: "Brain",
|
||||
},
|
||||
{
|
||||
id: "profile-balanced",
|
||||
name: "Balanced",
|
||||
description: "Claude Sonnet with medium thinking for typical development tasks.",
|
||||
model: "sonnet",
|
||||
thinkingLevel: "medium",
|
||||
provider: "claude",
|
||||
isBuiltIn: true,
|
||||
icon: "Scale",
|
||||
},
|
||||
{
|
||||
id: "profile-quick-edit",
|
||||
name: "Quick Edit",
|
||||
description: "Claude Haiku for fast, simple edits and minor fixes.",
|
||||
model: "haiku",
|
||||
thinkingLevel: "none",
|
||||
provider: "claude",
|
||||
isBuiltIn: true,
|
||||
icon: "Zap",
|
||||
},
|
||||
{
|
||||
id: "profile-codex-power",
|
||||
name: "Codex Power",
|
||||
description: "GPT-5.1 Codex Max for deep coding tasks via OpenAI CLI.",
|
||||
model: "gpt-5.1-codex-max",
|
||||
thinkingLevel: "none",
|
||||
provider: "codex",
|
||||
isBuiltIn: true,
|
||||
icon: "Cpu",
|
||||
},
|
||||
{
|
||||
id: "profile-codex-fast",
|
||||
name: "Codex Fast",
|
||||
description: "GPT-5.1 Codex Mini for lightweight and quick edits.",
|
||||
model: "gpt-5.1-codex-mini",
|
||||
thinkingLevel: "none",
|
||||
provider: "codex",
|
||||
isBuiltIn: true,
|
||||
icon: "Rocket",
|
||||
},
|
||||
];
|
||||
|
||||
const initialState: AppState = {
|
||||
projects: [],
|
||||
currentProject: null,
|
||||
@@ -267,6 +343,7 @@ const initialState: AppState = {
|
||||
maxConcurrency: 3, // Default to 3 concurrent agents
|
||||
kanbanCardDetailLevel: "standard", // Default to standard detail level
|
||||
defaultSkipTests: false, // Default to TDD mode (tests enabled)
|
||||
aiProfiles: DEFAULT_AI_PROFILES,
|
||||
};
|
||||
|
||||
export const useAppStore = create<AppState & AppActions>()(
|
||||
@@ -595,6 +672,37 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
// Feature Default Settings actions
|
||||
setDefaultSkipTests: (skip) => set({ defaultSkipTests: skip }),
|
||||
|
||||
// AI Profile actions
|
||||
addAIProfile: (profile) => {
|
||||
const id = `profile-${Date.now()}-${Math.random()
|
||||
.toString(36)
|
||||
.substr(2, 9)}`;
|
||||
set({ aiProfiles: [...get().aiProfiles, { ...profile, id }] });
|
||||
},
|
||||
|
||||
updateAIProfile: (id, updates) => {
|
||||
set({
|
||||
aiProfiles: get().aiProfiles.map((p) =>
|
||||
p.id === id ? { ...p, ...updates } : p
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
removeAIProfile: (id) => {
|
||||
// Only allow removing non-built-in profiles
|
||||
const profile = get().aiProfiles.find((p) => p.id === id);
|
||||
if (profile && !profile.isBuiltIn) {
|
||||
set({ aiProfiles: get().aiProfiles.filter((p) => p.id !== id) });
|
||||
}
|
||||
},
|
||||
|
||||
reorderAIProfiles: (oldIndex, newIndex) => {
|
||||
const profiles = [...get().aiProfiles];
|
||||
const [movedProfile] = profiles.splice(oldIndex, 1);
|
||||
profiles.splice(newIndex, 0, movedProfile);
|
||||
set({ aiProfiles: profiles });
|
||||
},
|
||||
|
||||
// Reset
|
||||
reset: () => set(initialState),
|
||||
}),
|
||||
@@ -613,6 +721,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
maxConcurrency: state.maxConcurrency,
|
||||
kanbanCardDetailLevel: state.kanbanCardDetailLevel,
|
||||
defaultSkipTests: state.defaultSkipTests,
|
||||
aiProfiles: state.aiProfiles,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user