diff --git a/src/trpc/runtime-api.ts b/src/trpc/runtime-api.ts index a494c86b9..04457a0a9 100644 --- a/src/trpc/runtime-api.ts +++ b/src/trpc/runtime-api.ts @@ -227,6 +227,20 @@ export function createRuntimeApi(deps: CreateRuntimeApiDependencies): RuntimeTrp } : {}), }); + const hasImages = Boolean(body.images && body.images.length > 0); + if (hasImages && clineLaunchConfig.modelId) { + const providerModels = await clineProviderService + .getProviderModels(clineLaunchConfig.providerId) + .catch(() => ({ models: [] })); + const selectedModel = providerModels.models.find((model) => model.id === clineLaunchConfig.modelId); + if (selectedModel?.supportsVision === false) { + return { + ok: false, + summary: null, + error: "The selected Cline model does not support image input. Switch to a vision-capable model or remove the images to start this task.", + }; + } + } const clineTaskSessionService = await deps.getScopedClineTaskSessionService(workspaceScope); const resolvedClineTitle = resolveTaskTitle(body.taskTitle?.trim(), body.prompt); const summary = await clineTaskSessionService.startTaskSession({ @@ -599,6 +613,29 @@ export function createRuntimeApi(deps: CreateRuntimeApiDependencies): RuntimeTrp message: null, }; } + const chatHasImages = Boolean(body.images && body.images.length > 0); + if (chatHasImages) { + try { + const clineLaunchConfig = await clineProviderService.resolveLaunchConfig(); + if (clineLaunchConfig.modelId) { + const providerModels = await clineProviderService + .getProviderModels(clineLaunchConfig.providerId) + .catch(() => ({ models: [] })); + const selectedModel = providerModels.models.find( + (model) => model.id === clineLaunchConfig.modelId, + ); + if (selectedModel?.supportsVision === false) { + return { + ok: false, + summary: null, + error: "The selected Cline model does not support image input. Switch to a vision-capable model or remove the images to send this message.", + }; + } + } + } catch { + // Provider lookup failed — skip validation, let the SDK handle it. + } + } const requestedMode = body.mode; let summary = await clineTaskSessionService.sendTaskSessionInput( body.taskId, diff --git a/web-ui/src/components/detail-panels/cline-agent-chat-panel.tsx b/web-ui/src/components/detail-panels/cline-agent-chat-panel.tsx index 1416b6e1b..9dfa8c475 100644 --- a/web-ui/src/components/detail-panels/cline-agent-chat-panel.tsx +++ b/web-ui/src/components/detail-panels/cline-agent-chat-panel.tsx @@ -219,7 +219,7 @@ export const ClineAgentChatPanel = React.forwardRef 0 && selectedModel?.supportsVision === false - ? "The selected Cline model may not accept image input. Choose a vision-capable model to use these images." + ? "The selected model does not support image input. Switch to a vision-capable model or remove the images to send." : null; const isPinnedToBottom = useCallback((container: HTMLDivElement): boolean => { diff --git a/web-ui/src/components/detail-panels/cline-chat-composer.tsx b/web-ui/src/components/detail-panels/cline-chat-composer.tsx index aa3f09a68..19b386dec 100644 --- a/web-ui/src/components/detail-panels/cline-chat-composer.tsx +++ b/web-ui/src/components/detail-panels/cline-chat-composer.tsx @@ -113,7 +113,7 @@ export function ClineChatComposer({ const [slashSuggestions, setSlashSuggestions] = useState([]); const [isMentionSearchLoading, setIsMentionSearchLoading] = useState(false); const [isSlashSearchLoading, setIsSlashSearchLoading] = useState(false); - const canSubmit = canSend && !isModelSaving && (draft.trim().length > 0 || images.length > 0); + const canSubmit = canSend && !isModelSaving && !attachmentWarningMessage && (draft.trim().length > 0 || images.length > 0); const activeToken = useMemo(() => detectActiveClineComposerToken(draft, cursorIndex), [cursorIndex, draft]); const completionSuggestions = useMemo(() => { diff --git a/web-ui/src/components/task-create-dialog.tsx b/web-ui/src/components/task-create-dialog.tsx index 6020693b5..9e7ad7acd 100644 --- a/web-ui/src/components/task-create-dialog.tsx +++ b/web-ui/src/components/task-create-dialog.tsx @@ -3,6 +3,7 @@ import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import * as RadixSwitch from "@radix-ui/react-switch"; import { + AlertTriangle, ArrowBigUp, ArrowLeft, Check, @@ -202,6 +203,18 @@ export function TaskCreateDialog({ onCreateStartAndOpen || primaryStartAction === "start" ? primaryStartAction : DEFAULT_PRIMARY_START_ACTION; const secondaryStartAction = effectivePrimaryStartAction === "start" ? "start_and_open" : "start"; + const effectiveAgentIdForImages = (agentId || defaultAgentId) ?? null; + const effectiveModelIdForImages = clineSettings?.modelId ?? effectiveDefaultModelId ?? ""; + const selectedClineModel = useMemo( + () => + effectiveModelIdForImages + ? (providerModels.find((model) => model.id === effectiveModelIdForImages) ?? null) + : null, + [effectiveModelIdForImages, providerModels], + ); + const imagesBlockedByModel = + images.length > 0 && effectiveAgentIdForImages === "cline" && selectedClineModel?.supportsVision === false; + // Reset state when dialog closes useEffect(() => { if (!open) { @@ -598,6 +611,15 @@ export function TaskCreateDialog({ + {imagesBlockedByModel ? ( +
+ +

+ The selected Cline model does not support image input. Switch to a vision-capable model in Override + Agent Settings or remove the images to continue. +

+
+ ) : null}