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 { useTranslation } from 'react-i18next';
|
||||||
import { useCreateMode } from '@/contexts/CreateModeContext';
|
import { useCreateMode } from '@/contexts/CreateModeContext';
|
||||||
import { useUserSystem } from '@/components/ConfigProvider';
|
import { useUserSystem } from '@/components/ConfigProvider';
|
||||||
import { useCreateWorkspace } from '@/hooks/useCreateWorkspace';
|
import { useCreateWorkspace } from '@/hooks/useCreateWorkspace';
|
||||||
import { useCreateAttachments } from '@/hooks/useCreateAttachments';
|
import { useCreateAttachments } from '@/hooks/useCreateAttachments';
|
||||||
import { getVariantOptions } from '@/utils/executor';
|
import { getVariantOptions, areProfilesEqual } from '@/utils/executor';
|
||||||
import { splitMessageToTitleDescription } from '@/utils/string';
|
import { splitMessageToTitleDescription } from '@/utils/string';
|
||||||
import type { ExecutorProfileId, BaseCodingAgent } from 'shared/types';
|
import type { ExecutorProfileId, BaseCodingAgent } from 'shared/types';
|
||||||
import { CreateChatBox } from '../primitives/CreateChatBox';
|
import { CreateChatBox } from '../primitives/CreateChatBox';
|
||||||
|
|
||||||
export function CreateChatBoxContainer() {
|
export function CreateChatBoxContainer() {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
const { profiles, config } = useUserSystem();
|
const { profiles, config, updateAndSaveConfig } = useUserSystem();
|
||||||
const {
|
const {
|
||||||
repos,
|
repos,
|
||||||
targetBranches,
|
targetBranches,
|
||||||
@@ -26,6 +26,7 @@ export function CreateChatBoxContainer() {
|
|||||||
|
|
||||||
const { createWorkspace } = useCreateWorkspace();
|
const { createWorkspace } = useCreateWorkspace();
|
||||||
const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false);
|
const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false);
|
||||||
|
const [saveAsDefault, setSaveAsDefault] = useState(false);
|
||||||
|
|
||||||
// Attachment handling - insert markdown and track image IDs
|
// Attachment handling - insert markdown and track image IDs
|
||||||
const handleInsertMarkdown = useCallback(
|
const handleInsertMarkdown = useCallback(
|
||||||
@@ -64,6 +65,19 @@ export function CreateChatBoxContainer() {
|
|||||||
[effectiveProfile?.executor, profiles]
|
[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
|
// Get project ID from context
|
||||||
const projectId = selectedProjectId;
|
const projectId = selectedProjectId;
|
||||||
|
|
||||||
@@ -86,17 +100,39 @@ export function CreateChatBoxContainer() {
|
|||||||
[effectiveProfile, setSelectedProfile]
|
[effectiveProfile, setSelectedProfile]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle executor change - reset variant to first available
|
// Handle executor change - use saved variant if switching to default executor
|
||||||
const handleExecutorChange = useCallback(
|
const handleExecutorChange = useCallback(
|
||||||
(executor: BaseCodingAgent) => {
|
(executor: BaseCodingAgent) => {
|
||||||
const executorConfig = profiles?.[executor];
|
const executorConfig = profiles?.[executor];
|
||||||
const variants = executorConfig ? Object.keys(executorConfig) : [];
|
if (!executorConfig) {
|
||||||
setSelectedProfile({
|
setSelectedProfile({ executor, variant: null });
|
||||||
executor,
|
return;
|
||||||
variant: variants[0] ?? null,
|
}
|
||||||
});
|
|
||||||
|
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
|
// Handle submit
|
||||||
@@ -104,6 +140,11 @@ export function CreateChatBoxContainer() {
|
|||||||
setHasAttemptedSubmit(true);
|
setHasAttemptedSubmit(true);
|
||||||
if (!canSubmit || !effectiveProfile || !projectId) return;
|
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);
|
const { title, description } = splitMessageToTitleDescription(message);
|
||||||
|
|
||||||
await createWorkspace.mutateAsync({
|
await createWorkspace.mutateAsync({
|
||||||
@@ -137,6 +178,9 @@ export function CreateChatBoxContainer() {
|
|||||||
getImageIds,
|
getImageIds,
|
||||||
clearAttachments,
|
clearAttachments,
|
||||||
clearDraft,
|
clearDraft,
|
||||||
|
saveAsDefault,
|
||||||
|
hasChangedFromDefault,
|
||||||
|
updateAndSaveConfig,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Determine error to display
|
// Determine error to display
|
||||||
@@ -194,6 +238,11 @@ export function CreateChatBoxContainer() {
|
|||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
saveAsDefault={{
|
||||||
|
checked: saveAsDefault,
|
||||||
|
onChange: setSaveAsDefault,
|
||||||
|
visible: hasChangedFromDefault,
|
||||||
|
}}
|
||||||
error={displayError}
|
error={displayError}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
agent={effectiveProfile?.executor ?? null}
|
agent={effectiveProfile?.executor ?? null}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { toPrettyCase } from '@/utils/string';
|
|||||||
import type { BaseCodingAgent } from 'shared/types';
|
import type { BaseCodingAgent } from 'shared/types';
|
||||||
import type { LocalImageMetadata } from '@/components/ui/wysiwyg/context/task-attempt-context';
|
import type { LocalImageMetadata } from '@/components/ui/wysiwyg/context/task-attempt-context';
|
||||||
import { AgentIcon } from '@/components/agents/AgentIcon';
|
import { AgentIcon } from '@/components/agents/AgentIcon';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
import {
|
||||||
ChatBoxBase,
|
ChatBoxBase,
|
||||||
VisualVariant,
|
VisualVariant,
|
||||||
@@ -21,12 +22,19 @@ export interface ExecutorProps {
|
|||||||
onChange: (executor: BaseCodingAgent) => void;
|
onChange: (executor: BaseCodingAgent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SaveAsDefaultProps {
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateChatBoxProps {
|
interface CreateChatBoxProps {
|
||||||
editor: EditorProps;
|
editor: EditorProps;
|
||||||
onSend: () => void;
|
onSend: () => void;
|
||||||
isSending: boolean;
|
isSending: boolean;
|
||||||
executor: ExecutorProps;
|
executor: ExecutorProps;
|
||||||
variant?: VariantProps;
|
variant?: VariantProps;
|
||||||
|
saveAsDefault?: SaveAsDefaultProps;
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
agent?: BaseCodingAgent | null;
|
agent?: BaseCodingAgent | null;
|
||||||
@@ -45,6 +53,7 @@ export function CreateChatBox({
|
|||||||
isSending,
|
isSending,
|
||||||
executor,
|
executor,
|
||||||
variant,
|
variant,
|
||||||
|
saveAsDefault,
|
||||||
error,
|
error,
|
||||||
projectId,
|
projectId,
|
||||||
agent,
|
agent,
|
||||||
@@ -107,6 +116,16 @@ export function CreateChatBox({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
</ToolbarDropdown>
|
</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={
|
footerLeft={
|
||||||
|
|||||||
@@ -220,6 +220,7 @@
|
|||||||
},
|
},
|
||||||
"conversation": {
|
"conversation": {
|
||||||
"executors": "Executors",
|
"executors": "Executors",
|
||||||
|
"saveAsDefault": "Save as default",
|
||||||
"you": "You",
|
"you": "You",
|
||||||
"thinking": "Thinking",
|
"thinking": "Thinking",
|
||||||
"todo": "Todo",
|
"todo": "Todo",
|
||||||
|
|||||||
@@ -623,6 +623,7 @@
|
|||||||
"conflicts_other": "{{count}} archivos en conflicto necesitan resolución manual"
|
"conflicts_other": "{{count}} archivos en conflicto necesitan resolución manual"
|
||||||
},
|
},
|
||||||
"executors": "Ejecutores",
|
"executors": "Ejecutores",
|
||||||
|
"saveAsDefault": "Guardar como predeterminado",
|
||||||
"script": {
|
"script": {
|
||||||
"clickToViewLogs": "Haz clic para ver registros",
|
"clickToViewLogs": "Haz clic para ver registros",
|
||||||
"completedSuccessfully": "Completado exitosamente",
|
"completedSuccessfully": "Completado exitosamente",
|
||||||
|
|||||||
@@ -623,6 +623,7 @@
|
|||||||
"conflicts_other": "{{count}}件の競合ファイルを手動で解決する必要があります"
|
"conflicts_other": "{{count}}件の競合ファイルを手動で解決する必要があります"
|
||||||
},
|
},
|
||||||
"executors": "エクゼキューター",
|
"executors": "エクゼキューター",
|
||||||
|
"saveAsDefault": "デフォルトとして保存",
|
||||||
"script": {
|
"script": {
|
||||||
"clickToViewLogs": "クリックしてログを表示",
|
"clickToViewLogs": "クリックしてログを表示",
|
||||||
"completedSuccessfully": "正常に完了しました",
|
"completedSuccessfully": "正常に完了しました",
|
||||||
|
|||||||
@@ -623,6 +623,7 @@
|
|||||||
"conflicts_other": "{{count}}개의 충돌 파일을 수동으로 해결해야 합니다"
|
"conflicts_other": "{{count}}개의 충돌 파일을 수동으로 해결해야 합니다"
|
||||||
},
|
},
|
||||||
"executors": "실행기",
|
"executors": "실행기",
|
||||||
|
"saveAsDefault": "기본값으로 저장",
|
||||||
"script": {
|
"script": {
|
||||||
"clickToViewLogs": "로그를 보려면 클릭하세요",
|
"clickToViewLogs": "로그를 보려면 클릭하세요",
|
||||||
"completedSuccessfully": "성공적으로 완료됨",
|
"completedSuccessfully": "성공적으로 완료됨",
|
||||||
|
|||||||
@@ -650,6 +650,7 @@
|
|||||||
"clickToViewLogs": "点击查看日志"
|
"clickToViewLogs": "点击查看日志"
|
||||||
},
|
},
|
||||||
"executors": "执行器",
|
"executors": "执行器",
|
||||||
|
"saveAsDefault": "设为默认",
|
||||||
"updatedTodos": "更新的待办事项",
|
"updatedTodos": "更新的待办事项",
|
||||||
"viewInChangesPanel": "在更改面板中查看",
|
"viewInChangesPanel": "在更改面板中查看",
|
||||||
"unableToRenderDiff": "无法显示差异。"
|
"unableToRenderDiff": "无法显示差异。"
|
||||||
|
|||||||
@@ -623,6 +623,7 @@
|
|||||||
"conflicts_other": "{{count}}個衝突檔案需要手動解決"
|
"conflicts_other": "{{count}}個衝突檔案需要手動解決"
|
||||||
},
|
},
|
||||||
"executors": "執行器",
|
"executors": "執行器",
|
||||||
|
"saveAsDefault": "設為預設",
|
||||||
"script": {
|
"script": {
|
||||||
"clickToViewLogs": "點擊查看日誌",
|
"clickToViewLogs": "點擊查看日誌",
|
||||||
"completedSuccessfully": "成功完成",
|
"completedSuccessfully": "成功完成",
|
||||||
|
|||||||
@@ -6,8 +6,25 @@ import type {
|
|||||||
ExecutionProcess,
|
ExecutionProcess,
|
||||||
} from 'shared/types';
|
} 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.
|
* Get variant options for a given executor from profiles.
|
||||||
|
* Returns variants sorted: DEFAULT first, then alphabetically.
|
||||||
*/
|
*/
|
||||||
export function getVariantOptions(
|
export function getVariantOptions(
|
||||||
executor: BaseCodingAgent | null | undefined,
|
executor: BaseCodingAgent | null | undefined,
|
||||||
@@ -15,7 +32,14 @@ export function getVariantOptions(
|
|||||||
): string[] {
|
): string[] {
|
||||||
if (!executor || !profiles) return [];
|
if (!executor || !profiles) return [];
|
||||||
const executorConfig = profiles[executor];
|
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