From 45d0359118551da81c8960bf8840ad82ce08a626 Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Mon, 8 Dec 2025 15:25:49 +0000 Subject: [PATCH] feat: move default agent configuration to Agent Settings (vibe-kanban) (#1453) --- frontend/src/i18n/locales/en/settings.json | 4 +- frontend/src/i18n/locales/es/settings.json | 4 +- frontend/src/i18n/locales/ja/settings.json | 4 +- frontend/src/i18n/locales/ko/settings.json | 4 +- .../src/i18n/locales/zh-Hans/settings.json | 4 +- frontend/src/pages/settings/AgentSettings.tsx | 224 +++++++++++++++++- .../src/pages/settings/GeneralSettings.tsx | 153 +----------- 7 files changed, 232 insertions(+), 165 deletions(-) diff --git a/frontend/src/i18n/locales/en/settings.json b/frontend/src/i18n/locales/en/settings.json index 7dc7e2ea..f96cf5aa 100644 --- a/frontend/src/i18n/locales/en/settings.json +++ b/frontend/src/i18n/locales/en/settings.json @@ -40,8 +40,8 @@ } }, "taskExecution": { - "title": "Task Execution", - "description": "Configure how tasks are executed and processed.", + "title": "Default Coding Agent", + "description": "Choose the default coding agent for tasks.", "executor": { "label": "Default Agent Configuration", "placeholder": "Select profile", diff --git a/frontend/src/i18n/locales/es/settings.json b/frontend/src/i18n/locales/es/settings.json index b7fa2221..f5b71c8d 100644 --- a/frontend/src/i18n/locales/es/settings.json +++ b/frontend/src/i18n/locales/es/settings.json @@ -40,8 +40,8 @@ } }, "taskExecution": { - "title": "Ejecución de Tareas", - "description": "Configura cómo se ejecutan y procesan las tareas.", + "title": "Agente de Código Predeterminado", + "description": "Elige el agente de código predeterminado para las tareas.", "executor": { "label": "Configuración predeterminada del Agente", "placeholder": "Seleccionar perfil", diff --git a/frontend/src/i18n/locales/ja/settings.json b/frontend/src/i18n/locales/ja/settings.json index ea7c62b0..abbd07be 100644 --- a/frontend/src/i18n/locales/ja/settings.json +++ b/frontend/src/i18n/locales/ja/settings.json @@ -40,8 +40,8 @@ } }, "taskExecution": { - "title": "タスク実行", - "description": "タスクの実行と処理方法を設定します。", + "title": "デフォルトコーディングエージェント", + "description": "タスクのデフォルトコーディングエージェントを選択します。", "executor": { "label": "デフォルトエージェント設定", "placeholder": "プロファイルを選択", diff --git a/frontend/src/i18n/locales/ko/settings.json b/frontend/src/i18n/locales/ko/settings.json index e785f36d..71577f9b 100644 --- a/frontend/src/i18n/locales/ko/settings.json +++ b/frontend/src/i18n/locales/ko/settings.json @@ -40,8 +40,8 @@ } }, "taskExecution": { - "title": "작업 실행", - "description": "작업이 실행되고 처리되는 방식을 구성하세요.", + "title": "기본 코딩 에이전트", + "description": "작업의 기본 코딩 에이전트를 선택하세요.", "executor": { "label": "기본 에이전트 구성", "placeholder": "프로필 선택", diff --git a/frontend/src/i18n/locales/zh-Hans/settings.json b/frontend/src/i18n/locales/zh-Hans/settings.json index 7b56569d..2818c06b 100644 --- a/frontend/src/i18n/locales/zh-Hans/settings.json +++ b/frontend/src/i18n/locales/zh-Hans/settings.json @@ -40,8 +40,8 @@ } }, "taskExecution": { - "title": "任务执行", - "description": "配置任务的执行和处理方式。", + "title": "默认编码代理", + "description": "选择任务的默认编码代理。", "executor": { "label": "默认代理配置", "placeholder": "选择配置文件", diff --git a/frontend/src/pages/settings/AgentSettings.tsx b/frontend/src/pages/settings/AgentSettings.tsx index 84c64b68..823e4505 100644 --- a/frontend/src/pages/settings/AgentSettings.tsx +++ b/frontend/src/pages/settings/AgentSettings.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { cloneDeep, isEqual } from 'lodash'; import { Card, CardContent, @@ -15,23 +16,35 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; 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 { ChevronDown, Loader2 } from 'lucide-react'; import { ExecutorConfigForm } from '@/components/ExecutorConfigForm'; import { useProfiles } from '@/hooks/useProfiles'; import { useUserSystem } from '@/components/ConfigProvider'; import { CreateConfigurationDialog } from '@/components/dialogs/settings/CreateConfigurationDialog'; import { DeleteConfigurationDialog } from '@/components/dialogs/settings/DeleteConfigurationDialog'; -import type { BaseCodingAgent, ExecutorConfigs } from 'shared/types'; +import { useAgentAvailability } from '@/hooks/useAgentAvailability'; +import { AgentAvailabilityIndicator } from '@/components/AgentAvailabilityIndicator'; +import type { + BaseCodingAgent, + ExecutorConfigs, + ExecutorProfileId, +} from 'shared/types'; type ExecutorsMap = Record>>; export function AgentSettings() { - const { t } = useTranslation('settings'); + const { t } = useTranslation(['settings', 'common']); // Use profiles hook for server state const { profilesContent: serverProfilesContent, @@ -42,7 +55,8 @@ export function AgentSettings() { save: saveProfiles, } = useProfiles(); - const { reloadSystem } = useUserSystem(); + const { config, updateAndSaveConfig, profiles, reloadSystem } = + useUserSystem(); // Local editor state (draft that may differ from server) const [localProfilesContent, setLocalProfilesContent] = useState(''); @@ -59,6 +73,17 @@ export function AgentSettings() { useState(null); const [isDirty, setIsDirty] = useState(false); + // Default executor profile state + const [executorDraft, setExecutorDraft] = useState( + () => (config?.executor_profile ? cloneDeep(config.executor_profile) : null) + ); + const [executorSaving, setExecutorSaving] = useState(false); + const [executorSuccess, setExecutorSuccess] = useState(false); + const [executorError, setExecutorError] = useState(null); + + // Check agent availability when draft executor changes + const agentAvailability = useAgentAvailability(executorDraft?.executor); + // Sync server state to local state when not dirty useEffect(() => { if (!isDirty && serverProfilesContent) { @@ -74,6 +99,50 @@ export function AgentSettings() { } }, [serverProfilesContent, isDirty]); + // Check if executor draft differs from saved config + const executorDirty = + executorDraft && config?.executor_profile + ? !isEqual(executorDraft, config.executor_profile) + : false; + + // Sync executor draft when config changes (only if not dirty) + useEffect(() => { + if (config?.executor_profile) { + setExecutorDraft((currentDraft) => { + // Only update if draft matches the old config (not dirty) + if (!currentDraft || isEqual(currentDraft, config.executor_profile)) { + return cloneDeep(config.executor_profile); + } + return currentDraft; + }); + } + }, [config?.executor_profile]); + + // Update executor draft + const updateExecutorDraft = (newProfile: ExecutorProfileId) => { + setExecutorDraft(newProfile); + }; + + // Save executor profile + const handleSaveExecutorProfile = async () => { + if (!executorDraft || !config) return; + + setExecutorSaving(true); + setExecutorError(null); + + try { + await updateAndSaveConfig({ executor_profile: executorDraft }); + setExecutorSuccess(true); + setTimeout(() => setExecutorSuccess(false), 3000); + reloadSystem(); + } catch (err) { + setExecutorError(t('settings.general.save.error')); + console.error('Error saving executor profile:', err); + } finally { + setExecutorSaving(false); + } + }; + // Sync raw profiles with parsed profiles const syncRawProfiles = (profiles: unknown) => { setLocalProfilesContent(JSON.stringify(profiles, null, 2)); @@ -382,6 +451,153 @@ export function AgentSettings() { )} + {executorError && ( + + {executorError} + + )} + + {executorSuccess && ( + + + {t('settings.general.save.success')} + + + )} + + + + {t('settings.general.taskExecution.title')} + + {t('settings.general.taskExecution.description')} + + + +
+ +
+ + + {/* Show variant selector if selected profile has variants */} + {(() => { + const currentProfileVariant = executorDraft; + const selectedProfile = + profiles?.[currentProfileVariant?.executor || '']; + const hasVariants = + selectedProfile && Object.keys(selectedProfile).length > 0; + + if (hasVariants) { + return ( + + + + + + {Object.entries(selectedProfile).map( + ([variantLabel]) => ( + { + const newProfile: ExecutorProfileId = { + executor: currentProfileVariant!.executor, + variant: variantLabel, + }; + updateExecutorDraft(newProfile); + }} + className={ + currentProfileVariant?.variant === variantLabel + ? 'bg-accent' + : '' + } + > + {variantLabel} + + ) + )} + + + ); + } else if (selectedProfile) { + // Show disabled button when profile exists but has no variants + return ( + + ); + } + return null; + })()} +
+ +

+ {t('settings.general.taskExecution.executor.helper')} +

+
+
+ +
+
+
+ {t('settings.agents.title')} diff --git a/frontend/src/pages/settings/GeneralSettings.tsx b/frontend/src/pages/settings/GeneralSettings.tsx index 7cb28c19..639d0311 100644 --- a/frontend/src/pages/settings/GeneralSettings.tsx +++ b/frontend/src/pages/settings/GeneralSettings.tsx @@ -16,32 +16,17 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Checkbox } from '@/components/ui/checkbox'; -import { ChevronDown, Loader2, Volume2 } from 'lucide-react'; -import { - BaseCodingAgent, - EditorType, - ExecutorProfileId, - SoundFile, - ThemeMode, - UiLanguage, -} from 'shared/types'; +import { Loader2, Volume2 } from 'lucide-react'; +import { EditorType, SoundFile, ThemeMode, UiLanguage } from 'shared/types'; import { getLanguageOptions } from '@/i18n/languages'; import { toPrettyCase } from '@/utils/string'; import { useEditorAvailability } from '@/hooks/useEditorAvailability'; import { EditorAvailabilityIndicator } from '@/components/EditorAvailabilityIndicator'; -import { useAgentAvailability } from '@/hooks/useAgentAvailability'; -import { AgentAvailabilityIndicator } from '@/components/AgentAvailabilityIndicator'; import { useTheme } from '@/components/ThemeProvider'; import { useUserSystem } from '@/components/ConfigProvider'; import { TagManager } from '@/components/TagManager'; @@ -60,7 +45,6 @@ export function GeneralSettings() { config, loading, updateAndSaveConfig, // Use this on Save - profiles, } = useUserSystem(); // Draft state management @@ -77,11 +61,6 @@ export function GeneralSettings() { // Check editor availability when draft editor changes const editorAvailability = useEditorAvailability(draft?.editor.editor_type); - // Check agent availability when draft executor changes - const agentAvailability = useAgentAvailability( - draft?.executor_profile?.executor - ); - const validateBranchPrefix = useCallback( (prefix: string): string | null => { if (!prefix) return null; // empty allowed @@ -299,134 +278,6 @@ export function GeneralSettings() { - - - {t('settings.general.taskExecution.title')} - - {t('settings.general.taskExecution.description')} - - - -
- -
- - - {/* Show variant selector if selected profile has variants */} - {(() => { - const currentProfileVariant = draft?.executor_profile; - const selectedProfile = - profiles?.[currentProfileVariant?.executor || '']; - const hasVariants = - selectedProfile && Object.keys(selectedProfile).length > 0; - - if (hasVariants) { - return ( - - - - - - {Object.entries(selectedProfile).map( - ([variantLabel]) => ( - { - const newProfile: ExecutorProfileId = { - executor: currentProfileVariant!.executor, - variant: variantLabel, - }; - updateDraft({ - executor_profile: newProfile, - }); - }} - className={ - currentProfileVariant?.variant === variantLabel - ? 'bg-accent' - : '' - } - > - {variantLabel} - - ) - )} - - - ); - } else if (selectedProfile) { - // Show disabled button when profile exists but has no variants - return ( - - ); - } - return null; - })()} -
- -

- {t('settings.general.taskExecution.executor.helper')} -

-
-
-
- {t('settings.general.editor.title')}