Fix provider refresh scope and self-healing
This commit is contained in:
@@ -48,7 +48,7 @@ const INITIAL_USER_MODIFIED: UserModifiedFields = {
|
||||
// Internal form state
|
||||
interface FormState {
|
||||
serverId: string | null;
|
||||
provider: AgentProvider;
|
||||
provider: AgentProvider | null;
|
||||
modeId: string;
|
||||
model: string;
|
||||
thinkingOptionId: string;
|
||||
@@ -68,7 +68,7 @@ export type UseAgentFormStateResult = {
|
||||
selectedServerId: string | null;
|
||||
setSelectedServerId: (value: string | null) => void;
|
||||
setSelectedServerIdFromUser: (value: string | null) => void;
|
||||
selectedProvider: AgentProvider;
|
||||
selectedProvider: AgentProvider | null;
|
||||
setProviderFromUser: (provider: AgentProvider) => void;
|
||||
selectedMode: string;
|
||||
setModeFromUser: (modeId: string) => void;
|
||||
@@ -97,9 +97,6 @@ export type UseAgentFormStateResult = {
|
||||
persistFormPreferences: () => Promise<void>;
|
||||
};
|
||||
|
||||
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() : "";
|
||||
if (!normalized) {
|
||||
@@ -177,8 +174,6 @@ function resolveFormState(
|
||||
): FormState {
|
||||
// Start with current state - we only update non-user-modified fields
|
||||
const result = { ...currentState };
|
||||
const fallbackProvider = allowedProviderMap.keys().next().value as AgentProvider | undefined;
|
||||
|
||||
// 1. Resolve provider first (other fields depend on it)
|
||||
if (!userModified.provider) {
|
||||
if (initialValues?.provider && allowedProviderMap.has(initialValues.provider)) {
|
||||
@@ -188,22 +183,33 @@ function resolveFormState(
|
||||
allowedProviderMap.has(preferences.provider as AgentProvider)
|
||||
) {
|
||||
result.provider = preferences.provider as AgentProvider;
|
||||
} else if (!allowedProviderMap.has(result.provider) && fallbackProvider) {
|
||||
result.provider = fallbackProvider;
|
||||
} else if (
|
||||
result.provider &&
|
||||
allowedProviderMap.size > 0 &&
|
||||
!allowedProviderMap.has(result.provider)
|
||||
) {
|
||||
result.provider = null;
|
||||
}
|
||||
// else keep current (initialized to DEFAULT_PROVIDER)
|
||||
} else if (!allowedProviderMap.has(result.provider) && fallbackProvider) {
|
||||
result.provider = fallbackProvider;
|
||||
} else if (
|
||||
result.provider &&
|
||||
allowedProviderMap.size > 0 &&
|
||||
!allowedProviderMap.has(result.provider)
|
||||
) {
|
||||
result.provider = null;
|
||||
}
|
||||
|
||||
const providerDef = allowedProviderMap.get(result.provider);
|
||||
const providerPrefs = preferences?.providerPreferences?.[result.provider];
|
||||
const providerDef = result.provider ? allowedProviderMap.get(result.provider) : undefined;
|
||||
const providerPrefs = result.provider
|
||||
? preferences?.providerPreferences?.[result.provider]
|
||||
: undefined;
|
||||
|
||||
// 2. Resolve modeId (depends on provider)
|
||||
if (!userModified.modeId) {
|
||||
const validModeIds = providerDef?.modes.map((m) => m.id) ?? [];
|
||||
|
||||
if (
|
||||
if (!result.provider) {
|
||||
result.modeId = "";
|
||||
} else if (
|
||||
typeof initialValues?.modeId === "string" &&
|
||||
initialValues.modeId.length > 0 &&
|
||||
validModeIds.includes(initialValues.modeId)
|
||||
@@ -223,7 +229,9 @@ function resolveFormState(
|
||||
const preferredModel = normalizeSelectedModelId(providerPrefs?.model);
|
||||
const defaultModelId = resolveDefaultModelId(availableModels);
|
||||
|
||||
if (initialModel) {
|
||||
if (!result.provider) {
|
||||
result.model = "";
|
||||
} else if (initialModel) {
|
||||
if (!availableModels || isValidModel(initialModel)) {
|
||||
result.model = initialModel;
|
||||
} else {
|
||||
@@ -246,7 +254,9 @@ function resolveFormState(
|
||||
? initialValues.thinkingOptionId.trim()
|
||||
: "";
|
||||
|
||||
if (!userModified.thinkingOptionId) {
|
||||
if (!result.provider) {
|
||||
result.thinkingOptionId = "";
|
||||
} else if (!userModified.thinkingOptionId) {
|
||||
const effectiveModelId = result.model.trim();
|
||||
const preferredThinking = effectiveModelId
|
||||
? (providerPrefs?.thinkingByModel?.[effectiveModelId]?.trim() ?? "")
|
||||
@@ -262,7 +272,7 @@ function resolveFormState(
|
||||
}
|
||||
|
||||
// Validate thinking option once model metadata is available.
|
||||
if (availableModels) {
|
||||
if (result.provider && availableModels) {
|
||||
result.thinkingOptionId = resolveThinkingOptionId({
|
||||
availableModels,
|
||||
modelId: result.model,
|
||||
@@ -312,6 +322,37 @@ function combineInitialValues(
|
||||
return initialValues;
|
||||
}
|
||||
|
||||
const RESOLVABLE_PROVIDER_STATUSES = new Set<ProviderSnapshotEntry["status"]>(["ready", "loading"]);
|
||||
const SELECTABLE_PROVIDER_STATUSES = new Set<ProviderSnapshotEntry["status"]>(["ready"]);
|
||||
|
||||
function buildProviderDefinitionMap(
|
||||
providerDefinitions: AgentProviderDefinition[],
|
||||
): Map<AgentProvider, AgentProviderDefinition> {
|
||||
return new Map<AgentProvider, AgentProviderDefinition>(
|
||||
providerDefinitions.map((definition) => [definition.id, definition]),
|
||||
);
|
||||
}
|
||||
|
||||
function buildProviderDefinitionMapForStatuses(args: {
|
||||
snapshotEntries: ProviderSnapshotEntry[] | undefined;
|
||||
providerDefinitions: AgentProviderDefinition[];
|
||||
statuses: ReadonlySet<ProviderSnapshotEntry["status"]>;
|
||||
}): Map<AgentProvider, AgentProviderDefinition> {
|
||||
if (!args.snapshotEntries?.length) {
|
||||
return buildProviderDefinitionMap(args.providerDefinitions);
|
||||
}
|
||||
|
||||
const matchingProviders = new Set(
|
||||
args.snapshotEntries
|
||||
.filter((entry) => args.statuses.has(entry.status))
|
||||
.map((entry) => entry.provider),
|
||||
);
|
||||
|
||||
return buildProviderDefinitionMap(
|
||||
args.providerDefinitions.filter((definition) => matchingProviders.has(definition.id)),
|
||||
);
|
||||
}
|
||||
|
||||
export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAgentFormStateResult {
|
||||
const {
|
||||
initialServerId = null,
|
||||
@@ -335,8 +376,8 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
// Form state
|
||||
const [formState, setFormState] = useState<FormState>(() => ({
|
||||
serverId: initialServerId,
|
||||
provider: DEFAULT_PROVIDER,
|
||||
modeId: DEFAULT_MODE_FOR_DEFAULT_PROVIDER,
|
||||
provider: null,
|
||||
modeId: "",
|
||||
model: "",
|
||||
thinkingOptionId: "",
|
||||
workingDir: "",
|
||||
@@ -371,25 +412,25 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
[snapshotEntries],
|
||||
);
|
||||
const snapshotProviderDefinitionMap = useMemo(
|
||||
() =>
|
||||
new Map<AgentProvider, AgentProviderDefinition>(
|
||||
snapshotProviderDefinitions.map((definition) => [definition.id, definition]),
|
||||
),
|
||||
() => buildProviderDefinitionMap(snapshotProviderDefinitions),
|
||||
[snapshotProviderDefinitions],
|
||||
);
|
||||
const snapshotResolvableProviderDefinitionMap = useMemo(
|
||||
() =>
|
||||
buildProviderDefinitionMapForStatuses({
|
||||
snapshotEntries,
|
||||
providerDefinitions: snapshotProviderDefinitions,
|
||||
statuses: RESOLVABLE_PROVIDER_STATUSES,
|
||||
}),
|
||||
[snapshotEntries, snapshotProviderDefinitions],
|
||||
);
|
||||
const snapshotSelectableProviderDefinitionMap = useMemo(() => {
|
||||
if (!snapshotEntries?.length) {
|
||||
return snapshotProviderDefinitionMap;
|
||||
}
|
||||
const readyProviders = new Set(
|
||||
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, snapshotProviderDefinitionMap, snapshotProviderDefinitions]);
|
||||
return buildProviderDefinitionMapForStatuses({
|
||||
snapshotEntries,
|
||||
providerDefinitions: snapshotProviderDefinitions,
|
||||
statuses: SELECTABLE_PROVIDER_STATUSES,
|
||||
});
|
||||
}, [snapshotEntries, snapshotProviderDefinitions]);
|
||||
const snapshotAllProviderModels = useMemo(() => {
|
||||
const map = new Map<string, AgentModelDefinition[]>();
|
||||
for (const entry of snapshotEntries ?? []) {
|
||||
@@ -398,13 +439,16 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
return map;
|
||||
}, [snapshotEntries]);
|
||||
const snapshotSelectedEntry = useMemo(
|
||||
() => (snapshotEntries ?? []).find((entry) => entry.provider === formState.provider) ?? null,
|
||||
() =>
|
||||
formState.provider
|
||||
? ((snapshotEntries ?? []).find((entry) => entry.provider === formState.provider) ?? null)
|
||||
: null,
|
||||
[formState.provider, snapshotEntries],
|
||||
);
|
||||
const snapshotSelectedProviderModels = snapshotSelectedEntry?.models ?? null;
|
||||
const snapshotSelectedProviderModes =
|
||||
snapshotSelectedEntry?.modes ??
|
||||
snapshotProviderDefinitionMap.get(formState.provider)?.modes ??
|
||||
(formState.provider ? snapshotProviderDefinitionMap.get(formState.provider)?.modes : []) ??
|
||||
[];
|
||||
const providerDefinitions = snapshotProviderDefinitions;
|
||||
const providerDefinitionMap = snapshotProviderDefinitionMap;
|
||||
@@ -437,7 +481,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
userModified,
|
||||
formStateRef.current,
|
||||
validServerIds,
|
||||
selectableProviderDefinitionMap,
|
||||
snapshotResolvableProviderDefinitionMap,
|
||||
);
|
||||
|
||||
// Only update if something changed
|
||||
@@ -462,7 +506,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
availableModels,
|
||||
userModified,
|
||||
validServerIds,
|
||||
selectableProviderDefinitionMap,
|
||||
snapshotResolvableProviderDefinitionMap,
|
||||
]);
|
||||
|
||||
// Auto-select the first online host when:
|
||||
@@ -629,12 +673,17 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
}, [refetchSnapshotIfStale]);
|
||||
|
||||
const persistFormPreferences = useCallback(async () => {
|
||||
if (!formState.provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = formState.provider;
|
||||
const resolvedModel = resolveEffectiveModel(availableModels, formState.model);
|
||||
const modelId = resolvedModel?.id ?? formState.model;
|
||||
await updatePreferences((current) =>
|
||||
mergeProviderPreferences({
|
||||
preferences: current,
|
||||
provider: formState.provider,
|
||||
provider,
|
||||
updates: {
|
||||
model: modelId || undefined,
|
||||
mode: formState.modeId || undefined,
|
||||
@@ -657,7 +706,9 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
updatePreferences,
|
||||
]);
|
||||
|
||||
const agentDefinition = providerDefinitionMap.get(formState.provider);
|
||||
const agentDefinition = formState.provider
|
||||
? providerDefinitionMap.get(formState.provider)
|
||||
: undefined;
|
||||
const effectiveModel = resolveEffectiveModel(availableModels, formState.model);
|
||||
const resolvedModelId = effectiveModel?.id ?? formState.model;
|
||||
const availableThinkingOptions = effectiveModel?.thinkingOptions ?? [];
|
||||
@@ -738,6 +789,8 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg
|
||||
export type CreateAgentInitialValues = FormInitialValues;
|
||||
|
||||
export const __private__ = {
|
||||
buildProviderDefinitionMap,
|
||||
buildProviderDefinitionMapForStatuses,
|
||||
combineInitialValues,
|
||||
resolveDefaultModel,
|
||||
resolveFormState,
|
||||
|
||||
Reference in New Issue
Block a user