feat: add marketplace_path setting to GUI (local and SaaS)

Adds a configurable marketplace_path setting that controls which marketplace
is used when loading public skills. Users can:
- Use the default marketplace (marketplaces/default.json) - default behavior
- Specify a custom marketplace path
- Leave empty to load all skills without marketplace filtering

Changes:
- Added marketplace_path field to Settings data model (backend)
- Added marketplace_path to frontend Settings type
- Added DEFAULT_MARKETPLACE_PATH constant to frontend settings service
- Added Skills Settings section in App Settings page with marketplace_path input

This applies to both Local GUI and SaaS GUI as they share the same frontend code.

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
openhands
2026-03-01 18:49:21 +00:00
parent 4a3a42c858
commit d14e858040
4 changed files with 55 additions and 2 deletions

View File

@@ -4,7 +4,7 @@ import { usePostHog } from "posthog-js/react";
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
import { useSettings } from "#/hooks/query/use-settings";
import { AvailableLanguages } from "#/i18n";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { DEFAULT_SETTINGS, DEFAULT_MARKETPLACE_PATH } from "#/services/settings";
import { BrandButton } from "#/components/features/settings/brand-button";
import { SettingsSwitch } from "#/components/features/settings/settings-switch";
import { SettingsInput } from "#/components/features/settings/settings-input";
@@ -50,6 +50,8 @@ function AppSettingsScreen() {
React.useState(false);
const [gitUserEmailHasChanged, setGitUserEmailHasChanged] =
React.useState(false);
const [marketplacePathHasChanged, setMarketplacePathHasChanged] =
React.useState(false);
const formAction = (formData: FormData) => {
const languageLabel = formData.get("language-input")?.toString();
@@ -82,6 +84,14 @@ function AppSettingsScreen() {
formData.get("git-user-email-input")?.toString() ||
DEFAULT_SETTINGS.git_user_email;
const marketplacePathValue = formData
.get("marketplace-path-input")
?.toString();
// Empty string means no marketplace filtering, null means use default
const marketplacePath = marketplacePathValue === ""
? null
: (marketplacePathValue || DEFAULT_MARKETPLACE_PATH);
saveSettings(
{
language,
@@ -92,6 +102,7 @@ function AppSettingsScreen() {
max_budget_per_task: maxBudgetPerTask,
git_user_name: gitUserName,
git_user_email: gitUserEmail,
marketplace_path: marketplacePath,
},
{
onSuccess: () => {
@@ -110,6 +121,7 @@ function AppSettingsScreen() {
setMaxBudgetPerTaskHasChanged(false);
setGitUserNameHasChanged(false);
setGitUserEmailHasChanged(false);
setMarketplacePathHasChanged(false);
},
},
);
@@ -170,6 +182,13 @@ function AppSettingsScreen() {
setGitUserEmailHasChanged(value !== currentValue);
};
const checkIfMarketplacePathHasChanged = (value: string) => {
const currentValue = settings?.marketplace_path || DEFAULT_MARKETPLACE_PATH;
// Empty string means no marketplace filtering
const newValue = value === "" ? null : value;
setMarketplacePathHasChanged(newValue !== currentValue);
};
const formIsClean =
!languageInputHasChanged &&
!analyticsSwitchHasChanged &&
@@ -178,7 +197,8 @@ function AppSettingsScreen() {
!solvabilityAnalysisSwitchHasChanged &&
!maxBudgetPerTaskHasChanged &&
!gitUserNameHasChanged &&
!gitUserEmailHasChanged;
!gitUserEmailHasChanged &&
!marketplacePathHasChanged;
const shouldBeLoading = !settings || isLoading || isPending;
@@ -284,6 +304,31 @@ function AppSettingsScreen() {
/>
</div>
</div>
<div className="border-t border-t-tertiary pt-6 mt-2">
<h3 className="text-lg font-medium mb-2">
Skills Settings
</h3>
<p className="text-xs mb-4">
Configure which skills are loaded from the public skills marketplace.
</p>
<div className="flex flex-col gap-6">
<SettingsInput
testId="marketplace-path-input"
name="marketplace-path-input"
type="text"
label="Marketplace Path"
defaultValue={settings.marketplace_path || DEFAULT_MARKETPLACE_PATH}
onChange={checkIfMarketplacePathHasChanged}
placeholder={DEFAULT_MARKETPLACE_PATH}
className="w-full max-w-[680px]"
/>
<p className="text-xs text-gray-500">
Path to the marketplace JSON file in the public skills repository.
Leave empty to load all skills without marketplace filtering.
</p>
</div>
</div>
</div>
)}

View File

@@ -2,6 +2,8 @@ import { Settings } from "#/types/settings";
export const LATEST_SETTINGS_VERSION = 5;
export const DEFAULT_MARKETPLACE_PATH = "marketplaces/default.json";
export const DEFAULT_SETTINGS: Settings = {
llm_model: "openhands/claude-opus-4-5-20251101",
llm_base_url: "",
@@ -33,6 +35,7 @@ export const DEFAULT_SETTINGS: Settings = {
git_user_name: "openhands",
git_user_email: "openhands@all-hands.dev",
v1_enabled: false,
marketplace_path: DEFAULT_MARKETPLACE_PATH,
};
/**

View File

@@ -66,4 +66,6 @@ export type Settings = {
git_user_name?: string;
git_user_email?: string;
v1_enabled?: boolean;
// Path to the marketplace JSON file for public skills loading
marketplace_path?: string | null;
};

View File

@@ -54,6 +54,9 @@ class Settings(BaseModel):
git_user_name: str | None = None
git_user_email: str | None = None
v1_enabled: bool = True
# Path to the marketplace JSON file for public skills loading
# Defaults to 'marketplaces/default.json' in the public skills repository
marketplace_path: str | None = 'marketplaces/default.json'
model_config = ConfigDict(
validate_assignment=True,