diff --git a/src/features/AgentBuilder/AgentBuilderConversation.tsx b/src/features/AgentBuilder/AgentBuilderConversation.tsx index b062a3fc31..de1a9a50c0 100644 --- a/src/features/AgentBuilder/AgentBuilderConversation.tsx +++ b/src/features/AgentBuilder/AgentBuilderConversation.tsx @@ -14,6 +14,7 @@ interface AgentBuilderConversationProps { agentId: string; } const actions: ActionKeys[] = ['model']; +const rightActions: ActionKeys[] = []; /** * Agent Builder Conversation Component @@ -32,7 +33,7 @@ const AgentBuilderConversation = memo(({ agentId } /> - + ); diff --git a/src/features/ChatInput/ActionBar/PromptTransform/index.tsx b/src/features/ChatInput/ActionBar/PromptTransform/index.tsx index 95dae016d5..e1066e0e81 100644 --- a/src/features/ChatInput/ActionBar/PromptTransform/index.tsx +++ b/src/features/ChatInput/ActionBar/PromptTransform/index.tsx @@ -12,13 +12,19 @@ const PromptTransform = memo(() => { const onPromptChange = useCallback( (prompt: string) => { if (!editor) return; - editor.setDocument('markdown', prompt); + // `keepHistory` prevents setDocument from wiping the undo/redo stacks. + editor.setDocument('markdown', prompt, { keepHistory: true }); }, [editor], ); + // Image mode expands vague inputs; text mode forbids expansion. return ( - + ); }); diff --git a/src/features/Conversation/ChatInput/index.tsx b/src/features/Conversation/ChatInput/index.tsx index 10c3137e21..c07d36ee73 100644 --- a/src/features/Conversation/ChatInput/index.tsx +++ b/src/features/Conversation/ChatInput/index.tsx @@ -124,7 +124,7 @@ const ChatInput = memo( allowExpand, leftActions = [], leftContent, - rightActions = ['promptTransform'], + rightActions = [], children, extraActionItems, mentionItems, diff --git a/src/features/PromptTransform/usePromptTransform.ts b/src/features/PromptTransform/usePromptTransform.ts index 52b0acedee..be116c27db 100644 --- a/src/features/PromptTransform/usePromptTransform.ts +++ b/src/features/PromptTransform/usePromptTransform.ts @@ -23,8 +23,12 @@ export const usePromptTransform = ({ mode, prompt, onPromptChange }: UsePromptTr const isRewriteActionEnabled = rewriteConfig?.enabled ?? false; const getConfigByAction = useCallback( - (action: PromptTransformAction) => - action === 'rewrite' ? (rewriteConfig ?? {}) : (translateConfig ?? {}), + (action: PromptTransformAction) => { + // Strip config-only fields (enabled, customPrompt); strict upstreams reject unknown OpenAI params. + const config = action === 'rewrite' ? rewriteConfig : translateConfig; + if (!config) return {}; + return { model: config.model, provider: config.provider }; + }, [rewriteConfig, translateConfig], ); diff --git a/src/hooks/useModelSupportImageOutput.ts b/src/hooks/useModelSupportImageOutput.ts new file mode 100644 index 0000000000..2905b795c9 --- /dev/null +++ b/src/hooks/useModelSupportImageOutput.ts @@ -0,0 +1,10 @@ +import { useAiInfraStore } from '@/store/aiInfra'; +import { aiModelSelectors } from '@/store/aiInfra/selectors'; + +export const useModelSupportImageOutput = (id?: string, provider?: string) => { + return useAiInfraStore((s) => { + if (!id || !provider) return false; + + return aiModelSelectors.isModelSupportImageOutput(id, provider)(s); + }); +}; diff --git a/src/routes/(main)/agent/features/Conversation/MainChatInput/index.tsx b/src/routes/(main)/agent/features/Conversation/MainChatInput/index.tsx index 2cf149a7bc..eab37d43bf 100644 --- a/src/routes/(main)/agent/features/Conversation/MainChatInput/index.tsx +++ b/src/routes/(main)/agent/features/Conversation/MainChatInput/index.tsx @@ -4,13 +4,17 @@ import { memo, useMemo } from 'react'; import { type ActionKeys } from '@/features/ChatInput'; import { ChatInput } from '@/features/Conversation'; +import { useModelSupportImageOutput } from '@/hooks/useModelSupportImageOutput'; +import { useAgentStore } from '@/store/agent'; +import { agentSelectors } from '@/store/agent/selectors'; import { useChatStore } from '@/store/chat'; import { useUserStore } from '@/store/user'; import { userGeneralSettingsSelectors } from '@/store/user/selectors'; import { useSendMenuItems } from './useSendMenuItems'; -const rightActions: ActionKeys[] = ['promptTransform']; +const emptyRightActions: ActionKeys[] = []; +const promptTransformRightActions: ActionKeys[] = ['promptTransform']; /** * MainChatInput @@ -24,6 +28,11 @@ const MainChatInput = memo(() => { const isDevMode = useUserStore((s) => userGeneralSettingsSelectors.config(s).isDevMode); const sendMenuItems = useSendMenuItems(); + const model = useAgentStore(agentSelectors.currentAgentModel); + const provider = useAgentStore(agentSelectors.currentAgentModelProvider); + const supportsImageOutput = useModelSupportImageOutput(model, provider); + const rightActions = supportsImageOutput ? promptTransformRightActions : emptyRightActions; + const leftActions: ActionKeys[] = useMemo( () => [ 'model', diff --git a/src/routes/(main)/group/profile/features/AgentBuilder/AgentBuilderConversation.tsx b/src/routes/(main)/group/profile/features/AgentBuilder/AgentBuilderConversation.tsx index 7f2053e92c..cc1eb88c93 100644 --- a/src/routes/(main)/group/profile/features/AgentBuilder/AgentBuilderConversation.tsx +++ b/src/routes/(main)/group/profile/features/AgentBuilder/AgentBuilderConversation.tsx @@ -11,6 +11,7 @@ interface AgentBuilderConversationProps { agentId: string; } const actions: ActionKeys[] = ['model']; +const rightActions: ActionKeys[] = []; /** * Agent Builder Conversation Component @@ -23,7 +24,7 @@ const AgentBuilderConversation = memo(({ agentId } /> - + ); }); diff --git a/src/store/aiInfra/slices/aiModel/selectors.test.ts b/src/store/aiInfra/slices/aiModel/selectors.test.ts index 3c761d069c..fc1edebb2e 100644 --- a/src/store/aiInfra/slices/aiModel/selectors.test.ts +++ b/src/store/aiInfra/slices/aiModel/selectors.test.ts @@ -45,6 +45,7 @@ describe('aiModelSelectors', () => { functionCall: true, vision: true, reasoning: true, + imageOutput: true, }, contextWindowTokens: 4000, settings: { @@ -210,6 +211,20 @@ describe('aiModelSelectors', () => { false, ); }); + + it('should check image output support', () => { + expect(aiModelSelectors.isModelSupportImageOutput('model1', 'provider1')(mockState)).toBe( + true, + ); + // Missing ability defaults to false via `|| false` coercion. + expect(aiModelSelectors.isModelSupportImageOutput('model4', 'provider2')(mockState)).toBe( + false, + ); + // Unknown model returns false instead of throwing. + expect(aiModelSelectors.isModelSupportImageOutput('missing', 'provider1')(mockState)).toBe( + false, + ); + }); }); describe('context window checks', () => { diff --git a/src/store/aiInfra/slices/aiModel/selectors.ts b/src/store/aiInfra/slices/aiModel/selectors.ts index f9352584a1..8cc18ec8d2 100644 --- a/src/store/aiInfra/slices/aiModel/selectors.ts +++ b/src/store/aiInfra/slices/aiModel/selectors.ts @@ -68,6 +68,12 @@ const isModelSupportVideo = (id: string, provider: string) => (s: AIProviderStor return model?.abilities?.video; }; +const isModelSupportImageOutput = (id: string, provider: string) => (s: AIProviderStoreState) => { + const model = getEnabledModelById(id, provider)(s); + + return model?.abilities?.imageOutput || false; +}; + const isModelSupportReasoning = (id: string, provider: string) => (s: AIProviderStoreState) => { const model = getEnabledModelById(id, provider)(s); @@ -155,6 +161,7 @@ export const aiModelSelectors = { isModelHasExtendParams, isModelLoading, isModelSupportFiles, + isModelSupportImageOutput, isModelSupportReasoning, isModelSupportToolUse, isModelSupportVideo,