Profile changes (#596)
* Make variants an object rather than array (vibe-kanban 63213864)
Profile variants should be an object, with key used instead of the current label.
The code should be refactored leaving no legacy trace.
crates/server/src/routes/config.rs
crates/executors/src/profile.rs
* Make variants an object rather than array (vibe-kanban 63213864)
Profile variants should be an object, with key used instead of the current label.
The code should be refactored leaving no legacy trace.
crates/server/src/routes/config.rs
crates/executors/src/profile.rs
* Remove the command builder from profiles (vibe-kanban d30abc92)
It should no longer be possible to customise the command builder in profiles.json.
Instead, anywhere where the command is customised, the code should be hardcoded as an enum field on the executor, eg claude code vs claude code router on the claude code struct.
crates/executors/src/profile.rs
crates/executors/src/executors/claude.rs
* fmt
* Refactor Qwen log normalization (vibe-kanban 076fdb3f)
Qwen basically uses the same log normalization as Gemini, can you refactor the code to make it more reusable.
A similar example exists in Amp, where we use Claude's log normalization.
crates/executors/src/executors/amp.rs
crates/executors/src/executors/qwen.rs
crates/executors/src/executors/claude.rs
* Add field overrides to executors (vibe-kanban cc3323a4)
We should add optional fields 'base_command_override' (String) and 'additional_params' (Vec<String>) to each executor, and wire these fields up to the command builder
* Update profiles (vibe-kanban e7545ab6)
Redesign the profile configuration storage system to store only differences from defaults instead of complete profile files. Implement partial profile functions (create_partial_profile, load_from_partials, save_as_diffs) that save human-readable partial profiles containing only changed values. Update ProfileConfigs::load() to handle the new partial format while maintaining backward compatibility with legacy full profile formats through automatic migration that creates backups. Implement smart variants handling that only stores changed, added, or removed variants rather than entire arrays. Fix the profile API consistency issue by replacing the manual file loading logic in the get_profiles() endpoint in crates/server/src/routes/config.rs with ProfileConfigs::get_cached() to ensure the GET endpoint uses the same cached data that PUT updates. Add comprehensive test coverage for all new functionality.
* Yolo mode becomes a field (vibe-kanban d8dd02f0)
Most executors implement some variation of yolo-mode, can you make this boolean field on each executor (if supported), where the name for the field aligns with the CLI field
* Change ClaudeCodeVariant to boolean (vibe-kanban cc05956f)
Instead of an enum ClaudeCodeVariant, let's use a variable claude_code_router to determine whether to use claude_code_router's command. If the user has also supplied a base_command_override this should take precedence (also write a warning to console)
crates/executors/src/executors/claude.rs
* Remove mcp_config_path from profile config (vibe-kanban 6c1e5947)
crates/executors/src/profile.rs
* One profile per executor (vibe-kanban b0adc27e)
Currently you can define arbitrary profiles, multiple profiles per executor. Let's refactor to simplify this configuration, instead we should only be able to configure one profile per executor.
The new format should be something like:
```json
{
"profiles": {
"CLAUDE_CODE": {
"default": {
"plan": false,
"dangerously_skip_permissions": true,
"append_prompt": null
},
"plan": {
"plan": true,
"dangerously_skip_permissions": false,
"append_prompt": null
}
}
}
}
```
Each profile's defaults should be defined as code instead of in default_profiles.json
profile.json will now contain:
- Overrides for default configurations
- Additional user defined configurations, for executors
It is not possible to remove a default configuration entirely, just override the configuration.
The user profile.json should still be a minimal set of overrides, to make upgrading easy.
Don't worry about migration, this will be done manually.
crates/executors/default_profiles.json
crates/executors/src/profile.rs
* SCREAMING_SNAKE_CASE
* update profile.rs
* config migration
* fmt
* delete binding
* config keys
* fmt
* shared types
* Profile variants should be saved as SCREAMING_SNAKE_CASE (vibe-kanban 5c6c124c)
crates/executors/src/profile.rs save_overrides
* rename default profiles
* remove defaulted executor fields
* backwards compatability
* fix legacy variants
This commit is contained in:
committed by
GitHub
parent
a10d3f0854
commit
6a7818e057
@@ -22,8 +22,9 @@ import CreatePRDialog from '@/components/tasks/Toolbar/CreatePRDialog';
|
||||
import { TaskDialogProvider } from '@/contexts/task-dialog-context';
|
||||
import { TaskFormDialogContainer } from '@/components/tasks/TaskFormDialogContainer';
|
||||
import { ProjectProvider } from '@/contexts/project-context';
|
||||
import type { EditorType, ProfileVariantLabel } from 'shared/types';
|
||||
import type { EditorType } from 'shared/types';
|
||||
import { ThemeMode } from 'shared/types';
|
||||
import type { ExecutorProfileId } from 'shared/types';
|
||||
import { configApi } from '@/lib/api';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Loader } from '@/components/ui/loader';
|
||||
@@ -83,7 +84,7 @@ function AppContent() {
|
||||
};
|
||||
|
||||
const handleOnboardingComplete = async (onboardingConfig: {
|
||||
profile: ProfileVariantLabel;
|
||||
profile: ExecutorProfileId;
|
||||
editor: { editor_type: EditorType; custom_command: string | null };
|
||||
}) => {
|
||||
if (!config) return;
|
||||
@@ -91,7 +92,7 @@ function AppContent() {
|
||||
const updatedConfig = {
|
||||
...config,
|
||||
onboarding_acknowledged: true,
|
||||
profile: onboardingConfig.profile,
|
||||
executor_profile: onboardingConfig.profile,
|
||||
editor: onboardingConfig.editor,
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ import {
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Sparkles, Code, ChevronDown, HandMetal } from 'lucide-react';
|
||||
import { EditorType, ProfileVariantLabel } from 'shared/types';
|
||||
import { EditorType } from 'shared/types';
|
||||
import type { ExecutorProfileId } from 'shared/types';
|
||||
import { useUserSystem } from '@/components/config-provider';
|
||||
|
||||
import { toPrettyCase } from '@/utils/string';
|
||||
@@ -32,14 +33,14 @@ import { toPrettyCase } from '@/utils/string';
|
||||
interface OnboardingDialogProps {
|
||||
open: boolean;
|
||||
onComplete: (config: {
|
||||
profile: ProfileVariantLabel;
|
||||
profile: ExecutorProfileId;
|
||||
editor: { editor_type: EditorType; custom_command: string | null };
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
const [profile, setProfile] = useState<ProfileVariantLabel>({
|
||||
profile: 'claude-code',
|
||||
const [profile, setProfile] = useState<ExecutorProfileId>({
|
||||
executor: 'CLAUDE_CODE',
|
||||
variant: null,
|
||||
});
|
||||
const [editorType, setEditorType] = useState<EditorType>(EditorType.VS_CODE);
|
||||
@@ -84,31 +85,29 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
<Label htmlFor="profile">Default Profile</Label>
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
value={profile.profile}
|
||||
value={profile.executor}
|
||||
onValueChange={(value) =>
|
||||
setProfile({ profile: value, variant: null })
|
||||
setProfile({ executor: value, variant: null })
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="profile" className="flex-1">
|
||||
<SelectValue placeholder="Select your preferred coding agent" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{profiles?.map((profile) => (
|
||||
<SelectItem key={profile.label} value={profile.label}>
|
||||
{profile.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{profiles &&
|
||||
Object.keys(profiles).map((profile) => (
|
||||
<SelectItem key={profile} value={profile}>
|
||||
{profile}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Show variant selector if selected profile has variants */}
|
||||
{(() => {
|
||||
const selectedProfile = profiles?.find(
|
||||
(p) => p.label === profile.profile
|
||||
);
|
||||
const selectedProfile = profiles?.[profile.executor];
|
||||
const hasVariants =
|
||||
selectedProfile?.variants &&
|
||||
selectedProfile.variants.length > 0;
|
||||
selectedProfile && Object.keys(selectedProfile).length > 0;
|
||||
|
||||
if (hasVariants) {
|
||||
return (
|
||||
@@ -119,36 +118,26 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
className="w-24 px-2 flex items-center justify-between"
|
||||
>
|
||||
<span className="text-xs truncate flex-1 text-left">
|
||||
{profile.variant || 'Default'}
|
||||
{profile.variant || 'DEFAULT'}
|
||||
</span>
|
||||
<ChevronDown className="h-3 w-3 ml-1 flex-shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
setProfile({ ...profile, variant: null })
|
||||
}
|
||||
className={!profile.variant ? 'bg-accent' : ''}
|
||||
>
|
||||
Default
|
||||
</DropdownMenuItem>
|
||||
{selectedProfile.variants.map((variant) => (
|
||||
{Object.keys(selectedProfile).map((variant) => (
|
||||
<DropdownMenuItem
|
||||
key={variant.label}
|
||||
key={variant}
|
||||
onClick={() =>
|
||||
setProfile({
|
||||
...profile,
|
||||
variant: variant.label,
|
||||
variant: variant,
|
||||
})
|
||||
}
|
||||
className={
|
||||
profile.variant === variant.label
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
profile.variant === variant ? 'bg-accent' : ''
|
||||
}
|
||||
>
|
||||
{variant.label}
|
||||
{variant}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ProfileVariantLabel } from 'shared/types';
|
||||
import type { ExecutorProfileId } from 'shared/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ProfileVariantBadgeProps {
|
||||
profileVariant: ProfileVariantLabel | null;
|
||||
profileVariant: ExecutorProfileId | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function ProfileVariantBadge({
|
||||
|
||||
return (
|
||||
<span className={cn('text-xs text-muted-foreground', className)}>
|
||||
{profileVariant.profile}
|
||||
{profileVariant.executor}
|
||||
{profileVariant.variant && (
|
||||
<>
|
||||
<span className="mx-1">/</span>
|
||||
|
||||
@@ -10,16 +10,16 @@ import {
|
||||
import {
|
||||
type Config,
|
||||
type Environment,
|
||||
type ProfileConfig,
|
||||
type UserSystemInfo,
|
||||
CheckTokenResponse,
|
||||
} from 'shared/types';
|
||||
import type { ExecutorProfile } from 'shared/types';
|
||||
import { configApi, githubAuthApi } from '../lib/api';
|
||||
|
||||
interface UserSystemState {
|
||||
config: Config | null;
|
||||
environment: Environment | null;
|
||||
profiles: ProfileConfig[] | null;
|
||||
profiles: Record<string, ExecutorProfile> | null;
|
||||
}
|
||||
|
||||
interface UserSystemContextType {
|
||||
@@ -34,9 +34,9 @@ interface UserSystemContextType {
|
||||
|
||||
// System data access
|
||||
environment: Environment | null;
|
||||
profiles: ProfileConfig[] | null;
|
||||
profiles: Record<string, ExecutorProfile> | null;
|
||||
setEnvironment: (env: Environment | null) => void;
|
||||
setProfiles: (profiles: ProfileConfig[] | null) => void;
|
||||
setProfiles: (profiles: Record<string, ExecutorProfile> | null) => void;
|
||||
|
||||
// Reload system data
|
||||
reloadSystem: () => Promise<void>;
|
||||
@@ -58,7 +58,10 @@ export function UserSystemProvider({ children }: UserSystemProviderProps) {
|
||||
// Split state for performance - independent re-renders
|
||||
const [config, setConfig] = useState<Config | null>(null);
|
||||
const [environment, setEnvironment] = useState<Environment | null>(null);
|
||||
const [profiles, setProfiles] = useState<ProfileConfig[] | null>(null);
|
||||
const [profiles, setProfiles] = useState<Record<
|
||||
string,
|
||||
ExecutorProfile
|
||||
> | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [githubTokenInvalid, setGithubTokenInvalid] = useState(false);
|
||||
|
||||
@@ -68,7 +71,9 @@ export function UserSystemProvider({ children }: UserSystemProviderProps) {
|
||||
const userSystemInfo: UserSystemInfo = await configApi.getConfig();
|
||||
setConfig(userSystemInfo.config);
|
||||
setEnvironment(userSystemInfo.environment);
|
||||
setProfiles(userSystemInfo.profiles);
|
||||
setProfiles(
|
||||
userSystemInfo.executors as Record<string, ExecutorProfile> | null
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error loading user system:', err);
|
||||
} finally {
|
||||
@@ -142,7 +147,9 @@ export function UserSystemProvider({ children }: UserSystemProviderProps) {
|
||||
const userSystemInfo: UserSystemInfo = await configApi.getConfig();
|
||||
setConfig(userSystemInfo.config);
|
||||
setEnvironment(userSystemInfo.environment);
|
||||
setProfiles(userSystemInfo.profiles);
|
||||
setProfiles(
|
||||
userSystemInfo.executors as Record<string, ExecutorProfile> | null
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Error reloading user system:', err);
|
||||
} finally {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ProfileVariantBadge } from '@/components/common/ProfileVariantBadge.tsx
|
||||
import { useAttemptExecution } from '@/hooks';
|
||||
import ProcessLogsViewer from './ProcessLogsViewer';
|
||||
import type { ExecutionProcessStatus, ExecutionProcess } from 'shared/types';
|
||||
|
||||
import { useProcessSelection } from '@/contexts/ProcessSelectionContext';
|
||||
|
||||
interface ProcessesTabProps {
|
||||
@@ -157,8 +158,7 @@ function ProcessesTab({ attemptId }: ProcessesTabProps) {
|
||||
'CodingAgentFollowUpRequest' ? (
|
||||
<ProfileVariantBadge
|
||||
profileVariant={
|
||||
process.executor_action.typ
|
||||
.profile_variant_label
|
||||
process.executor_action.typ.executor_profile_id
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -4,10 +4,10 @@ import { Button } from '@/components/ui/button';
|
||||
import { projectsApi } from '@/lib/api';
|
||||
import type {
|
||||
GitBranch,
|
||||
ProfileVariantLabel,
|
||||
TaskAttempt,
|
||||
TaskWithAttemptStatus,
|
||||
} from 'shared/types';
|
||||
import type { ExecutorProfileId } from 'shared/types';
|
||||
|
||||
import { useAttemptExecution } from '@/hooks';
|
||||
import { useTaskStopping } from '@/stores/useTaskDetailsUiStore';
|
||||
@@ -94,7 +94,7 @@ function TaskDetailsToolbar({
|
||||
const [branches, setBranches] = useState<GitBranch[]>([]);
|
||||
const [selectedBranch, setSelectedBranch] = useState<string | null>(null);
|
||||
const [selectedProfile, setSelectedProfile] =
|
||||
useState<ProfileVariantLabel | null>(null);
|
||||
useState<ExecutorProfileId | null>(null);
|
||||
// const { attemptId: urlAttemptId } = useParams<{ attemptId?: string }>();
|
||||
const { system, profiles } = useUserSystem();
|
||||
|
||||
@@ -143,10 +143,10 @@ function TaskDetailsToolbar({
|
||||
|
||||
// Set default executor from config
|
||||
useEffect(() => {
|
||||
if (system.config?.profile) {
|
||||
setSelectedProfile(system.config.profile);
|
||||
if (system.config?.executor_profile) {
|
||||
setSelectedProfile(system.config.executor_profile);
|
||||
}
|
||||
}, [system.config?.profile]);
|
||||
}, [system.config?.executor_profile]);
|
||||
|
||||
// Simplified - hooks handle data fetching and navigation
|
||||
// const fetchTaskAttempts = useCallback(() => {
|
||||
|
||||
@@ -61,7 +61,7 @@ export function TaskFollowUpSection({
|
||||
process.executor_action?.typ.type === 'CodingAgentInitialRequest' ||
|
||||
process.executor_action?.typ.type === 'CodingAgentFollowUpRequest'
|
||||
) {
|
||||
return process.executor_action?.typ.profile_variant_label;
|
||||
return process.executor_action?.typ.executor_profile_id;
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
@@ -73,9 +73,9 @@ export function TaskFollowUpSection({
|
||||
return null;
|
||||
} else if (selectedAttemptProfile && profiles) {
|
||||
// No processes yet, check if profile has default variant
|
||||
const profile = profiles.find((p) => p.label === selectedAttemptProfile);
|
||||
if (profile?.variants && profile.variants.length > 0) {
|
||||
return profile.variants[0].label;
|
||||
const profile = profiles?.[selectedAttemptProfile];
|
||||
if (profile && Object.keys(profile).length > 0) {
|
||||
return Object.keys(profile)[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export function TaskFollowUpSection({
|
||||
]);
|
||||
const currentProfile = useMemo(() => {
|
||||
if (!selectedProfile || !profiles) return null;
|
||||
return profiles.find((p) => p.label === selectedProfile);
|
||||
return profiles?.[selectedProfile];
|
||||
}, [selectedProfile, profiles]);
|
||||
|
||||
// Update selectedVariant when defaultFollowUpVariant changes
|
||||
@@ -264,8 +264,7 @@ export function TaskFollowUpSection({
|
||||
{/* Variant selector */}
|
||||
{(() => {
|
||||
const hasVariants =
|
||||
currentProfile?.variants &&
|
||||
currentProfile.variants.length > 0;
|
||||
currentProfile && Object.keys(currentProfile).length > 0;
|
||||
|
||||
if (hasVariants) {
|
||||
return (
|
||||
@@ -281,33 +280,29 @@ export function TaskFollowUpSection({
|
||||
)}
|
||||
>
|
||||
<span className="text-xs truncate flex-1 text-left">
|
||||
{selectedVariant || 'Default'}
|
||||
{selectedVariant || 'DEFAULT'}
|
||||
</span>
|
||||
<ChevronDown className="h-3 w-3 ml-1 flex-shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setSelectedVariant(null)}
|
||||
className={!selectedVariant ? 'bg-accent' : ''}
|
||||
>
|
||||
Default
|
||||
</DropdownMenuItem>
|
||||
{currentProfile.variants.map((variant) => (
|
||||
<DropdownMenuItem
|
||||
key={variant.label}
|
||||
onClick={() =>
|
||||
setSelectedVariant(variant.label)
|
||||
}
|
||||
className={
|
||||
selectedVariant === variant.label
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{variant.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{Object.entries(currentProfile).map(
|
||||
([variantLabel]) => (
|
||||
<DropdownMenuItem
|
||||
key={variantLabel}
|
||||
onClick={() =>
|
||||
setSelectedVariant(variantLabel)
|
||||
}
|
||||
className={
|
||||
selectedVariant === variantLabel
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{variantLabel}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
@@ -7,12 +7,9 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu.tsx';
|
||||
import type {
|
||||
ProfileConfig,
|
||||
GitBranch,
|
||||
ProfileVariantLabel,
|
||||
Task,
|
||||
} from 'shared/types';
|
||||
import type { GitBranch, Task } from 'shared/types';
|
||||
import type { ExecutorProfile } from 'shared/types';
|
||||
import type { ExecutorProfileId } from 'shared/types';
|
||||
import type { TaskAttempt } from 'shared/types';
|
||||
import { useAttemptCreation } from '@/hooks/useAttemptCreation';
|
||||
import { useAttemptExecution } from '@/hooks/useAttemptExecution';
|
||||
@@ -33,12 +30,12 @@ type Props = {
|
||||
branches: GitBranch[];
|
||||
taskAttempts: TaskAttempt[];
|
||||
createAttemptBranch: string | null;
|
||||
selectedProfile: ProfileVariantLabel | null;
|
||||
selectedProfile: ExecutorProfileId | null;
|
||||
selectedBranch: string | null;
|
||||
setIsInCreateAttemptMode: Dispatch<SetStateAction<boolean>>;
|
||||
setCreateAttemptBranch: Dispatch<SetStateAction<string | null>>;
|
||||
setSelectedProfile: Dispatch<SetStateAction<ProfileVariantLabel | null>>;
|
||||
availableProfiles: ProfileConfig[] | null;
|
||||
setSelectedProfile: Dispatch<SetStateAction<ExecutorProfileId | null>>;
|
||||
availableProfiles: Record<string, ExecutorProfile> | null;
|
||||
selectedAttempt: TaskAttempt | null;
|
||||
};
|
||||
|
||||
@@ -67,7 +64,7 @@ function CreateAttempt({
|
||||
|
||||
// Create attempt logic
|
||||
const actuallyCreateAttempt = useCallback(
|
||||
async (profile: ProfileVariantLabel, baseBranch?: string) => {
|
||||
async (profile: ExecutorProfileId, baseBranch?: string) => {
|
||||
const effectiveBaseBranch = baseBranch || selectedBranch;
|
||||
|
||||
if (!effectiveBaseBranch) {
|
||||
@@ -85,7 +82,7 @@ function CreateAttempt({
|
||||
// Handler for Enter key or Start button
|
||||
const onCreateNewAttempt = useCallback(
|
||||
(
|
||||
profile: ProfileVariantLabel,
|
||||
profile: ExecutorProfileId,
|
||||
baseBranch?: string,
|
||||
isKeyTriggered?: boolean
|
||||
) => {
|
||||
@@ -201,31 +198,34 @@ function CreateAttempt({
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Settings2 className="h-3 w-3" />
|
||||
<span className="truncate">
|
||||
{selectedProfile?.profile || 'Select profile'}
|
||||
{selectedProfile?.executor || 'Select profile'}
|
||||
</span>
|
||||
</div>
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
{availableProfiles.map((profile) => (
|
||||
<DropdownMenuItem
|
||||
key={profile.label}
|
||||
onClick={() => {
|
||||
setSelectedProfile({
|
||||
profile: profile.label,
|
||||
variant: null,
|
||||
});
|
||||
}}
|
||||
className={
|
||||
selectedProfile?.profile === profile.label
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{profile.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{availableProfiles &&
|
||||
Object.entries(availableProfiles)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([profileKey]) => (
|
||||
<DropdownMenuItem
|
||||
key={profileKey}
|
||||
onClick={() => {
|
||||
setSelectedProfile({
|
||||
executor: profileKey,
|
||||
variant: null,
|
||||
});
|
||||
}}
|
||||
className={
|
||||
selectedProfile?.executor === profileKey
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{profileKey}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
@@ -239,11 +239,10 @@ function CreateAttempt({
|
||||
</label>
|
||||
</div>
|
||||
{(() => {
|
||||
const currentProfile = availableProfiles?.find(
|
||||
(p) => p.label === selectedProfile?.profile
|
||||
);
|
||||
const currentProfile =
|
||||
availableProfiles?.[selectedProfile?.executor || ''];
|
||||
const hasVariants =
|
||||
currentProfile?.variants && currentProfile.variants.length > 0;
|
||||
currentProfile && Object.keys(currentProfile).length > 0;
|
||||
|
||||
if (hasVariants && currentProfile) {
|
||||
return (
|
||||
@@ -255,43 +254,30 @@ function CreateAttempt({
|
||||
className="w-full px-2 flex items-center justify-between text-xs"
|
||||
>
|
||||
<span className="truncate flex-1 text-left">
|
||||
{selectedProfile?.variant || 'Default'}
|
||||
{selectedProfile?.variant || 'DEFAULT'}
|
||||
</span>
|
||||
<ArrowDown className="h-3 w-3 ml-1 flex-shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (selectedProfile) {
|
||||
setSelectedProfile({
|
||||
...selectedProfile,
|
||||
variant: null,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={!selectedProfile?.variant ? 'bg-accent' : ''}
|
||||
>
|
||||
Default
|
||||
</DropdownMenuItem>
|
||||
{currentProfile.variants.map((variant) => (
|
||||
{Object.entries(currentProfile).map(([variantLabel]) => (
|
||||
<DropdownMenuItem
|
||||
key={variant.label}
|
||||
key={variantLabel}
|
||||
onClick={() => {
|
||||
if (selectedProfile) {
|
||||
setSelectedProfile({
|
||||
...selectedProfile,
|
||||
variant: variant.label,
|
||||
variant: variantLabel,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={
|
||||
selectedProfile?.variant === variant.label
|
||||
selectedProfile?.variant === variantLabel
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{variant.label}
|
||||
{variantLabel}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { attemptsApi } from '@/lib/api';
|
||||
import type { ProfileVariantLabel, TaskAttempt } from 'shared/types';
|
||||
import type { TaskAttempt } from 'shared/types';
|
||||
import type { ExecutorProfileId } from 'shared/types';
|
||||
|
||||
export function useAttemptCreation(taskId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -13,12 +14,12 @@ export function useAttemptCreation(taskId: string) {
|
||||
profile,
|
||||
baseBranch,
|
||||
}: {
|
||||
profile: ProfileVariantLabel;
|
||||
profile: ExecutorProfileId;
|
||||
baseBranch: string;
|
||||
}) =>
|
||||
attemptsApi.create({
|
||||
task_id: taskId,
|
||||
profile_variant_label: profile,
|
||||
executor_profile_id: profile,
|
||||
base_branch: baseBranch,
|
||||
}),
|
||||
onSuccess: (newAttempt: TaskAttempt) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import type { ProfileConfig } from 'shared/types';
|
||||
import type { ExecutorProfile } from 'shared/types';
|
||||
|
||||
// Define available keyboard shortcuts
|
||||
export interface KeyboardShortcut {
|
||||
@@ -273,13 +273,13 @@ export function useVariantCyclingShortcut({
|
||||
setSelectedVariant,
|
||||
setIsAnimating,
|
||||
}: {
|
||||
currentProfile: ProfileConfig | null | undefined;
|
||||
currentProfile: ExecutorProfile | null | undefined;
|
||||
selectedVariant: string | null;
|
||||
setSelectedVariant: (variant: string | null) => void;
|
||||
setIsAnimating: (animating: boolean) => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
if (!currentProfile?.variants || currentProfile.variants.length === 0) {
|
||||
if (!currentProfile || Object.keys(currentProfile).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -289,8 +289,8 @@ export function useVariantCyclingShortcut({
|
||||
e.preventDefault();
|
||||
|
||||
// Build the variant cycle: null (Default) → variant1 → variant2 → ... → null
|
||||
const variants = currentProfile.variants;
|
||||
const variantLabels = variants.map((v) => v.label);
|
||||
const variants = currentProfile;
|
||||
const variantLabels = Object.keys(variants);
|
||||
const allOptions = [null, ...variantLabels];
|
||||
|
||||
// Find current index and cycle to next
|
||||
|
||||
@@ -18,7 +18,8 @@ import { Label } from '@/components/ui/label';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { JSONEditor } from '@/components/ui/json-editor';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { ProfileConfig, McpConfig } from 'shared/types';
|
||||
import { McpConfig } from 'shared/types';
|
||||
import type { ExecutorProfile } from 'shared/types';
|
||||
import { useUserSystem } from '@/components/config-provider';
|
||||
import { mcpServersApi } from '../lib/api';
|
||||
import { McpConfigStrategyGeneral } from '../lib/mcp-strategies';
|
||||
@@ -29,32 +30,29 @@ export function McpServers() {
|
||||
const [mcpConfig, setMcpConfig] = useState<McpConfig | null>(null);
|
||||
const [mcpError, setMcpError] = useState<string | null>(null);
|
||||
const [mcpLoading, setMcpLoading] = useState(true);
|
||||
const [selectedProfile, setSelectedProfile] = useState<ProfileConfig | null>(
|
||||
null
|
||||
);
|
||||
const [selectedProfile, setSelectedProfile] =
|
||||
useState<ExecutorProfile | null>(null);
|
||||
const [mcpApplying, setMcpApplying] = useState(false);
|
||||
const [mcpConfigPath, setMcpConfigPath] = useState<string>('');
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
// Initialize selected profile when config loads
|
||||
useEffect(() => {
|
||||
if (config?.profile && profiles && !selectedProfile) {
|
||||
if (config?.executor_profile && profiles && !selectedProfile) {
|
||||
// Find the current profile
|
||||
const currentProfile = profiles.find(
|
||||
(p) => p.label === config.profile.profile
|
||||
);
|
||||
const currentProfile = profiles[config.executor_profile.executor];
|
||||
if (currentProfile) {
|
||||
setSelectedProfile(currentProfile);
|
||||
} else if (profiles.length > 0) {
|
||||
} else if (Object.keys(profiles).length > 0) {
|
||||
// Default to first profile if current profile not found
|
||||
setSelectedProfile(profiles[0]);
|
||||
setSelectedProfile(Object.values(profiles)[0]);
|
||||
}
|
||||
}
|
||||
}, [config?.profile, profiles, selectedProfile]);
|
||||
}, [config?.executor_profile, profiles, selectedProfile]);
|
||||
|
||||
// Load existing MCP configuration when selected profile changes
|
||||
useEffect(() => {
|
||||
const loadMcpServersForProfile = async (profile: ProfileConfig) => {
|
||||
const loadMcpServersForProfile = async (profile: ExecutorProfile) => {
|
||||
// Reset state when loading
|
||||
setMcpLoading(true);
|
||||
setMcpError(null);
|
||||
@@ -63,8 +61,16 @@ export function McpServers() {
|
||||
|
||||
try {
|
||||
// Load MCP servers for the selected profile/agent
|
||||
// Find the key for this profile
|
||||
const profileKey = profiles
|
||||
? Object.keys(profiles).find((key) => profiles[key] === profile)
|
||||
: null;
|
||||
if (!profileKey) {
|
||||
throw new Error('Profile key not found');
|
||||
}
|
||||
|
||||
const result = await mcpServersApi.load({
|
||||
profile: profile.label,
|
||||
profile: profileKey,
|
||||
});
|
||||
// Store the McpConfig from backend
|
||||
setMcpConfig(result.mcp_config);
|
||||
@@ -153,9 +159,19 @@ export function McpServers() {
|
||||
fullConfig
|
||||
);
|
||||
|
||||
// Find the key for the selected profile
|
||||
const selectedProfileKey = profiles
|
||||
? Object.keys(profiles).find(
|
||||
(key) => profiles[key] === selectedProfile
|
||||
)
|
||||
: null;
|
||||
if (!selectedProfileKey) {
|
||||
throw new Error('Selected profile key not found');
|
||||
}
|
||||
|
||||
await mcpServersApi.save(
|
||||
{
|
||||
profile: selectedProfile.label,
|
||||
profile: selectedProfileKey,
|
||||
},
|
||||
{ servers: mcpServersConfig }
|
||||
);
|
||||
@@ -231,9 +247,15 @@ export function McpServers() {
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mcp-executor">Profile</Label>
|
||||
<Select
|
||||
value={selectedProfile?.label || ''}
|
||||
value={
|
||||
selectedProfile
|
||||
? Object.keys(profiles || {}).find(
|
||||
(key) => profiles![key] === selectedProfile
|
||||
) || ''
|
||||
: ''
|
||||
}
|
||||
onValueChange={(value: string) => {
|
||||
const profile = profiles?.find((p) => p.label === value);
|
||||
const profile = profiles?.[value];
|
||||
if (profile) setSelectedProfile(profile);
|
||||
}}
|
||||
>
|
||||
@@ -241,11 +263,14 @@ export function McpServers() {
|
||||
<SelectValue placeholder="Select executor" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{profiles?.map((profile) => (
|
||||
<SelectItem key={profile.label} value={profile.label}>
|
||||
{profile.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{profiles &&
|
||||
Object.entries(profiles)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([profileKey]) => (
|
||||
<SelectItem key={profileKey} value={profileKey}>
|
||||
{profileKey}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
|
||||
@@ -26,12 +26,8 @@ import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { JSONEditor } from '@/components/ui/json-editor';
|
||||
import { ChevronDown, Key, Loader2, Volume2 } from 'lucide-react';
|
||||
import {
|
||||
ThemeMode,
|
||||
EditorType,
|
||||
SoundFile,
|
||||
ProfileVariantLabel,
|
||||
} from 'shared/types';
|
||||
import { ThemeMode, EditorType, SoundFile } from 'shared/types';
|
||||
import type { ExecutorProfileId } from 'shared/types';
|
||||
|
||||
import { toPrettyCase } from '@/utils/string';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
@@ -100,8 +96,8 @@ export function Settings() {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
// Basic structure validation
|
||||
if (!parsed.profiles || !Array.isArray(parsed.profiles)) {
|
||||
setProfilesError('Invalid structure: must have a "profiles" array');
|
||||
if (!parsed.executors) {
|
||||
setProfilesError('Invalid structure: must have a "executors" object');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
@@ -277,38 +273,43 @@ export function Settings() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="executor">Default Profile</Label>
|
||||
<Label htmlFor="executor">Default Executor Profile</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Select
|
||||
value={config.profile?.profile || ''}
|
||||
value={config.executor_profile?.executor ?? ''}
|
||||
onValueChange={(value: string) => {
|
||||
const newProfile: ProfileVariantLabel = {
|
||||
profile: value,
|
||||
const newProfile: ExecutorProfileId = {
|
||||
executor: value,
|
||||
variant: null,
|
||||
};
|
||||
updateConfig({ profile: newProfile });
|
||||
updateConfig({
|
||||
executor_profile: newProfile,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="executor">
|
||||
<SelectValue placeholder="Select profile" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{profiles?.map((profile) => (
|
||||
<SelectItem key={profile.label} value={profile.label}>
|
||||
{profile.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{profiles &&
|
||||
Object.entries(profiles)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([profileKey]) => (
|
||||
<SelectItem key={profileKey} value={profileKey}>
|
||||
{profileKey}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Show variant selector if selected profile has variants */}
|
||||
{(() => {
|
||||
const selectedProfile = profiles?.find(
|
||||
(p) => p.label === config.profile?.profile
|
||||
);
|
||||
const currentProfileVariant = config.executor_profile;
|
||||
const selectedProfile =
|
||||
profiles?.[currentProfileVariant?.executor || ''];
|
||||
const hasVariants =
|
||||
selectedProfile?.variants &&
|
||||
selectedProfile.variants.length > 0;
|
||||
selectedProfile &&
|
||||
Object.keys(selectedProfile).length > 0;
|
||||
|
||||
if (hasVariants) {
|
||||
return (
|
||||
@@ -319,45 +320,37 @@ export function Settings() {
|
||||
className="w-full h-10 px-2 flex items-center justify-between"
|
||||
>
|
||||
<span className="text-sm truncate flex-1 text-left">
|
||||
{config.profile?.variant || 'Default'}
|
||||
{currentProfileVariant?.variant || 'DEFAULT'}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 ml-1 flex-shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
const newProfile: ProfileVariantLabel = {
|
||||
profile: config.profile?.profile || '',
|
||||
variant: null,
|
||||
};
|
||||
updateConfig({ profile: newProfile });
|
||||
}}
|
||||
className={
|
||||
!config.profile?.variant ? 'bg-accent' : ''
|
||||
}
|
||||
>
|
||||
Default
|
||||
</DropdownMenuItem>
|
||||
{selectedProfile.variants.map((variant) => (
|
||||
<DropdownMenuItem
|
||||
key={variant.label}
|
||||
onClick={() => {
|
||||
const newProfile: ProfileVariantLabel = {
|
||||
profile: config.profile?.profile || '',
|
||||
variant: variant.label,
|
||||
};
|
||||
updateConfig({ profile: newProfile });
|
||||
}}
|
||||
className={
|
||||
config.profile?.variant === variant.label
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{variant.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{Object.entries(selectedProfile).map(
|
||||
([variantLabel]) => (
|
||||
<DropdownMenuItem
|
||||
key={variantLabel}
|
||||
onClick={() => {
|
||||
const newProfile: ExecutorProfileId = {
|
||||
executor:
|
||||
currentProfileVariant?.executor || '',
|
||||
variant: variantLabel,
|
||||
};
|
||||
updateConfig({
|
||||
executor_profile: newProfile,
|
||||
});
|
||||
}}
|
||||
className={
|
||||
currentProfileVariant?.variant ===
|
||||
variantLabel
|
||||
? 'bg-accent'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{variantLabel}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
@@ -379,8 +372,8 @@ export function Settings() {
|
||||
})()}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose the default profile to use when creating a task
|
||||
attempt.
|
||||
Choose the default executor profile to use when creating a
|
||||
task attempt.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user