mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
refactor: add General/Security pages, remove SDK_LEGACY_FIELD_MAP, fix inferInitialView
- Add /settings/general and /settings/security sidebar pages rendering their respective SDK schema sections - Reorder nav: General above LLM, Security below LLM (both SAAS + OSS) - Remove SDK_LEGACY_FIELD_MAP and all legacy field bridging — the only canonical store for SDK settings is now sdk_settings_values - Simplify to_agent_settings(), _extract_sdk_settings_values(), and _apply_settings_payload() to read/write sdk_settings_values only - Fix inferInitialView to accept an optional schemaOverride so SdkSectionPage passes filteredSchema (prevents cross-section minor-value overrides from elevating the view tier on unrelated pages) - Add SETTINGS$NAV_GENERAL and SETTINGS$NAV_SECURITY i18n keys with translations for all 14 languages - Use lock.svg for Security icon and settings.svg for General icon Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -139,8 +139,8 @@ export function SdkSectionPage({
|
||||
if (!settings?.sdk_settings_schema) return;
|
||||
setValues(buildInitialSettingsFormValues(settings));
|
||||
setDirty({});
|
||||
setView(inferInitialView(settings));
|
||||
}, [settings]);
|
||||
setView(inferInitialView(settings, filteredSchema));
|
||||
}, [settings, filteredSchema]);
|
||||
|
||||
const visibleSections = React.useMemo(() => {
|
||||
if (!filteredSchema) return [];
|
||||
|
||||
@@ -2,9 +2,11 @@ import { FiUsers, FiBriefcase } from "react-icons/fi";
|
||||
import CreditCardIcon from "#/icons/credit-card.svg?react";
|
||||
import KeyIcon from "#/icons/key.svg?react";
|
||||
import LightbulbIcon from "#/icons/lightbulb.svg?react";
|
||||
import LockIcon from "#/icons/lock.svg?react";
|
||||
import MemoryIcon from "#/icons/memory_icon.svg?react";
|
||||
import ServerProcessIcon from "#/icons/server-process.svg?react";
|
||||
import SettingsGearIcon from "#/icons/settings-gear.svg?react";
|
||||
import SettingsIcon from "#/icons/settings.svg?react";
|
||||
import CircuitIcon from "#/icons/u-circuit.svg?react";
|
||||
import PuzzlePieceIcon from "#/icons/u-puzzle-piece.svg?react";
|
||||
import UserIcon from "#/icons/user.svg?react";
|
||||
@@ -31,11 +33,21 @@ export const SAAS_NAV_ITEMS: SettingsNavItem[] = [
|
||||
to: "/settings/app",
|
||||
text: "SETTINGS$NAV_APPLICATION",
|
||||
},
|
||||
{
|
||||
icon: <SettingsIcon width={22} height={22} />,
|
||||
to: "/settings/general",
|
||||
text: "SETTINGS$NAV_GENERAL",
|
||||
},
|
||||
{
|
||||
icon: <CircuitIcon width={22} height={22} />,
|
||||
to: "/settings",
|
||||
text: "COMMON$LANGUAGE_MODEL_LLM",
|
||||
},
|
||||
{
|
||||
icon: <LockIcon width={22} height={22} />,
|
||||
to: "/settings/security",
|
||||
text: "SETTINGS$NAV_SECURITY",
|
||||
},
|
||||
{
|
||||
icon: <MemoryIcon width={22} height={22} />,
|
||||
to: "/settings/condenser",
|
||||
@@ -79,11 +91,21 @@ export const SAAS_NAV_ITEMS: SettingsNavItem[] = [
|
||||
];
|
||||
|
||||
export const OSS_NAV_ITEMS: SettingsNavItem[] = [
|
||||
{
|
||||
icon: <SettingsIcon width={22} height={22} />,
|
||||
to: "/settings/general",
|
||||
text: "SETTINGS$NAV_GENERAL",
|
||||
},
|
||||
{
|
||||
icon: <CircuitIcon width={22} height={22} />,
|
||||
to: "/settings",
|
||||
text: "SETTINGS$NAV_LLM",
|
||||
},
|
||||
{
|
||||
icon: <LockIcon width={22} height={22} />,
|
||||
to: "/settings/security",
|
||||
text: "SETTINGS$NAV_SECURITY",
|
||||
},
|
||||
{
|
||||
icon: <MemoryIcon width={22} height={22} />,
|
||||
to: "/settings/condenser",
|
||||
|
||||
@@ -137,7 +137,9 @@ export enum I18nKey {
|
||||
SETTINGS$GITLAB = "SETTINGS$GITLAB",
|
||||
SETTINGS$NAV_CONDENSER = "SETTINGS$NAV_CONDENSER",
|
||||
SETTINGS$NAV_CRITIC = "SETTINGS$NAV_CRITIC",
|
||||
SETTINGS$NAV_GENERAL = "SETTINGS$NAV_GENERAL",
|
||||
SETTINGS$NAV_LLM = "SETTINGS$NAV_LLM",
|
||||
SETTINGS$NAV_SECURITY = "SETTINGS$NAV_SECURITY",
|
||||
GIT$MERGE_REQUEST = "GIT$MERGE_REQUEST",
|
||||
GIT$GITLAB_API = "GIT$GITLAB_API",
|
||||
GIT$PULL_REQUEST = "GIT$PULL_REQUEST",
|
||||
|
||||
@@ -2191,6 +2191,22 @@
|
||||
"de": "Kritiker",
|
||||
"uk": "Критик"
|
||||
},
|
||||
"SETTINGS$NAV_GENERAL": {
|
||||
"en": "General",
|
||||
"ja": "一般",
|
||||
"zh-CN": "通用",
|
||||
"zh-TW": "通用",
|
||||
"ko-KR": "일반",
|
||||
"no": "Generelt",
|
||||
"it": "Generale",
|
||||
"pt": "Geral",
|
||||
"es": "General",
|
||||
"ar": "عام",
|
||||
"fr": "Général",
|
||||
"tr": "Genel",
|
||||
"de": "Allgemein",
|
||||
"uk": "Загальні"
|
||||
},
|
||||
"SETTINGS$NAV_LLM": {
|
||||
"en": "LLM",
|
||||
"ja": "LLM",
|
||||
@@ -2207,6 +2223,22 @@
|
||||
"de": "LLM",
|
||||
"uk": "LLM"
|
||||
},
|
||||
"SETTINGS$NAV_SECURITY": {
|
||||
"en": "Security",
|
||||
"ja": "セキュリティ",
|
||||
"zh-CN": "安全",
|
||||
"zh-TW": "安全",
|
||||
"ko-KR": "보안",
|
||||
"no": "Sikkerhet",
|
||||
"it": "Sicurezza",
|
||||
"pt": "Segurança",
|
||||
"es": "Seguridad",
|
||||
"ar": "الأمان",
|
||||
"fr": "Sécurité",
|
||||
"tr": "Güvenlik",
|
||||
"de": "Sicherheit",
|
||||
"uk": "Безпека"
|
||||
},
|
||||
"GIT$MERGE_REQUEST": {
|
||||
"en": "Merge Request",
|
||||
"ja": "マージリクエスト",
|
||||
|
||||
@@ -13,6 +13,8 @@ export default [
|
||||
route("accept-tos", "routes/accept-tos.tsx"),
|
||||
route("settings", "routes/settings.tsx", [
|
||||
index("routes/llm-settings.tsx"),
|
||||
route("general", "routes/general-settings.tsx"),
|
||||
route("security", "routes/security-settings.tsx"),
|
||||
route("condenser", "routes/condenser-settings.tsx"),
|
||||
route("critic", "routes/critic-settings.tsx"),
|
||||
route("mcp", "routes/mcp-settings.tsx"),
|
||||
|
||||
12
frontend/src/routes/general-settings.tsx
Normal file
12
frontend/src/routes/general-settings.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { SdkSectionPage } from "#/components/features/settings/sdk-settings/sdk-section-page";
|
||||
|
||||
function GeneralSettingsScreen() {
|
||||
return (
|
||||
<SdkSectionPage
|
||||
sectionKeys={["general"]}
|
||||
testId="general-settings-screen"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default GeneralSettingsScreen;
|
||||
12
frontend/src/routes/security-settings.tsx
Normal file
12
frontend/src/routes/security-settings.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { SdkSectionPage } from "#/components/features/settings/sdk-settings/sdk-section-page";
|
||||
|
||||
function SecuritySettingsScreen() {
|
||||
return (
|
||||
<SdkSectionPage
|
||||
sectionKeys={["security"]}
|
||||
testId="security-settings-screen"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SecuritySettingsScreen;
|
||||
@@ -156,8 +156,11 @@ export function buildInitialSettingsFormValues(
|
||||
|
||||
/** Determine which view tier to default to based on whether the user has
|
||||
* overridden any non-critical settings. */
|
||||
export function inferInitialView(settings: Settings): SettingsView {
|
||||
const schema = settings.sdk_settings_schema;
|
||||
export function inferInitialView(
|
||||
settings: Settings,
|
||||
schemaOverride?: SettingsSchema | null,
|
||||
): SettingsView {
|
||||
const schema = schemaOverride ?? settings.sdk_settings_schema;
|
||||
if (!schema) {
|
||||
return "basic";
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ from openhands.server.user_auth import (
|
||||
get_user_settings,
|
||||
get_user_settings_store,
|
||||
)
|
||||
from openhands.storage.data_models.settings import SDK_LEGACY_FIELD_MAP, Settings
|
||||
from openhands.storage.data_models.settings import Settings
|
||||
from openhands.storage.secrets.secrets_store import SecretsStore
|
||||
from openhands.storage.settings.settings_store import SettingsStore
|
||||
from openhands.utils.llm import get_provider_api_base, is_openhands_model
|
||||
@@ -82,21 +82,8 @@ def _extract_sdk_settings_values(
|
||||
settings: Settings, schema: dict[str, Any] | None
|
||||
) -> dict[str, Any]:
|
||||
values = dict(settings.sdk_settings_values)
|
||||
secret_field_keys = _get_sdk_secret_field_keys(schema)
|
||||
|
||||
for field_key in _get_sdk_field_keys(schema):
|
||||
if field_key in secret_field_keys:
|
||||
values[field_key] = None
|
||||
continue
|
||||
if field_key in values:
|
||||
continue
|
||||
|
||||
legacy_field = SDK_LEGACY_FIELD_MAP.get(field_key)
|
||||
if legacy_field is None or legacy_field not in Settings.model_fields:
|
||||
continue
|
||||
|
||||
values[field_key] = getattr(settings, legacy_field)
|
||||
|
||||
for field_key in _get_sdk_secret_field_keys(schema):
|
||||
values[field_key] = None
|
||||
return values
|
||||
|
||||
|
||||
@@ -112,11 +99,8 @@ def _apply_settings_payload(
|
||||
sdk_settings_values = dict(settings.sdk_settings_values)
|
||||
|
||||
for key, value in payload.items():
|
||||
legacy_field = SDK_LEGACY_FIELD_MAP.get(key)
|
||||
if key in Settings.model_fields:
|
||||
setattr(settings, key, value)
|
||||
elif legacy_field in Settings.model_fields:
|
||||
setattr(settings, legacy_field, value)
|
||||
|
||||
if key in sdk_field_keys and key not in secret_field_keys:
|
||||
sdk_settings_values[key] = value
|
||||
|
||||
@@ -20,14 +20,6 @@ from openhands.core.config.utils import load_openhands_config
|
||||
from openhands.sdk.settings import AgentSettings
|
||||
from openhands.storage.data_models.secrets import Secrets
|
||||
|
||||
SDK_LEGACY_FIELD_MAP: dict[str, str] = {
|
||||
'llm.model': 'llm_model',
|
||||
'llm.api_key': 'llm_api_key',
|
||||
'llm.base_url': 'llm_base_url',
|
||||
'condenser.enabled': 'enable_default_condenser',
|
||||
'condenser.max_size': 'condenser_max_size',
|
||||
}
|
||||
|
||||
|
||||
def _assign_dotted_value(target: dict[str, Any], dotted_key: str, value: Any) -> None:
|
||||
current = target
|
||||
@@ -227,23 +219,8 @@ class Settings(BaseModel):
|
||||
return self
|
||||
|
||||
def to_agent_settings(self) -> AgentSettings:
|
||||
"""Build SDK AgentSettings from persisted OpenHands settings.
|
||||
|
||||
Values stored in ``sdk_settings_values`` take precedence. Legacy flat fields
|
||||
are used as a fallback so older stored settings continue to work.
|
||||
"""
|
||||
"""Build SDK ``AgentSettings`` from persisted ``sdk_settings_values``."""
|
||||
payload: dict[str, Any] = {}
|
||||
sdk_values = dict(self.sdk_settings_values)
|
||||
|
||||
for key, value in sdk_values.items():
|
||||
for key, value in self.sdk_settings_values.items():
|
||||
_assign_dotted_value(payload, key, value)
|
||||
|
||||
for key, legacy_field in SDK_LEGACY_FIELD_MAP.items():
|
||||
if key in sdk_values:
|
||||
continue
|
||||
legacy_value = getattr(self, legacy_field)
|
||||
if legacy_value is None:
|
||||
continue
|
||||
_assign_dotted_value(payload, key, legacy_value)
|
||||
|
||||
return AgentSettings.model_validate(payload)
|
||||
|
||||
@@ -134,14 +134,13 @@ class TestLiveStatusAppConversationService:
|
||||
self.service._load_hooks_from_workspace = AsyncMock(return_value=None)
|
||||
|
||||
def _mock_user_to_agent_settings(self) -> AgentSettings:
|
||||
return Settings(
|
||||
llm_model=self.mock_user.llm_model,
|
||||
llm_api_key=self.mock_user.llm_api_key,
|
||||
llm_base_url=self.mock_user.llm_base_url,
|
||||
enable_default_condenser=True,
|
||||
condenser_max_size=self.mock_user.condenser_max_size,
|
||||
sdk_settings_values=dict(self.mock_user.sdk_settings_values),
|
||||
).to_agent_settings()
|
||||
sdk_values = dict(self.mock_user.sdk_settings_values)
|
||||
sdk_values.setdefault('llm.model', self.mock_user.llm_model or '')
|
||||
if self.mock_user.llm_api_key:
|
||||
sdk_values.setdefault('llm.api_key', self.mock_user.llm_api_key)
|
||||
if self.mock_user.llm_base_url:
|
||||
sdk_values.setdefault('llm.base_url', self.mock_user.llm_base_url)
|
||||
return Settings(sdk_settings_values=sdk_values).to_agent_settings()
|
||||
|
||||
def test_apply_suggested_task_sets_prompt_and_trigger(self):
|
||||
"""Test suggested task prompts populate initial message and trigger."""
|
||||
@@ -2472,14 +2471,13 @@ class TestPluginHandling:
|
||||
self.mock_sandbox.status = SandboxStatus.RUNNING
|
||||
|
||||
def _mock_user_to_agent_settings(self) -> AgentSettings:
|
||||
return Settings(
|
||||
llm_model=self.mock_user.llm_model,
|
||||
llm_api_key=self.mock_user.llm_api_key,
|
||||
llm_base_url=self.mock_user.llm_base_url,
|
||||
enable_default_condenser=True,
|
||||
condenser_max_size=self.mock_user.condenser_max_size,
|
||||
sdk_settings_values=dict(self.mock_user.sdk_settings_values),
|
||||
).to_agent_settings()
|
||||
sdk_values = dict(self.mock_user.sdk_settings_values)
|
||||
sdk_values.setdefault('llm.model', self.mock_user.llm_model or '')
|
||||
if self.mock_user.llm_api_key:
|
||||
sdk_values.setdefault('llm.api_key', self.mock_user.llm_api_key)
|
||||
if self.mock_user.llm_base_url:
|
||||
sdk_values.setdefault('llm.base_url', self.mock_user.llm_base_url)
|
||||
return Settings(sdk_settings_values=sdk_values).to_agent_settings()
|
||||
|
||||
def test_construct_initial_message_with_plugin_params_no_plugins(self):
|
||||
"""Test _construct_initial_message_with_plugin_params with no plugins returns original message."""
|
||||
|
||||
@@ -108,17 +108,14 @@ def test_settings_preserve_sdk_settings_values():
|
||||
}
|
||||
|
||||
|
||||
def test_settings_to_agent_settings_prefers_sdk_values_and_legacy_fallbacks():
|
||||
def test_settings_to_agent_settings_uses_sdk_values():
|
||||
settings = Settings(
|
||||
llm_model='legacy-model',
|
||||
llm_api_key='legacy-key',
|
||||
llm_base_url='https://legacy.example.com',
|
||||
enable_default_condenser=True,
|
||||
condenser_max_size=88,
|
||||
sdk_settings_values={
|
||||
'llm.model': 'sdk-model',
|
||||
'llm.base_url': 'https://sdk.example.com',
|
||||
'llm.litellm_extra_body': {'metadata': {'tier': 'enterprise'}},
|
||||
'condenser.enabled': False,
|
||||
'condenser.max_size': 88,
|
||||
'critic.enabled': True,
|
||||
'critic.mode': 'all_actions',
|
||||
},
|
||||
@@ -127,8 +124,7 @@ def test_settings_to_agent_settings_prefers_sdk_values_and_legacy_fallbacks():
|
||||
agent_settings = settings.to_agent_settings()
|
||||
|
||||
assert agent_settings.llm.model == 'sdk-model'
|
||||
assert agent_settings.llm.api_key.get_secret_value() == 'legacy-key'
|
||||
assert agent_settings.llm.base_url == 'https://legacy.example.com'
|
||||
assert agent_settings.llm.base_url == 'https://sdk.example.com'
|
||||
assert agent_settings.llm.litellm_extra_body == {'metadata': {'tier': 'enterprise'}}
|
||||
assert agent_settings.condenser.enabled is False
|
||||
assert agent_settings.condenser.max_size == 88
|
||||
|
||||
Reference in New Issue
Block a user