refactor(frontend): Consolidate duplicate Settings type definitions (#12006)

This commit is contained in:
Abhay Mishra 2025-12-12 19:46:31 +05:30 committed by GitHub
parent 976d9d1ab9
commit 5a21c59a3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 267 additions and 369 deletions

View File

@ -16,7 +16,7 @@ describe("SettingsForm", () => {
Component: () => (
<SettingsForm
settings={DEFAULT_SETTINGS}
models={[DEFAULT_SETTINGS.LLM_MODEL]}
models={[DEFAULT_SETTINGS.llm_model]}
onClose={onCloseMock}
/>
),
@ -33,7 +33,7 @@ describe("SettingsForm", () => {
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
llm_model: DEFAULT_SETTINGS.LLM_MODEL,
llm_model: DEFAULT_SETTINGS.llm_model,
}),
);
});

View File

@ -12,20 +12,20 @@ describe("hasAdvancedSettingsSet", () => {
});
describe("should be true if", () => {
test("LLM_BASE_URL is set", () => {
test("llm_base_url is set", () => {
expect(
hasAdvancedSettingsSet({
...DEFAULT_SETTINGS,
LLM_BASE_URL: "test",
llm_base_url: "test",
}),
).toBe(true);
});
test("AGENT is not default value", () => {
test("agent is not default value", () => {
expect(
hasAdvancedSettingsSet({
...DEFAULT_SETTINGS,
AGENT: "test",
agent: "test",
}),
).toBe(true);
});

View File

@ -13,7 +13,7 @@ describe("Model name case preservation", () => {
const settings = extractSettings(formData);
// Test that model names maintain their original casing
expect(settings.LLM_MODEL).toBe("SambaNova/Meta-Llama-3.1-8B-Instruct");
expect(settings.llm_model).toBe("SambaNova/Meta-Llama-3.1-8B-Instruct");
});
it("should preserve openai model case", () => {
@ -24,7 +24,7 @@ describe("Model name case preservation", () => {
formData.set("language", "en");
const settings = extractSettings(formData);
expect(settings.LLM_MODEL).toBe("openai/gpt-4o");
expect(settings.llm_model).toBe("openai/gpt-4o");
});
it("should preserve anthropic model case", () => {
@ -35,7 +35,7 @@ describe("Model name case preservation", () => {
formData.set("language", "en");
const settings = extractSettings(formData);
expect(settings.LLM_MODEL).toBe("anthropic/claude-sonnet-4-20250514");
expect(settings.llm_model).toBe("anthropic/claude-sonnet-4-20250514");
});
it("should not automatically lowercase model names", () => {
@ -48,7 +48,7 @@ describe("Model name case preservation", () => {
const settings = extractSettings(formData);
// Test that camelCase and PascalCase are preserved
expect(settings.LLM_MODEL).not.toBe("sambanova/meta-llama-3.1-8b-instruct");
expect(settings.LLM_MODEL).toBe("SambaNova/Meta-Llama-3.1-8B-Instruct");
expect(settings.llm_model).not.toBe("sambanova/meta-llama-3.1-8b-instruct");
expect(settings.llm_model).toBe("SambaNova/Meta-Llama-3.1-8B-Instruct");
});
});

View File

@ -1,5 +1,5 @@
import { openHands } from "../open-hands-axios";
import { ApiSettings, PostApiSettings } from "./settings.types";
import { Settings } from "#/types/settings";
/**
* Settings service for managing application settings
@ -8,8 +8,8 @@ class SettingsService {
/**
* Get the settings from the server or use the default settings if not found
*/
static async getSettings(): Promise<ApiSettings> {
const { data } = await openHands.get<ApiSettings>("/api/settings");
static async getSettings(): Promise<Settings> {
const { data } = await openHands.get<Settings>("/api/settings");
return data;
}
@ -17,9 +17,7 @@ class SettingsService {
* Save the settings to the server. Only valid settings are saved.
* @param settings - the settings to save
*/
static async saveSettings(
settings: Partial<PostApiSettings>,
): Promise<boolean> {
static async saveSettings(settings: Partial<Settings>): Promise<boolean> {
const data = await openHands.post("/api/settings", settings);
return data.status === 200;
}

View File

@ -1,54 +0,0 @@
import { Provider } from "#/types/settings";
export type ApiSettings = {
llm_model: string;
llm_base_url: string;
agent: string;
language: string;
llm_api_key: string | null;
llm_api_key_set: boolean;
search_api_key_set: boolean;
confirmation_mode: boolean;
security_analyzer: string | null;
remote_runtime_resource_factor: number | null;
enable_default_condenser: boolean;
// Max size for condenser in backend settings
condenser_max_size: number | null;
enable_sound_notifications: boolean;
enable_proactive_conversation_starters: boolean;
enable_solvability_analysis: boolean;
user_consents_to_analytics: boolean | null;
search_api_key?: string;
provider_tokens_set: Partial<Record<Provider, string | null>>;
max_budget_per_task: number | null;
mcp_config?: {
sse_servers: (string | { url: string; api_key?: string })[];
stdio_servers: {
name: string;
command: string;
args?: string[];
env?: Record<string, string>;
}[];
shttp_servers: (string | { url: string; api_key?: string })[];
};
email?: string;
email_verified?: boolean;
git_user_name?: string;
git_user_email?: string;
v1_enabled?: boolean;
};
export type PostApiSettings = ApiSettings & {
user_consents_to_analytics: boolean | null;
search_api_key?: string;
mcp_config?: {
sse_servers: (string | { url: string; api_key?: string })[];
stdio_servers: {
name: string;
command: string;
args?: string[];
env?: Record<string, string>;
}[];
shttp_servers: (string | { url: string; api_key?: string })[];
};
};

View File

@ -9,7 +9,7 @@ function ConfirmationModeEnabled() {
const { data: settings } = useSettings();
if (!settings?.CONFIRMATION_MODE) {
if (!settings?.confirmation_mode) {
return null;
}

View File

@ -20,13 +20,13 @@ export function EmailVerificationGuard({
if (isLoading) return;
// If EMAIL_VERIFIED is explicitly false (not undefined or null)
if (settings?.EMAIL_VERIFIED === false) {
if (settings?.email_verified === false) {
// Allow access to /settings/user but redirect from any other page
if (pathname !== "/settings/user") {
navigate("/settings/user", { replace: true });
}
}
}, [settings?.EMAIL_VERIFIED, pathname, navigate, isLoading]);
}, [settings?.email_verified, pathname, navigate, isLoading]);
return children;
}

View File

@ -71,19 +71,19 @@ export function Sidebar() {
<OpenHandsLogoButton />
</div>
<div>
<NewProjectButton disabled={settings?.EMAIL_VERIFIED === false} />
<NewProjectButton disabled={settings?.email_verified === false} />
</div>
<ConversationPanelButton
isOpen={conversationPanelIsOpen}
onClick={() =>
settings?.EMAIL_VERIFIED === false
settings?.email_verified === false
? null
: setConversationPanelIsOpen((prev) => !prev)
}
disabled={settings?.EMAIL_VERIFIED === false}
disabled={settings?.email_verified === false}
/>
<MicroagentManagementButton
disabled={settings?.EMAIL_VERIFIED === false}
disabled={settings?.email_verified === false}
/>
</div>

View File

@ -41,11 +41,11 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
onClose();
posthog.capture("settings_saved", {
LLM_MODEL: newSettings.LLM_MODEL,
LLM_API_KEY_SET: newSettings.LLM_API_KEY_SET ? "SET" : "UNSET",
SEARCH_API_KEY_SET: newSettings.SEARCH_API_KEY ? "SET" : "UNSET",
LLM_MODEL: newSettings.llm_model,
LLM_API_KEY_SET: newSettings.llm_api_key_set ? "SET" : "UNSET",
SEARCH_API_KEY_SET: newSettings.search_api_key ? "SET" : "UNSET",
REMOTE_RUNTIME_RESOURCE_FACTOR:
newSettings.REMOTE_RUNTIME_RESOURCE_FACTOR,
newSettings.remote_runtime_resource_factor,
});
},
});
@ -67,7 +67,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
}
};
const isLLMKeySet = settings.LLM_API_KEY_SET;
const isLLMKeySet = settings.llm_api_key_set;
return (
<div>
@ -80,7 +80,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
<div className="flex flex-col gap-[17px]">
<ModelSelector
models={organizeModelsAndProviders(models)}
currentModel={settings.LLM_MODEL}
currentModel={settings.llm_model}
wrapperClassName="!flex-col !gap-[17px]"
labelClassName={SETTINGS_FORM.LABEL_CLASSNAME}
/>

View File

@ -24,7 +24,7 @@ export function useAddMcpServer() {
mutationFn: async (server: MCPServerConfig): Promise<void> => {
if (!settings) return;
const currentConfig = settings.MCP_CONFIG || {
const currentConfig = settings.mcp_config || {
sse_servers: [],
stdio_servers: [],
shttp_servers: [],
@ -57,7 +57,7 @@ export function useAddMcpServer() {
const apiSettings = {
mcp_config: newConfig,
v1_enabled: settings.V1_ENABLED,
v1_enabled: settings.v1_enabled,
};
await SettingsService.saveSettings(apiSettings);

View File

@ -51,7 +51,7 @@ export const useCreateConversation = () => {
agentType,
} = variables;
const useV1 = !!settings?.V1_ENABLED && !createMicroagent;
const useV1 = !!settings?.v1_enabled && !createMicroagent;
if (useV1) {
// Use V1 API - creates a conversation start task

View File

@ -9,9 +9,9 @@ export function useDeleteMcpServer() {
return useMutation({
mutationFn: async (serverId: string): Promise<void> => {
if (!settings?.MCP_CONFIG) return;
if (!settings?.mcp_config) return;
const newConfig: MCPConfig = { ...settings.MCP_CONFIG };
const newConfig: MCPConfig = { ...settings.mcp_config };
const [serverType, indexStr] = serverId.split("-");
const index = parseInt(indexStr, 10);
@ -25,7 +25,7 @@ export function useDeleteMcpServer() {
const apiSettings = {
mcp_config: newConfig,
v1_enabled: settings.V1_ENABLED,
v1_enabled: settings.v1_enabled,
};
await SettingsService.saveSettings(apiSettings);

View File

@ -2,43 +2,28 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { usePostHog } from "posthog-js/react";
import { DEFAULT_SETTINGS } from "#/services/settings";
import SettingsService from "#/api/settings-service/settings-service.api";
import { PostSettings } from "#/types/settings";
import { PostApiSettings } from "#/api/settings-service/settings.types";
import { Settings } from "#/types/settings";
import { useSettings } from "../query/use-settings";
const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
const apiSettings: Partial<PostApiSettings> = {
llm_model: settings.LLM_MODEL,
llm_base_url: settings.LLM_BASE_URL,
agent: settings.AGENT || DEFAULT_SETTINGS.AGENT,
language: settings.LANGUAGE || DEFAULT_SETTINGS.LANGUAGE,
confirmation_mode: settings.CONFIRMATION_MODE,
security_analyzer: settings.SECURITY_ANALYZER,
const saveSettingsMutationFn = async (settings: Partial<Settings>) => {
const settingsToSave: Partial<Settings> = {
...settings,
agent: settings.agent || DEFAULT_SETTINGS.agent,
language: settings.language || DEFAULT_SETTINGS.language,
llm_api_key:
settings.llm_api_key === ""
? ""
: settings.llm_api_key?.trim() || undefined,
remote_runtime_resource_factor: settings.REMOTE_RUNTIME_RESOURCE_FACTOR,
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
condenser_max_size:
settings.CONDENSER_MAX_SIZE ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
user_consents_to_analytics: settings.user_consents_to_analytics,
provider_tokens_set: settings.PROVIDER_TOKENS_SET,
mcp_config: settings.MCP_CONFIG,
enable_proactive_conversation_starters:
settings.ENABLE_PROACTIVE_CONVERSATION_STARTERS,
enable_solvability_analysis: settings.ENABLE_SOLVABILITY_ANALYSIS,
search_api_key: settings.SEARCH_API_KEY?.trim() || "",
max_budget_per_task: settings.MAX_BUDGET_PER_TASK,
settings.condenser_max_size ?? DEFAULT_SETTINGS.condenser_max_size,
search_api_key: settings.search_api_key?.trim() || "",
git_user_name:
settings.GIT_USER_NAME?.trim() || DEFAULT_SETTINGS.GIT_USER_NAME,
settings.git_user_name?.trim() || DEFAULT_SETTINGS.git_user_name,
git_user_email:
settings.GIT_USER_EMAIL?.trim() || DEFAULT_SETTINGS.GIT_USER_EMAIL,
v1_enabled: settings.V1_ENABLED,
settings.git_user_email?.trim() || DEFAULT_SETTINGS.git_user_email,
};
await SettingsService.saveSettings(apiSettings);
await SettingsService.saveSettings(settingsToSave);
};
export const useSaveSettings = () => {
@ -47,18 +32,18 @@ export const useSaveSettings = () => {
const { data: currentSettings } = useSettings();
return useMutation({
mutationFn: async (settings: Partial<PostSettings>) => {
mutationFn: async (settings: Partial<Settings>) => {
const newSettings = { ...currentSettings, ...settings };
// Track MCP configuration changes
if (
settings.MCP_CONFIG &&
currentSettings?.MCP_CONFIG !== settings.MCP_CONFIG
settings.mcp_config &&
currentSettings?.mcp_config !== settings.mcp_config
) {
const hasMcpConfig = !!settings.MCP_CONFIG;
const sseServersCount = settings.MCP_CONFIG?.sse_servers?.length || 0;
const hasMcpConfig = !!settings.mcp_config;
const sseServersCount = settings.mcp_config?.sse_servers?.length || 0;
const stdioServersCount =
settings.MCP_CONFIG?.stdio_servers?.length || 0;
settings.mcp_config?.stdio_servers?.length || 0;
// Track MCP configuration usage
posthog.capture("mcp_config_updated", {

View File

@ -28,9 +28,9 @@ export function useUpdateMcpServer() {
serverId: string;
server: MCPServerConfig;
}): Promise<void> => {
if (!settings?.MCP_CONFIG) return;
if (!settings?.mcp_config) return;
const newConfig = { ...settings.MCP_CONFIG };
const newConfig = { ...settings.mcp_config };
const [serverType, indexStr] = serverId.split("-");
const index = parseInt(indexStr, 10);
@ -59,7 +59,7 @@ export function useUpdateMcpServer() {
const apiSettings = {
mcp_config: newConfig,
v1_enabled: settings.V1_ENABLED,
v1_enabled: settings.v1_enabled,
};
await SettingsService.saveSettings(apiSettings);

View File

@ -6,37 +6,18 @@ import { Settings } from "#/types/settings";
import { useIsAuthed } from "./use-is-authed";
const getSettingsQueryFn = async (): Promise<Settings> => {
const apiSettings = await SettingsService.getSettings();
const settings = await SettingsService.getSettings();
return {
LLM_MODEL: apiSettings.llm_model,
LLM_BASE_URL: apiSettings.llm_base_url,
AGENT: apiSettings.agent,
LANGUAGE: apiSettings.language,
CONFIRMATION_MODE: apiSettings.confirmation_mode,
SECURITY_ANALYZER: apiSettings.security_analyzer,
LLM_API_KEY_SET: apiSettings.llm_api_key_set,
SEARCH_API_KEY_SET: apiSettings.search_api_key_set,
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
PROVIDER_TOKENS_SET: apiSettings.provider_tokens_set,
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
CONDENSER_MAX_SIZE:
apiSettings.condenser_max_size ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
ENABLE_PROACTIVE_CONVERSATION_STARTERS:
apiSettings.enable_proactive_conversation_starters,
ENABLE_SOLVABILITY_ANALYSIS: apiSettings.enable_solvability_analysis,
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
SEARCH_API_KEY: apiSettings.search_api_key || "",
MAX_BUDGET_PER_TASK: apiSettings.max_budget_per_task,
EMAIL: apiSettings.email || "",
EMAIL_VERIFIED: apiSettings.email_verified,
MCP_CONFIG: apiSettings.mcp_config,
GIT_USER_NAME: apiSettings.git_user_name || DEFAULT_SETTINGS.GIT_USER_NAME,
GIT_USER_EMAIL:
apiSettings.git_user_email || DEFAULT_SETTINGS.GIT_USER_EMAIL,
IS_NEW_USER: false,
V1_ENABLED: apiSettings.v1_enabled ?? DEFAULT_SETTINGS.V1_ENABLED,
...settings,
condenser_max_size:
settings.condenser_max_size ?? DEFAULT_SETTINGS.condenser_max_size,
search_api_key: settings.search_api_key || "",
email: settings.email || "",
git_user_name: settings.git_user_name || DEFAULT_SETTINGS.git_user_name,
git_user_email: settings.git_user_email || DEFAULT_SETTINGS.git_user_email,
is_new_user: false,
v1_enabled: settings.v1_enabled ?? DEFAULT_SETTINGS.v1_enabled,
};
};

View File

@ -15,7 +15,7 @@ import { useSettings } from "#/hooks/query/use-settings";
*/
export const useStartTasks = (limit = 10) => {
const { data: settings } = useSettings();
const isV1Enabled = settings?.V1_ENABLED;
const isV1Enabled = settings?.v1_enabled;
return useQuery({
queryKey: ["start-tasks", "search", limit],

View File

@ -19,7 +19,7 @@ export const useSyncPostHogConsent = () => {
return;
}
const backendConsent = settings.USER_CONSENTS_TO_ANALYTICS;
const backendConsent = settings.user_consents_to_analytics;
// Only sync if there's a backend preference set
if (backendConsent !== null) {

View File

@ -17,7 +17,7 @@ export const useTracking = () => {
app_surface: config?.APP_MODE || "unknown",
plan_tier: null,
current_url: window.location.href,
user_email: settings?.EMAIL || settings?.GIT_USER_EMAIL || null,
user_email: settings?.email || settings?.git_user_email || null,
};
const trackLoginButtonClick = ({ provider }: { provider: Provider }) => {

View File

@ -6,8 +6,8 @@ export const useUserProviders = () => {
const { data: settings, isLoading: isLoadingSettings } = useSettings();
const providers = React.useMemo(
() => convertRawProvidersToList(settings?.PROVIDER_TOKENS_SET),
[settings?.PROVIDER_TOKENS_SET],
() => convertRawProvidersToList(settings?.provider_tokens_set),
[settings?.provider_tokens_set],
);
return {

View File

@ -3,7 +3,6 @@ import { FILE_SERVICE_HANDLERS } from "./file-service-handlers";
import { TASK_SUGGESTIONS_HANDLERS } from "./task-suggestions-handlers";
import { SECRETS_HANDLERS } from "./secrets-handlers";
import { GIT_REPOSITORY_HANDLERS } from "./git-repository-handlers";
import {
SETTINGS_HANDLERS,
MOCK_DEFAULT_USER_SETTINGS,

View File

@ -1,37 +1,33 @@
import { http, delay, HttpResponse } from "msw";
import {
ApiSettings,
PostApiSettings,
} from "#/api/settings-service/settings.types";
import { GetConfigResponse } from "#/api/option-service/option.types";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { Provider } from "#/types/settings";
import { Provider, Settings } from "#/types/settings";
export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
llm_model: DEFAULT_SETTINGS.LLM_MODEL,
llm_base_url: DEFAULT_SETTINGS.LLM_BASE_URL,
export const MOCK_DEFAULT_USER_SETTINGS: Settings = {
llm_model: DEFAULT_SETTINGS.llm_model,
llm_base_url: DEFAULT_SETTINGS.llm_base_url,
llm_api_key: null,
llm_api_key_set: DEFAULT_SETTINGS.LLM_API_KEY_SET,
search_api_key_set: DEFAULT_SETTINGS.SEARCH_API_KEY_SET,
agent: DEFAULT_SETTINGS.AGENT,
language: DEFAULT_SETTINGS.LANGUAGE,
confirmation_mode: DEFAULT_SETTINGS.CONFIRMATION_MODE,
security_analyzer: DEFAULT_SETTINGS.SECURITY_ANALYZER,
llm_api_key_set: DEFAULT_SETTINGS.llm_api_key_set,
search_api_key_set: DEFAULT_SETTINGS.search_api_key_set,
agent: DEFAULT_SETTINGS.agent,
language: DEFAULT_SETTINGS.language,
confirmation_mode: DEFAULT_SETTINGS.confirmation_mode,
security_analyzer: DEFAULT_SETTINGS.security_analyzer,
remote_runtime_resource_factor:
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
DEFAULT_SETTINGS.remote_runtime_resource_factor,
provider_tokens_set: {},
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
condenser_max_size: DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
enable_default_condenser: DEFAULT_SETTINGS.enable_default_condenser,
condenser_max_size: DEFAULT_SETTINGS.condenser_max_size,
enable_sound_notifications: DEFAULT_SETTINGS.enable_sound_notifications,
enable_proactive_conversation_starters:
DEFAULT_SETTINGS.ENABLE_PROACTIVE_CONVERSATION_STARTERS,
enable_solvability_analysis: DEFAULT_SETTINGS.ENABLE_SOLVABILITY_ANALYSIS,
user_consents_to_analytics: DEFAULT_SETTINGS.USER_CONSENTS_TO_ANALYTICS,
max_budget_per_task: DEFAULT_SETTINGS.MAX_BUDGET_PER_TASK,
DEFAULT_SETTINGS.enable_proactive_conversation_starters,
enable_solvability_analysis: DEFAULT_SETTINGS.enable_solvability_analysis,
user_consents_to_analytics: DEFAULT_SETTINGS.user_consents_to_analytics,
max_budget_per_task: DEFAULT_SETTINGS.max_budget_per_task,
};
const MOCK_USER_PREFERENCES: {
settings: ApiSettings | PostApiSettings | null;
settings: Settings | null;
} = {
settings: null,
};
@ -111,7 +107,7 @@ export const SETTINGS_HANDLERS = [
MOCK_USER_PREFERENCES.settings = {
...current,
...(body as Partial<ApiSettings>),
...(body as Partial<Settings>),
};
return HttpResponse.json(null, { status: 200 });

View File

@ -56,7 +56,7 @@ function AppSettingsScreen() {
const languageValue = AvailableLanguages.find(
({ label }) => label === languageLabel,
)?.value;
const language = languageValue || DEFAULT_SETTINGS.LANGUAGE;
const language = languageValue || DEFAULT_SETTINGS.language;
const enableAnalytics =
formData.get("enable-analytics-switch")?.toString() === "on";
@ -77,21 +77,21 @@ function AppSettingsScreen() {
const gitUserName =
formData.get("git-user-name-input")?.toString() ||
DEFAULT_SETTINGS.GIT_USER_NAME;
DEFAULT_SETTINGS.git_user_name;
const gitUserEmail =
formData.get("git-user-email-input")?.toString() ||
DEFAULT_SETTINGS.GIT_USER_EMAIL;
DEFAULT_SETTINGS.git_user_email;
saveSettings(
{
LANGUAGE: language,
language,
user_consents_to_analytics: enableAnalytics,
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
ENABLE_PROACTIVE_CONVERSATION_STARTERS: enableProactiveConversations,
ENABLE_SOLVABILITY_ANALYSIS: enableSolvabilityAnalysis,
MAX_BUDGET_PER_TASK: maxBudgetPerTask,
GIT_USER_NAME: gitUserName,
GIT_USER_EMAIL: gitUserEmail,
enable_sound_notifications: enableSoundNotifications,
enable_proactive_conversation_starters: enableProactiveConversations,
enable_solvability_analysis: enableSolvabilityAnalysis,
max_budget_per_task: maxBudgetPerTask,
git_user_name: gitUserName,
git_user_email: gitUserEmail,
},
{
onSuccess: () => {
@ -120,7 +120,7 @@ function AppSettingsScreen() {
({ label: langValue }) => langValue === value,
)?.label;
const currentLanguage = AvailableLanguages.find(
({ value: langValue }) => langValue === settings?.LANGUAGE,
({ value: langValue }) => langValue === settings?.language,
)?.label;
setLanguageInputHasChanged(selectedLanguage !== currentLanguage);
@ -128,12 +128,12 @@ function AppSettingsScreen() {
const checkIfAnalyticsSwitchHasChanged = (checked: boolean) => {
// Treat null as true since analytics is opt-in by default
const currentAnalytics = settings?.USER_CONSENTS_TO_ANALYTICS ?? true;
const currentAnalytics = settings?.user_consents_to_analytics ?? true;
setAnalyticsSwitchHasChanged(checked !== currentAnalytics);
};
const checkIfSoundNotificationsSwitchHasChanged = (checked: boolean) => {
const currentSoundNotifications = !!settings?.ENABLE_SOUND_NOTIFICATIONS;
const currentSoundNotifications = !!settings?.enable_sound_notifications;
setSoundNotificationsSwitchHasChanged(
checked !== currentSoundNotifications,
);
@ -141,14 +141,14 @@ function AppSettingsScreen() {
const checkIfProactiveConversationsSwitchHasChanged = (checked: boolean) => {
const currentProactiveConversations =
!!settings?.ENABLE_PROACTIVE_CONVERSATION_STARTERS;
!!settings?.enable_proactive_conversation_starters;
setProactiveConversationsSwitchHasChanged(
checked !== currentProactiveConversations,
);
};
const checkIfSolvabilityAnalysisSwitchHasChanged = (checked: boolean) => {
const currentSolvabilityAnalysis = !!settings?.ENABLE_SOLVABILITY_ANALYSIS;
const currentSolvabilityAnalysis = !!settings?.enable_solvability_analysis;
setSolvabilityAnalysisSwitchHasChanged(
checked !== currentSolvabilityAnalysis,
);
@ -156,17 +156,17 @@ function AppSettingsScreen() {
const checkIfMaxBudgetPerTaskHasChanged = (value: string) => {
const newValue = parseMaxBudgetPerTask(value);
const currentValue = settings?.MAX_BUDGET_PER_TASK;
const currentValue = settings?.max_budget_per_task;
setMaxBudgetPerTaskHasChanged(newValue !== currentValue);
};
const checkIfGitUserNameHasChanged = (value: string) => {
const currentValue = settings?.GIT_USER_NAME;
const currentValue = settings?.git_user_name;
setGitUserNameHasChanged(value !== currentValue);
};
const checkIfGitUserEmailHasChanged = (value: string) => {
const currentValue = settings?.GIT_USER_EMAIL;
const currentValue = settings?.git_user_email;
setGitUserEmailHasChanged(value !== currentValue);
};
@ -193,14 +193,14 @@ function AppSettingsScreen() {
<div className="flex flex-col gap-6">
<LanguageInput
name="language-input"
defaultKey={settings.LANGUAGE}
defaultKey={settings.language}
onChange={checkIfLanguageInputHasChanged}
/>
<SettingsSwitch
testId="enable-analytics-switch"
name="enable-analytics-switch"
defaultIsToggled={settings.USER_CONSENTS_TO_ANALYTICS ?? true}
defaultIsToggled={settings.user_consents_to_analytics ?? true}
onToggle={checkIfAnalyticsSwitchHasChanged}
>
{t(I18nKey.ANALYTICS$SEND_ANONYMOUS_DATA)}
@ -209,7 +209,7 @@ function AppSettingsScreen() {
<SettingsSwitch
testId="enable-sound-notifications-switch"
name="enable-sound-notifications-switch"
defaultIsToggled={!!settings.ENABLE_SOUND_NOTIFICATIONS}
defaultIsToggled={!!settings.enable_sound_notifications}
onToggle={checkIfSoundNotificationsSwitchHasChanged}
>
{t(I18nKey.SETTINGS$SOUND_NOTIFICATIONS)}
@ -220,7 +220,7 @@ function AppSettingsScreen() {
testId="enable-proactive-conversations-switch"
name="enable-proactive-conversations-switch"
defaultIsToggled={
!!settings.ENABLE_PROACTIVE_CONVERSATION_STARTERS
!!settings.enable_proactive_conversation_starters
}
onToggle={checkIfProactiveConversationsSwitchHasChanged}
>
@ -232,20 +232,20 @@ function AppSettingsScreen() {
<SettingsSwitch
testId="enable-solvability-analysis-switch"
name="enable-solvability-analysis-switch"
defaultIsToggled={!!settings.ENABLE_SOLVABILITY_ANALYSIS}
defaultIsToggled={!!settings.enable_solvability_analysis}
onToggle={checkIfSolvabilityAnalysisSwitchHasChanged}
>
{t(I18nKey.SETTINGS$SOLVABILITY_ANALYSIS)}
</SettingsSwitch>
)}
{!settings?.V1_ENABLED && (
{!settings?.v1_enabled && (
<SettingsInput
testId="max-budget-per-task-input"
name="max-budget-per-task-input"
type="number"
label={t(I18nKey.SETTINGS$MAX_BUDGET_PER_CONVERSATION)}
defaultValue={settings.MAX_BUDGET_PER_TASK?.toString() || ""}
defaultValue={settings.max_budget_per_task?.toString() || ""}
onChange={checkIfMaxBudgetPerTaskHasChanged}
placeholder={t(I18nKey.SETTINGS$MAXIMUM_BUDGET_USD)}
min={1}
@ -267,7 +267,7 @@ function AppSettingsScreen() {
name="git-user-name-input"
type="text"
label={t(I18nKey.SETTINGS$GIT_USERNAME)}
defaultValue={settings.GIT_USER_NAME || ""}
defaultValue={settings.git_user_name || ""}
onChange={checkIfGitUserNameHasChanged}
placeholder="Username for git commits"
className="w-full max-w-[680px]"
@ -277,7 +277,7 @@ function AppSettingsScreen() {
name="git-user-email-input"
type="email"
label={t(I18nKey.SETTINGS$GIT_EMAIL)}
defaultValue={settings.GIT_USER_EMAIL || ""}
defaultValue={settings.git_user_email || ""}
onChange={checkIfGitUserEmailHasChanged}
placeholder="Email for git commits"
className="w-full max-w-[680px]"

View File

@ -50,10 +50,10 @@ function GitSettingsScreen() {
const [azureDevOpsHostInputHasValue, setAzureDevOpsHostInputHasValue] =
React.useState(false);
const existingGithubHost = settings?.PROVIDER_TOKENS_SET.github;
const existingGitlabHost = settings?.PROVIDER_TOKENS_SET.gitlab;
const existingBitbucketHost = settings?.PROVIDER_TOKENS_SET.bitbucket;
const existingAzureDevOpsHost = settings?.PROVIDER_TOKENS_SET.azure_devops;
const existingGithubHost = settings?.provider_tokens_set.github;
const existingGitlabHost = settings?.provider_tokens_set.gitlab;
const existingBitbucketHost = settings?.provider_tokens_set.bitbucket;
const existingAzureDevOpsHost = settings?.provider_tokens_set.azure_devops;
const isSaas = config?.APP_MODE === "saas";
const isGitHubTokenSet = providers.includes("github");

View File

@ -91,15 +91,15 @@ function LlmSettingsScreen() {
// Track confirmation mode state to control security analyzer visibility
const [confirmationModeEnabled, setConfirmationModeEnabled] = React.useState(
settings?.CONFIRMATION_MODE ?? DEFAULT_SETTINGS.CONFIRMATION_MODE,
settings?.confirmation_mode ?? DEFAULT_SETTINGS.confirmation_mode,
);
// Track selected security analyzer for form submission
const [selectedSecurityAnalyzer, setSelectedSecurityAnalyzer] =
React.useState(
settings?.SECURITY_ANALYZER === null
settings?.security_analyzer === null
? "none"
: (settings?.SECURITY_ANALYZER ?? DEFAULT_SETTINGS.SECURITY_ANALYZER),
: (settings?.security_analyzer ?? DEFAULT_SETTINGS.security_analyzer),
);
const [selectedProvider, setSelectedProvider] = React.useState<string | null>(
@ -111,7 +111,7 @@ function LlmSettingsScreen() {
);
// Determine if we should hide the API key input and use OpenHands-managed key (when using OpenHands provider in SaaS mode)
const currentModel = currentSelectedModel || settings?.LLM_MODEL;
const currentModel = currentSelectedModel || settings?.llm_model;
const isSaasMode = config?.APP_MODE === "saas";
@ -124,7 +124,7 @@ function LlmSettingsScreen() {
if (dirtyInputs.model) {
return currentModel?.startsWith("openhands/");
}
return settings?.LLM_MODEL?.startsWith("openhands/");
return settings?.llm_model?.startsWith("openhands/");
}
return false;
@ -133,13 +133,13 @@ function LlmSettingsScreen() {
const shouldUseOpenHandsKey = isOpenHandsProvider() && isSaasMode;
// Determine if we should hide the agent dropdown when V1 conversation API is enabled
const isV1Enabled = settings?.V1_ENABLED;
const isV1Enabled = settings?.v1_enabled;
React.useEffect(() => {
const determineWhetherToToggleAdvancedSettings = () => {
if (resources && settings) {
return (
isCustomModel(resources.models, settings.LLM_MODEL) ||
isCustomModel(resources.models, settings.llm_model) ||
hasAdvancedSettingsSet({
...settings,
})
@ -157,24 +157,24 @@ function LlmSettingsScreen() {
// Initialize currentSelectedModel with the current settings
React.useEffect(() => {
if (settings?.LLM_MODEL) {
setCurrentSelectedModel(settings.LLM_MODEL);
if (settings?.llm_model) {
setCurrentSelectedModel(settings.llm_model);
}
}, [settings?.LLM_MODEL]);
}, [settings?.llm_model]);
// Update confirmation mode state when settings change
React.useEffect(() => {
if (settings?.CONFIRMATION_MODE !== undefined) {
setConfirmationModeEnabled(settings.CONFIRMATION_MODE);
if (settings?.confirmation_mode !== undefined) {
setConfirmationModeEnabled(settings.confirmation_mode);
}
}, [settings?.CONFIRMATION_MODE]);
}, [settings?.confirmation_mode]);
// Update selected security analyzer state when settings change
React.useEffect(() => {
if (settings?.SECURITY_ANALYZER !== undefined) {
setSelectedSecurityAnalyzer(settings.SECURITY_ANALYZER || "none");
if (settings?.security_analyzer !== undefined) {
setSelectedSecurityAnalyzer(settings.security_analyzer || "none");
}
}, [settings?.SECURITY_ANALYZER]);
}, [settings?.security_analyzer]);
// Handle URL parameters for SaaS subscription redirects
React.useEffect(() => {
@ -230,19 +230,19 @@ function LlmSettingsScreen() {
saveSettings(
{
LLM_MODEL: fullLlmModel,
llm_model: fullLlmModel,
llm_api_key: finalApiKey || null,
SEARCH_API_KEY: searchApiKey || "",
CONFIRMATION_MODE: confirmationMode,
SECURITY_ANALYZER:
search_api_key: searchApiKey || "",
confirmation_mode: confirmationMode,
security_analyzer:
securityAnalyzer === "none"
? null
: securityAnalyzer || DEFAULT_SETTINGS.SECURITY_ANALYZER,
: securityAnalyzer || DEFAULT_SETTINGS.security_analyzer,
// reset advanced settings
LLM_BASE_URL: DEFAULT_SETTINGS.LLM_BASE_URL,
AGENT: DEFAULT_SETTINGS.AGENT,
ENABLE_DEFAULT_CONDENSER: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
llm_base_url: DEFAULT_SETTINGS.llm_base_url,
agent: DEFAULT_SETTINGS.agent,
enable_default_condenser: DEFAULT_SETTINGS.enable_default_condenser,
},
{
onSuccess: handleSuccessfulMutation,
@ -281,19 +281,19 @@ function LlmSettingsScreen() {
saveSettings(
{
LLM_MODEL: model,
LLM_BASE_URL: baseUrl,
llm_model: model,
llm_base_url: baseUrl,
llm_api_key: finalApiKey || null,
SEARCH_API_KEY: searchApiKey || "",
AGENT: agent,
CONFIRMATION_MODE: confirmationMode,
ENABLE_DEFAULT_CONDENSER: enableDefaultCondenser,
CONDENSER_MAX_SIZE:
condenserMaxSize ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
SECURITY_ANALYZER:
search_api_key: searchApiKey || "",
agent,
confirmation_mode: confirmationMode,
enable_default_condenser: enableDefaultCondenser,
condenser_max_size:
condenserMaxSize ?? DEFAULT_SETTINGS.condenser_max_size,
security_analyzer:
securityAnalyzer === "none"
? null
: securityAnalyzer || DEFAULT_SETTINGS.SECURITY_ANALYZER,
: securityAnalyzer || DEFAULT_SETTINGS.security_analyzer,
},
{
onSuccess: handleSuccessfulMutation,
@ -323,7 +323,7 @@ function LlmSettingsScreen() {
) => {
// openai providers are special case; see ModelSelector
// component for details
const modelIsDirty = model !== settings?.LLM_MODEL.replace("openai/", "");
const modelIsDirty = model !== settings?.llm_model.replace("openai/", "");
setDirtyInputs((prev) => ({
...prev,
model: modelIsDirty,
@ -351,7 +351,7 @@ function LlmSettingsScreen() {
};
const handleSearchApiKeyIsDirty = (searchApiKey: string) => {
const searchApiKeyIsDirty = searchApiKey !== settings?.SEARCH_API_KEY;
const searchApiKeyIsDirty = searchApiKey !== settings?.search_api_key;
setDirtyInputs((prev) => ({
...prev,
searchApiKey: searchApiKeyIsDirty,
@ -359,7 +359,7 @@ function LlmSettingsScreen() {
};
const handleCustomModelIsDirty = (model: string) => {
const modelIsDirty = model !== settings?.LLM_MODEL && model !== "";
const modelIsDirty = model !== settings?.llm_model && model !== "";
setDirtyInputs((prev) => ({
...prev,
model: modelIsDirty,
@ -370,7 +370,7 @@ function LlmSettingsScreen() {
};
const handleBaseUrlIsDirty = (baseUrl: string) => {
const baseUrlIsDirty = baseUrl !== settings?.LLM_BASE_URL;
const baseUrlIsDirty = baseUrl !== settings?.llm_base_url;
setDirtyInputs((prev) => ({
...prev,
baseUrl: baseUrlIsDirty,
@ -378,7 +378,7 @@ function LlmSettingsScreen() {
};
const handleAgentIsDirty = (agent: string) => {
const agentIsDirty = agent !== settings?.AGENT && agent !== "";
const agentIsDirty = agent !== settings?.agent && agent !== "";
setDirtyInputs((prev) => ({
...prev,
agent: agentIsDirty,
@ -386,7 +386,7 @@ function LlmSettingsScreen() {
};
const handleConfirmationModeIsDirty = (isToggled: boolean) => {
const confirmationModeIsDirty = isToggled !== settings?.CONFIRMATION_MODE;
const confirmationModeIsDirty = isToggled !== settings?.confirmation_mode;
setDirtyInputs((prev) => ({
...prev,
confirmationMode: confirmationModeIsDirty,
@ -395,7 +395,7 @@ function LlmSettingsScreen() {
// When confirmation mode is enabled, set default security analyzer to "llm" if not already set
if (isToggled && !selectedSecurityAnalyzer) {
setSelectedSecurityAnalyzer(DEFAULT_SETTINGS.SECURITY_ANALYZER);
setSelectedSecurityAnalyzer(DEFAULT_SETTINGS.security_analyzer);
setDirtyInputs((prev) => ({
...prev,
securityAnalyzer: true,
@ -405,7 +405,7 @@ function LlmSettingsScreen() {
const handleEnableDefaultCondenserIsDirty = (isToggled: boolean) => {
const enableDefaultCondenserIsDirty =
isToggled !== settings?.ENABLE_DEFAULT_CONDENSER;
isToggled !== settings?.enable_default_condenser;
setDirtyInputs((prev) => ({
...prev,
enableDefaultCondenser: enableDefaultCondenserIsDirty,
@ -416,8 +416,8 @@ function LlmSettingsScreen() {
const parsed = value ? Number.parseInt(value, 10) : undefined;
const bounded = parsed !== undefined ? Math.max(20, parsed) : undefined;
const condenserMaxSizeIsDirty =
(bounded ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE) !==
(settings?.CONDENSER_MAX_SIZE ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE);
(bounded ?? DEFAULT_SETTINGS.condenser_max_size) !==
(settings?.condenser_max_size ?? DEFAULT_SETTINGS.condenser_max_size);
setDirtyInputs((prev) => ({
...prev,
condenserMaxSize: condenserMaxSizeIsDirty,
@ -426,7 +426,7 @@ function LlmSettingsScreen() {
const handleSecurityAnalyzerIsDirty = (securityAnalyzer: string) => {
const securityAnalyzerIsDirty =
securityAnalyzer !== settings?.SECURITY_ANALYZER;
securityAnalyzer !== settings?.security_analyzer;
setDirtyInputs((prev) => ({
...prev,
securityAnalyzer: securityAnalyzerIsDirty,
@ -512,12 +512,12 @@ function LlmSettingsScreen() {
<>
<ModelSelector
models={modelsAndProviders}
currentModel={settings.LLM_MODEL || DEFAULT_OPENHANDS_MODEL}
currentModel={settings.llm_model || DEFAULT_OPENHANDS_MODEL}
onChange={handleModelIsDirty}
onDefaultValuesChanged={onDefaultValuesChanged}
wrapperClassName="!flex-col !gap-6"
/>
{(settings.LLM_MODEL?.startsWith("openhands/") ||
{(settings.llm_model?.startsWith("openhands/") ||
currentSelectedModel?.startsWith("openhands/")) && (
<OpenHandsApiKeyHelp testId="openhands-api-key-help" />
)}
@ -532,11 +532,11 @@ function LlmSettingsScreen() {
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
type="password"
className="w-full max-w-[680px]"
placeholder={settings.LLM_API_KEY_SET ? "<hidden>" : ""}
placeholder={settings.llm_api_key_set ? "<hidden>" : ""}
onChange={handleApiKeyIsDirty}
startContent={
settings.LLM_API_KEY_SET && (
<KeyStatusIcon isSet={settings.LLM_API_KEY_SET} />
settings.llm_api_key_set && (
<KeyStatusIcon isSet={settings.llm_api_key_set} />
)
}
/>
@ -561,13 +561,13 @@ function LlmSettingsScreen() {
testId="llm-custom-model-input"
name="llm-custom-model-input"
label={t(I18nKey.SETTINGS$CUSTOM_MODEL)}
defaultValue={settings.LLM_MODEL || DEFAULT_OPENHANDS_MODEL}
defaultValue={settings.llm_model || DEFAULT_OPENHANDS_MODEL}
placeholder={DEFAULT_OPENHANDS_MODEL}
type="text"
className="w-full max-w-[680px]"
onChange={handleCustomModelIsDirty}
/>
{(settings.LLM_MODEL?.startsWith("openhands/") ||
{(settings.llm_model?.startsWith("openhands/") ||
currentSelectedModel?.startsWith("openhands/")) && (
<OpenHandsApiKeyHelp testId="openhands-api-key-help-2" />
)}
@ -576,7 +576,7 @@ function LlmSettingsScreen() {
testId="base-url-input"
name="base-url-input"
label={t(I18nKey.SETTINGS$BASE_URL)}
defaultValue={settings.LLM_BASE_URL}
defaultValue={settings.llm_base_url}
placeholder="https://api.openai.com"
type="text"
className="w-full max-w-[680px]"
@ -591,11 +591,11 @@ function LlmSettingsScreen() {
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
type="password"
className="w-full max-w-[680px]"
placeholder={settings.LLM_API_KEY_SET ? "<hidden>" : ""}
placeholder={settings.llm_api_key_set ? "<hidden>" : ""}
onChange={handleApiKeyIsDirty}
startContent={
settings.LLM_API_KEY_SET && (
<KeyStatusIcon isSet={settings.LLM_API_KEY_SET} />
settings.llm_api_key_set && (
<KeyStatusIcon isSet={settings.llm_api_key_set} />
)
}
/>
@ -616,12 +616,12 @@ function LlmSettingsScreen() {
label={t(I18nKey.SETTINGS$SEARCH_API_KEY)}
type="password"
className="w-full max-w-[680px]"
defaultValue={settings.SEARCH_API_KEY || ""}
defaultValue={settings.search_api_key || ""}
onChange={handleSearchApiKeyIsDirty}
placeholder={t(I18nKey.API$TVLY_KEY_EXAMPLE)}
startContent={
settings.SEARCH_API_KEY_SET && (
<KeyStatusIcon isSet={settings.SEARCH_API_KEY_SET} />
settings.search_api_key_set && (
<KeyStatusIcon isSet={settings.search_api_key_set} />
)
}
/>
@ -644,7 +644,7 @@ function LlmSettingsScreen() {
label: agent, // TODO: Add i18n support for agent names
})) || []
}
defaultSelectedKey={settings.AGENT}
defaultSelectedKey={settings.agent}
isClearable={false}
onInputChange={handleAgentIsDirty}
wrapperClassName="w-full max-w-[680px]"
@ -662,11 +662,11 @@ function LlmSettingsScreen() {
step={1}
label={t(I18nKey.SETTINGS$CONDENSER_MAX_SIZE)}
defaultValue={(
settings.CONDENSER_MAX_SIZE ??
DEFAULT_SETTINGS.CONDENSER_MAX_SIZE
settings.condenser_max_size ??
DEFAULT_SETTINGS.condenser_max_size
)?.toString()}
onChange={(value) => handleCondenserMaxSizeIsDirty(value)}
isDisabled={!settings.ENABLE_DEFAULT_CONDENSER}
isDisabled={!settings.enable_default_condenser}
/>
<p className="text-xs text-tertiary-alt mt-1">
{t(I18nKey.SETTINGS$CONDENSER_MAX_SIZE_TOOLTIP)}
@ -676,7 +676,7 @@ function LlmSettingsScreen() {
<SettingsSwitch
testId="enable-memory-condenser-switch"
name="enable-memory-condenser-switch"
defaultIsToggled={settings.ENABLE_DEFAULT_CONDENSER}
defaultIsToggled={settings.enable_default_condenser}
onToggle={handleEnableDefaultCondenserIsDirty}
>
{t(I18nKey.SETTINGS$ENABLE_MEMORY_CONDENSATION)}
@ -688,7 +688,7 @@ function LlmSettingsScreen() {
testId="enable-confirmation-mode-switch"
name="enable-confirmation-mode-switch"
onToggle={handleConfirmationModeIsDirty}
defaultIsToggled={settings.CONFIRMATION_MODE}
defaultIsToggled={settings.confirmation_mode}
isBeta
>
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}

View File

@ -41,7 +41,7 @@ function MCPSettingsScreen() {
useState(false);
const [serverToDelete, setServerToDelete] = useState<string | null>(null);
const mcpConfig: MCPConfig = settings?.MCP_CONFIG || {
const mcpConfig: MCPConfig = settings?.mcp_config || {
sse_servers: [],
stdio_servers: [],
shttp_servers: [],

View File

@ -106,16 +106,16 @@ export default function MainApp() {
React.useEffect(() => {
// Don't change language when on TOS page
if (!isOnTosPage && settings?.LANGUAGE) {
i18n.changeLanguage(settings.LANGUAGE);
if (!isOnTosPage && settings?.language) {
i18n.changeLanguage(settings.language);
}
}, [settings?.LANGUAGE, isOnTosPage]);
}, [settings?.language, isOnTosPage]);
React.useEffect(() => {
// Don't show consent form when on TOS page
if (!isOnTosPage) {
const consentFormModalIsOpen =
settings?.USER_CONSENTS_TO_ANALYTICS === null;
settings?.user_consents_to_analytics === null;
setConsentFormIsOpen(consentFormModalIsOpen);
}
@ -134,10 +134,10 @@ export default function MainApp() {
}, [isOnTosPage]);
React.useEffect(() => {
if (settings?.IS_NEW_USER && config.data?.APP_MODE === "saas") {
if (settings?.is_new_user && config.data?.APP_MODE === "saas") {
displaySuccessToast(t(I18nKey.BILLING$YOURE_IN));
}
}, [settings?.IS_NEW_USER, config.data?.APP_MODE]);
}, [settings?.is_new_user, config.data?.APP_MODE]);
React.useEffect(() => {
// Don't do any redirects when on TOS page
@ -249,7 +249,7 @@ export default function MainApp() {
{config.data?.FEATURE_FLAGS.ENABLE_BILLING &&
config.data?.APP_MODE === "saas" &&
settings?.IS_NEW_USER && <SetupPaymentModal />}
settings?.is_new_user && <SetupPaymentModal />}
</div>
);
}

View File

@ -122,12 +122,12 @@ function UserSettingsScreen() {
const prevVerificationStatusRef = useRef<boolean | undefined>(undefined);
useEffect(() => {
if (settings?.EMAIL) {
setEmail(settings.EMAIL);
setOriginalEmail(settings.EMAIL);
setIsEmailValid(EMAIL_REGEX.test(settings.EMAIL));
if (settings?.email) {
setEmail(settings.email);
setOriginalEmail(settings.email);
setIsEmailValid(EMAIL_REGEX.test(settings.email));
}
}, [settings?.EMAIL]);
}, [settings?.email]);
useEffect(() => {
if (pollingIntervalRef.current) {
@ -137,7 +137,7 @@ function UserSettingsScreen() {
if (
prevVerificationStatusRef.current === false &&
settings?.EMAIL_VERIFIED === true
settings?.email_verified === true
) {
// Display toast notification instead of setting state
displaySuccessToast(t("SETTINGS$EMAIL_VERIFIED_SUCCESSFULLY"));
@ -146,9 +146,9 @@ function UserSettingsScreen() {
}, 2000);
}
prevVerificationStatusRef.current = settings?.EMAIL_VERIFIED;
prevVerificationStatusRef.current = settings?.email_verified;
if (settings?.EMAIL_VERIFIED === false) {
if (settings?.email_verified === false) {
pollingIntervalRef.current = window.setInterval(() => {
refetch();
}, 5000);
@ -160,7 +160,7 @@ function UserSettingsScreen() {
pollingIntervalRef.current = null;
}
};
}, [settings?.EMAIL_VERIFIED, refetch, queryClient, t]);
}, [settings?.email_verified, refetch, queryClient, t]);
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newEmail = e.target.value;
@ -215,10 +215,10 @@ function UserSettingsScreen() {
isSaving={isSaving}
isResendingVerification={isResendingVerification}
isEmailChanged={isEmailChanged}
emailVerified={settings?.EMAIL_VERIFIED}
emailVerified={settings?.email_verified}
isEmailValid={isEmailValid}
>
{settings?.EMAIL_VERIFIED === false && <VerificationAlert />}
{settings?.email_verified === false && <VerificationAlert />}
</EmailInputSection>
)}
</div>

View File

@ -3,35 +3,36 @@ import { Settings } from "#/types/settings";
export const LATEST_SETTINGS_VERSION = 5;
export const DEFAULT_SETTINGS: Settings = {
LLM_MODEL: "openhands/claude-sonnet-4-20250514",
LLM_BASE_URL: "",
AGENT: "CodeActAgent",
LANGUAGE: "en",
LLM_API_KEY_SET: false,
SEARCH_API_KEY_SET: false,
CONFIRMATION_MODE: false,
SECURITY_ANALYZER: "llm",
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
PROVIDER_TOKENS_SET: {},
ENABLE_DEFAULT_CONDENSER: true,
CONDENSER_MAX_SIZE: 120,
ENABLE_SOUND_NOTIFICATIONS: false,
USER_CONSENTS_TO_ANALYTICS: false,
ENABLE_PROACTIVE_CONVERSATION_STARTERS: false,
ENABLE_SOLVABILITY_ANALYSIS: false,
SEARCH_API_KEY: "",
IS_NEW_USER: true,
MAX_BUDGET_PER_TASK: null,
EMAIL: "",
EMAIL_VERIFIED: true, // Default to true to avoid restricting access unnecessarily
MCP_CONFIG: {
llm_model: "openhands/claude-sonnet-4-20250514",
llm_base_url: "",
agent: "CodeActAgent",
language: "en",
llm_api_key: null,
llm_api_key_set: false,
search_api_key_set: false,
confirmation_mode: false,
security_analyzer: "llm",
remote_runtime_resource_factor: 1,
provider_tokens_set: {},
enable_default_condenser: true,
condenser_max_size: 120,
enable_sound_notifications: false,
user_consents_to_analytics: false,
enable_proactive_conversation_starters: false,
enable_solvability_analysis: false,
search_api_key: "",
is_new_user: true,
max_budget_per_task: null,
email: "",
email_verified: true, // Default to true to avoid restricting access unnecessarily
mcp_config: {
sse_servers: [],
stdio_servers: [],
shttp_servers: [],
},
GIT_USER_NAME: "openhands",
GIT_USER_EMAIL: "openhands@all-hands.dev",
V1_ENABLED: false,
git_user_name: "openhands",
git_user_email: "openhands@all-hands.dev",
v1_enabled: false,
};
/**

View File

@ -38,37 +38,31 @@ export type MCPConfig = {
};
export type Settings = {
LLM_MODEL: string;
LLM_BASE_URL: string;
AGENT: string;
LANGUAGE: string;
LLM_API_KEY_SET: boolean;
SEARCH_API_KEY_SET: boolean;
CONFIRMATION_MODE: boolean;
SECURITY_ANALYZER: string | null;
REMOTE_RUNTIME_RESOURCE_FACTOR: number | null;
PROVIDER_TOKENS_SET: Partial<Record<Provider, string | null>>;
ENABLE_DEFAULT_CONDENSER: boolean;
llm_model: string;
llm_base_url: string;
agent: string;
language: string;
llm_api_key: string | null;
llm_api_key_set: boolean;
search_api_key_set: boolean;
confirmation_mode: boolean;
security_analyzer: string | null;
remote_runtime_resource_factor: number | null;
provider_tokens_set: Partial<Record<Provider, string | null>>;
enable_default_condenser: boolean;
// Maximum number of events before the condenser runs
CONDENSER_MAX_SIZE: number | null;
ENABLE_SOUND_NOTIFICATIONS: boolean;
ENABLE_PROACTIVE_CONVERSATION_STARTERS: boolean;
ENABLE_SOLVABILITY_ANALYSIS: boolean;
USER_CONSENTS_TO_ANALYTICS: boolean | null;
SEARCH_API_KEY?: string;
IS_NEW_USER?: boolean;
MCP_CONFIG?: MCPConfig;
MAX_BUDGET_PER_TASK: number | null;
EMAIL?: string;
EMAIL_VERIFIED?: boolean;
GIT_USER_NAME?: string;
GIT_USER_EMAIL?: string;
V1_ENABLED?: boolean;
};
export type PostSettings = Settings & {
condenser_max_size: number | null;
enable_sound_notifications: boolean;
enable_proactive_conversation_starters: boolean;
enable_solvability_analysis: boolean;
user_consents_to_analytics: boolean | null;
llm_api_key?: string | null;
search_api_key?: string;
is_new_user?: boolean;
mcp_config?: MCPConfig;
max_budget_per_task: number | null;
email?: string;
email_verified?: boolean;
git_user_name?: string;
git_user_email?: string;
v1_enabled?: boolean;
};

View File

@ -67,10 +67,10 @@ describe("extractSettings", () => {
// Verify that the model name case is preserved
const expectedModel = `${provider}/${model}`;
expect(settings.LLM_MODEL).toBe(expectedModel);
expect(settings.llm_model).toBe(expectedModel);
// Only test that it's not lowercased if the original has uppercase letters
if (expectedModel !== expectedModel.toLowerCase()) {
expect(settings.LLM_MODEL).not.toBe(expectedModel.toLowerCase());
expect(settings.llm_model).not.toBe(expectedModel.toLowerCase());
}
});
});
@ -85,7 +85,7 @@ describe("extractSettings", () => {
const settings = extractSettings(formData);
// Custom model should take precedence and preserve case
expect(settings.LLM_MODEL).toBe("Custom-Model-Name");
expect(settings.LLM_MODEL).not.toBe("custom-model-name");
expect(settings.llm_model).toBe("Custom-Model-Name");
expect(settings.llm_model).not.toBe("custom-model-name");
});
});

View File

@ -3,4 +3,4 @@ import { Settings } from "#/types/settings";
export const hasAdvancedSettingsSet = (settings: Partial<Settings>): boolean =>
Object.keys(settings).length > 0 &&
(!!settings.LLM_BASE_URL || settings.AGENT !== DEFAULT_SETTINGS.AGENT);
(!!settings.llm_base_url || settings.agent !== DEFAULT_SETTINGS.agent);

View File

@ -67,9 +67,7 @@ export const parseMaxBudgetPerTask = (value: string): number | null => {
: null;
};
export const extractSettings = (
formData: FormData,
): Partial<Settings> & { llm_api_key?: string | null } => {
export const extractSettings = (formData: FormData): Partial<Settings> => {
const { LLM_MODEL, LLM_API_KEY, AGENT, LANGUAGE } =
extractBasicFormData(formData);
@ -82,14 +80,14 @@ export const extractSettings = (
} = extractAdvancedFormData(formData);
return {
LLM_MODEL: CUSTOM_LLM_MODEL || LLM_MODEL,
LLM_API_KEY_SET: !!LLM_API_KEY,
AGENT,
LANGUAGE,
LLM_BASE_URL,
CONFIRMATION_MODE,
SECURITY_ANALYZER,
ENABLE_DEFAULT_CONDENSER,
llm_model: CUSTOM_LLM_MODEL || LLM_MODEL,
llm_api_key_set: !!LLM_API_KEY,
agent: AGENT,
language: LANGUAGE,
llm_base_url: LLM_BASE_URL,
confirmation_mode: CONFIRMATION_MODE,
security_analyzer: SECURITY_ANALYZER,
enable_default_condenser: ENABLE_DEFAULT_CONDENSER,
llm_api_key: LLM_API_KEY,
};
};