refactor(forms): extract OpenCode/OMO/OpenClaw state from ProviderForm into dedicated hooks

Split ProviderForm.tsx (2227 → 1526 lines) by extracting:
- opencodeFormUtils.ts: pure functions and default config constants
- useOmoModelSource: OMO model source collection from OpenCode providers
- useOpencodeFormState: OpenCode provider form state and handlers
- useOmoDraftState: OMO profile draft editing state
- useOpenclawFormState: OpenClaw provider form state and handlers
This commit is contained in:
Jason
2026-02-18 23:36:36 +08:00
parent 9514d08ef6
commit 0f4ce74916
7 changed files with 1161 additions and 854 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,161 @@
import type { OpenCodeModel, OpenCodeProviderConfig } from "@/types";
import type { OmoGlobalConfig } from "@/types/omo";
import { parseOmoOtherFieldsObject } from "@/types/omo";
import type { PricingModelSourceOption } from "../ProviderAdvancedConfig";
// ── Default configs ──────────────────────────────────────────────────
export const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {} }, null, 2);
export const CODEX_DEFAULT_CONFIG = JSON.stringify(
{ auth: {}, config: "" },
null,
2,
);
export const GEMINI_DEFAULT_CONFIG = JSON.stringify(
{
env: {
GOOGLE_GEMINI_BASE_URL: "",
GEMINI_API_KEY: "",
GEMINI_MODEL: "gemini-3-pro-preview",
},
},
null,
2,
);
export const OPENCODE_DEFAULT_NPM = "@ai-sdk/openai-compatible";
export const OPENCODE_DEFAULT_CONFIG = JSON.stringify(
{
npm: OPENCODE_DEFAULT_NPM,
options: {
baseURL: "",
apiKey: "",
},
models: {},
},
null,
2,
);
export const OPENCODE_KNOWN_OPTION_KEYS = [
"baseURL",
"apiKey",
"headers",
] as const;
export const OPENCLAW_DEFAULT_CONFIG = JSON.stringify(
{
baseUrl: "",
apiKey: "",
api: "openai-completions",
models: [],
},
null,
2,
);
export const EMPTY_OMO_GLOBAL_CONFIG: OmoGlobalConfig = {
id: "global",
disabledAgents: [],
disabledMcps: [],
disabledHooks: [],
disabledSkills: [],
updatedAt: "",
};
// ── Pure functions ───────────────────────────────────────────────────
export function isKnownOpencodeOptionKey(key: string): boolean {
return OPENCODE_KNOWN_OPTION_KEYS.includes(
key as (typeof OPENCODE_KNOWN_OPTION_KEYS)[number],
);
}
export function parseOpencodeConfig(
settingsConfig?: Record<string, unknown>,
): OpenCodeProviderConfig {
const normalize = (
parsed: Partial<OpenCodeProviderConfig>,
): OpenCodeProviderConfig => ({
npm: parsed.npm || OPENCODE_DEFAULT_NPM,
options:
parsed.options && typeof parsed.options === "object"
? (parsed.options as OpenCodeProviderConfig["options"])
: {},
models:
parsed.models && typeof parsed.models === "object"
? (parsed.models as Record<string, OpenCodeModel>)
: {},
});
try {
const parsed = JSON.parse(
settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG,
) as Partial<OpenCodeProviderConfig>;
return normalize(parsed);
} catch {
return {
npm: OPENCODE_DEFAULT_NPM,
options: {},
models: {},
};
}
}
export function parseOpencodeConfigStrict(
settingsConfig?: Record<string, unknown>,
): OpenCodeProviderConfig {
const parsed = JSON.parse(
settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG,
) as Partial<OpenCodeProviderConfig>;
return {
npm: parsed.npm || OPENCODE_DEFAULT_NPM,
options:
parsed.options && typeof parsed.options === "object"
? (parsed.options as OpenCodeProviderConfig["options"])
: {},
models:
parsed.models && typeof parsed.models === "object"
? (parsed.models as Record<string, OpenCodeModel>)
: {},
};
}
export function toOpencodeExtraOptions(
options: OpenCodeProviderConfig["options"],
): Record<string, string> {
const extra: Record<string, string> = {};
for (const [k, v] of Object.entries(options || {})) {
if (!isKnownOpencodeOptionKey(k)) {
extra[k] = typeof v === "string" ? v : JSON.stringify(v);
}
}
return extra;
}
export function buildOmoProfilePreview(
agents: Record<string, Record<string, unknown>>,
categories: Record<string, Record<string, unknown>>,
otherFieldsStr: string,
): Record<string, unknown> {
const profileOnly: Record<string, unknown> = {};
if (Object.keys(agents).length > 0) {
profileOnly.agents = agents;
}
if (Object.keys(categories).length > 0) {
profileOnly.categories = categories;
}
if (otherFieldsStr.trim()) {
try {
const other = parseOmoOtherFieldsObject(otherFieldsStr);
if (other) {
Object.assign(profileOnly, other);
}
} catch {}
}
return profileOnly;
}
export const normalizePricingSource = (
value?: string,
): PricingModelSourceOption =>
value === "request" || value === "response" ? value : "inherit";

View File

@@ -12,3 +12,7 @@ export { useSpeedTestEndpoints } from "./useSpeedTestEndpoints";
export { useCodexTomlValidation } from "./useCodexTomlValidation";
export { useGeminiConfigState } from "./useGeminiConfigState";
export { useGeminiCommonConfig } from "./useGeminiCommonConfig";
export { useOmoModelSource } from "./useOmoModelSource";
export { useOpencodeFormState } from "./useOpencodeFormState";
export { useOmoDraftState } from "./useOmoDraftState";
export { useOpenclawFormState } from "./useOpenclawFormState";

View File

@@ -0,0 +1,190 @@
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import type { OmoGlobalConfig } from "@/types/omo";
import { mergeOmoConfigPreview } from "@/types/omo";
import { type OmoGlobalConfigFieldsRef } from "../OmoGlobalConfigFields";
import * as configApi from "@/lib/api/config";
import {
EMPTY_OMO_GLOBAL_CONFIG,
buildOmoProfilePreview,
} from "../helpers/opencodeFormUtils";
interface UseOmoDraftStateParams {
initialOmoSettings: Record<string, unknown> | undefined;
queriedOmoGlobalConfig: OmoGlobalConfig | undefined;
isEditMode: boolean;
appId: string;
category?: string;
}
export interface OmoDraftState {
omoAgents: Record<string, Record<string, unknown>>;
setOmoAgents: React.Dispatch<
React.SetStateAction<Record<string, Record<string, unknown>>>
>;
omoCategories: Record<string, Record<string, unknown>>;
setOmoCategories: React.Dispatch<
React.SetStateAction<Record<string, Record<string, unknown>>>
>;
omoOtherFieldsStr: string;
setOmoOtherFieldsStr: React.Dispatch<React.SetStateAction<string>>;
useOmoCommonConfig: boolean;
setUseOmoCommonConfig: React.Dispatch<React.SetStateAction<boolean>>;
isOmoConfigModalOpen: boolean;
setIsOmoConfigModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
isOmoSaving: boolean;
omoGlobalConfigRef: React.RefObject<OmoGlobalConfigFieldsRef | null>;
omoFieldsKey: number;
effectiveOmoGlobalConfig: OmoGlobalConfig;
mergedOmoJsonPreview: string;
handleOmoGlobalConfigSave: () => Promise<void>;
handleOmoEditClick: () => void;
resetOmoDraftState: (useCommonConfig?: boolean) => void;
setOmoGlobalState: React.Dispatch<
React.SetStateAction<OmoGlobalConfig | null>
>;
}
export function useOmoDraftState({
initialOmoSettings,
queriedOmoGlobalConfig,
isEditMode,
appId,
category,
}: UseOmoDraftStateParams): OmoDraftState {
const { t } = useTranslation();
const [omoAgents, setOmoAgents] = useState<
Record<string, Record<string, unknown>>
>(
() =>
(initialOmoSettings?.agents as Record<string, Record<string, unknown>>) ||
{},
);
const [omoCategories, setOmoCategories] = useState<
Record<string, Record<string, unknown>>
>(
() =>
(initialOmoSettings?.categories as Record<
string,
Record<string, unknown>
>) || {},
);
const [omoOtherFieldsStr, setOmoOtherFieldsStr] = useState(() => {
const otherFields = initialOmoSettings?.otherFields;
return otherFields ? JSON.stringify(otherFields, null, 2) : "";
});
const [omoGlobalState, setOmoGlobalState] = useState<OmoGlobalConfig | null>(
null,
);
const [isOmoConfigModalOpen, setIsOmoConfigModalOpen] = useState(false);
const [useOmoCommonConfig, setUseOmoCommonConfig] = useState(() => {
const raw = initialOmoSettings?.useCommonConfig;
return typeof raw === "boolean" ? raw : true;
});
const [isOmoSaving, setIsOmoSaving] = useState(false);
const omoGlobalConfigRef = useRef<OmoGlobalConfigFieldsRef>(null);
const [omoFieldsKey, setOmoFieldsKey] = useState(0);
const effectiveOmoGlobalConfig =
omoGlobalState ?? queriedOmoGlobalConfig ?? EMPTY_OMO_GLOBAL_CONFIG;
const mergedOmoJsonPreview = useMemo(() => {
if (useOmoCommonConfig) {
const merged = mergeOmoConfigPreview(
effectiveOmoGlobalConfig,
omoAgents,
omoCategories,
omoOtherFieldsStr,
);
return JSON.stringify(merged, null, 2);
} else {
return JSON.stringify(
buildOmoProfilePreview(omoAgents, omoCategories, omoOtherFieldsStr),
null,
2,
);
}
}, [
useOmoCommonConfig,
effectiveOmoGlobalConfig,
omoAgents,
omoCategories,
omoOtherFieldsStr,
]);
// Auto-detect whether common config has content for new OMO profiles
useEffect(() => {
if (appId !== "opencode" || category !== "omo" || isEditMode) return;
let active = true;
(async () => {
let next = false;
try {
const raw = await configApi.getCommonConfigSnippet("omo");
if (raw) {
const parsed = JSON.parse(raw) as Record<string, unknown>;
next = Object.keys(parsed).some(
(k) => k !== "id" && k !== "updatedAt",
);
}
} catch {}
if (active) setUseOmoCommonConfig(next);
})();
return () => {
active = false;
};
}, [appId, category, isEditMode]);
const handleOmoGlobalConfigSave = useCallback(async () => {
if (!omoGlobalConfigRef.current) return;
setIsOmoSaving(true);
try {
const config = omoGlobalConfigRef.current.buildCurrentConfigStrict();
await configApi.setCommonConfigSnippet("omo", JSON.stringify(config));
setIsOmoConfigModalOpen(false);
toast.success(
t("omo.globalConfigSaved", { defaultValue: "Global config saved" }),
);
} catch (err) {
toast.error(String(err));
} finally {
setIsOmoSaving(false);
}
}, [t]);
const handleOmoEditClick = useCallback(() => {
setOmoFieldsKey((k) => k + 1);
setIsOmoConfigModalOpen(true);
}, []);
const resetOmoDraftState = useCallback((useCommonConfig = true) => {
setOmoAgents({});
setOmoCategories({});
setOmoOtherFieldsStr("");
setUseOmoCommonConfig(useCommonConfig);
}, []);
return {
omoAgents,
setOmoAgents,
omoCategories,
setOmoCategories,
omoOtherFieldsStr,
setOmoOtherFieldsStr,
useOmoCommonConfig,
setUseOmoCommonConfig,
isOmoConfigModalOpen,
setIsOmoConfigModalOpen,
isOmoSaving,
omoGlobalConfigRef,
omoFieldsKey,
effectiveOmoGlobalConfig,
mergedOmoJsonPreview,
handleOmoGlobalConfigSave,
handleOmoEditClick,
resetOmoDraftState,
setOmoGlobalState,
};
}

View File

@@ -0,0 +1,280 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { providersApi } from "@/lib/api";
import { useProvidersQuery } from "@/lib/query/queries";
import type { OpenCodeProviderConfig } from "@/types";
import { OPENCODE_PRESET_MODEL_VARIANTS } from "@/config/opencodeProviderPresets";
import { parseOpencodeConfigStrict } from "../helpers/opencodeFormUtils";
interface UseOmoModelSourceParams {
isOmoCategory: boolean;
providerId?: string;
}
interface OmoModelBuild {
options: Array<{ value: string; label: string }>;
variantsMap: Record<string, string[]>;
presetMetaMap: Record<
string,
{
options?: Record<string, unknown>;
limit?: { context?: number; output?: number };
}
>;
parseFailedProviders: string[];
usedFallbackSource: boolean;
}
export interface OmoModelSourceResult {
omoModelOptions: Array<{ value: string; label: string }>;
omoModelVariantsMap: Record<string, string[]>;
omoPresetMetaMap: Record<
string,
{
options?: Record<string, unknown>;
limit?: { context?: number; output?: number };
}
>;
existingOpencodeKeys: string[];
}
export function useOmoModelSource({
isOmoCategory,
providerId,
}: UseOmoModelSourceParams): OmoModelSourceResult {
const { t } = useTranslation();
const { data: opencodeProvidersData } = useProvidersQuery("opencode");
const existingOpencodeKeys = useMemo(() => {
if (!opencodeProvidersData?.providers) return [];
return Object.keys(opencodeProvidersData.providers).filter(
(k) => k !== providerId,
);
}, [opencodeProvidersData?.providers, providerId]);
const [enabledOpencodeProviderIds, setEnabledOpencodeProviderIds] = useState<
string[] | null
>(null);
const [omoLiveIdsLoadFailed, setOmoLiveIdsLoadFailed] = useState(false);
const lastOmoModelSourceWarningRef = useRef<string>("");
useEffect(() => {
let active = true;
if (!isOmoCategory) {
setEnabledOpencodeProviderIds(null);
setOmoLiveIdsLoadFailed(false);
return () => {
active = false;
};
}
setEnabledOpencodeProviderIds(null);
setOmoLiveIdsLoadFailed(false);
(async () => {
try {
const ids = await providersApi.getOpenCodeLiveProviderIds();
if (active) {
setEnabledOpencodeProviderIds(ids);
}
} catch (error) {
console.warn(
"[OMO_MODEL_SOURCE_LIVE_IDS_FAILED] failed to load live provider ids",
error,
);
if (active) {
setOmoLiveIdsLoadFailed(true);
setEnabledOpencodeProviderIds(null);
}
}
})();
return () => {
active = false;
};
}, [isOmoCategory]);
const omoModelBuild = useMemo<OmoModelBuild>(() => {
const empty: OmoModelBuild = {
options: [],
variantsMap: {},
presetMetaMap: {},
parseFailedProviders: [],
usedFallbackSource: false,
};
if (!isOmoCategory) {
return empty;
}
const allProviders = opencodeProvidersData?.providers;
if (!allProviders) {
return empty;
}
const shouldFilterByLive = !omoLiveIdsLoadFailed;
if (shouldFilterByLive && enabledOpencodeProviderIds === null) {
return empty;
}
const liveSet =
shouldFilterByLive && enabledOpencodeProviderIds
? new Set(enabledOpencodeProviderIds)
: null;
const dedupedOptions = new Map<string, string>();
const variantsMap: Record<string, string[]> = {};
const presetMetaMap: Record<
string,
{
options?: Record<string, unknown>;
limit?: { context?: number; output?: number };
}
> = {};
const parseFailedProviders: string[] = [];
for (const [providerKey, provider] of Object.entries(allProviders)) {
if (provider.category === "omo") {
continue;
}
if (liveSet && !liveSet.has(providerKey)) {
continue;
}
let parsedConfig: OpenCodeProviderConfig;
try {
parsedConfig = parseOpencodeConfigStrict(provider.settingsConfig);
} catch (error) {
parseFailedProviders.push(providerKey);
console.warn(
"[OMO_MODEL_SOURCE_PARSE_FAILED] failed to parse provider settings",
{
providerKey,
error,
},
);
continue;
}
for (const [modelId, model] of Object.entries(
parsedConfig.models || {},
)) {
const modelName =
typeof model.name === "string" && model.name.trim()
? model.name
: modelId;
const providerDisplayName =
typeof provider.name === "string" && provider.name.trim()
? provider.name
: providerKey;
const value = `${providerKey}/${modelId}`;
const label = `${providerDisplayName} / ${modelName} (${modelId})`;
if (!dedupedOptions.has(value)) {
dedupedOptions.set(value, label);
}
const rawVariants = model.variants;
if (
rawVariants &&
typeof rawVariants === "object" &&
!Array.isArray(rawVariants)
) {
const variantKeys = Object.keys(rawVariants).filter(Boolean);
if (variantKeys.length > 0) {
variantsMap[value] = variantKeys;
}
}
}
// Preset fallback: for models without config-defined variants,
// check if the npm package has preset variant definitions.
// Also collect preset metadata (options, limit) for enrichment.
const presetModels = OPENCODE_PRESET_MODEL_VARIANTS[parsedConfig.npm];
if (presetModels) {
for (const modelId of Object.keys(parsedConfig.models || {})) {
const fullKey = `${providerKey}/${modelId}`;
const preset = presetModels.find((p) => p.id === modelId);
if (!preset) continue;
// Variant fallback
if (!variantsMap[fullKey] && preset.variants) {
const presetKeys = Object.keys(preset.variants).filter(Boolean);
if (presetKeys.length > 0) {
variantsMap[fullKey] = presetKeys;
}
}
// Collect preset metadata for model enrichment
const meta: (typeof presetMetaMap)[string] = {};
if (preset.options) meta.options = preset.options;
if (preset.contextLimit || preset.outputLimit) {
meta.limit = {};
if (preset.contextLimit) meta.limit.context = preset.contextLimit;
if (preset.outputLimit) meta.limit.output = preset.outputLimit;
}
if (Object.keys(meta).length > 0) {
presetMetaMap[fullKey] = meta;
}
}
}
}
return {
options: Array.from(dedupedOptions.entries())
.map(([value, label]) => ({ value, label }))
.sort((a, b) => a.label.localeCompare(b.label, "zh-CN")),
variantsMap,
presetMetaMap,
parseFailedProviders,
usedFallbackSource: omoLiveIdsLoadFailed,
};
}, [
isOmoCategory,
opencodeProvidersData?.providers,
enabledOpencodeProviderIds,
omoLiveIdsLoadFailed,
]);
// Warning toast for parse failures / fallback
useEffect(() => {
if (!isOmoCategory) return;
const failed = omoModelBuild.parseFailedProviders;
const fallback = omoModelBuild.usedFallbackSource;
if (failed.length === 0 && !fallback) return;
const signature = `${fallback ? "fallback:" : ""}${failed
.slice()
.sort()
.join(",")}`;
if (lastOmoModelSourceWarningRef.current === signature) return;
lastOmoModelSourceWarningRef.current = signature;
if (failed.length > 0) {
toast.warning(
t("omo.modelSourcePartialWarning", {
count: failed.length,
defaultValue:
"Some provider model configs are invalid and were skipped.",
}),
);
}
if (fallback) {
toast.warning(
t("omo.modelSourceFallbackWarning", {
defaultValue:
"Failed to load live provider state. Falling back to configured providers.",
}),
);
}
}, [
isOmoCategory,
omoModelBuild.parseFailedProviders,
omoModelBuild.usedFallbackSource,
t,
]);
return {
omoModelOptions: omoModelBuild.options,
omoModelVariantsMap: omoModelBuild.variantsMap,
omoPresetMetaMap: omoModelBuild.presetMetaMap,
existingOpencodeKeys,
};
}

View File

@@ -0,0 +1,180 @@
import { useState, useCallback, useMemo } from "react";
import type { OpenClawModel } from "@/types";
import type { AppId } from "@/lib/api";
import { useProvidersQuery } from "@/lib/query/queries";
import { OPENCLAW_DEFAULT_CONFIG } from "../helpers/opencodeFormUtils";
interface UseOpenclawFormStateParams {
initialData?: {
settingsConfig?: Record<string, unknown>;
};
appId: AppId;
providerId?: string;
onSettingsConfigChange: (config: string) => void;
getSettingsConfig: () => string;
}
export interface OpenclawFormState {
openclawProviderKey: string;
setOpenclawProviderKey: (key: string) => void;
openclawBaseUrl: string;
openclawApiKey: string;
openclawApi: string;
openclawModels: OpenClawModel[];
existingOpenclawKeys: string[];
handleOpenclawBaseUrlChange: (baseUrl: string) => void;
handleOpenclawApiKeyChange: (apiKey: string) => void;
handleOpenclawApiChange: (api: string) => void;
handleOpenclawModelsChange: (models: OpenClawModel[]) => void;
resetOpenclawState: (config?: {
baseUrl?: string;
apiKey?: string;
api?: string;
models?: OpenClawModel[];
}) => void;
}
function parseOpenclawField<T>(
initialData: UseOpenclawFormStateParams["initialData"],
field: string,
fallback: T,
): T {
try {
const config = JSON.parse(
initialData?.settingsConfig
? JSON.stringify(initialData.settingsConfig)
: OPENCLAW_DEFAULT_CONFIG,
);
return (config[field] as T) || fallback;
} catch {
return fallback;
}
}
export function useOpenclawFormState({
initialData,
appId,
providerId,
onSettingsConfigChange,
getSettingsConfig,
}: UseOpenclawFormStateParams): OpenclawFormState {
// Query existing providers for duplicate key checking
const { data: openclawProvidersData } = useProvidersQuery("openclaw");
const existingOpenclawKeys = useMemo(() => {
if (!openclawProvidersData?.providers) return [];
return Object.keys(openclawProvidersData.providers).filter(
(k) => k !== providerId,
);
}, [openclawProvidersData?.providers, providerId]);
const [openclawProviderKey, setOpenclawProviderKey] = useState<string>(() => {
if (appId !== "openclaw") return "";
return providerId || "";
});
const [openclawBaseUrl, setOpenclawBaseUrl] = useState<string>(() => {
if (appId !== "openclaw") return "";
return parseOpenclawField(initialData, "baseUrl", "");
});
const [openclawApiKey, setOpenclawApiKey] = useState<string>(() => {
if (appId !== "openclaw") return "";
return parseOpenclawField(initialData, "apiKey", "");
});
const [openclawApi, setOpenclawApi] = useState<string>(() => {
if (appId !== "openclaw") return "openai-completions";
return parseOpenclawField(initialData, "api", "openai-completions");
});
const [openclawModels, setOpenclawModels] = useState<OpenClawModel[]>(() => {
if (appId !== "openclaw") return [];
return parseOpenclawField<OpenClawModel[]>(initialData, "models", []);
});
const updateOpenclawConfig = useCallback(
(updater: (config: Record<string, any>) => void) => {
try {
const config = JSON.parse(
getSettingsConfig() || OPENCLAW_DEFAULT_CONFIG,
);
updater(config);
onSettingsConfigChange(JSON.stringify(config, null, 2));
} catch {
// ignore
}
},
[getSettingsConfig, onSettingsConfigChange],
);
const handleOpenclawBaseUrlChange = useCallback(
(baseUrl: string) => {
setOpenclawBaseUrl(baseUrl);
updateOpenclawConfig((config) => {
config.baseUrl = baseUrl.trim().replace(/\/+$/, "");
});
},
[updateOpenclawConfig],
);
const handleOpenclawApiKeyChange = useCallback(
(apiKey: string) => {
setOpenclawApiKey(apiKey);
updateOpenclawConfig((config) => {
config.apiKey = apiKey;
});
},
[updateOpenclawConfig],
);
const handleOpenclawApiChange = useCallback(
(api: string) => {
setOpenclawApi(api);
updateOpenclawConfig((config) => {
config.api = api;
});
},
[updateOpenclawConfig],
);
const handleOpenclawModelsChange = useCallback(
(models: OpenClawModel[]) => {
setOpenclawModels(models);
updateOpenclawConfig((config) => {
config.models = models;
});
},
[updateOpenclawConfig],
);
const resetOpenclawState = useCallback(
(config?: {
baseUrl?: string;
apiKey?: string;
api?: string;
models?: OpenClawModel[];
}) => {
setOpenclawProviderKey("");
setOpenclawBaseUrl(config?.baseUrl || "");
setOpenclawApiKey(config?.apiKey || "");
setOpenclawApi(config?.api || "openai-completions");
setOpenclawModels(config?.models || []);
},
[],
);
return {
openclawProviderKey,
setOpenclawProviderKey,
openclawBaseUrl,
openclawApiKey,
openclawApi,
openclawModels,
existingOpenclawKeys,
handleOpenclawBaseUrlChange,
handleOpenclawApiKeyChange,
handleOpenclawApiChange,
handleOpenclawModelsChange,
resetOpenclawState,
};
}

View File

@@ -0,0 +1,192 @@
import { useState, useCallback } from "react";
import type { OpenCodeModel, OpenCodeProviderConfig } from "@/types";
import {
OPENCODE_DEFAULT_NPM,
OPENCODE_DEFAULT_CONFIG,
isKnownOpencodeOptionKey,
parseOpencodeConfig,
toOpencodeExtraOptions,
} from "../helpers/opencodeFormUtils";
interface UseOpencodeFormStateParams {
initialData?: {
settingsConfig?: Record<string, unknown>;
};
appId: string;
providerId?: string;
onSettingsConfigChange: (config: string) => void;
getSettingsConfig: () => string;
}
export interface OpencodeFormState {
opencodeProviderKey: string;
setOpencodeProviderKey: (key: string) => void;
opencodeNpm: string;
opencodeApiKey: string;
opencodeBaseUrl: string;
opencodeModels: Record<string, OpenCodeModel>;
opencodeExtraOptions: Record<string, string>;
handleOpencodeNpmChange: (npm: string) => void;
handleOpencodeApiKeyChange: (apiKey: string) => void;
handleOpencodeBaseUrlChange: (baseUrl: string) => void;
handleOpencodeModelsChange: (models: Record<string, OpenCodeModel>) => void;
handleOpencodeExtraOptionsChange: (options: Record<string, string>) => void;
resetOpencodeState: (config?: OpenCodeProviderConfig) => void;
}
export function useOpencodeFormState({
initialData,
appId,
providerId,
onSettingsConfigChange,
getSettingsConfig,
}: UseOpencodeFormStateParams): OpencodeFormState {
const initialOpencodeConfig =
appId === "opencode"
? parseOpencodeConfig(initialData?.settingsConfig)
: null;
const initialOpencodeOptions = initialOpencodeConfig?.options || {};
const [opencodeProviderKey, setOpencodeProviderKey] = useState<string>(() => {
if (appId !== "opencode") return "";
return providerId || "";
});
const [opencodeNpm, setOpencodeNpm] = useState<string>(() => {
if (appId !== "opencode") return OPENCODE_DEFAULT_NPM;
return initialOpencodeConfig?.npm || OPENCODE_DEFAULT_NPM;
});
const [opencodeApiKey, setOpencodeApiKey] = useState<string>(() => {
if (appId !== "opencode") return "";
const value = initialOpencodeOptions.apiKey;
return typeof value === "string" ? value : "";
});
const [opencodeBaseUrl, setOpencodeBaseUrl] = useState<string>(() => {
if (appId !== "opencode") return "";
const value = initialOpencodeOptions.baseURL;
return typeof value === "string" ? value : "";
});
const [opencodeModels, setOpencodeModels] = useState<
Record<string, OpenCodeModel>
>(() => {
if (appId !== "opencode") return {};
return initialOpencodeConfig?.models || {};
});
const [opencodeExtraOptions, setOpencodeExtraOptions] = useState<
Record<string, string>
>(() => {
if (appId !== "opencode") return {};
return toOpencodeExtraOptions(initialOpencodeOptions);
});
const updateOpencodeSettings = useCallback(
(updater: (config: Record<string, any>) => void) => {
try {
const config = JSON.parse(
getSettingsConfig() || OPENCODE_DEFAULT_CONFIG,
) as Record<string, any>;
updater(config);
onSettingsConfigChange(JSON.stringify(config, null, 2));
} catch {}
},
[getSettingsConfig, onSettingsConfigChange],
);
const handleOpencodeNpmChange = useCallback(
(npm: string) => {
setOpencodeNpm(npm);
updateOpencodeSettings((config) => {
config.npm = npm;
});
},
[updateOpencodeSettings],
);
const handleOpencodeApiKeyChange = useCallback(
(apiKey: string) => {
setOpencodeApiKey(apiKey);
updateOpencodeSettings((config) => {
if (!config.options) config.options = {};
config.options.apiKey = apiKey;
});
},
[updateOpencodeSettings],
);
const handleOpencodeBaseUrlChange = useCallback(
(baseUrl: string) => {
setOpencodeBaseUrl(baseUrl);
updateOpencodeSettings((config) => {
if (!config.options) config.options = {};
config.options.baseURL = baseUrl.trim().replace(/\/+$/, "");
});
},
[updateOpencodeSettings],
);
const handleOpencodeModelsChange = useCallback(
(models: Record<string, OpenCodeModel>) => {
setOpencodeModels(models);
updateOpencodeSettings((config) => {
config.models = models;
});
},
[updateOpencodeSettings],
);
const handleOpencodeExtraOptionsChange = useCallback(
(options: Record<string, string>) => {
setOpencodeExtraOptions(options);
updateOpencodeSettings((config) => {
if (!config.options) config.options = {};
for (const k of Object.keys(config.options)) {
if (!isKnownOpencodeOptionKey(k)) {
delete config.options[k];
}
}
for (const [k, v] of Object.entries(options)) {
const trimmedKey = k.trim();
if (trimmedKey && !trimmedKey.startsWith("option-")) {
try {
config.options[trimmedKey] = JSON.parse(v);
} catch {
config.options[trimmedKey] = v;
}
}
}
});
},
[updateOpencodeSettings],
);
const resetOpencodeState = useCallback((config?: OpenCodeProviderConfig) => {
setOpencodeProviderKey("");
setOpencodeNpm(config?.npm || OPENCODE_DEFAULT_NPM);
setOpencodeBaseUrl(config?.options?.baseURL || "");
setOpencodeApiKey(config?.options?.apiKey || "");
setOpencodeModels(config?.models || {});
setOpencodeExtraOptions(toOpencodeExtraOptions(config?.options || {}));
}, []);
return {
opencodeProviderKey,
setOpencodeProviderKey,
opencodeNpm,
opencodeApiKey,
opencodeBaseUrl,
opencodeModels,
opencodeExtraOptions,
handleOpencodeNpmChange,
handleOpencodeApiKeyChange,
handleOpencodeBaseUrlChange,
handleOpencodeModelsChange,
handleOpencodeExtraOptionsChange,
resetOpencodeState,
};
}