mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 06:51:35 +00:00
fix(ui): use bundled model setup icons
This commit is contained in:
@@ -62,6 +62,7 @@ describe("OpenClaw setup detection protocol", () => {
|
||||
candidates: [
|
||||
{
|
||||
kind: "provider-auto:ollama",
|
||||
brandId: "ollama",
|
||||
label: "Ollama",
|
||||
detail: "available locally",
|
||||
modelRef: "ollama/qwen3",
|
||||
@@ -73,6 +74,7 @@ describe("OpenClaw setup detection protocol", () => {
|
||||
manualProviders: [
|
||||
{
|
||||
id: "ollama",
|
||||
brandId: "ollama",
|
||||
label: "Ollama",
|
||||
icon: "https://cdn.simpleicons.org/ollama",
|
||||
website: "https://ollama.com/download",
|
||||
@@ -82,6 +84,7 @@ describe("OpenClaw setup detection protocol", () => {
|
||||
recommendedInstalls: [
|
||||
{
|
||||
id: "ollama",
|
||||
brandId: "ollama",
|
||||
label: "Ollama",
|
||||
hint: "Run open models locally",
|
||||
website: "https://ollama.com/download",
|
||||
@@ -93,6 +96,18 @@ describe("OpenClaw setup detection protocol", () => {
|
||||
};
|
||||
|
||||
expect(Value.Check(SystemAgentSetupDetectResultSchema, result)).toBe(true);
|
||||
expect(
|
||||
Value.Check(SystemAgentSetupDetectResultSchema, {
|
||||
...result,
|
||||
candidates: result.candidates.map(({ brandId: _brandId, ...candidate }) => candidate),
|
||||
manualProviders: result.manualProviders.map(
|
||||
({ brandId: _brandId, ...provider }) => provider,
|
||||
),
|
||||
recommendedInstalls: result.recommendedInstalls.map(
|
||||
({ brandId: _brandId, ...install }) => install,
|
||||
),
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
Value.Check(SystemAgentSetupDetectResultSchema, {
|
||||
...result,
|
||||
|
||||
@@ -187,6 +187,8 @@ export const SystemAgentSetupDetectResultSchema = closedObject({
|
||||
candidates: Type.Array(
|
||||
closedObject({
|
||||
kind: SetupInferenceKind,
|
||||
/** Canonical provider identity for clients with bundled brand artwork. */
|
||||
brandId: Type.Optional(NonEmptyString),
|
||||
label: NonEmptyString,
|
||||
detail: Type.String(),
|
||||
modelRef: NonEmptyString,
|
||||
@@ -212,6 +214,8 @@ export const SystemAgentSetupDetectResultSchema = closedObject({
|
||||
closedObject({
|
||||
/** Opaque provider-auth choice sent back during activation. */
|
||||
id: NonEmptyString,
|
||||
/** Canonical provider identity for clients with bundled brand artwork. */
|
||||
brandId: Type.Optional(NonEmptyString),
|
||||
label: NonEmptyString,
|
||||
hint: Type.Optional(Type.String()),
|
||||
icon: Type.Optional(SetupInferenceHttpsUrl),
|
||||
@@ -223,6 +227,8 @@ export const SystemAgentSetupDetectResultSchema = closedObject({
|
||||
Type.Array(
|
||||
closedObject({
|
||||
id: NonEmptyString,
|
||||
/** Canonical provider identity for clients with bundled brand artwork. */
|
||||
brandId: Type.Optional(NonEmptyString),
|
||||
label: NonEmptyString,
|
||||
hint: Type.Optional(Type.String()),
|
||||
groupLabel: Type.Optional(Type.String()),
|
||||
@@ -237,6 +243,8 @@ export const SystemAgentSetupDetectResultSchema = closedObject({
|
||||
Type.Array(
|
||||
closedObject({
|
||||
id: NonEmptyString,
|
||||
/** Canonical provider or tool identity for bundled client artwork. */
|
||||
brandId: Type.Optional(NonEmptyString),
|
||||
label: NonEmptyString,
|
||||
hint: NonEmptyString,
|
||||
website: SetupInferenceHttpsUrl,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"entries": [
|
||||
{
|
||||
"id": "ollama",
|
||||
"brandId": "ollama",
|
||||
"label": "Ollama",
|
||||
"hint": "Run open models locally",
|
||||
"website": "https://ollama.com/download",
|
||||
@@ -16,6 +17,7 @@
|
||||
},
|
||||
{
|
||||
"id": "claude-code",
|
||||
"brandId": "claude",
|
||||
"label": "Claude Code",
|
||||
"hint": "Anthropic's coding agent CLI",
|
||||
"website": "https://code.claude.com/docs/en/quickstart",
|
||||
@@ -23,6 +25,7 @@
|
||||
},
|
||||
{
|
||||
"id": "codex-cli",
|
||||
"brandId": "openai",
|
||||
"label": "Codex CLI",
|
||||
"hint": "OpenAI's coding agent CLI",
|
||||
"website": "https://developers.openai.com/codex/cli/",
|
||||
@@ -37,6 +40,7 @@
|
||||
},
|
||||
{
|
||||
"id": "opencode",
|
||||
"brandId": "opencode",
|
||||
"label": "OpenCode",
|
||||
"hint": "Open-source coding agent",
|
||||
"website": "https://opencode.ai/docs/",
|
||||
@@ -44,6 +48,7 @@
|
||||
},
|
||||
{
|
||||
"id": "gemini-cli",
|
||||
"brandId": "gemini",
|
||||
"label": "Gemini CLI",
|
||||
"hint": "Google's coding agent CLI",
|
||||
"website": "https://geminicli.com/docs/get-started/installation/",
|
||||
@@ -51,6 +56,7 @@
|
||||
},
|
||||
{
|
||||
"id": "kimi-code",
|
||||
"brandId": "kimi",
|
||||
"label": "Kimi Code",
|
||||
"hint": "Moonshot's coding agent CLI",
|
||||
"website": "https://www.kimi.com/code",
|
||||
@@ -58,6 +64,7 @@
|
||||
},
|
||||
{
|
||||
"id": "grok-build",
|
||||
"brandId": "grok",
|
||||
"label": "Grok Build",
|
||||
"hint": "xAI's coding agent CLI",
|
||||
"website": "https://x.ai/cli",
|
||||
|
||||
@@ -56,6 +56,7 @@ function detectResult() {
|
||||
candidates: [
|
||||
{
|
||||
kind: "claude-cli",
|
||||
brandId: "claude",
|
||||
label: "Claude Code",
|
||||
detail: "logged in",
|
||||
modelRef: "claude-cli/opus",
|
||||
@@ -64,6 +65,7 @@ function detectResult() {
|
||||
},
|
||||
{
|
||||
kind: "codex-cli",
|
||||
brandId: "openai",
|
||||
label: "Codex",
|
||||
detail: "logged in",
|
||||
modelRef: "openai/gpt-5.5",
|
||||
@@ -101,6 +103,9 @@ function exerciseGuidedAdapters(): RunGuidedOnboarding {
|
||||
if (!selected) {
|
||||
throw new Error("remote detection returned no candidate");
|
||||
}
|
||||
if (selected.brandId !== "claude") {
|
||||
throw new Error("remote detection dropped bundled brand identity");
|
||||
}
|
||||
const activation = await guidedDeps.activate({
|
||||
kind: selected.kind,
|
||||
modelRef: selected.modelRef,
|
||||
|
||||
@@ -46,6 +46,7 @@ function toSetupInferenceDetection(result: SystemAgentSetupDetectResult): SetupI
|
||||
return {
|
||||
candidates: result.candidates.map((candidate) => ({
|
||||
kind: candidate.kind,
|
||||
...(candidate.brandId !== undefined ? { brandId: candidate.brandId } : {}),
|
||||
label: candidate.label,
|
||||
detail: candidate.detail,
|
||||
modelRef: candidate.modelRef,
|
||||
@@ -58,6 +59,7 @@ function toSetupInferenceDetection(result: SystemAgentSetupDetectResult): SetupI
|
||||
})),
|
||||
manualProviders: result.manualProviders.map((provider) => ({
|
||||
id: provider.id,
|
||||
...(provider.brandId !== undefined ? { brandId: provider.brandId } : {}),
|
||||
label: provider.label,
|
||||
...(provider.hint !== undefined ? { hint: provider.hint } : {}),
|
||||
...(provider.icon !== undefined ? { icon: provider.icon } : {}),
|
||||
@@ -67,6 +69,7 @@ function toSetupInferenceDetection(result: SystemAgentSetupDetectResult): SetupI
|
||||
Object.assign(
|
||||
{
|
||||
id: option.id,
|
||||
...(option.brandId !== undefined ? { brandId: option.brandId } : {}),
|
||||
label: option.label,
|
||||
kind: option.kind,
|
||||
featured: option.featured,
|
||||
|
||||
@@ -17,6 +17,19 @@ describe("recommended tool installs", () => {
|
||||
"grok-build",
|
||||
]);
|
||||
expect(new Set(installs.map((entry) => entry.id)).size).toBe(installs.length);
|
||||
expect(
|
||||
Object.fromEntries(
|
||||
installs.flatMap((entry) => (entry.brandId ? [[entry.id, entry.brandId]] : [])),
|
||||
),
|
||||
).toEqual({
|
||||
ollama: "ollama",
|
||||
"claude-code": "claude",
|
||||
"codex-cli": "openai",
|
||||
opencode: "opencode",
|
||||
"gemini-cli": "gemini",
|
||||
"kimi-code": "kimi",
|
||||
"grok-build": "grok",
|
||||
});
|
||||
for (const entry of installs) {
|
||||
expect(entry.label).not.toBe("");
|
||||
expect(entry.hint).not.toBe("");
|
||||
|
||||
@@ -4,6 +4,7 @@ import { isRecord } from "../utils.js";
|
||||
|
||||
export type SetupRecommendedInstall = {
|
||||
id: string;
|
||||
brandId?: string;
|
||||
label: string;
|
||||
hint: string;
|
||||
website: string;
|
||||
@@ -42,6 +43,7 @@ export function listRecommendedToolInstalls(): SetupRecommendedInstall[] {
|
||||
continue;
|
||||
}
|
||||
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
||||
const brandId = typeof entry.brandId === "string" ? entry.brandId.trim() : "";
|
||||
const label = typeof entry.label === "string" ? entry.label.trim() : "";
|
||||
const hint = typeof entry.hint === "string" ? entry.hint.trim() : "";
|
||||
const website = normalizeHttpsUrl(entry.website);
|
||||
@@ -50,7 +52,7 @@ export function listRecommendedToolInstalls(): SetupRecommendedInstall[] {
|
||||
continue;
|
||||
}
|
||||
seenIds.add(id);
|
||||
installs.push({ id, label, hint, website, icon });
|
||||
installs.push({ id, ...(brandId ? { brandId } : {}), label, hint, website, icon });
|
||||
}
|
||||
return installs;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { ProviderAuthChoiceMetadata } from "../plugins/provider-auth-choice
|
||||
export type SetupInferenceManualProvider = {
|
||||
/** Provider-auth choice id sent back to `openclaw.setup.activate`. */
|
||||
id: string;
|
||||
/** Canonical provider identity for clients with bundled brand artwork. */
|
||||
brandId?: string;
|
||||
label: string;
|
||||
hint?: string;
|
||||
icon?: string;
|
||||
@@ -13,6 +15,8 @@ export type SetupInferenceManualProvider = {
|
||||
export type SetupInferenceAuthOption = {
|
||||
/** Provider-auth choice id sent to `openclaw.setup.auth.start`. */
|
||||
id: string;
|
||||
/** Canonical provider identity for clients with bundled brand artwork. */
|
||||
brandId?: string;
|
||||
label: string;
|
||||
hint?: string;
|
||||
groupLabel?: string;
|
||||
@@ -43,6 +47,7 @@ export function listSetupInferenceManualProviders(
|
||||
}
|
||||
choices.set(id, {
|
||||
id,
|
||||
brandId: choice.providerId,
|
||||
label: choice.choiceLabel,
|
||||
...(choice.choiceHint?.trim() ? { hint: choice.choiceHint.trim() } : {}),
|
||||
...(choice.icon ? { icon: choice.icon } : {}),
|
||||
@@ -76,6 +81,7 @@ export function listSetupInferenceAuthOptions(
|
||||
metadata: choice,
|
||||
option: {
|
||||
id,
|
||||
brandId: choice.providerId,
|
||||
label: choice.choiceLabel,
|
||||
...(choice.choiceHint?.trim() ? { hint: choice.choiceHint.trim() } : {}),
|
||||
...(choice.groupLabel?.trim() ? { groupLabel: choice.groupLabel.trim() } : {}),
|
||||
|
||||
34
src/system-agent/setup-inference-brand.test.ts
Normal file
34
src/system-agent/setup-inference-brand.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSetupInferenceCandidateBrandId } from "./setup-inference-brand.js";
|
||||
|
||||
describe("resolveSetupInferenceCandidateBrandId", () => {
|
||||
it("uses canonical brands for built-in coding CLI candidates", () => {
|
||||
expect(
|
||||
resolveSetupInferenceCandidateBrandId(
|
||||
{ kind: "claude-cli", modelRef: "claude-cli/claude-opus-4-8" },
|
||||
"anthropic",
|
||||
),
|
||||
).toBe("claude");
|
||||
expect(
|
||||
resolveSetupInferenceCandidateBrandId(
|
||||
{ kind: "codex-cli", modelRef: "openai/gpt-5.5" },
|
||||
"openai",
|
||||
),
|
||||
).toBe("openai");
|
||||
});
|
||||
|
||||
it("preserves owner identity for other candidates", () => {
|
||||
expect(
|
||||
resolveSetupInferenceCandidateBrandId(
|
||||
{ kind: "provider-auto:local", modelRef: "local/qwen-tool" },
|
||||
"local",
|
||||
),
|
||||
).toBe("local");
|
||||
expect(
|
||||
resolveSetupInferenceCandidateBrandId({
|
||||
kind: "anthropic-api-key",
|
||||
modelRef: "anthropic/claude-opus-4-8",
|
||||
}),
|
||||
).toBe("anthropic");
|
||||
});
|
||||
});
|
||||
13
src/system-agent/setup-inference-brand.ts
Normal file
13
src/system-agent/setup-inference-brand.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function resolveSetupInferenceCandidateBrandId(
|
||||
candidate: { kind: string; modelRef: string },
|
||||
providerId?: string,
|
||||
): string | undefined {
|
||||
// Built-in CLI detection kinds are runtime identities, not display brands.
|
||||
if (candidate.kind === "claude-cli") {
|
||||
return "claude";
|
||||
}
|
||||
if (candidate.kind === "codex-cli") {
|
||||
return "openai";
|
||||
}
|
||||
return providerId?.trim() || candidate.modelRef.split("/", 1)[0]?.trim() || undefined;
|
||||
}
|
||||
@@ -119,6 +119,7 @@ describe("isolated setup inference detection", () => {
|
||||
expect(detection.candidates).toEqual([
|
||||
{
|
||||
kind: "openai-api-key",
|
||||
brandId: "openai",
|
||||
modelRef: "openai/gpt-5.6",
|
||||
label: "OpenAI API key",
|
||||
detail: "OPENAI_API_KEY set",
|
||||
@@ -127,6 +128,7 @@ describe("isolated setup inference detection", () => {
|
||||
},
|
||||
{
|
||||
kind: "anthropic-api-key",
|
||||
brandId: "anthropic",
|
||||
modelRef: "anthropic/claude-opus-4-8",
|
||||
label: "Anthropic API key",
|
||||
detail: "ANTHROPIC_API_KEY set",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DEFAULT_AGENT_WORKSPACE_DIR } from "../agents/workspace-default.js";
|
||||
import { detectAmbientInferenceBackends } from "../commands/onboard-inference-ambient.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { listRecommendedToolInstalls } from "../plugins/recommended-tool-installs.js";
|
||||
import { resolveSetupInferenceCandidateBrandId } from "./setup-inference-brand.js";
|
||||
import type { SetupInferenceDetection } from "./setup-inference.js";
|
||||
|
||||
const SETUP_INFERENCE_DETECTION_TIMEOUT_MS = 10_000;
|
||||
@@ -86,7 +87,10 @@ function withAmbientCandidates(
|
||||
);
|
||||
const ambient = detectAmbientInferenceBackends(env)
|
||||
.filter((candidate) => !existing.has(`${candidate.kind}\0${candidate.modelRef}`))
|
||||
.map((candidate) => Object.assign(candidate, { recommended: false as const }));
|
||||
.map((candidate) => {
|
||||
const brandId = resolveSetupInferenceCandidateBrandId(candidate);
|
||||
return Object.assign(candidate, brandId ? { brandId } : {}, { recommended: false as const });
|
||||
});
|
||||
if (ambient.length === 0) {
|
||||
return detection;
|
||||
}
|
||||
|
||||
@@ -451,11 +451,16 @@ describe("detectSetupInference", () => {
|
||||
expect(detection.candidates).toHaveLength(2);
|
||||
expect(detection.candidates[0]).toMatchObject({
|
||||
kind: "claude-cli",
|
||||
brandId: "claude",
|
||||
recommended: false,
|
||||
icon: "https://cdn.example.com/claude.svg",
|
||||
website: "https://claude.example.com/download",
|
||||
});
|
||||
expect(detection.candidates[1]).toMatchObject({ kind: "codex-cli", recommended: false });
|
||||
expect(detection.candidates[1]).toMatchObject({
|
||||
kind: "codex-cli",
|
||||
brandId: "openai",
|
||||
recommended: false,
|
||||
});
|
||||
expect(detection.setupComplete).toBe(false);
|
||||
expect(detection.recommendedInstalls).toHaveLength(9);
|
||||
expect(detection.workspace.length).toBeGreaterThan(0);
|
||||
@@ -525,6 +530,7 @@ describe("detectSetupInference", () => {
|
||||
expect.objectContaining({ kind: "claude-cli" }),
|
||||
{
|
||||
kind: "provider-auto:local-model",
|
||||
brandId: "local",
|
||||
label: "Local Server",
|
||||
detail: "qwen-tool at http://127.0.0.1:9999",
|
||||
modelRef: "local/qwen-tool",
|
||||
@@ -613,14 +619,17 @@ describe("detectSetupInference", () => {
|
||||
expect(listSetupInferenceManualProviders(choices)).toEqual([
|
||||
{
|
||||
id: "alpha-api-key",
|
||||
brandId: "alpha",
|
||||
label: "Alpha API key",
|
||||
},
|
||||
{
|
||||
id: "github-copilot",
|
||||
brandId: "github-copilot",
|
||||
label: "GitHub Copilot",
|
||||
},
|
||||
{
|
||||
id: "zeta-api-key",
|
||||
brandId: "zeta",
|
||||
label: "Zeta API key",
|
||||
hint: "Direct key",
|
||||
icon: "https://cdn.example.com/zeta.svg",
|
||||
@@ -699,6 +708,7 @@ describe("detectSetupInference", () => {
|
||||
expect(listSetupInferenceAuthOptions(choices)).toEqual([
|
||||
{
|
||||
id: "openai",
|
||||
brandId: "openai",
|
||||
label: "ChatGPT Login",
|
||||
hint: "Browser sign-in",
|
||||
groupLabel: "OpenAI",
|
||||
@@ -709,6 +719,7 @@ describe("detectSetupInference", () => {
|
||||
},
|
||||
{
|
||||
id: "openrouter-oauth",
|
||||
brandId: "openrouter",
|
||||
label: "OpenRouter OAuth",
|
||||
groupLabel: "OpenRouter",
|
||||
kind: "oauth",
|
||||
@@ -716,6 +727,7 @@ describe("detectSetupInference", () => {
|
||||
},
|
||||
{
|
||||
id: "xai-oauth",
|
||||
brandId: "xai",
|
||||
label: "xAI OAuth",
|
||||
groupLabel: "xAI (Grok)",
|
||||
kind: "device-code",
|
||||
@@ -723,6 +735,7 @@ describe("detectSetupInference", () => {
|
||||
},
|
||||
{
|
||||
id: "google-gemini-cli",
|
||||
brandId: "google-gemini-cli",
|
||||
label: "Gemini CLI OAuth",
|
||||
groupLabel: "Google",
|
||||
kind: "oauth",
|
||||
@@ -730,6 +743,7 @@ describe("detectSetupInference", () => {
|
||||
},
|
||||
{
|
||||
id: "github-copilot",
|
||||
brandId: "github-copilot",
|
||||
label: "GitHub Copilot",
|
||||
kind: "device-code",
|
||||
featured: false,
|
||||
|
||||
@@ -99,6 +99,7 @@ import {
|
||||
type SetupInferenceAuthOption,
|
||||
type SetupInferenceManualProvider,
|
||||
} from "./setup-inference-auth-options.js";
|
||||
import { resolveSetupInferenceCandidateBrandId } from "./setup-inference-brand.js";
|
||||
import { resolveSetupInferenceProbeStreamParams } from "./setup-inference-probe.js";
|
||||
import {
|
||||
captureSystemAgentOwnerPluginArtifacts,
|
||||
@@ -130,6 +131,8 @@ export type SetupInferenceKind = InferenceBackendKind | ProviderAutoSetupInferen
|
||||
|
||||
export type SetupInferenceCandidate = {
|
||||
kind: SetupInferenceKind;
|
||||
/** Canonical provider identity for clients with bundled brand artwork. */
|
||||
brandId?: string;
|
||||
label: string;
|
||||
detail: string;
|
||||
modelRef: string;
|
||||
@@ -376,14 +379,17 @@ function invalidSetupConfigError(snapshot: {
|
||||
}
|
||||
|
||||
function resolveCandidatePresentation(
|
||||
kind: SetupInferenceKind,
|
||||
candidate: Pick<SetupInferenceCandidate, "kind" | "modelRef">,
|
||||
authChoices: readonly ProviderAuthChoiceMetadata[],
|
||||
): Pick<SetupInferenceCandidate, "icon" | "website"> {
|
||||
): Pick<SetupInferenceCandidate, "brandId" | "icon" | "website"> {
|
||||
const choice = authChoices.find(
|
||||
(candidate) =>
|
||||
candidate.choiceId === kind || candidate.deprecatedChoiceIds?.includes(kind) === true,
|
||||
(entry) =>
|
||||
entry.choiceId === candidate.kind ||
|
||||
entry.deprecatedChoiceIds?.includes(candidate.kind) === true,
|
||||
);
|
||||
const brandId = resolveSetupInferenceCandidateBrandId(candidate, choice?.providerId);
|
||||
return {
|
||||
...(brandId ? { brandId } : {}),
|
||||
...(choice?.icon ? { icon: choice.icon } : {}),
|
||||
...(choice?.website ? { website: choice.website } : {}),
|
||||
};
|
||||
@@ -519,7 +525,7 @@ export async function detectSetupInference(
|
||||
Object.assign(
|
||||
candidate,
|
||||
{ recommended: false as const },
|
||||
resolveCandidatePresentation(candidate.kind, authChoices),
|
||||
resolveCandidatePresentation(candidate, authChoices),
|
||||
),
|
||||
);
|
||||
const configuredModel = candidates.find(
|
||||
@@ -583,6 +589,7 @@ export async function detectSetupInference(
|
||||
return Object.assign(
|
||||
{
|
||||
kind: toProviderAutoSetupKind(choice.choiceId),
|
||||
brandId: choice.providerId,
|
||||
label: choice.choiceLabel,
|
||||
detail: candidate.detail?.trim() || "available locally",
|
||||
modelRef: candidate.modelRef,
|
||||
|
||||
@@ -67,9 +67,11 @@ const PROVIDER_ICON_ALIASES: Readonly<Record<string, string>> = {
|
||||
anthropic: "claude",
|
||||
"amazon-bedrock": "bedrock",
|
||||
"aws-bedrock": "bedrock",
|
||||
"claude-cli": "claude",
|
||||
google: "gemini",
|
||||
"google-gemini-cli": "gemini",
|
||||
"github-copilot": "copilot",
|
||||
// CodexBar names its bundled OpenAI knot asset "codex".
|
||||
openai: "codex",
|
||||
"opencode-go": "opencodego",
|
||||
"opencode-zen": "opencode",
|
||||
@@ -109,10 +111,29 @@ function resolveProviderIconName(provider: string): string | null {
|
||||
return PROVIDER_ICON_NAMES.has(icon) ? icon : null;
|
||||
}
|
||||
|
||||
/** Whether a provider identity has a bundled brand mark. */
|
||||
export function hasProviderBrandIcon(provider: string): boolean {
|
||||
return resolveProviderIconName(provider) !== null;
|
||||
}
|
||||
|
||||
function providerIconAssetPath(icon: string): string {
|
||||
return inferControlUiPublicAssetPath(`provider-icons/ProviderIcon-${icon}.svg`);
|
||||
}
|
||||
|
||||
/** Lettered badge for surfaces that must not infer a provider identity. */
|
||||
export function renderProviderFallbackIcon(label: string, options?: { className?: string }) {
|
||||
const surfaceClass = options?.className ? ` ${options.className}` : "";
|
||||
const letter = takeGraphemes(label.trim().toUpperCase(), 1) || "?";
|
||||
return html`
|
||||
<span
|
||||
class="provider-brand-icon provider-brand-icon--fallback${surfaceClass}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
${letter}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Brand icon span for a provider id; falls back to a lettered badge when no
|
||||
* brand mark ships. `className` lets surfaces attach their sizing class.
|
||||
@@ -121,15 +142,7 @@ export function renderProviderBrandIcon(provider: string, options?: { className?
|
||||
const surfaceClass = options?.className ? ` ${options.className}` : "";
|
||||
const icon = resolveProviderIconName(provider);
|
||||
if (!icon) {
|
||||
const letter = takeGraphemes(provider.trim().toUpperCase(), 1) || "?";
|
||||
return html`
|
||||
<span
|
||||
class="provider-brand-icon provider-brand-icon--fallback${surfaceClass}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
${letter}
|
||||
</span>
|
||||
`;
|
||||
return renderProviderFallbackIcon(provider, options);
|
||||
}
|
||||
return html`
|
||||
<span
|
||||
|
||||
@@ -51,6 +51,7 @@ describeControlUiE2e("Control UI Model Setup mocked Gateway E2E", () => {
|
||||
candidates: [
|
||||
{
|
||||
kind: "codex-cli",
|
||||
brandId: "openai",
|
||||
label: "Codex CLI",
|
||||
detail: "Signed in locally",
|
||||
modelRef: "openai/gpt-5",
|
||||
@@ -92,6 +93,7 @@ describeControlUiE2e("Control UI Model Setup mocked Gateway E2E", () => {
|
||||
expect(response?.status()).toBe(200);
|
||||
await page.getByRole("heading", { name: "Connect your AI" }).waitFor();
|
||||
const candidate = page.locator('[data-candidate-kind="codex-cli"]');
|
||||
await expect.poll(() => candidate.locator('[data-provider-icon="codex"]').count()).toBe(1);
|
||||
await candidate.getByRole("button", { name: "Test & use" }).click();
|
||||
|
||||
const detect = await gateway.waitForRequest("openclaw.setup.detect");
|
||||
|
||||
@@ -18,6 +18,7 @@ type TestModelSetupPage = HTMLElement & {
|
||||
};
|
||||
|
||||
const recommendedIconUrl = "https://cdn.simpleicons.org/ollama";
|
||||
const customIconUrl = "https://cdn.example.com/acme.png";
|
||||
|
||||
const detection: SystemAgentSetupDetectResult = {
|
||||
candidates: [],
|
||||
@@ -27,6 +28,7 @@ const detection: SystemAgentSetupDetectResult = {
|
||||
recommendedInstalls: [
|
||||
{
|
||||
id: "ollama",
|
||||
brandId: "ollama",
|
||||
label: "Ollama",
|
||||
hint: "Run open models locally",
|
||||
website: "https://ollama.com/download",
|
||||
@@ -105,9 +107,27 @@ describe("ModelSetupPage catalog icons", () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("loads wire icons through the authenticated same-origin catalog proxy", async () => {
|
||||
it("uses bundled brand icons without enqueueing their remote artwork", async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
const { context, client } = createContext();
|
||||
const { page } = await mountPage(context, {
|
||||
state: { phase: "ready", result: detection },
|
||||
client,
|
||||
firstRun: false,
|
||||
});
|
||||
|
||||
expect(
|
||||
page.querySelector('.model-setup__recommendation [data-provider-icon="ollama"]'),
|
||||
).not.toBeNull();
|
||||
expect(page.querySelector(".model-setup__recommendation img")).toBeNull();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(page.innerHTML).not.toContain(recommendedIconUrl);
|
||||
});
|
||||
|
||||
it("loads unknown wire icons through the authenticated same-origin catalog proxy", async () => {
|
||||
const NativeUrl = URL;
|
||||
const createObjectURL = vi.fn(() => "blob:ollama-icon");
|
||||
const createObjectURL = vi.fn(() => "blob:acme-icon");
|
||||
const revokeObjectURL = vi.fn();
|
||||
vi.stubGlobal(
|
||||
"URL",
|
||||
@@ -125,7 +145,21 @@ describe("ModelSetupPage catalog icons", () => {
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
const { context, client } = createContext();
|
||||
const { page } = await mountPage(context, {
|
||||
state: { phase: "ready", result: detection },
|
||||
state: {
|
||||
phase: "ready",
|
||||
result: {
|
||||
...detection,
|
||||
recommendedInstalls: [
|
||||
{
|
||||
id: "acme",
|
||||
label: "Acme",
|
||||
hint: "Install the Acme runtime",
|
||||
website: "https://example.com/acme",
|
||||
icon: customIconUrl,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
client,
|
||||
firstRun: false,
|
||||
});
|
||||
@@ -135,15 +169,60 @@ describe("ModelSetupPage catalog icons", () => {
|
||||
page
|
||||
.querySelector<HTMLImageElement>(".model-setup__recommendation img")
|
||||
?.getAttribute("src"),
|
||||
).toBe("blob:ollama-icon");
|
||||
).toBe("blob:acme-icon");
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`/openclaw/__openclaw__/catalog-icon/${encodeURIComponent(customIconUrl)}`,
|
||||
expect.objectContaining({ credentials: "same-origin" }),
|
||||
);
|
||||
expect(page.innerHTML).not.toContain(customIconUrl);
|
||||
|
||||
page.remove();
|
||||
expect(revokeObjectURL).toHaveBeenCalledWith("blob:acme-icon");
|
||||
});
|
||||
|
||||
it("keeps legacy known-provider artwork on the authenticated proxy path", async () => {
|
||||
const NativeUrl = URL;
|
||||
vi.stubGlobal(
|
||||
"URL",
|
||||
class extends NativeUrl {
|
||||
static override createObjectURL = vi.fn(() => "blob:legacy-ollama");
|
||||
static override revokeObjectURL = vi.fn();
|
||||
},
|
||||
);
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(new Blob([new Uint8Array([0x89, 0x50, 0x4e, 0x47])], { type: "image/png" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "image/png" },
|
||||
}),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||
const { context, client } = createContext();
|
||||
const { page } = await mountPage(context, {
|
||||
state: {
|
||||
phase: "ready",
|
||||
result: {
|
||||
...detection,
|
||||
recommendedInstalls: detection.recommendedInstalls?.map(
|
||||
({ brandId: _brandId, ...entry }) => entry,
|
||||
),
|
||||
},
|
||||
},
|
||||
client,
|
||||
firstRun: false,
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(
|
||||
page
|
||||
.querySelector<HTMLImageElement>(".model-setup__recommendation img")
|
||||
?.getAttribute("src"),
|
||||
).toBe("blob:legacy-ollama");
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`/openclaw/__openclaw__/catalog-icon/${encodeURIComponent(recommendedIconUrl)}`,
|
||||
expect.objectContaining({ credentials: "same-origin" }),
|
||||
);
|
||||
expect(page.innerHTML).not.toContain(recommendedIconUrl);
|
||||
|
||||
page.remove();
|
||||
expect(revokeObjectURL).toHaveBeenCalledWith("blob:ollama-icon");
|
||||
expect(page.querySelector(".model-setup__recommendation [data-provider-icon]")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
type ModelSetupVerifyState,
|
||||
type ModelSetupWizardState,
|
||||
} from "./state.ts";
|
||||
import { renderModelSetup } from "./view.ts";
|
||||
import { renderModelSetup, resolveSetupBrandIcon } from "./view.ts";
|
||||
import { ModelSetupWizardRunner } from "./wizard-runner.ts";
|
||||
|
||||
type Candidate = SystemAgentSetupDetectResult["candidates"][number];
|
||||
@@ -172,7 +172,7 @@ export class ModelSetupPage extends OpenClawLightDomElement {
|
||||
...result.manualProviders,
|
||||
...(result.authOptions ?? []),
|
||||
...(result.recommendedInstalls ?? []),
|
||||
].flatMap((entry) => (entry.icon ? [entry.icon] : [])),
|
||||
].flatMap((entry) => (entry.icon && !resolveSetupBrandIcon(entry) ? [entry.icon] : [])),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ const detected: SystemAgentSetupDetectResult = {
|
||||
candidates: [
|
||||
{
|
||||
kind: "codex-cli",
|
||||
brandId: "openai",
|
||||
label: "Codex CLI",
|
||||
detail: "Signed in locally",
|
||||
modelRef: "openai/gpt-5",
|
||||
@@ -31,6 +32,7 @@ const detected: SystemAgentSetupDetectResult = {
|
||||
manualProviders: [
|
||||
{
|
||||
id: "openai",
|
||||
brandId: "openai",
|
||||
label: "OpenAI",
|
||||
hint: "Use a project API key.",
|
||||
icon: "https://cdn.example.com/openai.png",
|
||||
@@ -39,6 +41,7 @@ const detected: SystemAgentSetupDetectResult = {
|
||||
authOptions: [
|
||||
{
|
||||
id: "openai-oauth",
|
||||
brandId: "openai",
|
||||
label: "OpenAI",
|
||||
kind: "oauth",
|
||||
featured: true,
|
||||
@@ -55,6 +58,7 @@ const detected: SystemAgentSetupDetectResult = {
|
||||
recommendedInstalls: [
|
||||
{
|
||||
id: "ollama",
|
||||
brandId: "ollama",
|
||||
label: "Ollama",
|
||||
hint: "Run open models locally",
|
||||
website: "https://ollama.com/download",
|
||||
@@ -158,7 +162,20 @@ describe("renderModelSetup", () => {
|
||||
);
|
||||
expect(container.querySelector('input[type="password"]')).not.toBeNull();
|
||||
expect(container.querySelector("details")?.open).toBe(false);
|
||||
expect(container.querySelectorAll("img")).toHaveLength(3);
|
||||
expect(
|
||||
container.querySelector('[data-candidate-kind="codex-cli"] [data-provider-icon="codex"]'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-auth-choice="openai-oauth"] [data-provider-icon="codex"]'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('.model-setup__manual [data-provider-icon="codex"]'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-auth-choice="other-device"] .provider-brand-icon--fallback')
|
||||
?.textContent,
|
||||
).toContain("O");
|
||||
expect(container.querySelectorAll("img")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("renders recommended install cards only when candidates and sign-ins are empty", () => {
|
||||
@@ -174,11 +191,10 @@ describe("renderModelSetup", () => {
|
||||
expect(text(container)).toContain("Recommended installs");
|
||||
expect(text(container)).toContain("Ollama Run open models locally");
|
||||
const card = container.querySelector('[data-recommended-install="ollama"]');
|
||||
const image = card?.querySelector<HTMLImageElement>("img");
|
||||
const icon = card?.querySelector<HTMLElement>('[data-provider-icon="ollama"]');
|
||||
const link = card?.querySelector<HTMLAnchorElement>("a");
|
||||
expect(image?.getAttribute("src")).toBe("blob:ollama");
|
||||
expect(image?.alt).toBe("Ollama");
|
||||
expect(image?.width).toBe(24);
|
||||
expect(icon).not.toBeNull();
|
||||
expect(card?.querySelector("img")).toBeNull();
|
||||
expect(link?.href).toBe("https://ollama.com/download");
|
||||
expect(link?.target).toBe("_blank");
|
||||
expect(link?.rel).toBe("noopener");
|
||||
@@ -191,6 +207,48 @@ describe("renderModelSetup", () => {
|
||||
expect(withSignIn.querySelector(".model-setup__empty")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders Claude Code with the Claude mark and Codex with the OpenAI mark", () => {
|
||||
const container = mount(
|
||||
props({
|
||||
page: {
|
||||
phase: "ready",
|
||||
result: {
|
||||
...detected,
|
||||
candidates: [],
|
||||
authOptions: [],
|
||||
recommendedInstalls: [
|
||||
{
|
||||
id: "claude-code",
|
||||
brandId: "claude",
|
||||
label: "Claude Code",
|
||||
hint: "Anthropic's coding agent CLI",
|
||||
website: "https://code.claude.com/docs/en/quickstart",
|
||||
},
|
||||
{
|
||||
id: "codex-cli",
|
||||
brandId: "openai",
|
||||
label: "Codex CLI",
|
||||
hint: "OpenAI's coding agent CLI",
|
||||
website: "https://developers.openai.com/codex/cli/",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
container.querySelector(
|
||||
'[data-recommended-install="claude-code"] [data-provider-icon="claude"]',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector(
|
||||
'[data-recommended-install="codex-cli"] [data-provider-icon="codex"]',
|
||||
),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it("never renders remote icon URLs directly", () => {
|
||||
const container = mount(props({ iconUrls: {} }));
|
||||
|
||||
@@ -198,6 +256,132 @@ describe("renderModelSetup", () => {
|
||||
expect(container.innerHTML).not.toContain("https://cdn.example.com");
|
||||
});
|
||||
|
||||
it("uses explicit brand identity without guessing from labels or opaque ids", () => {
|
||||
const container = mount(
|
||||
props({
|
||||
page: {
|
||||
phase: "ready",
|
||||
result: {
|
||||
...detected,
|
||||
candidates: [],
|
||||
authOptions: [],
|
||||
recommendedInstalls: [],
|
||||
manualProviders: [
|
||||
{
|
||||
id: "custom-login",
|
||||
brandId: "claude",
|
||||
label: "Company account",
|
||||
icon: "https://cdn.example.com/custom.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
manualProviderId: "custom-login",
|
||||
iconUrls: {},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
container.querySelector('.model-setup__manual [data-provider-icon="claude"]'),
|
||||
).not.toBeNull();
|
||||
expect(container.querySelector(".model-setup__manual img")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps legacy entries without brand identity on the remote artwork path", () => {
|
||||
const iconUrl = "https://cdn.example.com/openai.png";
|
||||
const container = mount(
|
||||
props({
|
||||
page: {
|
||||
phase: "ready",
|
||||
result: {
|
||||
...detected,
|
||||
candidates: [],
|
||||
authOptions: [],
|
||||
recommendedInstalls: [],
|
||||
manualProviders: [
|
||||
{
|
||||
id: "openai-api-key",
|
||||
label: "OpenAI",
|
||||
icon: iconUrl,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
manualProviderId: "openai-api-key",
|
||||
iconUrls: { [iconUrl]: "blob:legacy-openai" },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(container.querySelector(".model-setup__manual [data-provider-icon]")).toBeNull();
|
||||
expect(container.querySelector<HTMLImageElement>(".model-setup__manual img")?.src).toBe(
|
||||
"blob:legacy-openai",
|
||||
);
|
||||
|
||||
const loadingContainer = mount(
|
||||
props({
|
||||
page: {
|
||||
phase: "ready",
|
||||
result: {
|
||||
...detected,
|
||||
candidates: [],
|
||||
authOptions: [],
|
||||
recommendedInstalls: [],
|
||||
manualProviders: [
|
||||
{
|
||||
id: "openai-api-key",
|
||||
label: "OpenAI",
|
||||
icon: iconUrl,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
manualProviderId: "openai-api-key",
|
||||
iconUrls: {},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(loadingContainer.querySelector(".model-setup__manual [data-provider-icon]")).toBeNull();
|
||||
expect(
|
||||
loadingContainer.querySelector(".model-setup__manual .provider-brand-icon--fallback")
|
||||
?.textContent,
|
||||
).toContain("O");
|
||||
});
|
||||
|
||||
it("uses proxied artwork for unknown providers and invalidates broken blobs", () => {
|
||||
const iconUrl = "https://cdn.example.com/acme.png";
|
||||
const onIconError = vi.fn();
|
||||
const container = mount(
|
||||
props({
|
||||
page: {
|
||||
phase: "ready",
|
||||
result: {
|
||||
...detected,
|
||||
candidates: [],
|
||||
authOptions: [],
|
||||
recommendedInstalls: [],
|
||||
manualProviders: [
|
||||
{
|
||||
id: "acme",
|
||||
label: "Acme",
|
||||
icon: iconUrl,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
manualProviderId: "acme",
|
||||
iconUrls: { [iconUrl]: "blob:acme" },
|
||||
onIconError,
|
||||
}),
|
||||
);
|
||||
|
||||
const image = container.querySelector<HTMLImageElement>(".model-setup__manual img");
|
||||
expect(image?.src).toBe("blob:acme");
|
||||
expect(image?.alt).toBe("Acme");
|
||||
image?.dispatchEvent(new Event("error"));
|
||||
expect(onIconError).toHaveBeenCalledWith(iconUrl);
|
||||
expect(container.innerHTML).not.toContain(iconUrl);
|
||||
});
|
||||
|
||||
it("renders admin and older-gateway gates without actions", () => {
|
||||
const admin = mount(props({ canAdmin: false }));
|
||||
expect(text(admin)).toContain("Model setup requires operator.admin access.");
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { html, nothing, type TemplateResult } from "lit";
|
||||
import type { SystemAgentSetupDetectResult } from "../../api/types.ts";
|
||||
import {
|
||||
hasProviderBrandIcon,
|
||||
renderProviderBrandIcon,
|
||||
renderProviderFallbackIcon,
|
||||
} from "../../components/provider-icon.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import "../../styles/model-setup.css";
|
||||
import type {
|
||||
@@ -13,24 +18,42 @@ import { renderModelSetupWizard } from "./wizard-view.ts";
|
||||
|
||||
type Candidate = SystemAgentSetupDetectResult["candidates"][number];
|
||||
type AuthOption = NonNullable<SystemAgentSetupDetectResult["authOptions"]>[number];
|
||||
type SetupIconEntry = {
|
||||
brandId?: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
export function resolveSetupBrandIcon(entry: SetupIconEntry): string | null {
|
||||
// Only new Gateways provide authoritative local brand identity. Legacy
|
||||
// payloads stay on their remote artwork path instead of guessing from labels.
|
||||
return entry.brandId && hasProviderBrandIcon(entry.brandId) ? entry.brandId : null;
|
||||
}
|
||||
|
||||
function renderProviderIcon(
|
||||
props: Pick<ModelSetupViewProps, "iconUrls" | "onIconError">,
|
||||
icon: string | undefined,
|
||||
label: string,
|
||||
entry: SetupIconEntry,
|
||||
className = "",
|
||||
) {
|
||||
const blobUrl = icon ? props.iconUrls[icon] : undefined;
|
||||
if (!icon || !blobUrl) {
|
||||
return nothing;
|
||||
const localBrand = resolveSetupBrandIcon(entry);
|
||||
if (localBrand) {
|
||||
return renderProviderBrandIcon(localBrand, {
|
||||
className: `model-setup__icon ${className}`.trim(),
|
||||
});
|
||||
}
|
||||
const blobUrl = entry.icon ? props.iconUrls[entry.icon] : undefined;
|
||||
if (!entry.icon || !blobUrl) {
|
||||
return renderProviderFallbackIcon(entry.label, {
|
||||
className: `model-setup__icon ${className}`.trim(),
|
||||
});
|
||||
}
|
||||
return html`<img
|
||||
class=${`model-setup__icon ${className}`.trim()}
|
||||
src=${blobUrl}
|
||||
alt=${label}
|
||||
alt=${entry.label}
|
||||
width="24"
|
||||
height="24"
|
||||
@error=${() => props.onIconError(icon)}
|
||||
@error=${() => props.onIconError(entry.icon!)}
|
||||
/>`;
|
||||
}
|
||||
|
||||
@@ -138,7 +161,7 @@ function renderCandidateRows(props: ModelSetupViewProps, result: SystemAgentSetu
|
||||
<div class="model-setup__row" data-candidate-kind=${candidate.kind}>
|
||||
<div class="model-setup__row-main">
|
||||
<div class="model-setup__row-title">
|
||||
${renderProviderIcon(props, candidate.icon, candidate.label)}
|
||||
${renderProviderIcon(props, candidate)}
|
||||
<strong>${candidate.label}</strong>
|
||||
<span class="model-setup__chip">${candidateStatus(candidate)}</span>
|
||||
</div>
|
||||
@@ -191,12 +214,7 @@ function renderEmptyState(props: ModelSetupViewProps, result: SystemAgentSetupDe
|
||||
${installs.map(
|
||||
(install) => html`
|
||||
<div class="model-setup__recommendation" data-recommended-install=${install.id}>
|
||||
${renderProviderIcon(
|
||||
props,
|
||||
install.icon,
|
||||
install.label,
|
||||
"model-setup__icon--recommendation",
|
||||
)}
|
||||
${renderProviderIcon(props, install, "model-setup__icon--recommendation")}
|
||||
<div class="model-setup__row-main">
|
||||
<strong>${install.label}</strong>
|
||||
<div class="muted">${install.hint}</div>
|
||||
@@ -284,7 +302,7 @@ function renderAuthRow(props: ModelSetupViewProps, option: AuthOption) {
|
||||
return html`
|
||||
<div class="model-setup__row" data-auth-choice=${option.id}>
|
||||
<div class="model-setup__provider-copy">
|
||||
${renderProviderIcon(props, option.icon, option.label)}
|
||||
${renderProviderIcon(props, option)}
|
||||
<div>
|
||||
<strong>${option.label}</strong>
|
||||
${option.groupLabel ? html`<div class="muted">${option.groupLabel}</div>` : nothing}
|
||||
@@ -354,7 +372,7 @@ function renderManual(props: ModelSetupViewProps, result: SystemAgentSetupDetect
|
||||
<label class="field">
|
||||
<span>${t("modelSetup.manual.provider")}</span>
|
||||
<div class="model-setup__manual-provider">
|
||||
${renderProviderIcon(props, provider?.icon, provider?.label ?? "")}
|
||||
${provider ? renderProviderIcon(props, provider) : nothing}
|
||||
<select
|
||||
?disabled=${props.actionsDisabled}
|
||||
@change=${(event: Event) =>
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.model-setup__icon.provider-brand-icon--fallback {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.model-setup__provider-copy,
|
||||
.model-setup__manual-provider {
|
||||
display: flex;
|
||||
@@ -104,6 +108,10 @@
|
||||
flex-basis: 28px;
|
||||
}
|
||||
|
||||
.model-setup__icon--recommendation.provider-brand-icon--fallback {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.model-setup__chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user