feat: provider profiles — custom provider definitions (#290)

* feat: add provider profiles for custom provider definitions

Users can define custom providers in config.json that appear as first-class
entries alongside built-ins. A provider can override a built-in (custom binary,
env, models) or create a new one by extending a base via `extends`. Generic
ACP transport supported via `extends: "acp"`. Providers can be hidden with
`enabled: false`. Hardcoded models merge with runtime-fetched ones.

- Config schema with Zod validation, auto-migration from old format
- Dynamic provider registry replaces static provider lists
- GenericACPAgentClient for user-defined ACP providers
- Snapshot entries carry label/description/defaultModeId over the wire
- MCP tools accept dynamic provider IDs
- App derives provider definitions from snapshot with static fallback
- CLI `provider ls` calls daemon with label column
- Schedule/session rehydration validates providers against registry

* fix: accept any provider status in CLI provider ls test

The test now connects to a real daemon where providers may be loading
or unavailable, not just the static "available" fallback.

* ci: re-trigger CI checks

* style: fix checkout-git.ts formatting to match CI Biome version

* fix: relax provider ls test assertions for daemon-backed responses

The daemon snapshot may not include all 5 built-in providers in CI
(some require external binaries). Assert at least the core 3
(claude, codex, opencode) instead of all 5.

* fix app combobox dropdown positioning flash

* refactor: stop merging models in provider registry, use override models directly

Override models now replace instead of merge with base provider models.
Also add icon/colorTier fallback from definition modes in fetchModes.

* refactor: make provider definitions fully dynamic from server snapshots

Remove static AGENT_PROVIDER_DEFINITIONS fallbacks from the client — providers,
modes, icons, and color tiers now flow entirely from runtime snapshots. Add icon
and colorTier to AgentMode schema so the server can advertise mode visuals
directly. Fix setAgentMode to persist modeId in agent config so the selected
mode survives session reload. Simplify model merging so profile models replace
runtime models instead of prepending.

* docs: add ad-hoc daemon testing guide
This commit is contained in:
Mohamed Boudra
2026-04-13 19:42:01 +07:00
committed by GitHub
parent 8122d501b0
commit 6f28027687
40 changed files with 2762 additions and 198 deletions
@@ -557,7 +557,11 @@ export function AgentConfigRow({
const effectiveSelectedThinkingOption =
selectedThinkingOptionId || thinkingSelectOptions[0]?.id || "";
const selectedModeVisuals = getModeVisuals(selectedProvider, effectiveSelectedMode);
const selectedModeVisuals = getModeVisuals(
selectedProvider,
effectiveSelectedMode,
providerDefinitions,
);
const ModeIcon = MODE_ICON_MAP[selectedModeVisuals?.icon ?? "ShieldCheck"];
const modeIconColor = MODE_COLOR_MAP[selectedModeVisuals?.colorTier ?? "safe"];
@@ -17,6 +17,7 @@ import { getProviderIcon } from "@/components/provider-icons";
import { CombinedModelSelector } from "@/components/combined-model-selector";
import { useSessionStore } from "@/stores/session-store";
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
import { resolveProviderDefinition } from "@/utils/provider-definitions";
import {
buildFavoriteModelKey,
mergeProviderPreferences,
@@ -40,7 +41,6 @@ import type {
} from "@server/server/agent/agent-sdk-types";
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
import {
AGENT_PROVIDER_DEFINITIONS,
getModeVisuals,
type AgentModeColorTier,
type AgentModeIcon,
@@ -60,10 +60,6 @@ type StatusOption = {
type StatusSelector = "provider" | "mode" | "model" | "thinking" | `feature-${string}`;
const PROVIDER_DEFINITION_MAP = new Map(
AGENT_PROVIDER_DEFINITIONS.map((definition) => [definition.id, definition]),
);
type ControlledAgentStatusBarProps = {
provider: string;
providerOptions?: StatusOption[];
@@ -81,7 +77,7 @@ type ControlledAgentStatusBarProps = {
onSelectThinkingOption?: (thinkingOptionId: string) => void;
disabled?: boolean;
isModelLoading?: boolean;
providerDefinitions?: AgentProviderDefinition[];
providerDefinitions: AgentProviderDefinition[];
allProviderModels?: Map<string, AgentModelDefinition[]>;
canSelectModelProvider?: (providerId: string) => boolean;
favoriteKeys?: Set<string>;
@@ -252,7 +248,9 @@ function ControlledStatusBar({
thinkingOptions?.[0]?.label ?? "Unknown",
);
const modeVisuals = selectedModeId ? getModeVisuals(provider, selectedModeId) : undefined;
const modeVisuals = selectedModeId
? getModeVisuals(provider, selectedModeId, providerDefinitions)
: undefined;
const ModeIconComponent = modeVisuals?.icon ? MODE_ICONS[modeVisuals.icon] : null;
const modeIconColor = getModeIconColor(modeVisuals?.colorTier, theme.colors.palette);
const ProviderIcon = getProviderIcon(provider);
@@ -300,9 +298,7 @@ function ControlledStatusBar({
);
return map;
}, [modelOptions, provider]);
const effectiveProviderDefinitions =
providerDefinitions ??
(PROVIDER_DEFINITION_MAP.has(provider) ? [PROVIDER_DEFINITION_MAP.get(provider)!] : []);
const effectiveProviderDefinitions = providerDefinitions;
const effectiveAllProviderModels = allProviderModels ?? fallbackAllProviderModels;
const canSelectProviderInModelMenu = canSelectModelProvider ?? (() => true);
const comboboxThinkingOptions = useMemo<ComboboxOption[]>(
@@ -322,7 +318,7 @@ function ControlledStatusBar({
active: boolean;
onPress: () => void;
}) => {
const visuals = getModeVisuals(provider, option.id);
const visuals = getModeVisuals(provider, option.id, providerDefinitions);
const IconComponent = visuals?.icon ? MODE_ICONS[visuals.icon] : ShieldCheck;
return (
<ComboboxItem
@@ -334,7 +330,7 @@ function ControlledStatusBar({
/>
);
},
[provider, theme.colors.foreground],
[provider, providerDefinitions, theme.colors.foreground],
);
const handleOpenChange = useCallback(
@@ -745,7 +741,7 @@ function ControlledStatusBar({
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start">
{modeOptions.map((mode) => {
const visuals = getModeVisuals(provider, mode.id);
const visuals = getModeVisuals(provider, mode.id, providerDefinitions);
const Icon = visuals?.icon ? MODE_ICONS[visuals.icon] : ShieldCheck;
return (
<DropdownMenuItem
@@ -896,9 +892,11 @@ export const AgentStatusBar = memo(function AgentStatusBar({
const models = snapshotModels;
const agentProviderDefinitions = useMemo(() => {
const definition = AGENT_PROVIDER_DEFINITIONS.find((d) => d.id === agent?.provider);
const definition = agent?.provider
? resolveProviderDefinition(agent.provider, snapshotEntries)
: undefined;
return definition ? [definition] : [];
}, [agent?.provider]);
}, [agent?.provider, snapshotEntries]);
const agentProviderModels = useMemo(() => {
const map = new Map<string, AgentModelDefinition[]>();
@@ -1128,6 +1126,7 @@ export function DraftAgentStatusBar({
/>
<ControlledStatusBar
provider={selectedProvider}
providerDefinitions={providerDefinitions}
modeOptions={mappedModeOptions}
selectedModeId={effectiveSelectedMode}
onSelectMode={onSelectMode}
@@ -2,9 +2,10 @@ import { useCallback, useEffect, useState } from "react";
import { View, Text, ActivityIndicator, ScrollView } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
import { useHostRuntimeClient } from "@/runtime/host-runtime";
import { resolveProviderLabel } from "@/utils/provider-definitions";
import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
import { AGENT_PROVIDER_DEFINITIONS } from "@server/server/agent/provider-manifest";
interface ProviderDiagnosticSheetProps {
provider: string;
@@ -21,11 +22,11 @@ export function ProviderDiagnosticSheet({
}: ProviderDiagnosticSheetProps) {
const { theme } = useUnistyles();
const client = useHostRuntimeClient(serverId);
const { entries: snapshotEntries } = useProvidersSnapshot(serverId);
const [diagnostic, setDiagnostic] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const providerLabel =
AGENT_PROVIDER_DEFINITIONS.find((d) => d.id === provider)?.label ?? provider;
const providerLabel = resolveProviderLabel(provider, snapshotEntries);
const fetchDiagnostic = useCallback(async () => {
if (!client || !provider) return;
+56 -21
View File
@@ -268,7 +268,7 @@ export function Combobox({
const isMobile = useIsCompactFormFactor();
const effectiveOptionsPosition = isMobile ? "below-search" : optionsPosition;
const isDesktopAboveSearch = !isMobile && isWeb && effectiveOptionsPosition === "above-search";
const { height: windowHeight } = useWindowDimensions();
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
const bottomSheetRef = useRef<BottomSheetModal>(null);
const hasPresentedBottomSheetRef = useRef(false);
const snapPoints = useMemo(() => ["60%", "90%"], []);
@@ -276,11 +276,13 @@ export function Combobox({
null,
);
const [referenceWidth, setReferenceWidth] = useState<number | null>(null);
const [referenceLeft, setReferenceLeft] = useState<number | null>(null);
const [referenceTop, setReferenceTop] = useState<number | null>(null);
const [referenceAtOrigin, setReferenceAtOrigin] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [activeIndex, setActiveIndex] = useState<number>(-1);
const desktopOptionsScrollRef = useRef<ScrollView>(null);
const [desktopContentWidth, setDesktopContentWidth] = useState<number | null>(null);
const isControlled = typeof open === "boolean";
const [internalOpen, setInternalOpen] = useState(false);
@@ -324,7 +326,7 @@ export function Combobox({
const middleware = useMemo(
() => [
floatingOffset(isWeb ? 0 : 4),
floatingOffset(isWeb ? 5 : 4),
...(isWeb ? [] : [flip({ padding: collisionPadding })]),
...(isDesktopAboveSearch ? [] : [shift({ padding: collisionPadding })]),
floatingSize({
@@ -338,6 +340,9 @@ export function Combobox({
});
setReferenceWidth((prev) => {
const next = rects.reference.width;
if (!(next > 0)) {
return prev;
}
if (prev === next) return prev;
return next;
});
@@ -359,15 +364,18 @@ export function Combobox({
useEffect(() => {
if (!isOpen || isMobile) {
setAvailableSize(null);
setDesktopContentWidth(null);
setReferenceLeft(null);
setReferenceWidth(null);
return;
}
const raf = requestAnimationFrame(() => void update());
return () => cancelAnimationFrame(raf);
}, [desktopPlacement, isMobile, update, isOpen]);
}, [desktopPlacement, isMobile, isOpen, update]);
useEffect(() => {
if (!isOpen || isMobile) {
setReferenceLeft(null);
setReferenceAtOrigin(false);
setReferenceTop(null);
return;
@@ -381,9 +389,16 @@ export function Combobox({
}
const measure = () => {
referenceEl.measureInWindow((x, y) => {
referenceEl.measureInWindow((x, y, width, height) => {
setReferenceLeft((prev) => (prev === x ? prev : x));
setReferenceAtOrigin(Math.abs(x) <= 1 && Math.abs(y) <= 1);
setReferenceTop((prev) => (prev === y ? prev : y));
setReferenceWidth((prev) => {
if (!(width > 0)) {
return prev;
}
return prev === width ? prev : width;
});
});
};
@@ -398,32 +413,46 @@ export function Combobox({
isDesktopAboveSearch && referenceTop !== null
? Math.max(windowHeight - referenceTop, collisionPadding)
: null;
const hasResolvedDesktopPosition =
referenceWidth !== null &&
floatingLeft !== null &&
(isDesktopAboveSearch ? desktopAboveSearchBottom !== null : floatingTop !== null) &&
((floatingTop ?? 0) !== 0 || floatingLeft !== 0 || referenceAtOrigin);
const shouldHideDesktopContent = desktopPreventInitialFlash && !hasResolvedDesktopPosition;
const shouldUseDesktopFade = !desktopPreventInitialFlash;
// For top-placed popups: once position resolves, use bottom-based CSS positioning
// so height changes grow upward naturally without floating-ui needing to reposition.
const useStableBottom =
const hasNonZeroFloatingPosition = (floatingTop ?? 0) !== 0 || floatingLeft !== 0;
const useMeasuredTopStartPosition =
!isDesktopAboveSearch &&
IS_WEB &&
!isMobile &&
hasResolvedDesktopPosition &&
desktopPlacement.startsWith("top") &&
referenceTop !== null;
desktopPlacement === "top-start" &&
referenceTop !== null &&
referenceLeft !== null &&
desktopContentWidth !== null;
const clampedMeasuredTopStartLeft = useMeasuredTopStartPosition
? Math.max(
collisionPadding,
Math.min(windowWidth - desktopContentWidth - collisionPadding, referenceLeft),
)
: null;
const measuredTopStartBottom = useMeasuredTopStartPosition
? Math.max(windowHeight - referenceTop + 5, collisionPadding)
: null;
const hasResolvedDesktopPosition =
referenceWidth !== null &&
referenceWidth > 0 &&
(isDesktopAboveSearch
? floatingLeft !== null && desktopAboveSearchBottom !== null
: useMeasuredTopStartPosition
? clampedMeasuredTopStartLeft !== null && measuredTopStartBottom !== null
: floatingLeft !== null &&
floatingTop !== null &&
(hasNonZeroFloatingPosition || !referenceAtOrigin));
const shouldHideDesktopContent = desktopPreventInitialFlash && !hasResolvedDesktopPosition;
const shouldUseDesktopFade = !desktopPreventInitialFlash;
const desktopPositionStyle = isDesktopAboveSearch
? {
left: floatingLeft ?? 0,
bottom: desktopAboveSearchBottom ?? 0,
}
: useStableBottom
: useMeasuredTopStartPosition
? {
left: floatingLeft ?? 0,
bottom: Math.max(windowHeight - referenceTop!, collisionPadding),
left: clampedMeasuredTopStartLeft ?? 0,
bottom: measuredTopStartBottom ?? 0,
}
: floatingStyles;
@@ -728,7 +757,13 @@ export function Combobox({
]}
ref={refs.setFloating}
collapsable={false}
onLayout={() => update()}
onLayout={(event) => {
const { width, height } = event.nativeEvent.layout;
setDesktopContentWidth((prev) => (prev === width ? prev : width));
if (!useMeasuredTopStartPosition || !hasResolvedDesktopPosition) {
void update();
}
}}
>
{children ? (
<>
@@ -1,12 +1,117 @@
import { describe, expect, it } from "vitest";
import { __private__ } from "./use-agent-form-state";
import {
AGENT_PROVIDER_DEFINITIONS,
type AgentProviderDefinition,
} from "@server/server/agent/provider-manifest";
import type { AgentModelDefinition, AgentProvider } from "@server/server/agent/agent-sdk-types";
import { buildProviderDefinitions } from "@/utils/provider-definitions";
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
import type {
AgentModelDefinition,
AgentProvider,
ProviderSnapshotEntry,
} from "@server/server/agent/agent-sdk-types";
const TEST_CODEX_DEFINITION: AgentProviderDefinition = {
id: "codex",
label: "Codex",
description: "Codex test provider",
defaultModeId: "auto",
modes: [
{ id: "auto", label: "Auto", icon: "ShieldAlert", colorTier: "moderate" },
{ id: "full-access", label: "Full Access", icon: "ShieldAlert", colorTier: "dangerous" },
],
};
const TEST_CLAUDE_DEFINITION: AgentProviderDefinition = {
id: "claude",
label: "Claude",
description: "Claude test provider",
defaultModeId: "default",
modes: [
{ id: "default", label: "Always Ask", icon: "ShieldCheck", colorTier: "safe" },
{ id: "acceptEdits", label: "Accept File Edits", icon: "ShieldAlert", colorTier: "moderate" },
{ id: "plan", label: "Plan Mode", icon: "ShieldCheck", colorTier: "planning" },
{ id: "bypassPermissions", label: "Bypass", icon: "ShieldAlert", colorTier: "dangerous" },
],
};
function makeProviderMap(
...definitions: AgentProviderDefinition[]
): Map<AgentProvider, AgentProviderDefinition> {
return new Map(definitions.map((d) => [d.id as AgentProvider, d]));
}
const codexProviderMap = makeProviderMap(TEST_CODEX_DEFINITION);
const claudeProviderMap = makeProviderMap(TEST_CLAUDE_DEFINITION);
describe("useAgentFormState", () => {
describe("buildProviderDefinitions", () => {
it("returns empty array when snapshot data is unavailable", () => {
expect(buildProviderDefinitions(undefined)).toEqual([]);
expect(buildProviderDefinitions([])).toEqual([]);
});
it("builds custom provider definitions from snapshot metadata", () => {
const entries: ProviderSnapshotEntry[] = [
{
provider: "zai",
status: "ready",
label: "ZAI",
description: "Claude with ZAI config",
defaultModeId: "default",
modes: [
{
id: "default",
label: "Default",
description: "Safe mode",
icon: "ShieldCheck",
colorTier: "safe",
},
],
},
{
provider: "claude",
status: "ready",
label: "Claude",
description: "Anthropic Claude",
defaultModeId: "default",
modes: [{ id: "default", label: "Always Ask", icon: "ShieldCheck", colorTier: "safe" }],
},
];
const definitions = buildProviderDefinitions(entries);
expect(definitions).toEqual([
{
id: "zai",
label: "ZAI",
description: "Claude with ZAI config",
defaultModeId: "default",
modes: [
{
id: "default",
label: "Default",
description: "Safe mode",
icon: "ShieldCheck",
colorTier: "safe",
},
],
},
{
id: "claude",
label: "Claude",
description: "Anthropic Claude",
defaultModeId: "default",
modes: [
{
id: "default",
label: "Always Ask",
icon: "ShieldCheck",
colorTier: "safe",
},
],
},
]);
});
});
describe("__private__.combineInitialValues", () => {
it("returns undefined when no initial values and no initial server id", () => {
expect(__private__.combineInitialValues(undefined, null)).toBeUndefined();
@@ -78,6 +183,7 @@ describe("useAgentFormState", () => {
workingDir: "",
},
new Set<string>(),
codexProviderMap,
);
expect(resolved.model).toBe("gpt-5.3-codex");
@@ -106,6 +212,7 @@ describe("useAgentFormState", () => {
workingDir: "",
},
new Set<string>(),
codexProviderMap,
);
expect(resolved.model).toBe("gpt-5.3-codex");
@@ -134,6 +241,7 @@ describe("useAgentFormState", () => {
workingDir: "",
},
new Set<string>(),
codexProviderMap,
);
expect(resolved.thinkingOptionId).toBe("xhigh");
@@ -161,6 +269,7 @@ describe("useAgentFormState", () => {
workingDir: "",
},
new Set<string>(),
codexProviderMap,
);
expect(resolved.model).toBe("gpt-5.3-codex");
@@ -188,6 +297,7 @@ describe("useAgentFormState", () => {
workingDir: "",
},
new Set<string>(),
codexProviderMap,
);
expect(resolved.model).toBe("gpt-5.3-codex");
@@ -215,6 +325,7 @@ describe("useAgentFormState", () => {
workingDir: "",
},
new Set<string>(),
codexProviderMap,
);
expect(resolved.model).toBe("gpt-5.3-codex");
@@ -256,6 +367,7 @@ describe("useAgentFormState", () => {
workingDir: "",
},
new Set<string>(),
claudeProviderMap,
);
expect(resolved.model).toBe("default");
@@ -263,11 +375,6 @@ describe("useAgentFormState", () => {
});
it("resolves provider only from allowed provider map", () => {
const allowedProviderMap = new Map<AgentProvider, AgentProviderDefinition>(
AGENT_PROVIDER_DEFINITIONS.filter((definition) => definition.id === "claude").map(
(definition) => [definition.id as AgentProvider, definition],
),
);
const resolved = __private__.resolveFormState(
undefined,
{ provider: "codex" },
@@ -289,7 +396,7 @@ describe("useAgentFormState", () => {
workingDir: "",
},
new Set<string>(),
allowedProviderMap,
claudeProviderMap,
);
expect(resolved.provider).toBe("claude");
+14 -20
View File
@@ -1,8 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
AGENT_PROVIDER_DEFINITIONS,
type AgentProviderDefinition,
} from "@server/server/agent/provider-manifest";
import type { AgentProviderDefinition } from "@server/server/agent/provider-manifest";
import type {
AgentMode,
AgentModelDefinition,
@@ -10,6 +7,7 @@ import type {
ProviderSnapshotEntry,
} from "@server/server/agent/agent-sdk-types";
import { useHosts } from "@/runtime/host-runtime";
import { buildProviderDefinitions } from "@/utils/provider-definitions";
import { useProvidersSnapshot } from "./use-providers-snapshot";
import {
useFormPreferences,
@@ -99,13 +97,8 @@ type UseAgentFormStateResult = {
persistFormPreferences: () => Promise<void>;
};
const allProviderDefinitions = AGENT_PROVIDER_DEFINITIONS;
const allProviderDefinitionMap = new Map<AgentProvider, AgentProviderDefinition>(
allProviderDefinitions.map((definition) => [definition.id, definition]),
);
const fallbackDefinition = allProviderDefinitions[0];
const DEFAULT_PROVIDER: AgentProvider = fallbackDefinition?.id ?? "claude";
const DEFAULT_MODE_FOR_DEFAULT_PROVIDER = fallbackDefinition?.defaultModeId ?? "";
const DEFAULT_PROVIDER: AgentProvider = "claude";
const DEFAULT_MODE_FOR_DEFAULT_PROVIDER = "default";
function normalizeSelectedModelId(modelId: string | null | undefined): string {
const normalized = typeof modelId === "string" ? modelId.trim() : "";
@@ -180,7 +173,7 @@ function resolveFormState(
userModified: UserModifiedFields,
currentState: FormState,
validServerIds: Set<string>,
allowedProviderMap: Map<AgentProvider, AgentProviderDefinition> = allProviderDefinitionMap,
allowedProviderMap: Map<AgentProvider, AgentProviderDefinition>,
): FormState {
// Start with current state - we only update non-user-modified fields
const result = { ...currentState };
@@ -376,10 +369,10 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
} = useProvidersSnapshot(formState.serverId);
const allProviderEntries = useMemo(() => snapshotEntries ?? [], [snapshotEntries]);
const snapshotProviderDefinitions = useMemo(() => {
const snapshotProviders = new Set((snapshotEntries ?? []).map((entry) => entry.provider));
return allProviderDefinitions.filter((definition) => snapshotProviders.has(definition.id));
}, [snapshotEntries]);
const snapshotProviderDefinitions = useMemo(
() => buildProviderDefinitions(snapshotEntries),
[snapshotEntries],
);
const snapshotProviderDefinitionMap = useMemo(
() =>
new Map<AgentProvider, AgentProviderDefinition>(
@@ -388,17 +381,18 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
[snapshotProviderDefinitions],
);
const snapshotSelectableProviderDefinitionMap = useMemo(() => {
if (!snapshotEntries?.length) {
return snapshotProviderDefinitionMap;
}
const readyProviders = new Set(
(snapshotEntries ?? [])
.filter((entry) => entry.status === "ready")
.map((entry) => entry.provider),
snapshotEntries.filter((entry) => entry.status === "ready").map((entry) => entry.provider),
);
return new Map<AgentProvider, AgentProviderDefinition>(
snapshotProviderDefinitions
.filter((definition) => readyProviders.has(definition.id))
.map((definition) => [definition.id, definition]),
);
}, [snapshotEntries, snapshotProviderDefinitions]);
}, [snapshotEntries, snapshotProviderDefinitionMap, snapshotProviderDefinitions]);
const snapshotAllProviderModels = useMemo(() => {
const map = new Map<string, AgentModelDefinition[]>();
for (const entry of snapshotEntries ?? []) {
@@ -49,7 +49,6 @@ import type {
AgentCapabilityFlags,
AgentSessionConfig,
} from "@server/server/agent/agent-sdk-types";
import { AGENT_PROVIDER_DEFINITIONS } from "@server/server/agent/provider-manifest";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
@@ -68,10 +67,6 @@ const DRAFT_CAPABILITIES: AgentCapabilityFlags = {
supportsReasoningStream: false,
supportsToolInvocations: false,
};
const PROVIDER_DEFINITION_MAP = new Map(
AGENT_PROVIDER_DEFINITIONS.map((definition) => [definition.id, definition]),
);
function getParamValue(value: string | string[] | undefined) {
if (typeof value === "string") {
const trimmed = value.trim();
@@ -92,16 +87,14 @@ function getValidProvider(value: string | undefined) {
if (!value) {
return undefined;
}
return PROVIDER_DEFINITION_MAP.has(value as AgentProvider) ? (value as AgentProvider) : undefined;
return value as AgentProvider;
}
function getValidMode(provider: AgentProvider | undefined, value: string | undefined) {
if (!provider || !value) {
return undefined;
}
const definition = PROVIDER_DEFINITION_MAP.get(provider);
const modes = definition?.modes ?? [];
return modes.some((mode) => mode.id === value) ? value : undefined;
return value;
}
type DraftAgentParams = {
+3 -2
View File
@@ -68,10 +68,10 @@ import { useIsLocalDaemon } from "@/hooks/use-is-local-daemon";
import { useDaemonConfig } from "@/hooks/use-daemon-config";
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
import { useIsCompactFormFactor } from "@/constants/layout";
import { AGENT_PROVIDER_DEFINITIONS } from "@server/server/agent/provider-manifest";
import { getProviderIcon } from "@/components/provider-icons";
import { ProviderDiagnosticSheet } from "@/components/provider-diagnostic-sheet";
import { StatusBadge } from "@/components/ui/status-badge";
import { buildProviderDefinitions } from "@/utils/provider-definitions";
import { isWeb } from "@/constants/platform";
// ---------------------------------------------------------------------------
@@ -523,6 +523,7 @@ function ProvidersSection({ routeServerId }: ProvidersSectionProps) {
const isConnected = useHostRuntimeIsConnected(routeServerId);
const { entries, isLoading, isFetching, refresh } = useProvidersSnapshot(routeServerId);
const [diagnosticProvider, setDiagnosticProvider] = useState<string | null>(null);
const providerDefinitions = buildProviderDefinitions(entries);
const hasServer = routeServerId.length > 0;
@@ -558,7 +559,7 @@ function ProvidersSection({ routeServerId }: ProvidersSectionProps) {
</View>
) : (
<View style={[settingsStyles.card, styles.audioCard]}>
{AGENT_PROVIDER_DEFINITIONS.map((def) => {
{providerDefinitions.map((def) => {
const entry = entries?.find((e) => e.provider === def.id);
const status = entry?.status ?? "unavailable";
const ProviderIcon = getProviderIcon(def.id);
@@ -0,0 +1,47 @@
import type { ProviderSnapshotEntry, AgentProvider } from "@server/server/agent/agent-sdk-types";
import {
type AgentModeColorTier,
type AgentModeIcon,
type AgentProviderDefinition,
type AgentProviderModeDefinition,
} from "@server/server/agent/provider-manifest";
function buildProviderModes(entry: ProviderSnapshotEntry): AgentProviderModeDefinition[] {
const entryModes = entry.modes ?? [];
return entryModes.map((mode) => ({
...mode,
icon: (mode.icon ?? "ShieldCheck") as AgentModeIcon,
colorTier: (mode.colorTier ?? "moderate") as AgentModeColorTier,
}));
}
export function buildProviderDefinitions(
snapshotEntries: ProviderSnapshotEntry[] | undefined,
): AgentProviderDefinition[] {
if (!snapshotEntries?.length) {
return [];
}
return snapshotEntries.map((entry) => ({
id: entry.provider,
label: entry.label ?? entry.provider,
description: entry.description ?? "",
defaultModeId: entry.defaultModeId ?? null,
modes: buildProviderModes(entry),
}));
}
export function resolveProviderLabel(
provider: string,
snapshotEntries: ProviderSnapshotEntry[] | undefined,
): string {
return snapshotEntries?.find((entry) => entry.provider === provider)?.label ?? provider;
}
export function resolveProviderDefinition(
provider: string,
snapshotEntries: ProviderSnapshotEntry[] | undefined,
): AgentProviderDefinition | undefined {
return buildProviderDefinitions(snapshotEntries).find((definition) => definition.id === provider);
}