Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/trpc/runtime-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export const ClineAgentChatPanel = React.forwardRef<ClineAgentChatPanelHandle, C
const panelError = composerError ?? error;
const attachmentWarningMessage =
draftImages.length > 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 => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export function ClineChatComposer({
const [slashSuggestions, setSlashSuggestions] = useState<ClineComposerCompletionSuggestion[]>([]);
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(() => {
Expand Down
32 changes: 29 additions & 3 deletions web-ui/src/components/task-create-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -598,6 +611,15 @@ export function TaskCreateDialog({
</div>
</DialogBody>
<DialogFooter>
{imagesBlockedByModel ? (
<div className="mb-2 flex items-start gap-1.5 text-xs text-status-orange">
<AlertTriangle size={14} className="mt-0.5 shrink-0" />
<p className="m-0 min-w-0">
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.
</p>
</div>
) : null}
<label
htmlFor={createMoreId}
className="mr-auto flex items-center gap-2 text-[12px] text-text-primary cursor-pointer select-none"
Expand All @@ -614,7 +636,11 @@ export function TaskCreateDialog({
</label>
{mode === "single" ? (
<>
<Button size="sm" onClick={handleCreateSingle} disabled={!prompt.trim() || !branchRef}>
<Button
size="sm"
onClick={handleCreateSingle}
disabled={!prompt.trim() || !branchRef || imagesBlockedByModel}
>
<span className="inline-flex items-center">
Create
<ButtonShortcut />
Expand All @@ -627,7 +653,7 @@ export function TaskCreateDialog({
variant="primary"
size="sm"
onClick={() => handleRunSingleStartAction(primaryStartAction)}
disabled={!prompt.trim() || !branchRef}
disabled={!prompt.trim() || !branchRef || imagesBlockedByModel}
className={onCreateStartAndOpen ? "rounded-r-none" : undefined}
>
<span className="inline-flex items-center">
Expand All @@ -640,7 +666,7 @@ export function TaskCreateDialog({
<Button
variant="primary"
size="sm"
disabled={!prompt.trim() || !branchRef}
disabled={!prompt.trim() || !branchRef || imagesBlockedByModel}
className="rounded-l-none border-l border-white/20 px-1"
aria-label="More start options"
>
Expand Down