(null);
const [isStashMenuOpen, setIsStashMenuOpen] = useState(false);
const [isTasksDrawerOpen, setIsTasksDrawerOpen] = useState(false);
@@ -1092,7 +1090,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
label: "/model",
description: "Switch response model for this thread",
},
- ...(planModeUiEnabled
+ ...(planModeUiEnabled && selectedProviderStatus?.showInteractionModeToggle !== false
? ([
{
id: "slash:plan",
@@ -1101,6 +1099,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
label: "/plan",
description: "Switch this thread into plan mode",
},
+ ] as const)
+ : []),
+ ...(planModeUiEnabled &&
+ (selectedProviderStatus?.showInteractionModeToggle !== false || interactionMode === "plan")
+ ? ([
{
id: "slash:default",
type: "slash-command",
@@ -1163,6 +1166,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
return [];
}, [
composerTrigger,
+ interactionMode,
planModeUiEnabled,
selectedProvider,
selectedProviderStatus,
@@ -1394,6 +1398,18 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
selectedProvider,
]);
+ useEffect(() => {
+ const previousEffectiveSendDisabledReason = previousEffectiveSendDisabledReasonRef.current;
+ previousEffectiveSendDisabledReasonRef.current = effectiveSendDisabledReason;
+ setProviderInputSubmissionError((error) =>
+ updateProviderInputSubmissionError(error, {
+ type: "effective-send-disabled-reason-changed",
+ previousEffectiveSendDisabledReason,
+ effectiveSendDisabledReason,
+ }),
+ );
+ }, [effectiveSendDisabledReason]);
+
useEffect(() => {
composerImagesRef.current = composerImages;
}, [composerImages, composerImagesRef]);
@@ -1896,8 +1912,21 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
const submitComposer = useCallback(
(event?: { preventDefault: () => void }, intent: ComposerSubmissionIntent = "foreground") => {
- if (noProviderAvailable || isSendDisabled) {
+ const submissionTarget = activePendingProgress ? "pending-user-input" : "provider-turn";
+ if (
+ shouldBlockComposerSubmissionForProviderAvailability({
+ submissionTarget,
+ noProviderAvailable,
+ isSendDisabled,
+ })
+ ) {
event?.preventDefault();
+ setProviderInputSubmissionError((error) =>
+ updateProviderInputSubmissionError(error, {
+ type: "blocked-submit",
+ effectiveSendDisabledReason,
+ }),
+ );
return;
}
// A send while a pasted image is still compressing would strand that
@@ -1915,7 +1944,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
}
const submission = submitComposerDraft({
prompt: promptRef.current,
- submissionTarget: activePendingProgress ? "pending-user-input" : "provider-turn",
+ submissionTarget,
event,
onSend: (sendEvent) => {
// ChatView reports its final composed-input preflight through the
@@ -1935,6 +1964,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
activeThreadId,
activePendingProgress,
blurMobileComposerAfterSend,
+ effectiveSendDisabledReason,
isSendDisabled,
noProviderAvailable,
onSend,
@@ -1973,7 +2003,15 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
event: KeyboardEvent,
) => {
if (key === "Tab" && event.shiftKey) {
- if (!planModeUiEnabled) return false;
+ if (
+ !canUseProviderInteractionModeShortcut({
+ planModeUiEnabled,
+ showInteractionModeToggle: composerProviderControls.showInteractionModeToggle,
+ interactionMode,
+ })
+ ) {
+ return false;
+ }
toggleInteractionMode();
return true;
}
@@ -2474,6 +2512,17 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
// ------------------------------------------------------------------
const addComposerImages = async (files: File[]) => {
if (!activeThreadId || files.length === 0) return;
+ const unsupportedAttachmentReason = getUnsupportedProviderAttachmentReason(
+ selectedProviderStatus,
+ files.length,
+ );
+ if (unsupportedAttachmentReason !== null) {
+ toastManager.add({
+ type: "error",
+ title: unsupportedAttachmentReason,
+ });
+ return;
+ }
if (pendingUserInputs.length > 0) {
toastManager.add({
type: "error",
@@ -2780,6 +2829,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
selectedModelSelection,
providerAvailable: !noProviderAvailable,
selectedProvider,
+ selectedProviderStatus,
selectedModel,
selectedProviderModels,
}),
@@ -2821,6 +2871,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
noProviderAvailable,
selectedPromptEffort,
selectedProvider,
+ selectedProviderStatus,
selectedProviderModels,
],
);
@@ -2946,11 +2997,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
showPlanFollowUpPrompt={false}
promptHasText={false}
isSendBusy={isSendBusy}
- sendDisabledReason={sendDisabledReason}
+ sendDisabledReason={effectiveSendDisabledReason}
isConnecting={isConnecting}
isEnvironmentUnavailable={
environmentUnavailable !== null ||
- noProviderAvailable ||
+ noProviderEntryAvailable ||
projectSelectionRequired
}
isPreparingWorktree={false}
@@ -3251,11 +3302,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
? "Add feedback to refine the plan, or leave this blank to implement it"
: projectSelectionRequired
? "Choose a project above to start a thread"
- : noProviderAvailable
+ : noProviderEntryAvailable
? "Enable a provider in Settings to send a message"
- : phase === "disconnected"
- ? DISCONNECTED_COMPOSER_PLACEHOLDER
- : "Ask anything, @tag files/folders, $use skills, or / for commands"
+ : noSelectableModelAvailable
+ ? "No models are available for the selected provider"
+ : phase === "disconnected"
+ ? DISCONNECTED_COMPOSER_PLACEHOLDER
+ : "Ask anything, @tag files/folders, $use skills, or / for commands"
}
disabled={isConnecting || isComposerApprovalState || projectSelectionRequired}
/>
@@ -3273,11 +3326,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
showPlanFollowUpPrompt={false}
promptHasText={false}
isSendBusy={isSendBusy}
- sendDisabledReason={sendDisabledReason}
+ sendDisabledReason={effectiveSendDisabledReason}
isConnecting={isConnecting}
isEnvironmentUnavailable={
environmentUnavailable !== null ||
- noProviderAvailable ||
+ noProviderEntryAvailable ||
projectSelectionRequired
}
isPreparingWorktree={false}
@@ -3309,7 +3362,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
)}
>
- {noProviderAvailable ? (
+ {!showProviderModelPicker ? (
@@ -3402,11 +3457,11 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
}
promptHasText={prompt.trim().length > 0}
isSendBusy={isSendBusy}
- sendDisabledReason={sendDisabledReason}
+ sendDisabledReason={effectiveSendDisabledReason}
isConnecting={isConnecting}
isEnvironmentUnavailable={
environmentUnavailable !== null ||
- noProviderAvailable ||
+ noProviderEntryAvailable ||
projectSelectionRequired
}
isPreparingWorktree={isPreparingWorktree}
diff --git a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx
index 20b57dea8c31..135f2e3ec7cc 100644
--- a/apps/web/src/components/chat/CompactComposerControlsMenu.tsx
+++ b/apps/web/src/components/chat/CompactComposerControlsMenu.tsx
@@ -14,6 +14,7 @@ import {
export const CompactComposerControlsMenu = memo(function CompactComposerControlsMenu(props: {
interactionMode: ProviderInteractionMode;
runtimeMode: RuntimeMode;
+ supportedRuntimeModes: ReadonlyArray;
showInteractionModeToggle: boolean;
traitsMenuContent?: ReactNode;
onToggleInteractionMode: () => void;
@@ -64,10 +65,18 @@ export const CompactComposerControlsMenu = memo(function CompactComposerControls
props.onRuntimeModeChange(value as RuntimeMode);
}}
>
- Supervised
- Auto-accept edits
- Auto
- Full access
+ {props.supportedRuntimeModes.includes("approval-required") ? (
+ Supervised
+ ) : null}
+ {props.supportedRuntimeModes.includes("auto-accept-edits") ? (
+ Auto-accept edits
+ ) : null}
+ {props.supportedRuntimeModes.includes("auto") ? (
+ Auto
+ ) : null}
+ {props.supportedRuntimeModes.includes("full-access") ? (
+ Full access
+ ) : null}
diff --git a/apps/web/src/components/chat/ProviderModelPicker.logic.test.ts b/apps/web/src/components/chat/ProviderModelPicker.logic.test.ts
new file mode 100644
index 000000000000..81ec32562c94
--- /dev/null
+++ b/apps/web/src/components/chat/ProviderModelPicker.logic.test.ts
@@ -0,0 +1,98 @@
+import { describe, expect, it } from "vite-plus/test";
+
+import {
+ getComposerProviderAvailability,
+ getFallbackProviderModelLabel,
+ getNoSelectableProviderModelReason,
+ hasSelectableProviderModel,
+ updateProviderInputSubmissionError,
+} from "./ProviderModelPicker.logic";
+
+describe("getFallbackProviderModelLabel", () => {
+ it("presents an empty provider model state without exposing an internal value", () => {
+ expect(getFallbackProviderModelLabel("")).toBe("No model available");
+ expect(getFallbackProviderModelLabel(" ")).toBe("No model available");
+ expect(getFallbackProviderModelLabel("composer-2")).toBe("composer-2");
+ });
+
+ it("treats an empty model as unavailable for composer submission", () => {
+ expect(hasSelectableProviderModel("")).toBe(false);
+ expect(hasSelectableProviderModel(" ")).toBe(false);
+ expect(hasSelectableProviderModel("composer-2")).toBe(true);
+ });
+
+ it("provides an actionable disabled reason for an empty provider catalog", () => {
+ expect(getNoSelectableProviderModelReason(true)).toBe("No models available for this provider");
+ expect(getNoSelectableProviderModelReason(false)).toBeNull();
+ });
+
+ it("clears a blocked-submit error when the provider mode is corrected", () => {
+ const blockedReason =
+ "Cline does not support the selected access mode. Choose Full access to continue.";
+ const error = updateProviderInputSubmissionError(null, {
+ type: "blocked-submit",
+ effectiveSendDisabledReason: blockedReason,
+ });
+ expect(error).toBe(blockedReason);
+ expect(
+ updateProviderInputSubmissionError(error, {
+ type: "effective-send-disabled-reason-changed",
+ previousEffectiveSendDisabledReason: blockedReason,
+ effectiveSendDisabledReason: null,
+ }),
+ ).toBeNull();
+ });
+
+ it("clears a blocked-submit error when an unsupported image is removed", () => {
+ const blockedReason =
+ "Cline does not support image attachments. Remove the images to continue.";
+ const error = updateProviderInputSubmissionError(null, {
+ type: "blocked-submit",
+ effectiveSendDisabledReason: blockedReason,
+ });
+ expect(error).toBe(blockedReason);
+ expect(
+ updateProviderInputSubmissionError(error, {
+ type: "effective-send-disabled-reason-changed",
+ previousEffectiveSendDisabledReason: blockedReason,
+ effectiveSendDisabledReason: null,
+ }),
+ ).toBeNull();
+ });
+
+ it("retains a blocked-submit error while its reason is unchanged", () => {
+ const blockedReason = "No models available for this provider";
+ expect(
+ updateProviderInputSubmissionError(blockedReason, {
+ type: "effective-send-disabled-reason-changed",
+ previousEffectiveSendDisabledReason: blockedReason,
+ effectiveSendDisabledReason: blockedReason,
+ }),
+ ).toBe(blockedReason);
+ });
+});
+
+describe("getComposerProviderAvailability", () => {
+ it("keeps the picker mounted while an available provider reports an empty catalog", () => {
+ expect(
+ getComposerProviderAvailability({
+ hasProviderEntry: true,
+ selectedModel: "",
+ }),
+ ).toEqual({
+ noProviderEntryAvailable: false,
+ noSelectableModelAvailable: true,
+ noProviderAvailable: true,
+ showProviderModelPicker: true,
+ });
+ });
+
+ it("replaces the picker only when there is no provider entry", () => {
+ expect(
+ getComposerProviderAvailability({
+ hasProviderEntry: false,
+ selectedModel: "",
+ }).showProviderModelPicker,
+ ).toBe(false);
+ });
+});
diff --git a/apps/web/src/components/chat/ProviderModelPicker.logic.ts b/apps/web/src/components/chat/ProviderModelPicker.logic.ts
new file mode 100644
index 000000000000..e9ab3c05130c
--- /dev/null
+++ b/apps/web/src/components/chat/ProviderModelPicker.logic.ts
@@ -0,0 +1,47 @@
+export function getFallbackProviderModelLabel(model: string): string {
+ return model.trim() || "No model available";
+}
+
+export function hasSelectableProviderModel(model: string): boolean {
+ return model.trim().length > 0;
+}
+
+export function getNoSelectableProviderModelReason(
+ noSelectableModelAvailable: boolean,
+): string | null {
+ return noSelectableModelAvailable ? "No models available for this provider" : null;
+}
+
+export function updateProviderInputSubmissionError(
+ error: string | null,
+ event:
+ | {
+ readonly type: "blocked-submit";
+ readonly effectiveSendDisabledReason: string | null;
+ }
+ | {
+ readonly type: "effective-send-disabled-reason-changed";
+ readonly previousEffectiveSendDisabledReason: string | null;
+ readonly effectiveSendDisabledReason: string | null;
+ },
+): string | null {
+ if (event.type === "blocked-submit") return event.effectiveSendDisabledReason;
+ return event.previousEffectiveSendDisabledReason === event.effectiveSendDisabledReason
+ ? error
+ : null;
+}
+
+export function getComposerProviderAvailability(input: {
+ readonly hasProviderEntry: boolean;
+ readonly selectedModel: string;
+}) {
+ const noProviderEntryAvailable = !input.hasProviderEntry;
+ const noSelectableModelAvailable =
+ input.hasProviderEntry && !hasSelectableProviderModel(input.selectedModel);
+ return {
+ noProviderEntryAvailable,
+ noSelectableModelAvailable,
+ noProviderAvailable: noProviderEntryAvailable || noSelectableModelAvailable,
+ showProviderModelPicker: input.hasProviderEntry,
+ } as const;
+}
diff --git a/apps/web/src/components/chat/ProviderModelPicker.tsx b/apps/web/src/components/chat/ProviderModelPicker.tsx
index 5566160bcf3e..1834c6285ea5 100644
--- a/apps/web/src/components/chat/ProviderModelPicker.tsx
+++ b/apps/web/src/components/chat/ProviderModelPicker.tsx
@@ -18,6 +18,7 @@ import {
} from "./providerIconUtils";
import { shouldShowInstanceBadge, type ProviderInstanceEntry } from "../../providerInstances";
import { ComposerControl, ComposerControlChevron } from "./ComposerControl";
+import { getFallbackProviderModelLabel } from "./ProviderModelPicker.logic";
export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
/**
@@ -65,8 +66,13 @@ export const ProviderModelPicker = memo(function ProviderModelPicker(props: {
const selectedModel =
selectedInstanceOptions.find((option) => option.slug === props.model) ??
selectedInstanceOptions[0];
- const triggerTitle = selectedModel ? getTriggerDisplayModelName(selectedModel) : props.model;
- const triggerLabel = selectedModel ? getTriggerDisplayModelLabel(selectedModel) : props.model;
+ const fallbackModelLabel = getFallbackProviderModelLabel(props.model);
+ const triggerTitle = selectedModel
+ ? getTriggerDisplayModelName(selectedModel)
+ : fallbackModelLabel;
+ const triggerLabel = selectedModel
+ ? getTriggerDisplayModelLabel(selectedModel)
+ : fallbackModelLabel;
const showInstanceBadge =
activeEntry !== null && shouldShowInstanceBadge(activeEntry, props.instanceEntries);
diff --git a/apps/web/src/components/chat/composerSubmission.test.ts b/apps/web/src/components/chat/composerSubmission.test.ts
index 239db28a6002..21a3c27955bc 100644
--- a/apps/web/src/components/chat/composerSubmission.test.ts
+++ b/apps/web/src/components/chat/composerSubmission.test.ts
@@ -1,7 +1,35 @@
import { PROVIDER_SEND_TURN_MAX_INPUT_CHARS } from "@t3tools/contracts";
import { describe, expect, it, vi } from "vite-plus/test";
-import { submitComposerDraft } from "./composerSubmission";
+import {
+ shouldBlockComposerSubmissionForProviderAvailability,
+ submitComposerDraft,
+} from "./composerSubmission";
+
+describe("shouldBlockComposerSubmissionForProviderAvailability", () => {
+ it.each([
+ { noProviderAvailable: true, isSendDisabled: false },
+ { noProviderAvailable: false, isSendDisabled: true },
+ { noProviderAvailable: true, isSendDisabled: true },
+ ])("blocks ordinary provider sends when provider sending is unavailable", (availability) => {
+ expect(
+ shouldBlockComposerSubmissionForProviderAvailability({
+ submissionTarget: "provider-turn",
+ ...availability,
+ }),
+ ).toBe(true);
+ });
+
+ it("allows a pending user-input answer through the provider availability guards", () => {
+ expect(
+ shouldBlockComposerSubmissionForProviderAvailability({
+ submissionTarget: "pending-user-input",
+ noProviderAvailable: true,
+ isSendDisabled: true,
+ }),
+ ).toBe(false);
+ });
+});
describe("submitComposerDraft", () => {
it("keeps an oversized draft editable and sends a corrected follow-up", () => {
@@ -167,4 +195,18 @@ describe("submitComposerDraft", () => {
expect(onSend).toHaveBeenCalledOnce();
expect(preventDefault).not.toHaveBeenCalled();
});
+
+ it("still honors a final dispatch rejection for pending user input", () => {
+ const preventDefault = vi.fn();
+
+ const result = submitComposerDraft({
+ prompt: "Pending answer",
+ submissionTarget: "pending-user-input",
+ event: { preventDefault },
+ onSend: () => false,
+ });
+
+ expect(result).toEqual({ validationMessage: null, didDispatch: false });
+ expect(preventDefault).toHaveBeenCalledOnce();
+ });
});
diff --git a/apps/web/src/components/chat/composerSubmission.ts b/apps/web/src/components/chat/composerSubmission.ts
index 528ac75bcabe..c4d0749b99df 100644
--- a/apps/web/src/components/chat/composerSubmission.ts
+++ b/apps/web/src/components/chat/composerSubmission.ts
@@ -8,6 +8,17 @@ type ComposerSubmissionInput = {
submissionTarget: "provider-turn" | "pending-user-input";
};
+export function shouldBlockComposerSubmissionForProviderAvailability(options: {
+ submissionTarget: ComposerSubmissionInput["submissionTarget"];
+ noProviderAvailable: boolean;
+ isSendDisabled: boolean;
+}): boolean {
+ return (
+ options.submissionTarget === "provider-turn" &&
+ (options.noProviderAvailable || options.isSendDisabled)
+ );
+}
+
export function getComposerPromptLengthValidationMessage(prompt: string): string | null {
const excessCharacters = prompt.trim().length - PROVIDER_SEND_TURN_MAX_INPUT_CHARS;
if (excessCharacters <= 0) return null;
diff --git a/apps/web/src/components/chat/providerIconUtils.ts b/apps/web/src/components/chat/providerIconUtils.ts
index 842c616fe1fe..5c7da479caaf 100644
--- a/apps/web/src/components/chat/providerIconUtils.ts
+++ b/apps/web/src/components/chat/providerIconUtils.ts
@@ -1,5 +1,5 @@
import { ProviderDriverKind } from "@t3tools/contracts";
-import { ClaudeAI, CursorIcon, GrokIcon, Icon, OpenAI, OpenCodeIcon } from "../Icons";
+import { ClaudeAI, ClineIcon, CursorIcon, GrokIcon, Icon, OpenAI, OpenCodeIcon } from "../Icons";
import { PROVIDER_OPTIONS } from "../../session-logic";
export const PROVIDER_ICON_BY_PROVIDER: Partial> = {
@@ -8,6 +8,7 @@ export const PROVIDER_ICON_BY_PROVIDER: Partial
[ProviderDriverKind.make("opencode")]: OpenCodeIcon,
[ProviderDriverKind.make("cursor")]: CursorIcon,
[ProviderDriverKind.make("grok")]: GrokIcon,
+ [ProviderDriverKind.make("cline")]: ClineIcon,
};
function isAvailableProviderOption(option: (typeof PROVIDER_OPTIONS)[number]): option is {
diff --git a/apps/web/src/components/settings/DiagnosticsSettings.tsx b/apps/web/src/components/settings/DiagnosticsSettings.tsx
index 9c36d32ff51a..32f241492d2e 100644
--- a/apps/web/src/components/settings/DiagnosticsSettings.tsx
+++ b/apps/web/src/components/settings/DiagnosticsSettings.tsx
@@ -299,7 +299,7 @@ function formatProcessName(command: string): string {
function formatProcessType(process: ServerProcessDiagnosticsEntry): string {
if (process.depth > 0) return "Subprocess";
- if (/\b(codex|claude|opencode|cursor)\b/i.test(process.command)) return "Agent";
+ if (/\b(codex|claude|cline|opencode|cursor)\b/i.test(process.command)) return "Agent";
return "Process";
}
diff --git a/apps/web/src/components/settings/ProviderInstanceCard.tsx b/apps/web/src/components/settings/ProviderInstanceCard.tsx
index a663aa90990d..4f04597e0bd1 100644
--- a/apps/web/src/components/settings/ProviderInstanceCard.tsx
+++ b/apps/web/src/components/settings/ProviderInstanceCard.tsx
@@ -443,12 +443,13 @@ export function ProviderInstanceCard({
: null;
const customModels = readConfigStringArray(instance.config, "customModels");
+ const supportsCustomModels = driverOption?.supportsCustomModels !== false;
// Server-returned models may lag behind settings writes. Treat probe
// models as the source for built-ins only; custom rows come directly
// from the current instance config so add/remove reflects immediately.
const modelsForDisplay = deriveProviderModelsForDisplay({
liveModels: liveProvider?.models,
- customModels,
+ customModels: supportsCustomModels ? customModels : [],
});
const updateDisplayName = (value: string) => {
@@ -781,6 +782,7 @@ export function ProviderInstanceCard({
hiddenModels={hiddenModels}
favoriteModels={favoriteModels}
modelOrder={modelOrder}
+ supportsCustomModels={supportsCustomModels}
onChange={updateCustomModels}
onHiddenModelsChange={onHiddenModelsChange}
onFavoriteModelsChange={onFavoriteModelsChange}
diff --git a/apps/web/src/components/settings/ProviderModelsSection.tsx b/apps/web/src/components/settings/ProviderModelsSection.tsx
index 9a42961d13ee..a551e6a7558b 100644
--- a/apps/web/src/components/settings/ProviderModelsSection.tsx
+++ b/apps/web/src/components/settings/ProviderModelsSection.tsx
@@ -62,6 +62,8 @@ interface ProviderModelsSectionProps {
readonly favoriteModels: ReadonlyArray;
/** Explicit user-authored model ordering for this provider instance. */
readonly modelOrder: ReadonlyArray;
+ /** Whether the provider accepts user-authored IDs outside its live catalog. */
+ readonly supportsCustomModels?: boolean;
/**
* Commit the new custom-model list. Caller is responsible for routing the
* write to the correct storage (legacy `settings.providers[kind]` vs.
@@ -92,6 +94,7 @@ export function ProviderModelsSection({
hiddenModels,
favoriteModels,
modelOrder,
+ supportsCustomModels = true,
onChange,
onHiddenModelsChange,
onFavoriteModelsChange,
@@ -376,27 +379,33 @@ export function ProviderModelsSection({
})}
-
-
{
- setInput(event.target.value);
- if (error) setError(null);
- }}
- onKeyDown={(event) => {
- if (event.key !== "Enter") return;
- event.preventDefault();
- handleAdd();
- }}
- placeholder={driverKind ? CUSTOM_MODEL_PLACEHOLDER_BY_KIND[driverKind] : "model-slug"}
- spellCheck={false}
- />
-
-
- Add
-
-
+ {supportsCustomModels ? (
+
+
{
+ setInput(event.target.value);
+ if (error) setError(null);
+ }}
+ onKeyDown={(event) => {
+ if (event.key !== "Enter") return;
+ event.preventDefault();
+ handleAdd();
+ }}
+ placeholder={driverKind ? CUSTOM_MODEL_PLACEHOLDER_BY_KIND[driverKind] : "model-slug"}
+ spellCheck={false}
+ />
+
+
+ Add
+
+
+ ) : (
+
+ Models are discovered from this provider and cannot be added manually.
+
+ )}
{error ? {error}
: null}