Improve executor profile selection with save-as-default toggle and variant ordering (Vibe Kanban) (#1912)
* All implementations are complete. Here's a summary of the changes: ## Summary of Changes ### 1. `frontend/src/utils/executor.ts` - Added `areProfilesEqual()` function to compare two `ExecutorProfileId` objects, treating `null`/`undefined` variant as equivalent to `"DEFAULT"` - Updated `getVariantOptions()` to sort variants with DEFAULT first, then alphabetically ### 2. `frontend/src/components/ui-new/containers/CreateChatBoxContainer.tsx` - Added `saveAsDefault` state (default: `false`) - Added `hasChangedFromDefault` memo to detect when user has changed from their saved default - Added `useEffect` to reset toggle when profile matches default again - Updated `handleExecutorChange` to: - Use saved variant when switching TO the user's default executor - Fall back to DEFAULT variant otherwise - Updated `handleSubmit` to save profile as default when toggle is checked - Added `saveAsDefault` prop to `CreateChatBox` ### 3. `frontend/src/components/ui-new/primitives/CreateChatBox.tsx` - Added `SaveAsDefaultProps` interface - Added `saveAsDefault` prop to `CreateChatBoxProps` - Rendered checkbox toggle in `headerLeft` (after executor dropdown) when `saveAsDefault.visible` is true ### 4. `frontend/src/i18n/locales/en/tasks.json` - Added `"saveAsDefault": "Save as default"` translation key under `conversation` * Cleanup script changes for workspace 143d0206-8a21-454b-96ad-ce53cbcfc24a * All translations have been added: | Language | Translation | |----------|-------------| | English (en) | "Save as default" | | Spanish (es) | "Guardar como predeterminado" | | Japanese (ja) | "デフォルトとして保存" | | Korean (ko) | "기본값으로 저장" | | Simplified Chinese (zh-Hans) | "设为默认" | | Traditional Chinese (zh-Hant) | "設為預設" |
This commit is contained in:
committed by
GitHub
parent
d3dc1439cc
commit
4e20df9823
@@ -1,17 +1,17 @@
|
||||
import { useMemo, useCallback, useState } from 'react';
|
||||
import { useMemo, useCallback, useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCreateMode } from '@/contexts/CreateModeContext';
|
||||
import { useUserSystem } from '@/components/ConfigProvider';
|
||||
import { useCreateWorkspace } from '@/hooks/useCreateWorkspace';
|
||||
import { useCreateAttachments } from '@/hooks/useCreateAttachments';
|
||||
import { getVariantOptions } from '@/utils/executor';
|
||||
import { getVariantOptions, areProfilesEqual } from '@/utils/executor';
|
||||
import { splitMessageToTitleDescription } from '@/utils/string';
|
||||
import type { ExecutorProfileId, BaseCodingAgent } from 'shared/types';
|
||||
import { CreateChatBox } from '../primitives/CreateChatBox';
|
||||
|
||||
export function CreateChatBoxContainer() {
|
||||
const { t } = useTranslation('common');
|
||||
const { profiles, config } = useUserSystem();
|
||||
const { profiles, config, updateAndSaveConfig } = useUserSystem();
|
||||
const {
|
||||
repos,
|
||||
targetBranches,
|
||||
@@ -26,6 +26,7 @@ export function CreateChatBoxContainer() {
|
||||
|
||||
const { createWorkspace } = useCreateWorkspace();
|
||||
const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false);
|
||||
const [saveAsDefault, setSaveAsDefault] = useState(false);
|
||||
|
||||
// Attachment handling - insert markdown and track image IDs
|
||||
const handleInsertMarkdown = useCallback(
|
||||
@@ -64,6 +65,19 @@ export function CreateChatBoxContainer() {
|
||||
[effectiveProfile?.executor, profiles]
|
||||
);
|
||||
|
||||
// Detect if user has changed from their saved default
|
||||
const hasChangedFromDefault = useMemo(() => {
|
||||
if (!config?.executor_profile || !effectiveProfile) return false;
|
||||
return !areProfilesEqual(effectiveProfile, config.executor_profile);
|
||||
}, [effectiveProfile, config?.executor_profile]);
|
||||
|
||||
// Reset toggle when profile matches default again
|
||||
useEffect(() => {
|
||||
if (!hasChangedFromDefault) {
|
||||
setSaveAsDefault(false);
|
||||
}
|
||||
}, [hasChangedFromDefault]);
|
||||
|
||||
// Get project ID from context
|
||||
const projectId = selectedProjectId;
|
||||
|
||||
@@ -86,17 +100,39 @@ export function CreateChatBoxContainer() {
|
||||
[effectiveProfile, setSelectedProfile]
|
||||
);
|
||||
|
||||
// Handle executor change - reset variant to first available
|
||||
// Handle executor change - use saved variant if switching to default executor
|
||||
const handleExecutorChange = useCallback(
|
||||
(executor: BaseCodingAgent) => {
|
||||
const executorConfig = profiles?.[executor];
|
||||
const variants = executorConfig ? Object.keys(executorConfig) : [];
|
||||
setSelectedProfile({
|
||||
executor,
|
||||
variant: variants[0] ?? null,
|
||||
});
|
||||
if (!executorConfig) {
|
||||
setSelectedProfile({ executor, variant: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const variants = Object.keys(executorConfig);
|
||||
let targetVariant: string | null = null;
|
||||
|
||||
// If switching to user's default executor, use their saved variant
|
||||
if (
|
||||
config?.executor_profile?.executor === executor &&
|
||||
config?.executor_profile?.variant
|
||||
) {
|
||||
const savedVariant = config.executor_profile.variant;
|
||||
if (variants.includes(savedVariant)) {
|
||||
targetVariant = savedVariant;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to DEFAULT or first available
|
||||
if (!targetVariant) {
|
||||
targetVariant = variants.includes('DEFAULT')
|
||||
? 'DEFAULT'
|
||||
: (variants[0] ?? null);
|
||||
}
|
||||
|
||||
setSelectedProfile({ executor, variant: targetVariant });
|
||||
},
|
||||
[profiles, setSelectedProfile]
|
||||
[profiles, setSelectedProfile, config?.executor_profile]
|
||||
);
|
||||
|
||||
// Handle submit
|
||||
@@ -104,6 +140,11 @@ export function CreateChatBoxContainer() {
|
||||
setHasAttemptedSubmit(true);
|
||||
if (!canSubmit || !effectiveProfile || !projectId) return;
|
||||
|
||||
// Save profile as default if toggle is checked
|
||||
if (saveAsDefault && hasChangedFromDefault) {
|
||||
await updateAndSaveConfig({ executor_profile: effectiveProfile });
|
||||
}
|
||||
|
||||
const { title, description } = splitMessageToTitleDescription(message);
|
||||
|
||||
await createWorkspace.mutateAsync({
|
||||
@@ -137,6 +178,9 @@ export function CreateChatBoxContainer() {
|
||||
getImageIds,
|
||||
clearAttachments,
|
||||
clearDraft,
|
||||
saveAsDefault,
|
||||
hasChangedFromDefault,
|
||||
updateAndSaveConfig,
|
||||
]);
|
||||
|
||||
// Determine error to display
|
||||
@@ -194,6 +238,11 @@ export function CreateChatBoxContainer() {
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
saveAsDefault={{
|
||||
checked: saveAsDefault,
|
||||
onChange: setSaveAsDefault,
|
||||
visible: hasChangedFromDefault,
|
||||
}}
|
||||
error={displayError}
|
||||
projectId={projectId}
|
||||
agent={effectiveProfile?.executor ?? null}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { toPrettyCase } from '@/utils/string';
|
||||
import type { BaseCodingAgent } from 'shared/types';
|
||||
import type { LocalImageMetadata } from '@/components/ui/wysiwyg/context/task-attempt-context';
|
||||
import { AgentIcon } from '@/components/agents/AgentIcon';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
ChatBoxBase,
|
||||
VisualVariant,
|
||||
@@ -21,12 +22,19 @@ export interface ExecutorProps {
|
||||
onChange: (executor: BaseCodingAgent) => void;
|
||||
}
|
||||
|
||||
export interface SaveAsDefaultProps {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
interface CreateChatBoxProps {
|
||||
editor: EditorProps;
|
||||
onSend: () => void;
|
||||
isSending: boolean;
|
||||
executor: ExecutorProps;
|
||||
variant?: VariantProps;
|
||||
saveAsDefault?: SaveAsDefaultProps;
|
||||
error?: string | null;
|
||||
projectId?: string;
|
||||
agent?: BaseCodingAgent | null;
|
||||
@@ -45,6 +53,7 @@ export function CreateChatBox({
|
||||
isSending,
|
||||
executor,
|
||||
variant,
|
||||
saveAsDefault,
|
||||
error,
|
||||
projectId,
|
||||
agent,
|
||||
@@ -107,6 +116,16 @@ export function CreateChatBox({
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</ToolbarDropdown>
|
||||
{saveAsDefault?.visible && (
|
||||
<label className="flex items-center gap-1.5 text-sm text-low cursor-pointer ml-2">
|
||||
<Checkbox
|
||||
checked={saveAsDefault.checked}
|
||||
onCheckedChange={saveAsDefault.onChange}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
<span>{t('conversation.saveAsDefault')}</span>
|
||||
</label>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
footerLeft={
|
||||
|
||||
@@ -220,6 +220,7 @@
|
||||
},
|
||||
"conversation": {
|
||||
"executors": "Executors",
|
||||
"saveAsDefault": "Save as default",
|
||||
"you": "You",
|
||||
"thinking": "Thinking",
|
||||
"todo": "Todo",
|
||||
|
||||
@@ -623,6 +623,7 @@
|
||||
"conflicts_other": "{{count}} archivos en conflicto necesitan resolución manual"
|
||||
},
|
||||
"executors": "Ejecutores",
|
||||
"saveAsDefault": "Guardar como predeterminado",
|
||||
"script": {
|
||||
"clickToViewLogs": "Haz clic para ver registros",
|
||||
"completedSuccessfully": "Completado exitosamente",
|
||||
|
||||
@@ -623,6 +623,7 @@
|
||||
"conflicts_other": "{{count}}件の競合ファイルを手動で解決する必要があります"
|
||||
},
|
||||
"executors": "エクゼキューター",
|
||||
"saveAsDefault": "デフォルトとして保存",
|
||||
"script": {
|
||||
"clickToViewLogs": "クリックしてログを表示",
|
||||
"completedSuccessfully": "正常に完了しました",
|
||||
|
||||
@@ -623,6 +623,7 @@
|
||||
"conflicts_other": "{{count}}개의 충돌 파일을 수동으로 해결해야 합니다"
|
||||
},
|
||||
"executors": "실행기",
|
||||
"saveAsDefault": "기본값으로 저장",
|
||||
"script": {
|
||||
"clickToViewLogs": "로그를 보려면 클릭하세요",
|
||||
"completedSuccessfully": "성공적으로 완료됨",
|
||||
|
||||
@@ -650,6 +650,7 @@
|
||||
"clickToViewLogs": "点击查看日志"
|
||||
},
|
||||
"executors": "执行器",
|
||||
"saveAsDefault": "设为默认",
|
||||
"updatedTodos": "更新的待办事项",
|
||||
"viewInChangesPanel": "在更改面板中查看",
|
||||
"unableToRenderDiff": "无法显示差异。"
|
||||
|
||||
@@ -623,6 +623,7 @@
|
||||
"conflicts_other": "{{count}}個衝突檔案需要手動解決"
|
||||
},
|
||||
"executors": "執行器",
|
||||
"saveAsDefault": "設為預設",
|
||||
"script": {
|
||||
"clickToViewLogs": "點擊查看日誌",
|
||||
"completedSuccessfully": "成功完成",
|
||||
|
||||
@@ -6,8 +6,25 @@ import type {
|
||||
ExecutionProcess,
|
||||
} from 'shared/types';
|
||||
|
||||
/**
|
||||
* Compare two ExecutorProfileIds for equality.
|
||||
* Treats null/undefined variant as equivalent to "DEFAULT".
|
||||
*/
|
||||
export function areProfilesEqual(
|
||||
a: ExecutorProfileId | null | undefined,
|
||||
b: ExecutorProfileId | null | undefined
|
||||
): boolean {
|
||||
if (!a || !b) return a === b;
|
||||
if (a.executor !== b.executor) return false;
|
||||
// Normalize variants: null/undefined -> 'DEFAULT'
|
||||
const variantA = a.variant ?? 'DEFAULT';
|
||||
const variantB = b.variant ?? 'DEFAULT';
|
||||
return variantA === variantB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get variant options for a given executor from profiles.
|
||||
* Returns variants sorted: DEFAULT first, then alphabetically.
|
||||
*/
|
||||
export function getVariantOptions(
|
||||
executor: BaseCodingAgent | null | undefined,
|
||||
@@ -15,7 +32,14 @@ export function getVariantOptions(
|
||||
): string[] {
|
||||
if (!executor || !profiles) return [];
|
||||
const executorConfig = profiles[executor];
|
||||
return executorConfig ? Object.keys(executorConfig) : [];
|
||||
if (!executorConfig) return [];
|
||||
|
||||
const variants = Object.keys(executorConfig);
|
||||
return variants.sort((a, b) => {
|
||||
if (a === 'DEFAULT') return -1;
|
||||
if (b === 'DEFAULT') return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user