mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-09 14:57:59 -05:00
Merge branch 'main' into reuse-running-sandboxes
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
53
frontend/__tests__/hooks/use-settings-nav-items.test.tsx
Normal file
53
frontend/__tests__/hooks/use-settings-nav-items.test.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { SAAS_NAV_ITEMS, OSS_NAV_ITEMS } from "#/constants/settings-nav";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import { useSettingsNavItems } from "#/hooks/use-settings-nav-items";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
const mockConfig = (appMode: "saas" | "oss", hideLlmSettings = false) => {
|
||||
vi.spyOn(OptionService, "getConfig").mockResolvedValue({
|
||||
APP_MODE: appMode,
|
||||
FEATURE_FLAGS: { HIDE_LLM_SETTINGS: hideLlmSettings },
|
||||
} as Awaited<ReturnType<typeof OptionService.getConfig>>);
|
||||
};
|
||||
|
||||
describe("useSettingsNavItems", () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
it("should return SAAS_NAV_ITEMS when APP_MODE is 'saas'", async () => {
|
||||
mockConfig("saas");
|
||||
const { result } = renderHook(() => useSettingsNavItems(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual(SAAS_NAV_ITEMS);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return OSS_NAV_ITEMS when APP_MODE is 'oss'", async () => {
|
||||
mockConfig("oss");
|
||||
const { result } = renderHook(() => useSettingsNavItems(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toEqual(OSS_NAV_ITEMS);
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter out '/settings' item when HIDE_LLM_SETTINGS feature flag is enabled", async () => {
|
||||
mockConfig("saas", true);
|
||||
const { result } = renderHook(() => useSettingsNavItems(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
result.current.find((item) => item.to === "/settings"),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
227
frontend/package-lock.json
generated
227
frontend/package-lock.json
generated
@@ -13,7 +13,7 @@
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.10.1",
|
||||
"@react-router/serve": "^7.10.1",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -28,13 +28,13 @@
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.32",
|
||||
"lucide-react": "^0.560.0",
|
||||
"lucide-react": "^0.561.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"posthog-js": "^1.302.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"posthog-js": "^1.306.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^16.4.0",
|
||||
"react-i18next": "^16.5.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.10.1",
|
||||
@@ -58,7 +58,7 @@
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^25.0.0",
|
||||
"@types/node": "^25.0.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
@@ -5431,10 +5431,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
|
||||
"integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==",
|
||||
"license": "MIT",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
|
||||
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
@@ -5442,40 +5441,38 @@
|
||||
"lightningcss": "1.30.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.17"
|
||||
"tailwindcss": "4.1.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz",
|
||||
"integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==",
|
||||
"license": "MIT",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
|
||||
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.17",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.17",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.17",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.17",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.17",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.17",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.17",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.17",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.17",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.17",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.17"
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.18",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.18",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.18",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.18",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.18",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz",
|
||||
"integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
|
||||
"integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
@@ -5485,13 +5482,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz",
|
||||
"integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
|
||||
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -5501,13 +5497,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz",
|
||||
"integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
|
||||
"integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -5517,13 +5512,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz",
|
||||
"integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
|
||||
"integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
@@ -5533,13 +5527,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz",
|
||||
"integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
|
||||
"integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -5549,13 +5542,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz",
|
||||
"integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
|
||||
"integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -5565,13 +5557,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz",
|
||||
"integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
|
||||
"integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -5581,13 +5572,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz",
|
||||
"integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
|
||||
"integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -5597,13 +5587,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz",
|
||||
"integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
|
||||
"integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -5613,9 +5602,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz",
|
||||
"integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
|
||||
"integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
@@ -5627,13 +5616,12 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.6.0",
|
||||
"@emnapi/runtime": "^1.6.0",
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@emnapi/wasi-threads": "^1.1.0",
|
||||
"@napi-rs/wasm-runtime": "^1.0.7",
|
||||
"@napi-rs/wasm-runtime": "^1.1.0",
|
||||
"@tybys/wasm-util": "^0.10.1",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
@@ -5642,7 +5630,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.1",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -5652,7 +5640,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.1",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -5670,13 +5658,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.0.7",
|
||||
"version": "1.1.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.5.0",
|
||||
"@emnapi/runtime": "^1.5.0",
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
}
|
||||
},
|
||||
@@ -5696,13 +5684,12 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz",
|
||||
"integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
|
||||
"integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -5712,13 +5699,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz",
|
||||
"integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
|
||||
"integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -5741,14 +5727,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/vite": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz",
|
||||
"integrity": "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==",
|
||||
"license": "MIT",
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz",
|
||||
"integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==",
|
||||
"dependencies": {
|
||||
"@tailwindcss/node": "4.1.17",
|
||||
"@tailwindcss/oxide": "4.1.17",
|
||||
"tailwindcss": "4.1.17"
|
||||
"@tailwindcss/node": "4.1.18",
|
||||
"@tailwindcss/oxide": "4.1.18",
|
||||
"tailwindcss": "4.1.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.2.0 || ^6 || ^7"
|
||||
@@ -6006,9 +5991,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.0.tgz",
|
||||
"integrity": "sha512-rl78HwuZlaDIUSeUKkmogkhebA+8K1Hy7tddZuJ3D0xV8pZSfsYGTsliGUol1JPzu9EKnTxPC4L1fiWouStRew==",
|
||||
"version": "25.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.1.tgz",
|
||||
"integrity": "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
@@ -8333,10 +8318,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||
"license": "MIT",
|
||||
"version": "5.18.4",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz",
|
||||
"integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
@@ -10020,8 +10004,7 @@
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"license": "ISC"
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
|
||||
},
|
||||
"node_modules/graphemer": {
|
||||
"version": "1.4.0",
|
||||
@@ -11733,9 +11716,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.560.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.560.0.tgz",
|
||||
"integrity": "sha512-NwKoUA/aBShsdL8WE5lukV2F/tjHzQRlonQs7fkNGI1sCT0Ay4a9Ap3ST2clUUkcY+9eQ0pBe2hybTQd2fmyDA==",
|
||||
"version": "0.561.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.561.0.tgz",
|
||||
"integrity": "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
@@ -13676,10 +13659,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.304.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.304.0.tgz",
|
||||
"integrity": "sha512-revqoppmJ5y1Oa9iRUb3P8w1htfxZdrSAe+elSNMxvl7wxY62qWN7Q0kE5Sk81o1qLHa6drPhVKa/dppWOfSUw==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"version": "1.306.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.306.0.tgz",
|
||||
"integrity": "sha512-sjsy0El4HL6PgbyWiUF0CaKb2d1Q8okbSeT4eajan3QSvkWus6ygHQuW2l4lfvp6NLRQrIZKH/0sUanhASptUQ==",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.7.1",
|
||||
"core-js": "^3.38.1",
|
||||
@@ -13911,10 +13893,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
|
||||
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
|
||||
"license": "MIT",
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13960,15 +13941,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
|
||||
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
|
||||
"license": "MIT",
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.1"
|
||||
"react": "^19.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hot-toast": {
|
||||
@@ -13989,10 +13969,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "16.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.4.1.tgz",
|
||||
"integrity": "sha512-GzsYomxb1/uE7nlJm0e1qQ8f+W9I3Xirh9VoycZIahk6C8Pmv/9Fd0ek6zjf1FSgtGLElDGqwi/4FOHEGUbsEQ==",
|
||||
"license": "MIT",
|
||||
"version": "16.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.0.tgz",
|
||||
"integrity": "sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"html-parse-stringify": "^3.0.1",
|
||||
@@ -15578,16 +15557,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.17",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
||||
"integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
|
||||
"license": "MIT"
|
||||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.10.1",
|
||||
"@react-router/serve": "^7.10.1",
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@@ -27,13 +27,13 @@
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.32",
|
||||
"lucide-react": "^0.560.0",
|
||||
"lucide-react": "^0.561.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"posthog-js": "^1.302.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"posthog-js": "^1.306.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^16.4.0",
|
||||
"react-i18next": "^16.5.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.10.1",
|
||||
@@ -89,7 +89,7 @@
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^25.0.0",
|
||||
"@types/node": "^25.0.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
ConfirmationResponseRequest,
|
||||
ConfirmationResponseResponse,
|
||||
} from "./event-service.types";
|
||||
import { openHands } from "../open-hands-axios";
|
||||
|
||||
class EventService {
|
||||
/**
|
||||
@@ -38,11 +37,27 @@ class EventService {
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getEventCount(conversationId: string): Promise<number> {
|
||||
const params = new URLSearchParams();
|
||||
params.append("conversation_id__eq", conversationId);
|
||||
const { data } = await openHands.get<number>(
|
||||
`/api/v1/events/count?${params.toString()}`,
|
||||
/**
|
||||
* Get event count for a V1 conversation
|
||||
* @param conversationId The conversation ID
|
||||
* @param conversationUrl The conversation URL (e.g., "http://localhost:54928/api/conversations/...")
|
||||
* @param sessionApiKey Session API key for authentication (required for V1)
|
||||
* @returns The event count
|
||||
*/
|
||||
static async getEventCount(
|
||||
conversationId: string,
|
||||
conversationUrl: string,
|
||||
sessionApiKey?: string | null,
|
||||
): Promise<number> {
|
||||
// Build the runtime URL using the conversation URL
|
||||
const runtimeUrl = buildHttpBaseUrl(conversationUrl);
|
||||
|
||||
// Build session headers for authentication
|
||||
const headers = buildSessionHeaders(sessionApiKey);
|
||||
|
||||
const { data } = await axios.get<number>(
|
||||
`${runtimeUrl}/api/conversations/${conversationId}/events/count`,
|
||||
{ headers },
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 })[];
|
||||
};
|
||||
};
|
||||
@@ -9,7 +9,7 @@ function ConfirmationModeEnabled() {
|
||||
|
||||
const { data: settings } = useSettings();
|
||||
|
||||
if (!settings?.CONFIRMATION_MODE) {
|
||||
if (!settings?.confirmation_mode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export function TaskItem({ task }: TaskItemProps) {
|
||||
case "todo":
|
||||
return <CircleIcon className="w-4 h-4 text-[#ffffff]" />;
|
||||
case "in_progress":
|
||||
return <LoadingIcon className="w-4 h-4 text-[#ffffff]" />;
|
||||
return <LoadingIcon className="w-4 h-4 text-[#ffffff] animate-spin" />;
|
||||
case "done":
|
||||
return <CheckCircleIcon className="w-4 h-4 text-[#A3A3A3]" />;
|
||||
default:
|
||||
|
||||
@@ -5,11 +5,10 @@ import { ContextMenu } from "#/ui/context-menu";
|
||||
import { ContextMenuListItem } from "./context-menu-list-item";
|
||||
import { Divider } from "#/ui/divider";
|
||||
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import LogOutIcon from "#/icons/log-out.svg?react";
|
||||
import DocumentIcon from "#/icons/document.svg?react";
|
||||
import { SAAS_NAV_ITEMS, OSS_NAV_ITEMS } from "#/constants/settings-nav";
|
||||
import { useSettingsNavItems } from "#/hooks/use-settings-nav-items";
|
||||
|
||||
interface AccountSettingsContextMenuProps {
|
||||
onLogout: () => void;
|
||||
@@ -22,15 +21,8 @@ export function AccountSettingsContextMenu({
|
||||
}: AccountSettingsContextMenuProps) {
|
||||
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
|
||||
const { t } = useTranslation();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const isSaas = config?.APP_MODE === "saas";
|
||||
|
||||
// Get navigation items and filter out LLM settings if the feature flag is enabled
|
||||
let items = isSaas ? SAAS_NAV_ITEMS : OSS_NAV_ITEMS;
|
||||
if (config?.FEATURE_FLAGS?.HIDE_LLM_SETTINGS) {
|
||||
items = items.filter((item) => item.to !== "/settings");
|
||||
}
|
||||
const items = useSettingsNavItems();
|
||||
|
||||
const navItems = items.map((item) => ({
|
||||
...item,
|
||||
@@ -39,11 +31,7 @@ export function AccountSettingsContextMenu({
|
||||
height: 16,
|
||||
} as React.SVGProps<SVGSVGElement>),
|
||||
}));
|
||||
|
||||
const handleNavigationClick = () => {
|
||||
onClose();
|
||||
// The Link component will handle the actual navigation
|
||||
};
|
||||
const handleNavigationClick = () => onClose();
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
@@ -55,7 +43,7 @@ export function AccountSettingsContextMenu({
|
||||
{navItems.map(({ to, text, icon }) => (
|
||||
<Link key={to} to={to} className="text-decoration-none">
|
||||
<ContextMenuListItem
|
||||
onClick={() => handleNavigationClick()}
|
||||
onClick={handleNavigationClick}
|
||||
className="flex items-center gap-2 p-2 hover:bg-[#5C5D62] rounded h-[30px]"
|
||||
>
|
||||
{icon}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { MobileHeader } from "./mobile-header";
|
||||
import { SettingsNavigation } from "./settings-navigation";
|
||||
|
||||
interface NavigationItem {
|
||||
to: string;
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
}
|
||||
import { SettingsNavItem } from "#/constants/settings-nav";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
navigationItems: NavigationItem[];
|
||||
navigationItems: SettingsNavItem[];
|
||||
}
|
||||
|
||||
export function SettingsLayout({
|
||||
@@ -19,13 +14,8 @@ export function SettingsLayout({
|
||||
}: SettingsLayoutProps) {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
const toggleMobileMenu = () => {
|
||||
setIsMobileMenuOpen(!isMobileMenuOpen);
|
||||
};
|
||||
|
||||
const closeMobileMenu = () => {
|
||||
setIsMobileMenuOpen(false);
|
||||
};
|
||||
const toggleMobileMenu = () => setIsMobileMenuOpen(!isMobileMenuOpen);
|
||||
const closeMobileMenu = () => setIsMobileMenuOpen(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full px-[14px] pt-8">
|
||||
@@ -34,7 +24,6 @@ export function SettingsLayout({
|
||||
isMobileMenuOpen={isMobileMenuOpen}
|
||||
onToggleMenu={toggleMobileMenu}
|
||||
/>
|
||||
|
||||
{/* Desktop layout with navigation and main content */}
|
||||
<div className="flex flex-1 overflow-hidden gap-10">
|
||||
{/* Navigation */}
|
||||
@@ -43,7 +32,6 @@ export function SettingsLayout({
|
||||
onCloseMobileMenu={closeMobileMenu}
|
||||
navigationItems={navigationItems}
|
||||
/>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 overflow-auto custom-scrollbar-always">
|
||||
{children}
|
||||
|
||||
@@ -5,17 +5,12 @@ import { Typography } from "#/ui/typography";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import SettingsIcon from "#/icons/settings-gear.svg?react";
|
||||
import CloseIcon from "#/icons/close.svg?react";
|
||||
|
||||
interface NavigationItem {
|
||||
to: string;
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
}
|
||||
import { SettingsNavItem } from "#/constants/settings-nav";
|
||||
|
||||
interface SettingsNavigationProps {
|
||||
isMobileMenuOpen: boolean;
|
||||
onCloseMobileMenu: () => void;
|
||||
navigationItems: NavigationItem[];
|
||||
navigationItems: SettingsNavItem[];
|
||||
}
|
||||
|
||||
export function SettingsNavigation({
|
||||
@@ -34,7 +29,6 @@ export function SettingsNavigation({
|
||||
onClick={onCloseMobileMenu}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Navigation sidebar */}
|
||||
<nav
|
||||
data-testid="settings-navbar"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -20,9 +20,7 @@ export function TaskItem({ task }: TaskItemProps) {
|
||||
case "todo":
|
||||
return <CircleIcon className="w-4 h-4 text-[#ffffff]" />;
|
||||
case "in_progress":
|
||||
return (
|
||||
<LoadingIcon className="w-4 h-4 text-[#ffffff]" strokeWidth={0.5} />
|
||||
);
|
||||
return <LoadingIcon className="w-4 h-4 text-[#ffffff] animate-spin" />;
|
||||
case "done":
|
||||
return <CheckCircleIcon className="w-4 h-4 text-[#A3A3A3]" />;
|
||||
default:
|
||||
|
||||
@@ -578,9 +578,13 @@ export function ConversationWebSocketProvider({
|
||||
removeErrorMessage(); // Clear any previous error messages on successful connection
|
||||
|
||||
// Fetch expected event count for history loading detection
|
||||
if (conversationId) {
|
||||
if (conversationId && conversationUrl) {
|
||||
try {
|
||||
const count = await EventService.getEventCount(conversationId);
|
||||
const count = await EventService.getEventCount(
|
||||
conversationId,
|
||||
conversationUrl,
|
||||
sessionApiKey,
|
||||
);
|
||||
setExpectedEventCountMain(count);
|
||||
|
||||
// If no events expected, mark as loaded immediately
|
||||
@@ -618,6 +622,7 @@ export function ConversationWebSocketProvider({
|
||||
removeErrorMessage,
|
||||
sessionApiKey,
|
||||
conversationId,
|
||||
conversationUrl,
|
||||
]);
|
||||
|
||||
// Separate WebSocket options for planning agent connection
|
||||
@@ -642,10 +647,15 @@ export function ConversationWebSocketProvider({
|
||||
removeErrorMessage(); // Clear any previous error messages on successful connection
|
||||
|
||||
// Fetch expected event count for history loading detection
|
||||
if (planningAgentConversation?.id) {
|
||||
if (
|
||||
planningAgentConversation?.id &&
|
||||
planningAgentConversation.conversation_url
|
||||
) {
|
||||
try {
|
||||
const count = await EventService.getEventCount(
|
||||
planningAgentConversation.id,
|
||||
planningAgentConversation.conversation_url,
|
||||
planningAgentConversation.session_api_key,
|
||||
);
|
||||
setExpectedEventCountPlanning(count);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
15
frontend/src/hooks/use-settings-nav-items.ts
Normal file
15
frontend/src/hooks/use-settings-nav-items.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { SAAS_NAV_ITEMS, OSS_NAV_ITEMS } from "#/constants/settings-nav";
|
||||
|
||||
export function useSettingsNavItems() {
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const shouldHideLlmSettings = !!config?.FEATURE_FLAGS?.HIDE_LLM_SETTINGS;
|
||||
const isSaasMode = config?.APP_MODE === "saas";
|
||||
|
||||
const items = isSaasMode ? SAAS_NAV_ITEMS : OSS_NAV_ITEMS;
|
||||
|
||||
return shouldHideLlmSettings
|
||||
? items.filter((item) => item.to !== "/settings")
|
||||
: items;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="7" viewBox="0 0 16 7" fill="none">
|
||||
<path d="M7.50684 0.25C9.24918 0.25 10.9332 0.87774 12.251 2.01758C13.5688 3.15746 14.4327 4.73379 14.6836 6.45801L14.7256 6.74316H13.2129L13.1777 6.53516C12.9499 5.19635 12.2554 3.98161 11.2178 3.10547C10.1799 2.22925 8.86511 1.74805 7.50684 1.74805C6.14866 1.74811 4.83466 2.22931 3.79688 3.10547C2.75913 3.98161 2.06476 5.19628 1.83691 6.53516L1.80078 6.74316H0.289063L0.331055 6.45801C0.581982 4.73389 1.44504 3.15745 2.7627 2.01758C4.08041 0.877757 5.76455 0.250069 7.50684 0.25Z" fill="currentColor" stroke="currentColor" stroke-width="0.5"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M8 0.5C9.74234 0.5 11.4264 1.12774 12.7442 2.26758C14.062 3.40746 14.9259 4.98379 15.1768 6.70801L15.2188 6.99316H13.7061L13.6709 6.78516C13.4431 5.44635 12.7486 4.23161 11.711 3.35547C10.6731 2.47925 9.35827 1.99805 8 1.99805C6.64182 1.99811 5.32782 2.47931 4.29004 3.35547C3.25229 4.23161 2.55792 5.44628 2.33007 6.78516L2.29394 6.99316H0.782227L0.824219 6.70801C1.07515 4.98389 1.9382 3.40745 3.25586 2.26758C4.57357 1.12776 6.25771 0.500069 8 0.5Z" fill="currentColor" stroke="currentColor" stroke-width="0.5"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 630 B |
7
frontend/src/mocks/analytics-handlers.ts
Normal file
7
frontend/src/mocks/analytics-handlers.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { http, HttpResponse } from "msw";
|
||||
|
||||
export const ANALYTICS_HANDLERS = [
|
||||
http.post("https://us.i.posthog.com/e", async () =>
|
||||
HttpResponse.json(null, { status: 200 }),
|
||||
),
|
||||
];
|
||||
23
frontend/src/mocks/auth-handlers.ts
Normal file
23
frontend/src/mocks/auth-handlers.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { GitUser } from "#/types/git";
|
||||
|
||||
export const AUTH_HANDLERS = [
|
||||
http.get("/api/user/info", () => {
|
||||
const user: GitUser = {
|
||||
id: "1",
|
||||
login: "octocat",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/583231?v=4",
|
||||
company: "GitHub",
|
||||
email: "placeholder@placeholder.placeholder",
|
||||
name: "monalisa octocat",
|
||||
};
|
||||
|
||||
return HttpResponse.json(user);
|
||||
}),
|
||||
|
||||
http.post("/api/authenticate", async () =>
|
||||
HttpResponse.json({ message: "Authenticated" }),
|
||||
),
|
||||
|
||||
http.post("/api/logout", () => HttpResponse.json(null, { status: 200 })),
|
||||
];
|
||||
118
frontend/src/mocks/conversation-handlers.ts
Normal file
118
frontend/src/mocks/conversation-handlers.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { http, delay, HttpResponse } from "msw";
|
||||
import { Conversation, ResultSet } from "#/api/open-hands.types";
|
||||
|
||||
const conversations: Conversation[] = [
|
||||
{
|
||||
conversation_id: "1",
|
||||
title: "My New Project",
|
||||
selected_repository: null,
|
||||
git_provider: null,
|
||||
selected_branch: null,
|
||||
last_updated_at: new Date().toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
status: "RUNNING",
|
||||
runtime_status: "STATUS$READY",
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
{
|
||||
conversation_id: "2",
|
||||
title: "Repo Testing",
|
||||
selected_repository: "octocat/hello-world",
|
||||
git_provider: "github",
|
||||
selected_branch: null,
|
||||
last_updated_at: new Date(
|
||||
Date.now() - 2 * 24 * 60 * 60 * 1000,
|
||||
).toISOString(),
|
||||
created_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
status: "STOPPED",
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
{
|
||||
conversation_id: "3",
|
||||
title: "Another Project",
|
||||
selected_repository: "octocat/earth",
|
||||
git_provider: null,
|
||||
selected_branch: "main",
|
||||
last_updated_at: new Date(
|
||||
Date.now() - 5 * 24 * 60 * 60 * 1000,
|
||||
).toISOString(),
|
||||
created_at: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
status: "STOPPED",
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
];
|
||||
|
||||
const CONVERSATIONS = new Map<string, Conversation>(
|
||||
conversations.map((c) => [c.conversation_id, c]),
|
||||
);
|
||||
|
||||
export const CONVERSATION_HANDLERS = [
|
||||
http.get("/api/conversations", async () => {
|
||||
const values = Array.from(CONVERSATIONS.values());
|
||||
const results: ResultSet<Conversation> = {
|
||||
results: values,
|
||||
next_page_id: null,
|
||||
};
|
||||
return HttpResponse.json(results);
|
||||
}),
|
||||
|
||||
http.get("/api/conversations/:conversationId", async ({ params }) => {
|
||||
const conversationId = params.conversationId as string;
|
||||
const project = CONVERSATIONS.get(conversationId);
|
||||
if (project) return HttpResponse.json(project);
|
||||
return HttpResponse.json(null, { status: 404 });
|
||||
}),
|
||||
|
||||
http.post("/api/conversations", async () => {
|
||||
await delay();
|
||||
const conversation: Conversation = {
|
||||
conversation_id: (Math.random() * 100).toString(),
|
||||
title: "New Conversation",
|
||||
selected_repository: null,
|
||||
git_provider: null,
|
||||
selected_branch: null,
|
||||
last_updated_at: new Date().toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
status: "RUNNING",
|
||||
runtime_status: "STATUS$READY",
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
};
|
||||
CONVERSATIONS.set(conversation.conversation_id, conversation);
|
||||
return HttpResponse.json(conversation, { status: 201 });
|
||||
}),
|
||||
|
||||
http.patch(
|
||||
"/api/conversations/:conversationId",
|
||||
async ({ params, request }) => {
|
||||
const conversationId = params.conversationId as string;
|
||||
const conversation = CONVERSATIONS.get(conversationId);
|
||||
|
||||
if (conversation) {
|
||||
const body = await request.json();
|
||||
if (typeof body === "object" && body?.title) {
|
||||
CONVERSATIONS.set(conversationId, {
|
||||
...conversation,
|
||||
title: body.title,
|
||||
});
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}
|
||||
}
|
||||
return HttpResponse.json(null, { status: 404 });
|
||||
},
|
||||
),
|
||||
|
||||
http.delete("/api/conversations/:conversationId", async ({ params }) => {
|
||||
const conversationId = params.conversationId as string;
|
||||
if (CONVERSATIONS.has(conversationId)) {
|
||||
CONVERSATIONS.delete(conversationId);
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}
|
||||
return HttpResponse.json(null, { status: 404 });
|
||||
}),
|
||||
];
|
||||
15
frontend/src/mocks/feedback-handlers.ts
Normal file
15
frontend/src/mocks/feedback-handlers.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { http, delay, HttpResponse } from "msw";
|
||||
|
||||
export const FEEDBACK_HANDLERS = [
|
||||
http.post("/api/submit-feedback", async () => {
|
||||
await delay(1200);
|
||||
return HttpResponse.json({
|
||||
statusCode: 200,
|
||||
body: { message: "Success", link: "fake-url.com", password: "abc123" },
|
||||
});
|
||||
}),
|
||||
|
||||
http.post("/api/submit-feedback", async () =>
|
||||
HttpResponse.json({ statusCode: 200 }, { status: 200 }),
|
||||
),
|
||||
];
|
||||
@@ -1,146 +1,17 @@
|
||||
import { delay, http, HttpResponse } from "msw";
|
||||
import { GetConfigResponse } from "#/api/option-service/option.types";
|
||||
import { Conversation, ResultSet } from "#/api/open-hands.types";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { STRIPE_BILLING_HANDLERS } from "./billing-handlers";
|
||||
import { Provider } from "#/types/settings";
|
||||
import {
|
||||
ApiSettings,
|
||||
PostApiSettings,
|
||||
} from "#/api/settings-service/settings.types";
|
||||
import { FILE_SERVICE_HANDLERS } from "./file-service-handlers";
|
||||
import { GitUser } from "#/types/git";
|
||||
import { TASK_SUGGESTIONS_HANDLERS } from "./task-suggestions-handlers";
|
||||
import { SECRETS_HANDLERS } from "./secrets-handlers";
|
||||
import { GIT_REPOSITORY_HANDLERS } from "./git-repository-handlers";
|
||||
|
||||
export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
|
||||
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,
|
||||
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_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,
|
||||
};
|
||||
|
||||
const MOCK_USER_PREFERENCES: {
|
||||
settings: ApiSettings | PostApiSettings | null;
|
||||
} = {
|
||||
settings: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the user settings to the default settings
|
||||
*
|
||||
* Useful for resetting the settings in tests
|
||||
*/
|
||||
export const resetTestHandlersMockSettings = () => {
|
||||
MOCK_USER_PREFERENCES.settings = MOCK_DEFAULT_USER_SETTINGS;
|
||||
};
|
||||
|
||||
const conversations: Conversation[] = [
|
||||
{
|
||||
conversation_id: "1",
|
||||
title: "My New Project",
|
||||
selected_repository: null,
|
||||
git_provider: null,
|
||||
selected_branch: null,
|
||||
last_updated_at: new Date().toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
status: "RUNNING",
|
||||
runtime_status: "STATUS$READY",
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
{
|
||||
conversation_id: "2",
|
||||
title: "Repo Testing",
|
||||
selected_repository: "octocat/hello-world",
|
||||
git_provider: "github",
|
||||
selected_branch: null,
|
||||
// 2 days ago
|
||||
last_updated_at: new Date(
|
||||
Date.now() - 2 * 24 * 60 * 60 * 1000,
|
||||
).toISOString(),
|
||||
created_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
status: "STOPPED",
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
{
|
||||
conversation_id: "3",
|
||||
title: "Another Project",
|
||||
selected_repository: "octocat/earth",
|
||||
git_provider: null,
|
||||
selected_branch: "main",
|
||||
// 5 days ago
|
||||
last_updated_at: new Date(
|
||||
Date.now() - 5 * 24 * 60 * 60 * 1000,
|
||||
).toISOString(),
|
||||
created_at: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
status: "STOPPED",
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
];
|
||||
|
||||
const CONVERSATIONS = new Map<string, Conversation>(
|
||||
conversations.map((conversation) => [
|
||||
conversation.conversation_id,
|
||||
conversation,
|
||||
]),
|
||||
);
|
||||
|
||||
const openHandsHandlers = [
|
||||
http.get("/api/options/models", async () =>
|
||||
HttpResponse.json([
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
"anthropic/claude-3.5",
|
||||
"anthropic/claude-sonnet-4-20250514",
|
||||
"anthropic/claude-sonnet-4-5-20250929",
|
||||
"anthropic/claude-haiku-4-5-20251001",
|
||||
"openhands/claude-sonnet-4-20250514",
|
||||
"openhands/claude-sonnet-4-5-20250929",
|
||||
"openhands/claude-haiku-4-5-20251001",
|
||||
"sambanova/Meta-Llama-3.1-8B-Instruct",
|
||||
]),
|
||||
),
|
||||
|
||||
http.get("/api/options/agents", async () =>
|
||||
HttpResponse.json(["CodeActAgent", "CoActAgent"]),
|
||||
),
|
||||
|
||||
http.get("/api/options/security-analyzers", async () =>
|
||||
HttpResponse.json(["llm", "none"]),
|
||||
),
|
||||
|
||||
http.post("http://localhost:3001/api/submit-feedback", async () => {
|
||||
await delay(1200);
|
||||
|
||||
return HttpResponse.json({
|
||||
statusCode: 200,
|
||||
body: { message: "Success", link: "fake-url.com", password: "abc123" },
|
||||
});
|
||||
}),
|
||||
];
|
||||
import {
|
||||
SETTINGS_HANDLERS,
|
||||
MOCK_DEFAULT_USER_SETTINGS,
|
||||
resetTestHandlersMockSettings,
|
||||
} from "./settings-handlers";
|
||||
import { CONVERSATION_HANDLERS } from "./conversation-handlers";
|
||||
import { AUTH_HANDLERS } from "./auth-handlers";
|
||||
import { FEEDBACK_HANDLERS } from "./feedback-handlers";
|
||||
import { ANALYTICS_HANDLERS } from "./analytics-handlers";
|
||||
|
||||
export const handlers = [
|
||||
...STRIPE_BILLING_HANDLERS,
|
||||
@@ -148,192 +19,11 @@ export const handlers = [
|
||||
...TASK_SUGGESTIONS_HANDLERS,
|
||||
...SECRETS_HANDLERS,
|
||||
...GIT_REPOSITORY_HANDLERS,
|
||||
...openHandsHandlers,
|
||||
http.get("/api/user/info", () => {
|
||||
const user: GitUser = {
|
||||
id: "1",
|
||||
login: "octocat",
|
||||
avatar_url: "https://avatars.githubusercontent.com/u/583231?v=4",
|
||||
company: "GitHub",
|
||||
email: "placeholder@placeholder.placeholder",
|
||||
name: "monalisa octocat",
|
||||
};
|
||||
|
||||
return HttpResponse.json(user);
|
||||
}),
|
||||
http.post("http://localhost:3001/api/submit-feedback", async () =>
|
||||
HttpResponse.json({ statusCode: 200 }, { status: 200 }),
|
||||
),
|
||||
http.post("https://us.i.posthog.com/e", async () =>
|
||||
HttpResponse.json(null, { status: 200 }),
|
||||
),
|
||||
http.get("/api/options/config", () => {
|
||||
const mockSaas = import.meta.env.VITE_MOCK_SAAS === "true";
|
||||
|
||||
const config: GetConfigResponse = {
|
||||
APP_MODE: mockSaas ? "saas" : "oss",
|
||||
GITHUB_CLIENT_ID: "fake-github-client-id",
|
||||
POSTHOG_CLIENT_KEY: "fake-posthog-client-key",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: mockSaas,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
// Uncomment the following to test the maintenance banner
|
||||
// MAINTENANCE: {
|
||||
// startTime: "2024-01-15T10:00:00-05:00", // EST timestamp
|
||||
// },
|
||||
};
|
||||
|
||||
return HttpResponse.json(config);
|
||||
}),
|
||||
http.get("/api/settings", async () => {
|
||||
await delay();
|
||||
|
||||
const { settings } = MOCK_USER_PREFERENCES;
|
||||
|
||||
if (!settings) return HttpResponse.json(null, { status: 404 });
|
||||
|
||||
return HttpResponse.json(settings);
|
||||
}),
|
||||
http.post("/api/settings", async ({ request }) => {
|
||||
await delay();
|
||||
const body = await request.json();
|
||||
|
||||
if (body) {
|
||||
const current = MOCK_USER_PREFERENCES.settings || {
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
};
|
||||
// Persist new values over current/mock defaults
|
||||
MOCK_USER_PREFERENCES.settings = {
|
||||
...current,
|
||||
...(body as Partial<ApiSettings>),
|
||||
};
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}
|
||||
|
||||
return HttpResponse.json(null, { status: 400 });
|
||||
}),
|
||||
|
||||
http.post("/api/authenticate", async () =>
|
||||
HttpResponse.json({ message: "Authenticated" }),
|
||||
),
|
||||
|
||||
http.get("/api/conversations", async () => {
|
||||
const values = Array.from(CONVERSATIONS.values());
|
||||
const results: ResultSet<Conversation> = {
|
||||
results: values,
|
||||
next_page_id: null,
|
||||
};
|
||||
|
||||
return HttpResponse.json(results, { status: 200 });
|
||||
}),
|
||||
|
||||
http.delete("/api/conversations/:conversationId", async ({ params }) => {
|
||||
const { conversationId } = params;
|
||||
|
||||
if (typeof conversationId === "string") {
|
||||
CONVERSATIONS.delete(conversationId);
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}
|
||||
|
||||
return HttpResponse.json(null, { status: 404 });
|
||||
}),
|
||||
|
||||
http.patch(
|
||||
"/api/conversations/:conversationId",
|
||||
async ({ params, request }) => {
|
||||
const { conversationId } = params;
|
||||
|
||||
if (typeof conversationId === "string") {
|
||||
const conversation = CONVERSATIONS.get(conversationId);
|
||||
|
||||
if (conversation) {
|
||||
const body = await request.json();
|
||||
if (typeof body === "object" && body?.title) {
|
||||
CONVERSATIONS.set(conversationId, {
|
||||
...conversation,
|
||||
title: body.title,
|
||||
});
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return HttpResponse.json(null, { status: 404 });
|
||||
},
|
||||
),
|
||||
|
||||
http.post("/api/conversations", async () => {
|
||||
await delay();
|
||||
|
||||
const conversation: Conversation = {
|
||||
conversation_id: (Math.random() * 100).toString(),
|
||||
title: "New Conversation",
|
||||
selected_repository: null,
|
||||
git_provider: null,
|
||||
selected_branch: null,
|
||||
last_updated_at: new Date().toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
status: "RUNNING",
|
||||
runtime_status: "STATUS$READY",
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
};
|
||||
|
||||
CONVERSATIONS.set(conversation.conversation_id, conversation);
|
||||
return HttpResponse.json(conversation, { status: 201 });
|
||||
}),
|
||||
|
||||
http.get("/api/conversations/:conversationId", async ({ params }) => {
|
||||
const { conversationId } = params;
|
||||
|
||||
if (typeof conversationId === "string") {
|
||||
const project = CONVERSATIONS.get(conversationId);
|
||||
|
||||
if (project) {
|
||||
return HttpResponse.json(project, { status: 200 });
|
||||
}
|
||||
}
|
||||
|
||||
return HttpResponse.json(null, { status: 404 });
|
||||
}),
|
||||
|
||||
http.post("/api/logout", () => HttpResponse.json(null, { status: 200 })),
|
||||
|
||||
http.post("/api/reset-settings", async () => {
|
||||
await delay();
|
||||
MOCK_USER_PREFERENCES.settings = { ...MOCK_DEFAULT_USER_SETTINGS };
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}),
|
||||
|
||||
http.post("/api/add-git-providers", async ({ request }) => {
|
||||
const body = await request.json();
|
||||
|
||||
if (typeof body === "object" && body?.provider_tokens) {
|
||||
const rawTokens = body.provider_tokens as Record<
|
||||
string,
|
||||
{ token?: string }
|
||||
>;
|
||||
|
||||
const providerTokensSet: Partial<Record<Provider, string | null>> =
|
||||
Object.fromEntries(
|
||||
Object.entries(rawTokens)
|
||||
.filter(([, val]) => val && val.token)
|
||||
.map(([provider]) => [provider as Provider, ""]),
|
||||
);
|
||||
|
||||
const newSettings = {
|
||||
...(MOCK_USER_PREFERENCES.settings ?? MOCK_DEFAULT_USER_SETTINGS),
|
||||
provider_tokens_set: providerTokensSet,
|
||||
};
|
||||
MOCK_USER_PREFERENCES.settings = newSettings;
|
||||
|
||||
return HttpResponse.json(true, { status: 200 });
|
||||
}
|
||||
|
||||
return HttpResponse.json(null, { status: 400 });
|
||||
}),
|
||||
...SETTINGS_HANDLERS,
|
||||
...CONVERSATION_HANDLERS,
|
||||
...AUTH_HANDLERS,
|
||||
...FEEDBACK_HANDLERS,
|
||||
...ANALYTICS_HANDLERS,
|
||||
];
|
||||
|
||||
export { MOCK_DEFAULT_USER_SETTINGS, resetTestHandlersMockSettings };
|
||||
|
||||
151
frontend/src/mocks/settings-handlers.ts
Normal file
151
frontend/src/mocks/settings-handlers.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { http, delay, HttpResponse } from "msw";
|
||||
import { GetConfigResponse } from "#/api/option-service/option.types";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { Provider, Settings } from "#/types/settings";
|
||||
|
||||
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,
|
||||
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_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,
|
||||
};
|
||||
|
||||
const MOCK_USER_PREFERENCES: {
|
||||
settings: Settings | null;
|
||||
} = {
|
||||
settings: null,
|
||||
};
|
||||
|
||||
// Reset mock
|
||||
export const resetTestHandlersMockSettings = () => {
|
||||
MOCK_USER_PREFERENCES.settings = MOCK_DEFAULT_USER_SETTINGS;
|
||||
};
|
||||
|
||||
// --- Handlers for options/config/settings ---
|
||||
|
||||
export const SETTINGS_HANDLERS = [
|
||||
http.get("/api/options/models", async () =>
|
||||
HttpResponse.json([
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
"anthropic/claude-3.5",
|
||||
"anthropic/claude-sonnet-4-20250514",
|
||||
"anthropic/claude-sonnet-4-5-20250929",
|
||||
"anthropic/claude-haiku-4-5-20251001",
|
||||
"openhands/claude-sonnet-4-20250514",
|
||||
"openhands/claude-sonnet-4-5-20250929",
|
||||
"openhands/claude-haiku-4-5-20251001",
|
||||
"sambanova/Meta-Llama-3.1-8B-Instruct",
|
||||
]),
|
||||
),
|
||||
|
||||
http.get("/api/options/agents", async () =>
|
||||
HttpResponse.json(["CodeActAgent", "CoActAgent"]),
|
||||
),
|
||||
|
||||
http.get("/api/options/security-analyzers", async () =>
|
||||
HttpResponse.json(["llm", "none"]),
|
||||
),
|
||||
|
||||
http.get("/api/options/config", () => {
|
||||
const mockSaas = import.meta.env.VITE_MOCK_SAAS === "true";
|
||||
|
||||
const config: GetConfigResponse = {
|
||||
APP_MODE: mockSaas ? "saas" : "oss",
|
||||
GITHUB_CLIENT_ID: "fake-github-client-id",
|
||||
POSTHOG_CLIENT_KEY: "fake-posthog-client-key",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: mockSaas,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
// Uncomment the following to test the maintenance banner
|
||||
// MAINTENANCE: {
|
||||
// startTime: "2024-01-15T10:00:00-05:00", // EST timestamp
|
||||
// },
|
||||
};
|
||||
|
||||
return HttpResponse.json(config);
|
||||
}),
|
||||
|
||||
http.get("/api/settings", async () => {
|
||||
await delay();
|
||||
const { settings } = MOCK_USER_PREFERENCES;
|
||||
|
||||
if (!settings) return HttpResponse.json(null, { status: 404 });
|
||||
|
||||
return HttpResponse.json(settings);
|
||||
}),
|
||||
|
||||
http.post("/api/settings", async ({ request }) => {
|
||||
await delay();
|
||||
const body = await request.json();
|
||||
|
||||
if (body) {
|
||||
const current = MOCK_USER_PREFERENCES.settings || {
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
};
|
||||
|
||||
MOCK_USER_PREFERENCES.settings = {
|
||||
...current,
|
||||
...(body as Partial<Settings>),
|
||||
};
|
||||
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}
|
||||
|
||||
return HttpResponse.json(null, { status: 400 });
|
||||
}),
|
||||
|
||||
http.post("/api/reset-settings", async () => {
|
||||
await delay();
|
||||
MOCK_USER_PREFERENCES.settings = { ...MOCK_DEFAULT_USER_SETTINGS };
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}),
|
||||
|
||||
http.post("/api/add-git-providers", async ({ request }) => {
|
||||
const body = await request.json();
|
||||
|
||||
if (typeof body === "object" && body?.provider_tokens) {
|
||||
const rawTokens = body.provider_tokens as Record<
|
||||
string,
|
||||
{ token?: string }
|
||||
>;
|
||||
|
||||
const providerTokensSet: Partial<Record<Provider, string | null>> =
|
||||
Object.fromEntries(
|
||||
Object.entries(rawTokens)
|
||||
.filter(([, val]) => val && val.token)
|
||||
.map(([provider]) => [provider as Provider, ""]),
|
||||
);
|
||||
|
||||
MOCK_USER_PREFERENCES.settings = {
|
||||
...(MOCK_USER_PREFERENCES.settings || MOCK_DEFAULT_USER_SETTINGS),
|
||||
provider_tokens_set: providerTokensSet,
|
||||
};
|
||||
|
||||
return HttpResponse.json(true, { status: 200 });
|
||||
}
|
||||
|
||||
return HttpResponse.json(null, { status: 400 });
|
||||
}),
|
||||
];
|
||||
@@ -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]"
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useMemo } from "react";
|
||||
import { Outlet, redirect, useLocation } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { Route } from "./+types/settings";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import { queryClient } from "#/query-client-config";
|
||||
import { GetConfigResponse } from "#/api/option-service/option.types";
|
||||
import { SAAS_NAV_ITEMS, OSS_NAV_ITEMS } from "#/constants/settings-nav";
|
||||
import { Typography } from "#/ui/typography";
|
||||
import { SettingsLayout } from "#/components/features/settings/settings-layout";
|
||||
import { Typography } from "#/ui/typography";
|
||||
import { useSettingsNavItems } from "#/hooks/use-settings-nav-items";
|
||||
|
||||
const SAAS_ONLY_PATHS = [
|
||||
"/settings/user",
|
||||
@@ -33,14 +32,10 @@ export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => {
|
||||
// if in OSS mode, do not allow access to saas-only paths
|
||||
return redirect("/settings");
|
||||
}
|
||||
|
||||
// If LLM settings are hidden and user tries to access the LLM settings page
|
||||
if (config?.FEATURE_FLAGS?.HIDE_LLM_SETTINGS && pathname === "/settings") {
|
||||
// Redirect to the first available settings page
|
||||
if (isSaas) {
|
||||
return redirect("/settings/user");
|
||||
}
|
||||
return redirect("/settings/mcp");
|
||||
return isSaas ? redirect("/settings/user") : redirect("/settings/mcp");
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -48,37 +43,15 @@ export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => {
|
||||
|
||||
function SettingsScreen() {
|
||||
const { t } = useTranslation();
|
||||
const { data: config } = useConfig();
|
||||
const location = useLocation();
|
||||
|
||||
const isSaas = config?.APP_MODE === "saas";
|
||||
|
||||
// Navigation items configuration
|
||||
const navItems = useMemo(() => {
|
||||
const items = [];
|
||||
if (isSaas) {
|
||||
items.push(...SAAS_NAV_ITEMS);
|
||||
} else {
|
||||
items.push(...OSS_NAV_ITEMS);
|
||||
}
|
||||
|
||||
// Filter out LLM settings if the feature flag is enabled
|
||||
if (config?.FEATURE_FLAGS?.HIDE_LLM_SETTINGS) {
|
||||
return items.filter((item) => item.to !== "/settings");
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [isSaas, config?.FEATURE_FLAGS?.HIDE_LLM_SETTINGS]);
|
||||
|
||||
const navItems = useSettingsNavItems();
|
||||
// Current section title for the main content area
|
||||
const currentSectionTitle = useMemo(() => {
|
||||
const currentItem = navItems.find((item) => item.to === location.pathname);
|
||||
if (currentItem) {
|
||||
return currentItem.text;
|
||||
}
|
||||
|
||||
// Default to the first available navigation item if current page is not found
|
||||
return navItems.length > 0 ? navItems[0].text : "SETTINGS$TITLE";
|
||||
return currentItem
|
||||
? currentItem.text
|
||||
: (navItems[0]?.text ?? "SETTINGS$TITLE");
|
||||
}, [navItems, location.pathname]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,7 +4,11 @@ import tempfile
|
||||
from abc import ABC
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator
|
||||
from typing import TYPE_CHECKING, AsyncGenerator
|
||||
from uuid import UUID
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import httpx
|
||||
|
||||
import base62
|
||||
|
||||
@@ -29,6 +33,14 @@ from openhands.sdk.context.agent_context import AgentContext
|
||||
from openhands.sdk.context.condenser import LLMSummarizingCondenser
|
||||
from openhands.sdk.context.skills import load_user_skills
|
||||
from openhands.sdk.llm import LLM
|
||||
from openhands.sdk.security.analyzer import SecurityAnalyzerBase
|
||||
from openhands.sdk.security.confirmation_policy import (
|
||||
AlwaysConfirm,
|
||||
ConfirmationPolicyBase,
|
||||
ConfirmRisky,
|
||||
NeverConfirm,
|
||||
)
|
||||
from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer
|
||||
from openhands.sdk.workspace.remote.async_remote_workspace import AsyncRemoteWorkspace
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -379,3 +391,95 @@ class AppConversationServiceBase(AppConversationService, ABC):
|
||||
condenser = LLMSummarizingCondenser(**condenser_kwargs)
|
||||
|
||||
return condenser
|
||||
|
||||
def _create_security_analyzer_from_string(
|
||||
self, security_analyzer_str: str | None
|
||||
) -> SecurityAnalyzerBase | None:
|
||||
"""Convert security analyzer string from settings to SecurityAnalyzerBase instance.
|
||||
|
||||
Args:
|
||||
security_analyzer_str: String value from settings. Valid values:
|
||||
- "llm" -> LLMSecurityAnalyzer
|
||||
- "none" or None -> None
|
||||
- Other values -> None (unsupported analyzers are ignored)
|
||||
|
||||
Returns:
|
||||
SecurityAnalyzerBase instance or None
|
||||
"""
|
||||
if not security_analyzer_str or security_analyzer_str.lower() == 'none':
|
||||
return None
|
||||
|
||||
if security_analyzer_str.lower() == 'llm':
|
||||
return LLMSecurityAnalyzer()
|
||||
|
||||
# For unknown values, log a warning and return None
|
||||
_logger.warning(
|
||||
f'Unknown security analyzer value: {security_analyzer_str}. '
|
||||
'Supported values: "llm", "none". Defaulting to None.'
|
||||
)
|
||||
return None
|
||||
|
||||
def _select_confirmation_policy(
|
||||
self, confirmation_mode: bool, security_analyzer: str | None
|
||||
) -> ConfirmationPolicyBase:
|
||||
"""Choose confirmation policy using only mode flag and analyzer string."""
|
||||
if not confirmation_mode:
|
||||
return NeverConfirm()
|
||||
|
||||
analyzer_kind = (security_analyzer or '').lower()
|
||||
if analyzer_kind == 'llm':
|
||||
return ConfirmRisky()
|
||||
|
||||
return AlwaysConfirm()
|
||||
|
||||
async def _set_security_analyzer_from_settings(
|
||||
self,
|
||||
agent_server_url: str,
|
||||
session_api_key: str | None,
|
||||
conversation_id: UUID,
|
||||
security_analyzer_str: str | None,
|
||||
httpx_client: 'httpx.AsyncClient',
|
||||
) -> None:
|
||||
"""Set security analyzer on conversation using only the analyzer string.
|
||||
|
||||
Args:
|
||||
agent_server_url: URL of the agent server
|
||||
session_api_key: Session API key for authentication
|
||||
conversation_id: ID of the conversation to update
|
||||
security_analyzer_str: String value from settings
|
||||
httpx_client: HTTP client for making API requests
|
||||
"""
|
||||
|
||||
if session_api_key is None:
|
||||
return
|
||||
|
||||
security_analyzer = self._create_security_analyzer_from_string(
|
||||
security_analyzer_str
|
||||
)
|
||||
|
||||
# Only make API call if we have a security analyzer to set
|
||||
# (None is the default, so we can skip the call if it's None)
|
||||
if security_analyzer is None:
|
||||
return
|
||||
|
||||
try:
|
||||
# Prepare the request payload
|
||||
payload = {'security_analyzer': security_analyzer.model_dump()}
|
||||
|
||||
# Call agent server API to set security analyzer
|
||||
response = await httpx_client.post(
|
||||
f'{agent_server_url}/api/conversations/{conversation_id}/security_analyzer',
|
||||
json=payload,
|
||||
headers={'X-Session-API-Key': session_api_key},
|
||||
timeout=30.0,
|
||||
)
|
||||
response.raise_for_status()
|
||||
_logger.info(
|
||||
f'Successfully set security analyzer for conversation {conversation_id}'
|
||||
)
|
||||
except Exception as e:
|
||||
# Log error but don't fail conversation creation
|
||||
_logger.warning(
|
||||
f'Failed to set security analyzer for conversation {conversation_id}: {e}',
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ from pydantic import Field, SecretStr, TypeAdapter
|
||||
|
||||
from openhands.agent_server.models import (
|
||||
ConversationInfo,
|
||||
NeverConfirm,
|
||||
SendMessageRequest,
|
||||
StartConversationRequest,
|
||||
)
|
||||
@@ -73,7 +72,6 @@ from openhands.integrations.provider import ProviderType
|
||||
from openhands.sdk import Agent, AgentContext, LocalWorkspace
|
||||
from openhands.sdk.llm import LLM
|
||||
from openhands.sdk.secret import LookupSecret, StaticSecret
|
||||
from openhands.sdk.security.confirmation_policy import AlwaysConfirm
|
||||
from openhands.sdk.workspace.remote.async_remote_workspace import AsyncRemoteWorkspace
|
||||
from openhands.server.types import AppMode
|
||||
from openhands.tools.preset.default import (
|
||||
@@ -320,6 +318,16 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
||||
)
|
||||
)
|
||||
|
||||
# Set security analyzer from settings
|
||||
user = await self.user_context.get_user_info()
|
||||
await self._set_security_analyzer_from_settings(
|
||||
agent_server_url,
|
||||
sandbox.session_api_key,
|
||||
info.id,
|
||||
user.security_analyzer,
|
||||
self.httpx_client,
|
||||
)
|
||||
|
||||
# Update the start task
|
||||
task.status = AppConversationStartTaskStatus.READY
|
||||
task.app_conversation_id = info.id
|
||||
@@ -884,8 +892,8 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
||||
conversation_id=conversation_id,
|
||||
agent=agent,
|
||||
workspace=workspace,
|
||||
confirmation_policy=(
|
||||
AlwaysConfirm() if user.confirmation_mode else NeverConfirm()
|
||||
confirmation_policy=self._select_confirmation_policy(
|
||||
bool(user.confirmation_mode), user.security_analyzer
|
||||
),
|
||||
initial_message=initial_message,
|
||||
secrets=secrets,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""Unit tests for git functionality in AppConversationServiceBase.
|
||||
"""Unit tests for git and security functionality in AppConversationServiceBase.
|
||||
|
||||
This module tests the git-related functionality, specifically the clone_or_init_git_repo method
|
||||
and the recent bug fixes for git checkout operations.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from types import MethodType
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -434,13 +436,298 @@ def test_create_condenser_plan_agent_with_custom_max_size(mock_condenser_class):
|
||||
mock_llm.model_copy.assert_called_once()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tests for security analyzer helpers
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@pytest.mark.parametrize('value', [None, '', 'none', 'NoNe'])
|
||||
def test_create_security_analyzer_returns_none_for_empty_values(value):
|
||||
"""_create_security_analyzer_from_string returns None for empty/none values."""
|
||||
# Arrange
|
||||
service, _ = _create_service_with_mock_user_context(
|
||||
MockUserInfo(), bind_methods=('_create_security_analyzer_from_string',)
|
||||
)
|
||||
|
||||
# Act
|
||||
result = service._create_security_analyzer_from_string(value)
|
||||
|
||||
# Assert
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_create_security_analyzer_returns_llm_analyzer():
|
||||
"""_create_security_analyzer_from_string returns LLMSecurityAnalyzer for llm string."""
|
||||
# Arrange
|
||||
security_analyzer_str = 'llm'
|
||||
service, _ = _create_service_with_mock_user_context(
|
||||
MockUserInfo(), bind_methods=('_create_security_analyzer_from_string',)
|
||||
)
|
||||
|
||||
# Act
|
||||
result = service._create_security_analyzer_from_string(security_analyzer_str)
|
||||
|
||||
# Assert
|
||||
from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer
|
||||
|
||||
assert isinstance(result, LLMSecurityAnalyzer)
|
||||
|
||||
|
||||
def test_create_security_analyzer_logs_warning_for_unknown_value():
|
||||
"""_create_security_analyzer_from_string logs warning and returns None for unknown."""
|
||||
# Arrange
|
||||
unknown_value = 'custom'
|
||||
service, _ = _create_service_with_mock_user_context(
|
||||
MockUserInfo(), bind_methods=('_create_security_analyzer_from_string',)
|
||||
)
|
||||
|
||||
# Act
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_service_base._logger'
|
||||
) as mock_logger:
|
||||
result = service._create_security_analyzer_from_string(unknown_value)
|
||||
|
||||
# Assert
|
||||
assert result is None
|
||||
mock_logger.warning.assert_called_once()
|
||||
|
||||
|
||||
def test_select_confirmation_policy_when_disabled_returns_never_confirm():
|
||||
"""_select_confirmation_policy returns NeverConfirm when confirmation_mode is False."""
|
||||
# Arrange
|
||||
confirmation_mode = False
|
||||
security_analyzer = 'llm'
|
||||
service, _ = _create_service_with_mock_user_context(
|
||||
MockUserInfo(), bind_methods=('_select_confirmation_policy',)
|
||||
)
|
||||
|
||||
# Act
|
||||
policy = service._select_confirmation_policy(confirmation_mode, security_analyzer)
|
||||
|
||||
# Assert
|
||||
from openhands.sdk.security.confirmation_policy import NeverConfirm
|
||||
|
||||
assert isinstance(policy, NeverConfirm)
|
||||
|
||||
|
||||
def test_select_confirmation_policy_llm_returns_confirm_risky():
|
||||
"""_select_confirmation_policy uses ConfirmRisky when analyzer is llm."""
|
||||
# Arrange
|
||||
confirmation_mode = True
|
||||
security_analyzer = 'llm'
|
||||
service, _ = _create_service_with_mock_user_context(
|
||||
MockUserInfo(), bind_methods=('_select_confirmation_policy',)
|
||||
)
|
||||
|
||||
# Act
|
||||
policy = service._select_confirmation_policy(confirmation_mode, security_analyzer)
|
||||
|
||||
# Assert
|
||||
from openhands.sdk.security.confirmation_policy import ConfirmRisky
|
||||
|
||||
assert isinstance(policy, ConfirmRisky)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('security_analyzer', [None, '', 'none', 'custom'])
|
||||
def test_select_confirmation_policy_non_llm_returns_always_confirm(
|
||||
security_analyzer,
|
||||
):
|
||||
"""_select_confirmation_policy falls back to AlwaysConfirm for non-llm values."""
|
||||
# Arrange
|
||||
confirmation_mode = True
|
||||
service, _ = _create_service_with_mock_user_context(
|
||||
MockUserInfo(), bind_methods=('_select_confirmation_policy',)
|
||||
)
|
||||
|
||||
# Act
|
||||
policy = service._select_confirmation_policy(confirmation_mode, security_analyzer)
|
||||
|
||||
# Assert
|
||||
from openhands.sdk.security.confirmation_policy import AlwaysConfirm
|
||||
|
||||
assert isinstance(policy, AlwaysConfirm)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_security_analyzer_skips_when_no_session_key():
|
||||
"""_set_security_analyzer_from_settings exits early without session_api_key."""
|
||||
# Arrange
|
||||
agent_server_url = 'https://agent.example.com'
|
||||
conversation_id = uuid4()
|
||||
httpx_client = AsyncMock()
|
||||
service, _ = _create_service_with_mock_user_context(
|
||||
MockUserInfo(),
|
||||
bind_methods=(
|
||||
'_create_security_analyzer_from_string',
|
||||
'_set_security_analyzer_from_settings',
|
||||
),
|
||||
)
|
||||
|
||||
with patch.object(service, '_create_security_analyzer_from_string') as mock_create:
|
||||
# Act
|
||||
await service._set_security_analyzer_from_settings(
|
||||
agent_server_url=agent_server_url,
|
||||
session_api_key=None,
|
||||
conversation_id=conversation_id,
|
||||
security_analyzer_str='llm',
|
||||
httpx_client=httpx_client,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_create.assert_not_called()
|
||||
httpx_client.post.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_security_analyzer_skips_when_analyzer_none():
|
||||
"""_set_security_analyzer_from_settings skips API call when analyzer resolves to None."""
|
||||
# Arrange
|
||||
agent_server_url = 'https://agent.example.com'
|
||||
session_api_key = 'session-key'
|
||||
conversation_id = uuid4()
|
||||
httpx_client = AsyncMock()
|
||||
service, _ = _create_service_with_mock_user_context(
|
||||
MockUserInfo(),
|
||||
bind_methods=(
|
||||
'_create_security_analyzer_from_string',
|
||||
'_set_security_analyzer_from_settings',
|
||||
),
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
service, '_create_security_analyzer_from_string', return_value=None
|
||||
) as mock_create:
|
||||
# Act
|
||||
await service._set_security_analyzer_from_settings(
|
||||
agent_server_url=agent_server_url,
|
||||
session_api_key=session_api_key,
|
||||
conversation_id=conversation_id,
|
||||
security_analyzer_str='none',
|
||||
httpx_client=httpx_client,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_create.assert_called_once_with('none')
|
||||
httpx_client.post.assert_not_called()
|
||||
|
||||
|
||||
class DummyAnalyzer:
|
||||
"""Simple analyzer stub for testing model_dump contract."""
|
||||
|
||||
def __init__(self, payload: dict):
|
||||
self._payload = payload
|
||||
|
||||
def model_dump(self) -> dict:
|
||||
return self._payload
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_security_analyzer_successfully_calls_agent_server():
|
||||
"""_set_security_analyzer_from_settings posts analyzer payload when available."""
|
||||
# Arrange
|
||||
agent_server_url = 'https://agent.example.com'
|
||||
session_api_key = 'session-key'
|
||||
conversation_id = uuid4()
|
||||
analyzer_payload = {'type': 'llm'}
|
||||
httpx_client = AsyncMock()
|
||||
http_response = MagicMock()
|
||||
http_response.raise_for_status = MagicMock()
|
||||
httpx_client.post.return_value = http_response
|
||||
service, _ = _create_service_with_mock_user_context(
|
||||
MockUserInfo(),
|
||||
bind_methods=(
|
||||
'_create_security_analyzer_from_string',
|
||||
'_set_security_analyzer_from_settings',
|
||||
),
|
||||
)
|
||||
|
||||
analyzer = DummyAnalyzer(analyzer_payload)
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
service,
|
||||
'_create_security_analyzer_from_string',
|
||||
return_value=analyzer,
|
||||
) as mock_create,
|
||||
patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_service_base._logger'
|
||||
) as mock_logger,
|
||||
):
|
||||
# Act
|
||||
await service._set_security_analyzer_from_settings(
|
||||
agent_server_url=agent_server_url,
|
||||
session_api_key=session_api_key,
|
||||
conversation_id=conversation_id,
|
||||
security_analyzer_str='llm',
|
||||
httpx_client=httpx_client,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_create.assert_called_once_with('llm')
|
||||
httpx_client.post.assert_awaited_once_with(
|
||||
f'{agent_server_url}/api/conversations/{conversation_id}/security_analyzer',
|
||||
json={'security_analyzer': analyzer_payload},
|
||||
headers={'X-Session-API-Key': session_api_key},
|
||||
timeout=30.0,
|
||||
)
|
||||
http_response.raise_for_status.assert_called_once()
|
||||
mock_logger.info.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_security_analyzer_logs_warning_on_failure():
|
||||
"""_set_security_analyzer_from_settings warns but does not raise on errors."""
|
||||
# Arrange
|
||||
agent_server_url = 'https://agent.example.com'
|
||||
session_api_key = 'session-key'
|
||||
conversation_id = uuid4()
|
||||
analyzer_payload = {'type': 'llm'}
|
||||
httpx_client = AsyncMock()
|
||||
httpx_client.post.side_effect = RuntimeError('network down')
|
||||
service, _ = _create_service_with_mock_user_context(
|
||||
MockUserInfo(),
|
||||
bind_methods=(
|
||||
'_create_security_analyzer_from_string',
|
||||
'_set_security_analyzer_from_settings',
|
||||
),
|
||||
)
|
||||
|
||||
analyzer = DummyAnalyzer(analyzer_payload)
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
service,
|
||||
'_create_security_analyzer_from_string',
|
||||
return_value=analyzer,
|
||||
) as mock_create,
|
||||
patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_service_base._logger'
|
||||
) as mock_logger,
|
||||
):
|
||||
# Act
|
||||
await service._set_security_analyzer_from_settings(
|
||||
agent_server_url=agent_server_url,
|
||||
session_api_key=session_api_key,
|
||||
conversation_id=conversation_id,
|
||||
security_analyzer_str='llm',
|
||||
httpx_client=httpx_client,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_create.assert_called_once_with('llm')
|
||||
httpx_client.post.assert_awaited_once()
|
||||
mock_logger.warning.assert_called()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tests for _configure_git_user_settings
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _create_service_with_mock_user_context(user_info: MockUserInfo) -> tuple:
|
||||
"""Create a mock service with the actual _configure_git_user_settings method.
|
||||
def _create_service_with_mock_user_context(
|
||||
user_info: MockUserInfo, bind_methods: tuple[str, ...] | None = None
|
||||
) -> tuple:
|
||||
"""Create a mock service with selected real methods bound for testing.
|
||||
|
||||
Uses MagicMock for the service but binds the real method for testing.
|
||||
|
||||
@@ -452,13 +739,16 @@ def _create_service_with_mock_user_context(user_info: MockUserInfo) -> tuple:
|
||||
# Create a simple mock service and set required attribute
|
||||
service = MagicMock()
|
||||
service.user_context = mock_user_context
|
||||
methods_to_bind = ['_configure_git_user_settings']
|
||||
if bind_methods:
|
||||
methods_to_bind.extend(bind_methods)
|
||||
# Remove potential duplicates while keeping order
|
||||
methods_to_bind = list(dict.fromkeys(methods_to_bind))
|
||||
|
||||
# Bind the actual method from the real class to test real implementation
|
||||
service._configure_git_user_settings = (
|
||||
lambda workspace: AppConversationServiceBase._configure_git_user_settings(
|
||||
service, workspace
|
||||
)
|
||||
)
|
||||
# Bind actual methods from the real class to test implementations directly
|
||||
for method_name in methods_to_bind:
|
||||
real_method = getattr(AppConversationServiceBase, method_name)
|
||||
setattr(service, method_name, MethodType(real_method, service))
|
||||
|
||||
return service, mock_user_context
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@ class TestExperimentManagerIntegration:
|
||||
llm_api_key=None,
|
||||
confirmation_mode=False,
|
||||
condenser_max_size=None,
|
||||
security_analyzer=None,
|
||||
)
|
||||
|
||||
async def get_secrets(self):
|
||||
|
||||
Reference in New Issue
Block a user