* ## ✅ Complete - First 3 Categories Fixed Successfully resolved **13 of 26 TypeScript warnings** (50% reduction): ### **Changes Made:** **Category 1: Intl Polyfill (1 warning fixed)** - [TaskPanel.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/panels/TaskPanel.tsx#L39-L42): Replaced `(Intl as any).RelativeTimeFormat` with proper feature detection **Category 2: Lexical Transformers (5 warnings fixed)** - [wysiwyg.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/ui/wysiwyg.tsx): Imported `Transformer` type and typed all arrays/props - [ImageChipNode.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/ui/wysiwyg/ImageChipNode.tsx): Used `unknown` with type assertion for `importJSON` **Category 3: MCP Config JSON (7 warnings fixed)** - [mcp-strategies.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/lib/mcp-strategies.ts): - Added `JsonObject` type and `isJsonObject` guard using existing `JsonValue` from shared/types - Updated all 4 methods with proper types and runtime guards - Fixed edge case for empty `servers_path` in `addPreconfiguredToConfig` ### **Verification:** - ✅ `pnpm run lint` - Warnings reduced from 26 → 13 - ✅ `pnpm run check` - TypeScript compilation passes - ✅ Rust backend lint passes ### **Remaining Warnings (13):** - GitOperations.tsx: 3 warnings (branch merge parsing) - ClickedElementsBanner.tsx: 1 warning - useFollowUpSend.ts: 1 warning - usePinnedTodos.ts: 1 warning - useTaskMutations.ts: 2 warnings - project-tasks.tsx: 3 warnings - AgentSettings.tsx: 2 warnings * Excellent! All warnings are resolved. Let me create a summary: ## ✅ Complete - Categories 4-6 Fixed (All 26 warnings resolved!) Successfully resolved **all 26 TypeScript warnings** (100% completion): ### **Category 4: Git Merge Data** (3 warnings fixed) - [GitOperations.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/tasks/Toolbar/GitOperations.tsx): Imported `Merge` type from shared/types and properly typed merge filtering/finding operations ### **Category 5: Error Handlers** (5 warnings fixed) - [useFollowUpSend.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/hooks/follow-up/useFollowUpSend.ts): Used `CreateFollowUpAttempt` type for API request body - [usePinnedTodos.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/hooks/usePinnedTodos.ts): Used `NormalizedEntry` type from shared/types - [useTaskMutations.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/hooks/useTaskMutations.ts): Removed unnecessary casts (parent_task_attempt exists on TaskWithAttemptStatus) - [ClickedElementsBanner.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/components/tasks/ClickedElementsBanner.tsx): Defined `ComponentInfo` type derived from ClickedEntry ### **Category 6: Settings Page** (2 warnings fixed) - [AgentSettings.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/pages/settings/AgentSettings.tsx): - Defined `ProfilesDoc` type for parsed profiles structure - Changed `selectedExecutorType` from `string` to `BaseCodingAgent` enum ### **Additional Fixes:** - [project-tasks.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/pages/project-tasks.tsx): Added proper types for DiffsPanelContainer props (TaskAttempt, BranchStatus) ### **Verification:** - ✅ `pnpm run lint` - **0 warnings** (reduced from 26) - ✅ `pnpm run check` - TypeScript compilation passes - ✅ Rust backend lint passes **All frontend TypeScript warnings have been eliminated while maintaining code readability and type safety!** * ## ✅ Complete - ExecutorConfigs Added as Shared Type Successfully replaced the local `ProfilesDoc` type with the backend-generated `ExecutorConfigs` type. ### **Changes Made:** **Backend:** - [generate_types.rs](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/crates/server/src/bin/generate_types.rs#L108): Added `ExecutorConfigs` to exported types - [shared/types.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/shared/types.ts#L225): Generated new `ExecutorConfigs` type **Frontend:** - [AgentSettings.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/pages/settings/AgentSettings.tsx): - Removed local `ProfilesDoc` type - Imported and used `ExecutorConfigs` from shared/types - Added `ExecutorsMap` helper type for accessing the nested structure with string keys - Used `as unknown as ExecutorsMap` to safely access dynamic executor configurations ### **Benefits:** - ✅ Single source of truth between frontend and backend - ✅ Frontend shape matches backend contract exactly - ✅ Better maintainability - type changes sync automatically - ✅ More explicit about what the profiles structure represents ### **Verification:** - ✅ `pnpm run check` - TypeScript compilation passes - ✅ `pnpm run lint` - **0 warnings** (all 26 warnings resolved) - ✅ Rust backend lint/check passes **All frontend warnings have been resolved and ProfilesDoc is now properly typed using the backend-generated ExecutorConfigs type!** * Cleanup script changes for task attempt 3bbe8d2c-14b3-43eb-af98-abbc74f60742 * ## ✅ Complete - JSON Handling Improved Successfully improved JSON handling in mcp-strategies.ts with a simple, idiomatic change: ### **Change Made:** [mcp-strategies.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/3bbe-resolve-frontend/frontend/src/lib/mcp-strategies.ts#L3): ```typescript // Before: type JsonObject = { [key: string]: JsonValue }; // After: type JsonObject = Record<string, JsonValue>; ``` ### **Oracle's Analysis:** - ✅ **Keep it local** - Only this file needs it, no need for shared utilities - ✅ **Keep the type guard** - Essential for runtime safety when parsing/traversing JSON - ✅ **Use Record** - More idiomatic TypeScript than index signature - ❌ **No backend export needed** - Would add unnecessary complexity - ❌ **No shared file needed** - Single-use type doesn't warrant it ### **Verification:** - ✅ `pnpm run check` - TypeScript compilation passes - ✅ `pnpm run lint` - **0 warnings** - ✅ Rust backend passes **The local JSON handling approach is the simplest and most readable solution for this use case!**
595 lines
19 KiB
TypeScript
595 lines
19 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { JSONEditor } from '@/components/ui/json-editor';
|
|
import { Loader2 } from 'lucide-react';
|
|
|
|
import { ExecutorConfigForm } from '@/components/ExecutorConfigForm';
|
|
import { useProfiles } from '@/hooks/useProfiles';
|
|
import { useUserSystem } from '@/components/config-provider';
|
|
import { CreateConfigurationDialog } from '@/components/dialogs/settings/CreateConfigurationDialog';
|
|
import { DeleteConfigurationDialog } from '@/components/dialogs/settings/DeleteConfigurationDialog';
|
|
import type { BaseCodingAgent, ExecutorConfigs } from 'shared/types';
|
|
|
|
type ExecutorsMap = Record<string, Record<string, Record<string, unknown>>>;
|
|
|
|
export function AgentSettings() {
|
|
const { t } = useTranslation('settings');
|
|
// Use profiles hook for server state
|
|
const {
|
|
profilesContent: serverProfilesContent,
|
|
profilesPath,
|
|
isLoading: profilesLoading,
|
|
isSaving: profilesSaving,
|
|
error: profilesError,
|
|
save: saveProfiles,
|
|
} = useProfiles();
|
|
|
|
const { reloadSystem } = useUserSystem();
|
|
|
|
// Local editor state (draft that may differ from server)
|
|
const [localProfilesContent, setLocalProfilesContent] = useState('');
|
|
const [profilesSuccess, setProfilesSuccess] = useState(false);
|
|
const [saveError, setSaveError] = useState<string | null>(null);
|
|
|
|
// Form-based editor state
|
|
const [useFormEditor, setUseFormEditor] = useState(true);
|
|
const [selectedExecutorType, setSelectedExecutorType] =
|
|
useState<BaseCodingAgent>('CLAUDE_CODE' as BaseCodingAgent);
|
|
const [selectedConfiguration, setSelectedConfiguration] =
|
|
useState<string>('DEFAULT');
|
|
const [localParsedProfiles, setLocalParsedProfiles] =
|
|
useState<ExecutorConfigs | null>(null);
|
|
const [isDirty, setIsDirty] = useState(false);
|
|
|
|
// Sync server state to local state when not dirty
|
|
useEffect(() => {
|
|
if (!isDirty && serverProfilesContent) {
|
|
setLocalProfilesContent(serverProfilesContent);
|
|
// Parse JSON inside effect to avoid object dependency
|
|
try {
|
|
const parsed = JSON.parse(serverProfilesContent);
|
|
setLocalParsedProfiles(parsed);
|
|
} catch (err) {
|
|
console.error('Failed to parse profiles JSON:', err);
|
|
setLocalParsedProfiles(null);
|
|
}
|
|
}
|
|
}, [serverProfilesContent, isDirty]);
|
|
|
|
// Sync raw profiles with parsed profiles
|
|
const syncRawProfiles = (profiles: unknown) => {
|
|
setLocalProfilesContent(JSON.stringify(profiles, null, 2));
|
|
};
|
|
|
|
// Mark profiles as dirty
|
|
const markDirty = (nextProfiles: unknown) => {
|
|
setLocalParsedProfiles(nextProfiles as ExecutorConfigs);
|
|
syncRawProfiles(nextProfiles);
|
|
setIsDirty(true);
|
|
};
|
|
|
|
// Open create dialog
|
|
const openCreateDialog = async () => {
|
|
try {
|
|
const result = await CreateConfigurationDialog.show({
|
|
executorType: selectedExecutorType,
|
|
existingConfigs: Object.keys(
|
|
localParsedProfiles?.executors?.[selectedExecutorType] || {}
|
|
),
|
|
});
|
|
|
|
if (result.action === 'created' && result.configName) {
|
|
createConfiguration(
|
|
selectedExecutorType,
|
|
result.configName,
|
|
result.cloneFrom
|
|
);
|
|
}
|
|
} catch (error) {
|
|
// User cancelled - do nothing
|
|
}
|
|
};
|
|
|
|
// Create new configuration
|
|
const createConfiguration = (
|
|
executorType: string,
|
|
configName: string,
|
|
baseConfig?: string | null
|
|
) => {
|
|
if (!localParsedProfiles || !localParsedProfiles.executors) return;
|
|
|
|
const executorsMap =
|
|
localParsedProfiles.executors as unknown as ExecutorsMap;
|
|
const base =
|
|
baseConfig && executorsMap[executorType]?.[baseConfig]?.[executorType]
|
|
? executorsMap[executorType][baseConfig][executorType]
|
|
: {};
|
|
|
|
const updatedProfiles = {
|
|
...localParsedProfiles,
|
|
executors: {
|
|
...localParsedProfiles.executors,
|
|
[executorType]: {
|
|
...executorsMap[executorType],
|
|
[configName]: {
|
|
[executorType]: base,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
markDirty(updatedProfiles);
|
|
setSelectedConfiguration(configName);
|
|
};
|
|
|
|
// Open delete dialog
|
|
const openDeleteDialog = async (configName: string) => {
|
|
try {
|
|
const result = await DeleteConfigurationDialog.show({
|
|
configName,
|
|
executorType: selectedExecutorType,
|
|
});
|
|
|
|
if (result === 'deleted') {
|
|
await handleDeleteConfiguration(configName);
|
|
}
|
|
} catch (error) {
|
|
// User cancelled - do nothing
|
|
}
|
|
};
|
|
|
|
// Handle delete configuration
|
|
const handleDeleteConfiguration = async (configToDelete: string) => {
|
|
if (!localParsedProfiles) {
|
|
return;
|
|
}
|
|
|
|
// Clear any previous errors
|
|
setSaveError(null);
|
|
|
|
try {
|
|
// Validate that the configuration exists
|
|
if (
|
|
!localParsedProfiles.executors[selectedExecutorType]?.[configToDelete]
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Check if this is the last configuration
|
|
const currentConfigs = Object.keys(
|
|
localParsedProfiles.executors[selectedExecutorType] || {}
|
|
);
|
|
if (currentConfigs.length <= 1) {
|
|
return;
|
|
}
|
|
|
|
// Remove the configuration from the executor
|
|
const remainingConfigs = {
|
|
...localParsedProfiles.executors[selectedExecutorType],
|
|
};
|
|
delete remainingConfigs[configToDelete];
|
|
|
|
const updatedProfiles = {
|
|
...localParsedProfiles,
|
|
executors: {
|
|
...localParsedProfiles.executors,
|
|
[selectedExecutorType]: remainingConfigs,
|
|
},
|
|
};
|
|
|
|
const executorsMap = updatedProfiles.executors as unknown as ExecutorsMap;
|
|
// If no configurations left, create a blank DEFAULT (should not happen due to check above)
|
|
if (Object.keys(remainingConfigs).length === 0) {
|
|
executorsMap[selectedExecutorType] = {
|
|
DEFAULT: { [selectedExecutorType]: {} },
|
|
};
|
|
}
|
|
|
|
try {
|
|
// Save using hook
|
|
await saveProfiles(JSON.stringify(updatedProfiles, null, 2));
|
|
|
|
// Update local state and reset dirty flag
|
|
setLocalParsedProfiles(updatedProfiles);
|
|
setLocalProfilesContent(JSON.stringify(updatedProfiles, null, 2));
|
|
setIsDirty(false);
|
|
|
|
// Select the next available configuration
|
|
const nextConfigs = Object.keys(
|
|
executorsMap[selectedExecutorType] || {}
|
|
);
|
|
const nextSelected = nextConfigs[0] || 'DEFAULT';
|
|
setSelectedConfiguration(nextSelected);
|
|
|
|
// Show success
|
|
setProfilesSuccess(true);
|
|
setTimeout(() => setProfilesSuccess(false), 3000);
|
|
|
|
// Refresh global system so deleted configs are removed elsewhere
|
|
reloadSystem();
|
|
} catch (saveError: unknown) {
|
|
console.error('Failed to save deletion to backend:', saveError);
|
|
setSaveError(t('settings.agents.errors.deleteFailed'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting configuration:', error);
|
|
}
|
|
};
|
|
|
|
const handleProfilesChange = (value: string) => {
|
|
setLocalProfilesContent(value);
|
|
setIsDirty(true);
|
|
|
|
// Validate JSON on change
|
|
if (value.trim()) {
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
setLocalParsedProfiles(parsed);
|
|
} catch (err) {
|
|
// Invalid JSON, keep local content but clear parsed
|
|
setLocalParsedProfiles(null);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleSaveProfiles = async () => {
|
|
// Clear any previous errors
|
|
setSaveError(null);
|
|
|
|
try {
|
|
const contentToSave =
|
|
useFormEditor && localParsedProfiles
|
|
? JSON.stringify(localParsedProfiles, null, 2)
|
|
: localProfilesContent;
|
|
|
|
await saveProfiles(contentToSave);
|
|
setProfilesSuccess(true);
|
|
setIsDirty(false);
|
|
setTimeout(() => setProfilesSuccess(false), 3000);
|
|
|
|
// Update the local content if using form editor
|
|
if (useFormEditor && localParsedProfiles) {
|
|
setLocalProfilesContent(contentToSave);
|
|
}
|
|
|
|
// Refresh global system so new profiles are available elsewhere
|
|
reloadSystem();
|
|
} catch (err: unknown) {
|
|
console.error('Failed to save profiles:', err);
|
|
setSaveError(t('settings.agents.errors.saveFailed'));
|
|
}
|
|
};
|
|
|
|
const handleExecutorConfigChange = (
|
|
executorType: string,
|
|
configuration: string,
|
|
formData: unknown
|
|
) => {
|
|
if (!localParsedProfiles || !localParsedProfiles.executors) return;
|
|
|
|
const executorsMap =
|
|
localParsedProfiles.executors as unknown as ExecutorsMap;
|
|
// Update the parsed profiles with the new config
|
|
const updatedProfiles = {
|
|
...localParsedProfiles,
|
|
executors: {
|
|
...localParsedProfiles.executors,
|
|
[executorType]: {
|
|
...executorsMap[executorType],
|
|
[configuration]: {
|
|
[executorType]: formData,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
markDirty(updatedProfiles);
|
|
};
|
|
|
|
const handleExecutorConfigSave = async (formData: unknown) => {
|
|
if (!localParsedProfiles || !localParsedProfiles.executors) return;
|
|
|
|
// Clear any previous errors
|
|
setSaveError(null);
|
|
|
|
// Update the parsed profiles with the saved config
|
|
const updatedProfiles = {
|
|
...localParsedProfiles,
|
|
executors: {
|
|
...localParsedProfiles.executors,
|
|
[selectedExecutorType]: {
|
|
...localParsedProfiles.executors[selectedExecutorType],
|
|
[selectedConfiguration]: {
|
|
[selectedExecutorType]: formData,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
// Update state
|
|
setLocalParsedProfiles(updatedProfiles);
|
|
|
|
// Save the updated profiles directly
|
|
try {
|
|
const contentToSave = JSON.stringify(updatedProfiles, null, 2);
|
|
|
|
await saveProfiles(contentToSave);
|
|
setProfilesSuccess(true);
|
|
setIsDirty(false);
|
|
setTimeout(() => setProfilesSuccess(false), 3000);
|
|
|
|
// Update the local content as well
|
|
setLocalProfilesContent(contentToSave);
|
|
|
|
// Refresh global system so new profiles are available elsewhere
|
|
reloadSystem();
|
|
} catch (err: unknown) {
|
|
console.error('Failed to save profiles:', err);
|
|
setSaveError(t('settings.agents.errors.saveConfigFailed'));
|
|
}
|
|
};
|
|
|
|
if (profilesLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="h-8 w-8 animate-spin" />
|
|
<span className="ml-2">{t('settings.agents.loading')}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{!!profilesError && (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>
|
|
{profilesError instanceof Error
|
|
? profilesError.message
|
|
: String(profilesError)}
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{profilesSuccess && (
|
|
<Alert variant="success">
|
|
<AlertDescription className="font-medium">
|
|
{t('settings.agents.save.success')}
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{saveError && (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>{saveError}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t('settings.agents.title')}</CardTitle>
|
|
<CardDescription>{t('settings.agents.description')}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Editor type toggle */}
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="use-form-editor"
|
|
checked={!useFormEditor}
|
|
onCheckedChange={(checked) => setUseFormEditor(!checked)}
|
|
disabled={profilesLoading || !localParsedProfiles}
|
|
/>
|
|
<Label htmlFor="use-form-editor">
|
|
{t('settings.agents.editor.formLabel')}
|
|
</Label>
|
|
</div>
|
|
|
|
{useFormEditor &&
|
|
localParsedProfiles &&
|
|
localParsedProfiles.executors ? (
|
|
// Form-based editor
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="executor-type">
|
|
{t('settings.agents.editor.agentLabel')}
|
|
</Label>
|
|
<Select
|
|
value={selectedExecutorType}
|
|
onValueChange={(value) => {
|
|
setSelectedExecutorType(value as BaseCodingAgent);
|
|
// Reset configuration selection when executor type changes
|
|
setSelectedConfiguration('DEFAULT');
|
|
}}
|
|
>
|
|
<SelectTrigger id="executor-type">
|
|
<SelectValue
|
|
placeholder={t(
|
|
'settings.agents.editor.agentPlaceholder'
|
|
)}
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Object.keys(localParsedProfiles.executors).map(
|
|
(type) => (
|
|
<SelectItem key={type} value={type}>
|
|
{type}
|
|
</SelectItem>
|
|
)
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="configuration">
|
|
{t('settings.agents.editor.configLabel')}
|
|
</Label>
|
|
<div className="flex gap-2">
|
|
<Select
|
|
value={selectedConfiguration}
|
|
onValueChange={(value) => {
|
|
if (value === '__create__') {
|
|
openCreateDialog();
|
|
} else {
|
|
setSelectedConfiguration(value);
|
|
}
|
|
}}
|
|
disabled={
|
|
!localParsedProfiles.executors[selectedExecutorType]
|
|
}
|
|
>
|
|
<SelectTrigger id="configuration">
|
|
<SelectValue
|
|
placeholder={t(
|
|
'settings.agents.editor.configPlaceholder'
|
|
)}
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Object.keys(
|
|
localParsedProfiles.executors[selectedExecutorType] ||
|
|
{}
|
|
).map((configuration) => (
|
|
<SelectItem key={configuration} value={configuration}>
|
|
{configuration}
|
|
</SelectItem>
|
|
))}
|
|
<SelectItem value="__create__">
|
|
{t('settings.agents.editor.createNew')}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
className="h-10"
|
|
onClick={() => openDeleteDialog(selectedConfiguration)}
|
|
disabled={
|
|
profilesSaving ||
|
|
!localParsedProfiles.executors[selectedExecutorType] ||
|
|
Object.keys(
|
|
localParsedProfiles.executors[selectedExecutorType] ||
|
|
{}
|
|
).length <= 1
|
|
}
|
|
title={
|
|
Object.keys(
|
|
localParsedProfiles.executors[selectedExecutorType] ||
|
|
{}
|
|
).length <= 1
|
|
? t('settings.agents.editor.deleteTitle')
|
|
: t('settings.agents.editor.deleteButton', {
|
|
name: selectedConfiguration,
|
|
})
|
|
}
|
|
>
|
|
{t('settings.agents.editor.deleteText')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{(() => {
|
|
const executorsMap =
|
|
localParsedProfiles.executors as unknown as ExecutorsMap;
|
|
return (
|
|
!!executorsMap[selectedExecutorType]?.[
|
|
selectedConfiguration
|
|
]?.[selectedExecutorType] && (
|
|
<ExecutorConfigForm
|
|
executor={selectedExecutorType}
|
|
value={
|
|
(executorsMap[selectedExecutorType][
|
|
selectedConfiguration
|
|
][selectedExecutorType] as Record<string, unknown>) ||
|
|
{}
|
|
}
|
|
onChange={(formData) =>
|
|
handleExecutorConfigChange(
|
|
selectedExecutorType,
|
|
selectedConfiguration,
|
|
formData
|
|
)
|
|
}
|
|
onSave={handleExecutorConfigSave}
|
|
disabled={profilesSaving}
|
|
isSaving={profilesSaving}
|
|
isDirty={isDirty}
|
|
/>
|
|
)
|
|
);
|
|
})()}
|
|
</div>
|
|
) : (
|
|
// Raw JSON editor
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="profiles-editor">
|
|
{t('settings.agents.editor.jsonLabel')}
|
|
</Label>
|
|
<JSONEditor
|
|
id="profiles-editor"
|
|
placeholder={t('settings.agents.editor.jsonPlaceholder')}
|
|
value={
|
|
profilesLoading
|
|
? t('settings.agents.editor.jsonLoading')
|
|
: localProfilesContent
|
|
}
|
|
onChange={handleProfilesChange}
|
|
disabled={profilesLoading}
|
|
minHeight={300}
|
|
/>
|
|
</div>
|
|
|
|
{!profilesError && profilesPath && (
|
|
<div className="space-y-2">
|
|
<p className="text-sm text-muted-foreground">
|
|
<span className="font-medium">
|
|
{t('settings.agents.editor.pathLabel')}
|
|
</span>{' '}
|
|
<span className="font-mono text-xs">{profilesPath}</span>
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{!useFormEditor && (
|
|
<div className="sticky bottom-0 z-10 bg-background/80 backdrop-blur-sm border-t py-4">
|
|
<div className="flex justify-end">
|
|
<Button
|
|
onClick={handleSaveProfiles}
|
|
disabled={!isDirty || profilesSaving || !!profilesError}
|
|
>
|
|
{profilesSaving && (
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
)}
|
|
{t('settings.agents.save.button')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|