mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4095770d1e | |||
| 74753036bb | |||
| 95d7c10608 | |||
| c142cc27ff | |||
| 0e20fc206b | |||
| e21475a88e | |||
| 921fec0019 | |||
| 049f839a62 | |||
| 0dde758e13 | |||
| 8257ae70cc | |||
| 4513bcc622 | |||
| b5b9a3f40b |
@@ -46,7 +46,8 @@ repos:
|
||||
- types-toml
|
||||
- types-redis
|
||||
- lxml
|
||||
# TODO: Add OpenHands in parent
|
||||
# OpenHands package in repo root
|
||||
- ./
|
||||
- stripe==11.5.0
|
||||
- pygithub==2.6.1
|
||||
# To see gaps add `--html-report mypy-report/`
|
||||
|
||||
@@ -7,15 +7,11 @@ warn_unreachable = True
|
||||
warn_redundant_casts = True
|
||||
no_implicit_optional = True
|
||||
strict_optional = True
|
||||
exclude = (^enterprise/migrations/.*|^openhands/.*)
|
||||
disable_error_code = type-abstract
|
||||
exclude = (^enterprise/migrations/.*)
|
||||
|
||||
[mypy-enterprise.tests.unit.test_auth_routes.*]
|
||||
disable_error_code = union-attr
|
||||
|
||||
[mypy-enterprise.sync.install_gitlab_webhooks.*]
|
||||
disable_error_code = redundant-cast
|
||||
|
||||
# Let the other config check base openhands packages
|
||||
[mypy-openhands.*]
|
||||
follow_imports = skip
|
||||
ignore_missing_imports = True
|
||||
|
||||
@@ -55,7 +55,7 @@ class SaaSExperimentManager(ExperimentManager):
|
||||
|
||||
@staticmethod
|
||||
def run_config_variant_test(
|
||||
user_id: str, conversation_id: str, config: OpenHandsConfig
|
||||
user_id: str | None, conversation_id: str, config: OpenHandsConfig
|
||||
) -> OpenHandsConfig:
|
||||
"""
|
||||
Run agent config variant test and potentially modify the OpenHands config
|
||||
|
||||
@@ -62,7 +62,13 @@ class GitlabManager(Manager):
|
||||
logger.warning(f'Got invalid keyloak user id for GitLab User {user_id}')
|
||||
return False
|
||||
|
||||
gitlab_service = GitLabServiceImpl(external_auth_id=keycloak_user_id)
|
||||
# Importing here prevents circular import
|
||||
from integrations.gitlab.gitlab_service import SaaSGitLabService
|
||||
|
||||
gitlab_service: SaaSGitLabService = GitLabServiceImpl(
|
||||
external_auth_id=keycloak_user_id
|
||||
)
|
||||
|
||||
return await gitlab_service.user_has_write_access(project_id)
|
||||
|
||||
async def receive_message(self, message: Message):
|
||||
@@ -119,7 +125,13 @@ class GitlabManager(Manager):
|
||||
gitlab_view: The GitLab view object containing issue/PR/comment info
|
||||
"""
|
||||
keycloak_user_id = gitlab_view.user_info.keycloak_user_id
|
||||
gitlab_service = GitLabServiceImpl(external_auth_id=keycloak_user_id)
|
||||
|
||||
# Importing here prevents circular import
|
||||
from integrations.gitlab.gitlab_service import SaaSGitLabService
|
||||
|
||||
gitlab_service: SaaSGitLabService = GitLabServiceImpl(
|
||||
external_auth_id=keycloak_user_id
|
||||
)
|
||||
|
||||
outgoing_message = message.message
|
||||
|
||||
|
||||
@@ -47,14 +47,14 @@ class GitlabIssue(ResolverViewInterface):
|
||||
)
|
||||
|
||||
self.previous_comments = await gitlab_service.get_issue_or_mr_comments(
|
||||
self.project_id, self.issue_number, is_mr=self.is_mr
|
||||
str(self.project_id), self.issue_number, is_mr=self.is_mr
|
||||
)
|
||||
|
||||
(
|
||||
self.title,
|
||||
self.description,
|
||||
) = await gitlab_service.get_issue_or_mr_title_and_body(
|
||||
self.project_id, self.issue_number, is_mr=self.is_mr
|
||||
str(self.project_id), self.issue_number, is_mr=self.is_mr
|
||||
)
|
||||
|
||||
async def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
|
||||
@@ -199,11 +199,11 @@ class GitlabInlineMRComment(GitlabMRComment):
|
||||
self.title,
|
||||
self.description,
|
||||
) = await gitlab_service.get_issue_or_mr_title_and_body(
|
||||
self.project_id, self.issue_number, is_mr=self.is_mr
|
||||
str(self.project_id), self.issue_number, is_mr=self.is_mr
|
||||
)
|
||||
|
||||
self.previous_comments = await gitlab_service.get_review_thread_comments(
|
||||
self.project_id, self.issue_number, self.discussion_id
|
||||
str(self.project_id), self.issue_number, self.discussion_id
|
||||
)
|
||||
|
||||
async def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
|
||||
|
||||
@@ -234,7 +234,7 @@ def _get_user_id(conversation_id: str) -> str:
|
||||
return conversation_metadata.user_id
|
||||
|
||||
|
||||
async def _get_session_api_key(user_id: str, conversation_id: str) -> str:
|
||||
async def _get_session_api_key(user_id: str, conversation_id: str) -> str | None:
|
||||
agent_loop_info = await conversation_manager.get_agent_loop_info(
|
||||
user_id, filter_to_sids={conversation_id}
|
||||
)
|
||||
|
||||
@@ -178,10 +178,8 @@ class SaasNestedConversationManager(ConversationManager):
|
||||
redis = self._get_redis_client()
|
||||
key = self._get_redis_conversation_key(user_id, sid)
|
||||
starting = await redis.get(key)
|
||||
logger.debug(f'maybe_start_agent_loop starting from redis: {starting}')
|
||||
|
||||
runtime = await self._get_runtime(sid)
|
||||
logger.debug(f'maybe_start_agent_loop runtime: {runtime}')
|
||||
|
||||
nested_url = None
|
||||
session_api_key = None
|
||||
@@ -189,20 +187,15 @@ class SaasNestedConversationManager(ConversationManager):
|
||||
event_store = EventStore(sid, self.file_store, user_id)
|
||||
if runtime:
|
||||
nested_url = self._get_nested_url_for_runtime(runtime['runtime_id'], sid)
|
||||
logger.debug(f'maybe_start_agent_loop nested_url: {nested_url}')
|
||||
session_api_key = runtime.get('session_api_key')
|
||||
logger.debug(f'maybe_start_agent_loop session_api_key: {session_api_key}')
|
||||
status_str = (runtime.get('status') or 'stopped').upper()
|
||||
logger.debug(f'maybe_start_agent_loop status_str: {status_str}')
|
||||
if status_str in ConversationStatus:
|
||||
status = ConversationStatus[status_str]
|
||||
if status is ConversationStatus.STOPPED and starting:
|
||||
logger.debug('maybe_start_agent_loop setting status to starting...')
|
||||
status = ConversationStatus.STARTING
|
||||
|
||||
if status is ConversationStatus.STOPPED:
|
||||
# Mark the agentloop as starting in redis
|
||||
logger.debug('maybe_start_agent_loop starting agent...')
|
||||
await redis.set(key, 1, ex=_REDIS_ENTRY_TIMEOUT_SECONDS)
|
||||
|
||||
# Start the agent loop in the background
|
||||
|
||||
@@ -276,12 +276,12 @@ class VerifyWebhookStatus:
|
||||
webhook
|
||||
)
|
||||
|
||||
gitlab_service = GitLabServiceImpl(external_auth_id=user_id)
|
||||
gitlab_service_impl = GitLabServiceImpl(external_auth_id=user_id)
|
||||
|
||||
if not isinstance(gitlab_service, SaaSGitLabService):
|
||||
if not isinstance(gitlab_service_impl, SaaSGitLabService):
|
||||
raise Exception('Only SaaSGitLabService is supported')
|
||||
# Cast needed when mypy can see OpenHands
|
||||
gitlab_service = cast(type[SaaSGitLabService], gitlab_service)
|
||||
gitlab_service = cast(type[SaaSGitLabService], gitlab_service_impl)
|
||||
|
||||
await self.verify_conditions_are_met(
|
||||
gitlab_service=gitlab_service,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { ExpandableMessage } from "#/components/features/chat/expandable-message";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
|
||||
vi.mock("react-i18next", async () => {
|
||||
const actual = await vi.importActual("react-i18next");
|
||||
@@ -113,7 +113,7 @@ describe("ExpandableMessage", () => {
|
||||
});
|
||||
|
||||
it("should render the out of credits message when the user is out of credits", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
// @ts-expect-error - We only care about the APP_MODE and FEATURE_FLAGS fields
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
|
||||
+2
-2
@@ -3,13 +3,13 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
|
||||
describe("AnalyticsConsentFormModal", () => {
|
||||
it("should call saveUserSettings with consent", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onCloseMock = vi.fn();
|
||||
const saveUserSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveUserSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
|
||||
render(<AnalyticsConsentFormModal onClose={onCloseMock} />, {
|
||||
wrapper: ({ children }) => (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { render, screen } from "@testing-library/react";
|
||||
import { Provider } from "react-redux";
|
||||
import { setupStore } from "test-utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { HomeHeader } from "#/components/features/home/home-header";
|
||||
import { HomeHeader } from "#/components/features/home/home-header/home-header";
|
||||
|
||||
// Mock the translation function
|
||||
vi.mock("react-i18next", async () => {
|
||||
|
||||
@@ -5,7 +5,10 @@ import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import { setupStore } from "test-utils";
|
||||
import { Provider } from "react-redux";
|
||||
import { createRoutesStub, Outlet } from "react-router";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { RepoConnector } from "#/components/features/home/repo-connector";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
@@ -66,7 +69,7 @@ const MOCK_RESPOSITORIES: GitRepository[] = [
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
@@ -84,7 +87,7 @@ describe("RepoConnector", () => {
|
||||
|
||||
it("should render the available repositories in the dropdown", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
@@ -93,7 +96,7 @@ describe("RepoConnector", () => {
|
||||
});
|
||||
|
||||
// Mock the search function that's used by the dropdown
|
||||
vi.spyOn(OpenHands, "searchGitRepositories").mockResolvedValue(
|
||||
vi.spyOn(GitService, "searchGitRepositories").mockResolvedValue(
|
||||
MOCK_RESPOSITORIES,
|
||||
);
|
||||
|
||||
@@ -121,7 +124,7 @@ describe("RepoConnector", () => {
|
||||
|
||||
it("should only enable the launch button if a repo is selected", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
@@ -135,10 +138,16 @@ describe("RepoConnector", () => {
|
||||
expect(launchButton).toBeDisabled();
|
||||
|
||||
// Mock the repository branches API call
|
||||
vi.spyOn(OpenHands, "getRepositoryBranches").mockResolvedValue({ branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
], has_next_page: false, current_page: 1, per_page: 30, total_count: 2 });
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
],
|
||||
has_next_page: false,
|
||||
current_page: 1,
|
||||
per_page: 30,
|
||||
total_count: 2,
|
||||
});
|
||||
|
||||
// First select the provider
|
||||
const providerDropdown = await waitFor(() =>
|
||||
@@ -170,14 +179,14 @@ describe("RepoConnector", () => {
|
||||
});
|
||||
|
||||
it("should render the 'add github repos' link in dropdown if saas mode and github provider is set", async () => {
|
||||
const getConfiSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfiSpy = vi.spyOn(OptionService, "getConfig");
|
||||
// @ts-expect-error - only return the APP_MODE and APP_SLUG
|
||||
getConfiSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
APP_SLUG: "openhands",
|
||||
});
|
||||
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
@@ -187,7 +196,7 @@ describe("RepoConnector", () => {
|
||||
});
|
||||
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
@@ -217,14 +226,14 @@ describe("RepoConnector", () => {
|
||||
});
|
||||
|
||||
it("should not render the 'add github repos' link if github provider is not set", async () => {
|
||||
const getConfiSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfiSpy = vi.spyOn(OptionService, "getConfig");
|
||||
// @ts-expect-error - only return the APP_MODE and APP_SLUG
|
||||
getConfiSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
APP_SLUG: "openhands",
|
||||
});
|
||||
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
@@ -234,7 +243,7 @@ describe("RepoConnector", () => {
|
||||
});
|
||||
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
@@ -262,13 +271,13 @@ describe("RepoConnector", () => {
|
||||
});
|
||||
|
||||
it("should not render the 'add github repos' link in dropdown if oss mode", async () => {
|
||||
const getConfiSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfiSpy = vi.spyOn(OptionService, "getConfig");
|
||||
// @ts-expect-error - only return the APP_MODE
|
||||
getConfiSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
});
|
||||
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
@@ -278,7 +287,7 @@ describe("RepoConnector", () => {
|
||||
});
|
||||
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
@@ -321,7 +330,7 @@ describe("RepoConnector", () => {
|
||||
session_api_key: null,
|
||||
});
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
@@ -340,10 +349,16 @@ describe("RepoConnector", () => {
|
||||
expect(createConversationSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Mock the repository branches API call
|
||||
vi.spyOn(OpenHands, "getRepositoryBranches").mockResolvedValue({ branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
], has_next_page: false, current_page: 1, per_page: 30, total_count: 2 });
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
],
|
||||
has_next_page: false,
|
||||
current_page: 1,
|
||||
per_page: 30,
|
||||
total_count: 2,
|
||||
});
|
||||
|
||||
// First select the provider
|
||||
const providerDropdown = await waitFor(() =>
|
||||
@@ -388,7 +403,7 @@ describe("RepoConnector", () => {
|
||||
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
|
||||
createConversationSpy.mockImplementation(() => new Promise(() => {})); // Never resolves to keep loading state
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
@@ -397,10 +412,16 @@ describe("RepoConnector", () => {
|
||||
});
|
||||
|
||||
// Mock the repository branches API call
|
||||
vi.spyOn(OpenHands, "getRepositoryBranches").mockResolvedValue({ branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
], has_next_page: false, current_page: 1, per_page: 30, total_count: 2 });
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
],
|
||||
has_next_page: false,
|
||||
current_page: 1,
|
||||
per_page: 30,
|
||||
total_count: 2,
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
@@ -448,7 +469,7 @@ describe("RepoConnector", () => {
|
||||
});
|
||||
|
||||
it("should display a button to settings if the user needs to sign in with their git provider", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {},
|
||||
|
||||
@@ -3,6 +3,8 @@ import { describe, expect, vi, beforeEach, it } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { RepositorySelectionForm } from "../../../../src/components/features/home/repo-selection-form";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import UserService from "#/api/user-service/user-service.api";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
// Create mock functions
|
||||
@@ -204,7 +206,7 @@ describe("RepositorySelectionForm", () => {
|
||||
];
|
||||
|
||||
// Create a spy on the API call
|
||||
const searchGitReposSpy = vi.spyOn(OpenHands, "searchGitRepositories");
|
||||
const searchGitReposSpy = vi.spyOn(GitService, "searchGitRepositories");
|
||||
searchGitReposSpy.mockResolvedValue(MOCK_SEARCH_REPOS);
|
||||
|
||||
mockUseGitRepositories.mockReturnValue({
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Provider } from "react-redux";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { setupStore } from "test-utils";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import UserService from "#/api/user-service/user-service.api";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import { TaskCard } from "#/components/features/home/tasks/task-card";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { SuggestedTask } from "#/utils/types";
|
||||
@@ -70,7 +72,7 @@ describe("TaskCard", () => {
|
||||
describe("creating suggested task conversation", () => {
|
||||
beforeEach(() => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
|
||||
+11
-10
@@ -8,6 +8,7 @@ import { renderWithProviders } from "test-utils";
|
||||
import MicroagentManagement from "#/routes/microagent-management";
|
||||
import { MicroagentManagementMain } from "#/components/features/microagent-management/microagent-management-main";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { RepositoryMicroagent } from "#/types/microagent-management";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
@@ -231,12 +232,12 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Setup default mock for retrieveUserGitRepositories
|
||||
vi.spyOn(OpenHands, "retrieveUserGitRepositories").mockResolvedValue({
|
||||
vi.spyOn(GitService, "retrieveUserGitRepositories").mockResolvedValue({
|
||||
data: [...mockRepositories],
|
||||
nextPage: null,
|
||||
});
|
||||
// Setup default mock for getRepositoryMicroagents
|
||||
vi.spyOn(OpenHands, "getRepositoryMicroagents").mockResolvedValue([
|
||||
vi.spyOn(GitService, "getRepositoryMicroagents").mockResolvedValue([
|
||||
...mockMicroagents,
|
||||
]);
|
||||
// Setup default mock for searchConversations
|
||||
@@ -244,7 +245,7 @@ describe("MicroagentManagement", () => {
|
||||
...mockConversations,
|
||||
]);
|
||||
// Setup default mock for getRepositoryMicroagentContent
|
||||
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
vi.spyOn(GitService, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
content: "Original microagent content for testing updates",
|
||||
path: ".openhands/microagents/update-test-microagent",
|
||||
git_provider: "github",
|
||||
@@ -1290,7 +1291,7 @@ describe("MicroagentManagement", () => {
|
||||
// Add microagent integration tests
|
||||
describe("Add microagent functionality", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(OpenHands, "getRepositoryBranches").mockResolvedValue({
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
branches: [{ name: "main", commit_sha: "abc123", protected: false }],
|
||||
has_next_page: false,
|
||||
current_page: 1,
|
||||
@@ -1983,7 +1984,7 @@ describe("MicroagentManagement", () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(OpenHands, "getRepositoryBranches").mockResolvedValue({
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
branches: [{ name: "main", commit_sha: "abc123", protected: false }],
|
||||
has_next_page: false,
|
||||
current_page: 1,
|
||||
@@ -2314,7 +2315,7 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock the content API to return empty content for this test
|
||||
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
vi.spyOn(GitService, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
content: "",
|
||||
path: ".openhands/microagents/update-test-microagent",
|
||||
git_provider: "github",
|
||||
@@ -2363,7 +2364,7 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock the content API to return content without triggers for this test
|
||||
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
vi.spyOn(GitService, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
content: "Original microagent content for testing updates",
|
||||
path: ".openhands/microagents/update-test-microagent",
|
||||
git_provider: "github",
|
||||
@@ -2647,7 +2648,7 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock the content API to return the expected content for this test
|
||||
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
vi.spyOn(GitService, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
content: "Test microagent content for learn functionality",
|
||||
path: ".openhands/microagents/learn-test-microagent",
|
||||
git_provider: "github",
|
||||
@@ -2707,7 +2708,7 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock the content API to return empty content for this test
|
||||
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
vi.spyOn(GitService, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
content: "",
|
||||
path: ".openhands/microagents/learn-test-microagent",
|
||||
git_provider: "github",
|
||||
@@ -2765,7 +2766,7 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock the content API to return content without triggers for this test
|
||||
vi.spyOn(OpenHands, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
vi.spyOn(GitService, "getRepositoryMicroagentContent").mockResolvedValue({
|
||||
content: "Test microagent content for learn functionality",
|
||||
path: ".openhands/microagents/learn-test-microagent",
|
||||
git_provider: "github",
|
||||
|
||||
@@ -3,12 +3,13 @@ import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import { PaymentForm } from "#/components/features/payment/payment-form";
|
||||
|
||||
describe("PaymentForm", () => {
|
||||
const getBalanceSpy = vi.spyOn(OpenHands, "getBalance");
|
||||
const createCheckoutSessionSpy = vi.spyOn(OpenHands, "createCheckoutSession");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
|
||||
const renderPaymentForm = () =>
|
||||
render(<PaymentForm />, {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { waitFor } from "@testing-library/react";
|
||||
import { Sidebar } from "#/components/features/sidebar/sidebar";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
|
||||
// These tests will now fail because the conversation panel is rendered through a portal
|
||||
// and technically not a child of the Sidebar component.
|
||||
@@ -19,7 +19,7 @@ const renderSidebar = () =>
|
||||
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);
|
||||
|
||||
describe("Sidebar", () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -3,13 +3,13 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { screen } from "@testing-library/react";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import { SettingsForm } from "#/components/shared/modals/settings/settings-form";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
|
||||
describe("SettingsForm", () => {
|
||||
const onCloseMock = vi.fn();
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
|
||||
const RouteStub = createRoutesStub([
|
||||
{
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
|
||||
describe("useSaveSettings", () => {
|
||||
it("should send an empty string for llm_api_key if an empty string is passed, otherwise undefined", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
const { result } = renderHook(() => useSaveSettings(), {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import MainApp from "#/routes/root-layout";
|
||||
import i18n from "#/i18n";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import * as ToastHandlers from "#/utils/custom-toast-handlers";
|
||||
|
||||
@@ -62,8 +64,8 @@ describe("frontend/routes/_oh", () => {
|
||||
// FIXME: This test fails when it shouldn't be, please investigate
|
||||
it.skip("should render and capture the user's consent if oss mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
const handleCaptureConsentSpy = vi.spyOn(
|
||||
CaptureConsent,
|
||||
"handleCaptureConsent",
|
||||
@@ -106,7 +108,7 @@ describe("frontend/routes/_oh", () => {
|
||||
});
|
||||
|
||||
it("should not render the user consent form if saas mode", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "test-id",
|
||||
@@ -184,8 +186,8 @@ describe("frontend/routes/_oh", () => {
|
||||
});
|
||||
|
||||
it("should render a you're in toast if it is a new user and in saas mode", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
const displaySuccessToastSpy = vi.spyOn(
|
||||
ToastHandlers,
|
||||
"displaySuccessToast",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import AppSettingsScreen from "#/routes/app-settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import { AvailableLanguages } from "#/i18n";
|
||||
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||
@@ -25,7 +25,7 @@ describe("Content", () => {
|
||||
});
|
||||
|
||||
it("should render the correct default values", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
language: "no",
|
||||
@@ -65,8 +65,8 @@ describe("Form submission", () => {
|
||||
});
|
||||
|
||||
it("should submit the form with the correct values", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
renderAppSettingsScreen();
|
||||
@@ -106,7 +106,7 @@ describe("Form submission", () => {
|
||||
});
|
||||
|
||||
it("should only enable the submit button when there are changes", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
renderAppSettingsScreen();
|
||||
@@ -146,7 +146,7 @@ describe("Form submission", () => {
|
||||
});
|
||||
|
||||
it("should call handleCaptureConsents with true when the analytics switch is toggled", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
const handleCaptureConsentsSpy = vi.spyOn(
|
||||
@@ -168,7 +168,7 @@ describe("Form submission", () => {
|
||||
});
|
||||
|
||||
it("should call handleCaptureConsents with false when the analytics switch is toggled", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
user_consents_to_analytics: true,
|
||||
@@ -215,8 +215,8 @@ describe("Form submission", () => {
|
||||
});
|
||||
|
||||
it("should disable the button after submitting changes", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
renderAppSettingsScreen();
|
||||
@@ -240,8 +240,8 @@ describe("Form submission", () => {
|
||||
|
||||
describe("Status toasts", () => {
|
||||
it("should call displaySuccessToast when the settings are saved", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
const displaySuccessToastSpy = vi.spyOn(
|
||||
@@ -265,8 +265,8 @@ describe("Status toasts", () => {
|
||||
});
|
||||
|
||||
it("should call displayErrorToast when the settings fail to save", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
const displayErrorToastSpy = vi.spyOn(ToastHandlers, "displayErrorToast");
|
||||
|
||||
@@ -6,9 +6,12 @@ import userEvent from "@testing-library/user-event";
|
||||
import i18next from "i18next";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import GitSettingsScreen from "#/routes/git-settings";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import AuthService from "#/api/auth-service/auth-service.api";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import { GetConfigResponse } from "#/api/open-hands.types";
|
||||
import { GetConfigResponse } from "#/api/option-service/option.types";
|
||||
import * as ToastHandlers from "#/utils/custom-toast-handlers";
|
||||
import { SecretsService } from "#/api/secrets-service";
|
||||
|
||||
@@ -108,7 +111,7 @@ describe("Content", () => {
|
||||
});
|
||||
|
||||
it("should render the inputs if OSS mode", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
const { rerender } = renderGitSettingsScreen();
|
||||
@@ -151,8 +154,8 @@ describe("Content", () => {
|
||||
});
|
||||
|
||||
it("should set '<hidden>' placeholder and indicator if the GitHub token is set", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
@@ -226,7 +229,7 @@ describe("Content", () => {
|
||||
});
|
||||
|
||||
it("should render the 'Configure GitHub Repositories' button if SaaS mode and app slug exists", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
const { rerender } = renderGitSettingsScreen();
|
||||
@@ -270,7 +273,7 @@ describe("Form submission", () => {
|
||||
it("should save the GitHub token", async () => {
|
||||
const saveProvidersSpy = vi.spyOn(SecretsService, "addGitProvider");
|
||||
saveProvidersSpy.mockImplementation(() => Promise.resolve(true));
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
renderGitSettingsScreen();
|
||||
@@ -291,7 +294,7 @@ describe("Form submission", () => {
|
||||
it("should save GitLab tokens", async () => {
|
||||
const saveProvidersSpy = vi.spyOn(SecretsService, "addGitProvider");
|
||||
saveProvidersSpy.mockImplementation(() => Promise.resolve(true));
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
renderGitSettingsScreen();
|
||||
@@ -312,7 +315,7 @@ describe("Form submission", () => {
|
||||
it("should save the Bitbucket token", async () => {
|
||||
const saveProvidersSpy = vi.spyOn(SecretsService, "addGitProvider");
|
||||
saveProvidersSpy.mockImplementation(() => Promise.resolve(true));
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
renderGitSettingsScreen();
|
||||
@@ -331,7 +334,7 @@ describe("Form submission", () => {
|
||||
});
|
||||
|
||||
it("should disable the button if there is no input", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
renderGitSettingsScreen();
|
||||
@@ -357,8 +360,8 @@ describe("Form submission", () => {
|
||||
});
|
||||
|
||||
it("should enable a disconnect tokens button if there is at least one token set", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
@@ -391,9 +394,9 @@ describe("Form submission", () => {
|
||||
});
|
||||
|
||||
it("should call logout when pressing the disconnect tokens button", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const logoutSpy = vi.spyOn(OpenHands, "logout");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
const logoutSpy = vi.spyOn(AuthService, "logout");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
@@ -418,7 +421,7 @@ describe("Form submission", () => {
|
||||
// flaky test
|
||||
it.skip("should disable the button when submitting changes", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(SecretsService, "addGitProvider");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
renderGitSettingsScreen();
|
||||
@@ -442,7 +445,7 @@ describe("Form submission", () => {
|
||||
|
||||
it("should disable the button after submitting changes", async () => {
|
||||
const saveProvidersSpy = vi.spyOn(SecretsService, "addGitProvider");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
renderGitSettingsScreen();
|
||||
@@ -476,7 +479,7 @@ describe("Form submission", () => {
|
||||
describe("Status toasts", () => {
|
||||
it("should call displaySuccessToast when the settings are saved", async () => {
|
||||
const saveProvidersSpy = vi.spyOn(SecretsService, "addGitProvider");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
const displaySuccessToastSpy = vi.spyOn(
|
||||
@@ -499,7 +502,7 @@ describe("Status toasts", () => {
|
||||
|
||||
it("should call displayErrorToast when the settings fail to save", async () => {
|
||||
const saveProvidersSpy = vi.spyOn(SecretsService, "addGitProvider");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
const displayErrorToastSpy = vi.spyOn(ToastHandlers, "displayErrorToast");
|
||||
|
||||
@@ -7,7 +7,9 @@ import { Provider } from "react-redux";
|
||||
import { createAxiosNotFoundErrorObject, setupStore } from "test-utils";
|
||||
import HomeScreen from "#/routes/home";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import MainApp from "#/routes/root-layout";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
|
||||
@@ -91,7 +93,7 @@ const MOCK_RESPOSITORIES: GitRepository[] = [
|
||||
|
||||
describe("HomeScreen", () => {
|
||||
beforeEach(() => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
@@ -139,7 +141,7 @@ describe("HomeScreen", () => {
|
||||
|
||||
it("should filter the suggested tasks based on the selected repository", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
@@ -148,7 +150,7 @@ describe("HomeScreen", () => {
|
||||
});
|
||||
|
||||
// Mock the repository branches API call
|
||||
vi.spyOn(OpenHands, "getRepositoryBranches").mockResolvedValue({
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
@@ -183,7 +185,7 @@ describe("HomeScreen", () => {
|
||||
|
||||
it("should filter tasks when different repositories are selected", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
@@ -192,7 +194,7 @@ describe("HomeScreen", () => {
|
||||
});
|
||||
|
||||
// Mock the repository branches API call
|
||||
vi.spyOn(OpenHands, "getRepositoryBranches").mockResolvedValue({
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
@@ -246,7 +248,7 @@ describe("HomeScreen", () => {
|
||||
await screen.findAllByTestId("task-launch-button");
|
||||
|
||||
// Mock the repository branches API call
|
||||
vi.spyOn(OpenHands, "getRepositoryBranches").mockResolvedValue({
|
||||
vi.spyOn(GitService, "getRepositoryBranches").mockResolvedValue({
|
||||
branches: [
|
||||
{ name: "main", commit_sha: "123", protected: false },
|
||||
{ name: "develop", commit_sha: "456", protected: false },
|
||||
@@ -282,7 +284,7 @@ describe("HomeScreen", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
OpenHands,
|
||||
GitService,
|
||||
"retrieveUserGitRepositories",
|
||||
);
|
||||
retrieveUserGitRepositoriesSpy.mockResolvedValue({
|
||||
@@ -358,8 +360,8 @@ describe("Settings 404", () => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
|
||||
it("should open the settings modal if GET /settings fails with a 404", async () => {
|
||||
const error = createAxiosNotFoundErrorObject();
|
||||
@@ -417,8 +419,8 @@ describe("Settings 404", () => {
|
||||
});
|
||||
|
||||
describe("Setup Payment modal", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
|
||||
it("should only render if SaaS mode and is new user", async () => {
|
||||
// @ts-expect-error - we only need the APP_MODE for this test
|
||||
|
||||
@@ -3,7 +3,9 @@ import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import LlmSettingsScreen from "#/routes/llm-settings";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import {
|
||||
MOCK_DEFAULT_USER_SETTINGS,
|
||||
resetTestHandlersMockSettings,
|
||||
@@ -56,7 +58,7 @@ describe("Content", () => {
|
||||
});
|
||||
|
||||
it("should render the existing settings values", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "openai/gpt-4o",
|
||||
@@ -84,7 +86,9 @@ describe("Content", () => {
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const confirmation = screen.getByTestId("enable-confirmation-mode-switch");
|
||||
const confirmation = screen.getByTestId(
|
||||
"enable-confirmation-mode-switch",
|
||||
);
|
||||
|
||||
// Initially confirmation mode is false, so security analyzer should not be visible
|
||||
expect(confirmation).not.toBeChecked();
|
||||
@@ -185,7 +189,7 @@ describe("Content", () => {
|
||||
});
|
||||
|
||||
it("should render existing advanced settings correctly", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "openai/gpt-4o",
|
||||
@@ -230,7 +234,7 @@ describe("Content", () => {
|
||||
|
||||
describe("Form submission", () => {
|
||||
it("should submit the basic form with the correct values", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
@@ -266,7 +270,7 @@ describe("Form submission", () => {
|
||||
});
|
||||
|
||||
it("should submit the advanced form with the correct values", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
@@ -310,7 +314,9 @@ describe("Form submission", () => {
|
||||
// select security analyzer
|
||||
const securityAnalyzer = screen.getByTestId("security-analyzer-input");
|
||||
await userEvent.click(securityAnalyzer);
|
||||
const securityAnalyzerOption = screen.getByText("SETTINGS$SECURITY_ANALYZER_NONE");
|
||||
const securityAnalyzerOption = screen.getByText(
|
||||
"SETTINGS$SECURITY_ANALYZER_NONE",
|
||||
);
|
||||
await userEvent.click(securityAnalyzerOption);
|
||||
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
@@ -329,7 +335,7 @@ describe("Form submission", () => {
|
||||
});
|
||||
|
||||
it("should disable the button if there are no changes in the basic form", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "openai/gpt-4o",
|
||||
@@ -372,7 +378,7 @@ describe("Form submission", () => {
|
||||
});
|
||||
|
||||
it("should disable the button if there are no changes in the advanced form", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "openai/gpt-4o",
|
||||
@@ -392,10 +398,14 @@ describe("Form submission", () => {
|
||||
const baseUrl = await screen.findByTestId("base-url-input");
|
||||
const apiKey = await screen.findByTestId("llm-api-key-input");
|
||||
const agent = await screen.findByTestId("agent-input");
|
||||
const condensor = await screen.findByTestId("enable-memory-condenser-switch");
|
||||
const condensor = await screen.findByTestId(
|
||||
"enable-memory-condenser-switch",
|
||||
);
|
||||
|
||||
// Confirmation mode switch is now in basic settings, always visible
|
||||
const confirmation = await screen.findByTestId("enable-confirmation-mode-switch");
|
||||
const confirmation = await screen.findByTestId(
|
||||
"enable-confirmation-mode-switch",
|
||||
);
|
||||
|
||||
// enter custom model
|
||||
await userEvent.type(model, "-mini");
|
||||
@@ -468,9 +478,13 @@ describe("Form submission", () => {
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// select security analyzer
|
||||
const securityAnalyzer = await screen.findByTestId("security-analyzer-input");
|
||||
const securityAnalyzer = await screen.findByTestId(
|
||||
"security-analyzer-input",
|
||||
);
|
||||
await userEvent.click(securityAnalyzer);
|
||||
const securityAnalyzerOption = screen.getByText("SETTINGS$SECURITY_ANALYZER_NONE");
|
||||
const securityAnalyzerOption = screen.getByText(
|
||||
"SETTINGS$SECURITY_ANALYZER_NONE",
|
||||
);
|
||||
await userEvent.click(securityAnalyzerOption);
|
||||
expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_NONE");
|
||||
|
||||
@@ -478,9 +492,13 @@ describe("Form submission", () => {
|
||||
|
||||
// revert back to original value
|
||||
await userEvent.click(securityAnalyzer);
|
||||
const originalSecurityAnalyzerOption = screen.getByText("SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT");
|
||||
const originalSecurityAnalyzerOption = screen.getByText(
|
||||
"SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT",
|
||||
);
|
||||
await userEvent.click(originalSecurityAnalyzerOption);
|
||||
expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT");
|
||||
expect(securityAnalyzer).toHaveValue(
|
||||
"SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT",
|
||||
);
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
@@ -512,7 +530,7 @@ describe("Form submission", () => {
|
||||
|
||||
// flaky test
|
||||
it.skip("should disable the button when submitting changes", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
@@ -539,7 +557,7 @@ describe("Form submission", () => {
|
||||
});
|
||||
|
||||
it("should clear advanced settings when saving basic settings", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "openai/gpt-4o",
|
||||
@@ -547,7 +565,7 @@ describe("Form submission", () => {
|
||||
llm_api_key_set: true,
|
||||
confirmation_mode: true,
|
||||
});
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
renderLlmSettingsScreen();
|
||||
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
@@ -583,7 +601,7 @@ describe("Form submission", () => {
|
||||
describe("Status toasts", () => {
|
||||
describe("Basic form", () => {
|
||||
it("should call displaySuccessToast when the settings are saved", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
|
||||
const displaySuccessToastSpy = vi.spyOn(
|
||||
ToastHandlers,
|
||||
@@ -604,7 +622,7 @@ describe("Status toasts", () => {
|
||||
});
|
||||
|
||||
it("should call displayErrorToast when the settings fail to save", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
|
||||
const displayErrorToastSpy = vi.spyOn(ToastHandlers, "displayErrorToast");
|
||||
|
||||
@@ -626,7 +644,7 @@ describe("Status toasts", () => {
|
||||
|
||||
describe("Advanced form", () => {
|
||||
it("should call displaySuccessToast when the settings are saved", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
|
||||
const displaySuccessToastSpy = vi.spyOn(
|
||||
ToastHandlers,
|
||||
@@ -652,7 +670,7 @@ describe("Status toasts", () => {
|
||||
});
|
||||
|
||||
it("should call displayErrorToast when the settings fail to save", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
|
||||
const displayErrorToastSpy = vi.spyOn(ToastHandlers, "displayErrorToast");
|
||||
|
||||
@@ -680,7 +698,7 @@ describe("Status toasts", () => {
|
||||
|
||||
describe("SaaS mode", () => {
|
||||
it("should not render the runtime settings input in oss mode", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
// @ts-expect-error - only return mode
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
@@ -698,7 +716,7 @@ describe("SaaS mode", () => {
|
||||
});
|
||||
|
||||
it("should render the runtime settings input in saas mode", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
// @ts-expect-error - only return mode
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
@@ -716,7 +734,7 @@ describe("SaaS mode", () => {
|
||||
});
|
||||
|
||||
it("should always render the runtime settings input as disabled", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
// @ts-expect-error - only return mode
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
|
||||
@@ -6,7 +6,9 @@ import { createRoutesStub, Outlet } from "react-router";
|
||||
import SecretsSettingsScreen from "#/routes/secrets-settings";
|
||||
import { SecretsService } from "#/api/secrets-service";
|
||||
import { GetSecretsResponse } from "#/api/secrets-service.types";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
|
||||
const MOCK_GET_SECRETS_RESPONSE: GetSecretsResponse["custom_secrets"] = [
|
||||
@@ -53,7 +55,7 @@ const renderSecretsSettings = () =>
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
// @ts-expect-error - only return the config we need
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
@@ -67,8 +69,8 @@ describe("Content", () => {
|
||||
});
|
||||
|
||||
it("should NOT render a button to connect with git if they havent already in oss", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
|
||||
// @ts-expect-error - only return the config we need
|
||||
getConfigSpy.mockResolvedValue({
|
||||
@@ -87,7 +89,7 @@ describe("Content", () => {
|
||||
});
|
||||
|
||||
it("should render add secret button in saas mode", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
|
||||
// @ts-expect-error - only return the config we need
|
||||
getConfigSpy.mockResolvedValue({
|
||||
@@ -476,7 +478,9 @@ describe("Secret actions", () => {
|
||||
|
||||
// make POST request
|
||||
expect(createSecretSpy).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText("SECRETS$SECRET_ALREADY_EXISTS")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("SECRETS$SECRET_ALREADY_EXISTS"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, "My_Custom_Secret");
|
||||
@@ -560,7 +564,9 @@ describe("Secret actions", () => {
|
||||
|
||||
// make POST request
|
||||
expect(createSecretSpy).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText("SECRETS$SECRET_ALREADY_EXISTS")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("SECRETS$SECRET_ALREADY_EXISTS"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(nameInput).toHaveValue(MOCK_GET_SECRETS_RESPONSE[0].name);
|
||||
expect(valueInput).toHaveValue("my-custom-secret-value");
|
||||
|
||||
@@ -4,6 +4,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import SettingsScreen, { clientLoader } from "#/routes/settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
|
||||
// Mock the i18next hook
|
||||
vi.mock("react-i18next", async () => {
|
||||
@@ -93,7 +94,7 @@ describe("Settings Screen", () => {
|
||||
it("should render the navbar", async () => {
|
||||
const sectionsToInclude = ["llm", "integrations", "application", "secrets"];
|
||||
const sectionsToExclude = ["api keys", "credits", "billing"];
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
// @ts-expect-error - only return app mode
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
@@ -156,7 +157,7 @@ describe("Settings Screen", () => {
|
||||
});
|
||||
|
||||
it("should not be able to access saas-only routes in oss mode", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
// @ts-expect-error - only return app mode
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { openHands } from "../open-hands-axios";
|
||||
import { AuthenticateResponse, GitHubAccessTokenResponse } from "./auth.types";
|
||||
import { GetConfigResponse } from "../option-service/option.types";
|
||||
|
||||
/**
|
||||
* Authentication service for handling all authentication-related API calls
|
||||
*/
|
||||
class AuthService {
|
||||
/**
|
||||
* Authenticate with GitHub token
|
||||
* @param appMode The application mode (saas or oss)
|
||||
* @returns Response with authentication status and user info if successful
|
||||
*/
|
||||
static async authenticate(
|
||||
appMode: GetConfigResponse["APP_MODE"],
|
||||
): Promise<boolean> {
|
||||
if (appMode === "oss") return true;
|
||||
|
||||
// Just make the request, if it succeeds (no exception thrown), return true
|
||||
await openHands.post<AuthenticateResponse>("/api/authenticate");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get GitHub access token from Keycloak callback
|
||||
* @param code Code provided by GitHub
|
||||
* @returns GitHub access token
|
||||
*/
|
||||
static async getGitHubAccessToken(
|
||||
code: string,
|
||||
): Promise<GitHubAccessTokenResponse> {
|
||||
const { data } = await openHands.post<GitHubAccessTokenResponse>(
|
||||
"/api/keycloak/callback",
|
||||
{
|
||||
code,
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user from the application
|
||||
* @param appMode The application mode (saas or oss)
|
||||
*/
|
||||
static async logout(appMode: GetConfigResponse["APP_MODE"]): Promise<void> {
|
||||
const endpoint =
|
||||
appMode === "saas" ? "/api/logout" : "/api/unset-provider-tokens";
|
||||
await openHands.post(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthService;
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface AuthenticateResponse {
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface GitHubAccessTokenResponse {
|
||||
access_token: string;
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import { openHands } from "../open-hands-axios";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { GitRepository, PaginatedBranchesResponse, Branch } from "#/types/git";
|
||||
import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link";
|
||||
import { RepositoryMicroagent } from "#/types/microagent-management";
|
||||
import { MicroagentContentResponse } from "../open-hands.types";
|
||||
|
||||
/**
|
||||
* Git Service API - Handles all Git-related API endpoints
|
||||
*/
|
||||
class GitService {
|
||||
/**
|
||||
* Search for Git repositories
|
||||
* @param query Search query
|
||||
* @param per_page Number of results per page
|
||||
* @param selected_provider Git provider to search in
|
||||
* @returns List of matching repositories
|
||||
*/
|
||||
static async searchGitRepositories(
|
||||
query: string,
|
||||
per_page = 5,
|
||||
selected_provider?: Provider,
|
||||
): Promise<GitRepository[]> {
|
||||
const response = await openHands.get<GitRepository[]>(
|
||||
"/api/user/search/repositories",
|
||||
{
|
||||
params: {
|
||||
query,
|
||||
per_page,
|
||||
selected_provider,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve user's Git repositories
|
||||
* @param selected_provider Git provider
|
||||
* @param page Page number
|
||||
* @param per_page Number of results per page
|
||||
* @returns User's repositories with pagination info
|
||||
*/
|
||||
static async retrieveUserGitRepositories(
|
||||
selected_provider: Provider,
|
||||
page = 1,
|
||||
per_page = 30,
|
||||
) {
|
||||
const { data } = await openHands.get<GitRepository[]>(
|
||||
"/api/user/repositories",
|
||||
{
|
||||
params: {
|
||||
selected_provider,
|
||||
sort: "pushed",
|
||||
page,
|
||||
per_page,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const link =
|
||||
data.length > 0 && data[0].link_header ? data[0].link_header : "";
|
||||
const nextPage = extractNextPageFromLink(link);
|
||||
|
||||
return { data, nextPage };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve repositories from a specific installation
|
||||
* @param selected_provider Git provider
|
||||
* @param installationIndex Current installation index
|
||||
* @param installations List of installation IDs
|
||||
* @param page Page number
|
||||
* @param per_page Number of results per page
|
||||
* @returns Installation repositories with pagination info
|
||||
*/
|
||||
static async retrieveInstallationRepositories(
|
||||
selected_provider: Provider,
|
||||
installationIndex: number,
|
||||
installations: string[],
|
||||
page = 1,
|
||||
per_page = 30,
|
||||
) {
|
||||
const installationId = installations[installationIndex];
|
||||
const response = await openHands.get<GitRepository[]>(
|
||||
"/api/user/repositories",
|
||||
{
|
||||
params: {
|
||||
selected_provider,
|
||||
sort: "pushed",
|
||||
page,
|
||||
per_page,
|
||||
installation_id: installationId,
|
||||
},
|
||||
},
|
||||
);
|
||||
const link =
|
||||
response.data.length > 0 && response.data[0].link_header
|
||||
? response.data[0].link_header
|
||||
: "";
|
||||
const nextPage = extractNextPageFromLink(link);
|
||||
let nextInstallation: number | null;
|
||||
if (nextPage) {
|
||||
nextInstallation = installationIndex;
|
||||
} else if (installationIndex + 1 < installations.length) {
|
||||
nextInstallation = installationIndex + 1;
|
||||
} else {
|
||||
nextInstallation = null;
|
||||
}
|
||||
return {
|
||||
data: response.data,
|
||||
nextPage,
|
||||
installationIndex: nextInstallation,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository branches
|
||||
* @param repository Repository name
|
||||
* @param page Page number
|
||||
* @param perPage Number of results per page
|
||||
* @returns Paginated branches response
|
||||
*/
|
||||
static async getRepositoryBranches(
|
||||
repository: string,
|
||||
page: number = 1,
|
||||
perPage: number = 30,
|
||||
): Promise<PaginatedBranchesResponse> {
|
||||
const { data } = await openHands.get<PaginatedBranchesResponse>(
|
||||
`/api/user/repository/branches?repository=${encodeURIComponent(repository)}&page=${page}&per_page=${perPage}`,
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search repository branches
|
||||
* @param repository Repository name
|
||||
* @param query Search query
|
||||
* @param perPage Number of results per page
|
||||
* @param selectedProvider Git provider
|
||||
* @returns List of matching branches
|
||||
*/
|
||||
static async searchRepositoryBranches(
|
||||
repository: string,
|
||||
query: string,
|
||||
perPage: number = 30,
|
||||
selectedProvider?: Provider,
|
||||
): Promise<Branch[]> {
|
||||
const { data } = await openHands.get<Branch[]>(
|
||||
`/api/user/search/branches`,
|
||||
{
|
||||
params: {
|
||||
repository,
|
||||
query,
|
||||
per_page: perPage,
|
||||
selected_provider: selectedProvider,
|
||||
},
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available microagents for a repository
|
||||
* @param owner The repository owner
|
||||
* @param repo The repository name
|
||||
* @returns The available microagents for the repository
|
||||
*/
|
||||
static async getRepositoryMicroagents(
|
||||
owner: string,
|
||||
repo: string,
|
||||
): Promise<RepositoryMicroagent[]> {
|
||||
const { data } = await openHands.get<RepositoryMicroagent[]>(
|
||||
`/api/user/repository/${owner}/${repo}/microagents`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of a specific microagent from a repository
|
||||
* @param owner The repository owner
|
||||
* @param repo The repository name
|
||||
* @param filePath The path to the microagent file within the repository
|
||||
* @returns The microagent content and metadata
|
||||
*/
|
||||
static async getRepositoryMicroagentContent(
|
||||
owner: string,
|
||||
repo: string,
|
||||
filePath: string,
|
||||
): Promise<MicroagentContentResponse> {
|
||||
const { data } = await openHands.get<MicroagentContentResponse>(
|
||||
`/api/user/repository/${owner}/${repo}/microagents/content`,
|
||||
{
|
||||
params: { file_path: filePath },
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user installation IDs
|
||||
* @param provider The provider to get installation IDs for (github, bitbucket, etc.)
|
||||
* @returns List of installation IDs
|
||||
*/
|
||||
static async getUserInstallationIds(provider: Provider): Promise<string[]> {
|
||||
const { data } = await openHands.get<string[]>(
|
||||
`/api/user/installations?provider=${provider}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export default GitService;
|
||||
@@ -2,10 +2,7 @@ import { AxiosHeaders } from "axios";
|
||||
import {
|
||||
Feedback,
|
||||
FeedbackResponse,
|
||||
GitHubAccessTokenResponse,
|
||||
GetConfigResponse,
|
||||
GetVSCodeUrlResponse,
|
||||
AuthenticateResponse,
|
||||
Conversation,
|
||||
ResultSet,
|
||||
GetTrajectoryResponse,
|
||||
@@ -14,22 +11,13 @@ import {
|
||||
GetMicroagentsResponse,
|
||||
GetMicroagentPromptResponse,
|
||||
CreateMicroagent,
|
||||
MicroagentContentResponse,
|
||||
FileUploadSuccessResponse,
|
||||
GetFilesResponse,
|
||||
GetFileResponse,
|
||||
} from "./open-hands.types";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
import { ApiSettings, PostApiSettings, Provider } from "#/types/settings";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { SuggestedTask } from "#/utils/types";
|
||||
import {
|
||||
GitUser,
|
||||
GitRepository,
|
||||
PaginatedBranchesResponse,
|
||||
Branch,
|
||||
} from "#/types/git";
|
||||
import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link";
|
||||
import { RepositoryMicroagent } from "#/types/microagent-management";
|
||||
import { BatchFeedbackData } from "#/hooks/query/use-batch-feedback";
|
||||
import { SubscriptionAccess } from "#/types/billing";
|
||||
|
||||
@@ -66,42 +54,6 @@ class OpenHands {
|
||||
return `/api/conversations/${conversationId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the list of models available
|
||||
* @returns List of models available
|
||||
*/
|
||||
static async getModels(): Promise<string[]> {
|
||||
const { data } = await openHands.get<string[]>("/api/options/models");
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the list of agents available
|
||||
* @returns List of agents available
|
||||
*/
|
||||
static async getAgents(): Promise<string[]> {
|
||||
const { data } = await openHands.get<string[]>("/api/options/agents");
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the list of security analyzers available
|
||||
* @returns List of security analyzers available
|
||||
*/
|
||||
static async getSecurityAnalyzers(): Promise<string[]> {
|
||||
const { data } = await openHands.get<string[]>(
|
||||
"/api/options/security-analyzers",
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getConfig(): Promise<GetConfigResponse> {
|
||||
const { data } = await openHands.get<GetConfigResponse>(
|
||||
"/api/options/config",
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
static getConversationHeaders(): AxiosHeaders {
|
||||
const headers = new AxiosHeaders();
|
||||
const sessionApiKey = this.currentConversation?.session_api_key;
|
||||
@@ -210,20 +162,6 @@ class OpenHands {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate with GitHub token
|
||||
* @returns Response with authentication status and user info if successful
|
||||
*/
|
||||
static async authenticate(
|
||||
appMode: GetConfigResponse["APP_MODE"],
|
||||
): Promise<boolean> {
|
||||
if (appMode === "oss") return true;
|
||||
|
||||
// Just make the request, if it succeeds (no exception thrown), return true
|
||||
await openHands.post<AuthenticateResponse>("/api/authenticate");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the blob of the workspace zip
|
||||
* @returns Blob of the workspace zip
|
||||
@@ -249,22 +187,6 @@ class OpenHands {
|
||||
return Object.keys(response.data.hosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code Code provided by GitHub
|
||||
* @returns GitHub access token
|
||||
*/
|
||||
static async getGitHubAccessToken(
|
||||
code: string,
|
||||
): Promise<GitHubAccessTokenResponse> {
|
||||
const { data } = await openHands.post<GitHubAccessTokenResponse>(
|
||||
"/api/keycloak/callback",
|
||||
{
|
||||
code,
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the VSCode URL
|
||||
* @returns VSCode URL
|
||||
@@ -373,15 +295,11 @@ class OpenHands {
|
||||
conversationId: string,
|
||||
providers?: Provider[],
|
||||
): Promise<Conversation | null> {
|
||||
const err = new Error("Call stack:");
|
||||
console.log("startConversation...");
|
||||
console.log(err.stack);
|
||||
const { data } = await openHands.post<Conversation | null>(
|
||||
`/api/conversations/${conversationId}/start`,
|
||||
providers ? { providers_set: providers } : {},
|
||||
);
|
||||
|
||||
console.log(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -395,25 +313,6 @@ class OpenHands {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
const data = await openHands.post("/api/settings", settings);
|
||||
return data.status === 200;
|
||||
}
|
||||
|
||||
static async createCheckoutSession(amount: number): Promise<string> {
|
||||
const { data } = await openHands.post(
|
||||
"/api/billing/create-checkout-session",
|
||||
@@ -445,42 +344,6 @@ class OpenHands {
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getGitUser(): Promise<GitUser> {
|
||||
const response = await openHands.get<GitUser>("/api/user/info");
|
||||
|
||||
const { data } = response;
|
||||
|
||||
const user: GitUser = {
|
||||
id: data.id,
|
||||
login: data.login,
|
||||
avatar_url: data.avatar_url,
|
||||
company: data.company,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
};
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
static async searchGitRepositories(
|
||||
query: string,
|
||||
per_page = 5,
|
||||
selected_provider?: Provider,
|
||||
): Promise<GitRepository[]> {
|
||||
const response = await openHands.get<GitRepository[]>(
|
||||
"/api/user/search/repositories",
|
||||
{
|
||||
params: {
|
||||
query,
|
||||
per_page,
|
||||
selected_provider,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
static async getTrajectory(
|
||||
conversationId: string,
|
||||
): Promise<GetTrajectoryResponse> {
|
||||
@@ -491,12 +354,6 @@ class OpenHands {
|
||||
return data;
|
||||
}
|
||||
|
||||
static async logout(appMode: GetConfigResponse["APP_MODE"]): Promise<void> {
|
||||
const endpoint =
|
||||
appMode === "saas" ? "/api/logout" : "/api/unset-provider-tokens";
|
||||
await openHands.post(endpoint);
|
||||
}
|
||||
|
||||
static async getGitChanges(conversationId: string): Promise<GitChange[]> {
|
||||
const url = `${this.getConversationUrl(conversationId)}/git/changes`;
|
||||
const { data } = await openHands.get<GitChange[]>(url, {
|
||||
@@ -520,101 +377,6 @@ class OpenHands {
|
||||
/**
|
||||
* @returns A list of repositories
|
||||
*/
|
||||
static async retrieveUserGitRepositories(
|
||||
selected_provider: Provider,
|
||||
page = 1,
|
||||
per_page = 30,
|
||||
) {
|
||||
const { data } = await openHands.get<GitRepository[]>(
|
||||
"/api/user/repositories",
|
||||
{
|
||||
params: {
|
||||
selected_provider,
|
||||
sort: "pushed",
|
||||
page,
|
||||
per_page,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const link =
|
||||
data.length > 0 && data[0].link_header ? data[0].link_header : "";
|
||||
const nextPage = extractNextPageFromLink(link);
|
||||
|
||||
return { data, nextPage };
|
||||
}
|
||||
|
||||
static async retrieveInstallationRepositories(
|
||||
selected_provider: Provider,
|
||||
installationIndex: number,
|
||||
installations: string[],
|
||||
page = 1,
|
||||
per_page = 30,
|
||||
) {
|
||||
const installationId = installations[installationIndex];
|
||||
const response = await openHands.get<GitRepository[]>(
|
||||
"/api/user/repositories",
|
||||
{
|
||||
params: {
|
||||
selected_provider,
|
||||
sort: "pushed",
|
||||
page,
|
||||
per_page,
|
||||
installation_id: installationId,
|
||||
},
|
||||
},
|
||||
);
|
||||
const link =
|
||||
response.data.length > 0 && response.data[0].link_header
|
||||
? response.data[0].link_header
|
||||
: "";
|
||||
const nextPage = extractNextPageFromLink(link);
|
||||
let nextInstallation: number | null;
|
||||
if (nextPage) {
|
||||
nextInstallation = installationIndex;
|
||||
} else if (installationIndex + 1 < installations.length) {
|
||||
nextInstallation = installationIndex + 1;
|
||||
} else {
|
||||
nextInstallation = null;
|
||||
}
|
||||
return {
|
||||
data: response.data,
|
||||
nextPage,
|
||||
installationIndex: nextInstallation,
|
||||
};
|
||||
}
|
||||
|
||||
static async getRepositoryBranches(
|
||||
repository: string,
|
||||
page: number = 1,
|
||||
perPage: number = 30,
|
||||
): Promise<PaginatedBranchesResponse> {
|
||||
const { data } = await openHands.get<PaginatedBranchesResponse>(
|
||||
`/api/user/repository/branches?repository=${encodeURIComponent(repository)}&page=${page}&per_page=${perPage}`,
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async searchRepositoryBranches(
|
||||
repository: string,
|
||||
query: string,
|
||||
perPage: number = 30,
|
||||
selectedProvider?: Provider,
|
||||
): Promise<Branch[]> {
|
||||
const { data } = await openHands.get<Branch[]>(
|
||||
`/api/user/search/branches`,
|
||||
{
|
||||
params: {
|
||||
repository,
|
||||
query,
|
||||
per_page: perPage,
|
||||
selected_provider: selectedProvider,
|
||||
},
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available microagents associated with a conversation
|
||||
@@ -637,15 +399,6 @@ class OpenHands {
|
||||
* @param repo The repository name
|
||||
* @returns The available microagents for the repository
|
||||
*/
|
||||
static async getRepositoryMicroagents(
|
||||
owner: string,
|
||||
repo: string,
|
||||
): Promise<RepositoryMicroagent[]> {
|
||||
const { data } = await openHands.get<RepositoryMicroagent[]>(
|
||||
`/api/user/repository/${owner}/${repo}/microagents`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of a specific microagent from a repository
|
||||
@@ -654,19 +407,6 @@ class OpenHands {
|
||||
* @param filePath The path to the microagent file within the repository
|
||||
* @returns The microagent content and metadata
|
||||
*/
|
||||
static async getRepositoryMicroagentContent(
|
||||
owner: string,
|
||||
repo: string,
|
||||
filePath: string,
|
||||
): Promise<MicroagentContentResponse> {
|
||||
const { data } = await openHands.get<MicroagentContentResponse>(
|
||||
`/api/user/repository/${owner}/${repo}/microagents/content`,
|
||||
{
|
||||
params: { file_path: filePath },
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getMicroagentPrompt(
|
||||
conversationId: string,
|
||||
@@ -755,39 +495,6 @@ class OpenHands {
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user installation IDs
|
||||
* @param provider The provider to get installation IDs for (github, bitbucket, etc.)
|
||||
* @returns List of installation IDs
|
||||
*/
|
||||
static async getUserInstallationIds(provider: Provider): Promise<string[]> {
|
||||
const { data } = await openHands.get<string[]>(
|
||||
`/api/user/installations?provider=${provider}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getMicroagentManagementConversations(
|
||||
selectedRepository: string,
|
||||
pageId?: string,
|
||||
limit: number = 100,
|
||||
): Promise<Conversation[]> {
|
||||
const params: Record<string, string | number> = {
|
||||
limit,
|
||||
selected_repository: selectedRepository,
|
||||
};
|
||||
|
||||
if (pageId) {
|
||||
params.page_id = pageId;
|
||||
}
|
||||
|
||||
const { data } = await openHands.get<ResultSet<Conversation>>(
|
||||
"/api/microagent-management/conversations",
|
||||
{ params },
|
||||
);
|
||||
return data.results;
|
||||
}
|
||||
}
|
||||
|
||||
export default OpenHands;
|
||||
|
||||
@@ -26,10 +26,6 @@ export interface FeedbackResponse {
|
||||
body: FeedbackBodyResponse;
|
||||
}
|
||||
|
||||
export interface GitHubAccessTokenResponse {
|
||||
access_token: string;
|
||||
}
|
||||
|
||||
export interface AuthenticationResponse {
|
||||
message: string;
|
||||
login?: string; // Only present when allow list is enabled
|
||||
@@ -44,25 +40,6 @@ export interface Feedback {
|
||||
trajectory: unknown[];
|
||||
}
|
||||
|
||||
export interface GetConfigResponse {
|
||||
APP_MODE: "saas" | "oss";
|
||||
APP_SLUG?: string;
|
||||
GITHUB_CLIENT_ID: string;
|
||||
POSTHOG_CLIENT_KEY: string;
|
||||
PROVIDERS_CONFIGURED?: Provider[];
|
||||
AUTH_URL?: string;
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: boolean;
|
||||
HIDE_LLM_SETTINGS: boolean;
|
||||
ENABLE_JIRA: boolean;
|
||||
ENABLE_JIRA_DC: boolean;
|
||||
ENABLE_LINEAR: boolean;
|
||||
};
|
||||
MAINTENANCE?: {
|
||||
startTime: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GetVSCodeUrlResponse {
|
||||
vscode_url: string | null;
|
||||
error?: string;
|
||||
@@ -73,11 +50,6 @@ export interface GetTrajectoryResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface AuthenticateResponse {
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface RepositorySelection {
|
||||
selected_repository: string | null;
|
||||
selected_branch: string | null;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { openHands } from "../open-hands-axios";
|
||||
import { GetConfigResponse } from "./option.types";
|
||||
|
||||
/**
|
||||
* Service for handling API options endpoints
|
||||
*/
|
||||
class OptionService {
|
||||
/**
|
||||
* Retrieve the list of models available
|
||||
* @returns List of models available
|
||||
*/
|
||||
static async getModels(): Promise<string[]> {
|
||||
const { data } = await openHands.get<string[]>("/api/options/models");
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the list of agents available
|
||||
* @returns List of agents available
|
||||
*/
|
||||
static async getAgents(): Promise<string[]> {
|
||||
const { data } = await openHands.get<string[]>("/api/options/agents");
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the list of security analyzers available
|
||||
* @returns List of security analyzers available
|
||||
*/
|
||||
static async getSecurityAnalyzers(): Promise<string[]> {
|
||||
const { data } = await openHands.get<string[]>(
|
||||
"/api/options/security-analyzers",
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration from the server
|
||||
* @returns Configuration response
|
||||
*/
|
||||
static async getConfig(): Promise<GetConfigResponse> {
|
||||
const { data } = await openHands.get<GetConfigResponse>(
|
||||
"/api/options/config",
|
||||
);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export default OptionService;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
export interface GetConfigResponse {
|
||||
APP_MODE: "saas" | "oss";
|
||||
APP_SLUG?: string;
|
||||
GITHUB_CLIENT_ID: string;
|
||||
POSTHOG_CLIENT_KEY: string;
|
||||
PROVIDERS_CONFIGURED?: Provider[];
|
||||
AUTH_URL?: string;
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: boolean;
|
||||
HIDE_LLM_SETTINGS: boolean;
|
||||
ENABLE_JIRA: boolean;
|
||||
ENABLE_JIRA_DC: boolean;
|
||||
ENABLE_LINEAR: boolean;
|
||||
};
|
||||
MAINTENANCE?: {
|
||||
startTime: string;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { openHands } from "../open-hands-axios";
|
||||
import { GitUser } from "#/types/git";
|
||||
|
||||
/**
|
||||
* User Service API - Handles all user-related API endpoints
|
||||
*/
|
||||
class UserService {
|
||||
/**
|
||||
* Get the current user's Git information
|
||||
* @returns Git user information
|
||||
*/
|
||||
static async getUser(): Promise<GitUser> {
|
||||
const response = await openHands.get<GitUser>("/api/user/info");
|
||||
|
||||
const { data } = response;
|
||||
|
||||
const user: GitUser = {
|
||||
id: data.id,
|
||||
login: data.login,
|
||||
avatar_url: data.avatar_url,
|
||||
company: data.company,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
};
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
export default UserService;
|
||||
@@ -31,27 +31,21 @@ export function GitControlBarBranchButton({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
"group flex flex-row items-center justify-between gap-2 pl-2.5 pr-2.5 py-1 rounded-[100px] w-fit max-w-none flex-shrink-0 max-w-[108px] truncate relative",
|
||||
"group flex flex-row items-center justify-between gap-2 pl-2.5 pr-2.5 py-1 rounded-[100px] w-fit flex-shrink-0 max-w-[200px] truncate relative",
|
||||
hasBranch
|
||||
? "border border-[#525252] bg-transparent hover:border-[#454545] cursor-pointer"
|
||||
: "border border-[rgba(71,74,84,0.50)] bg-transparent cursor-not-allowed min-w-[108px]",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center justify-start">
|
||||
<div className="w-3 h-3 flex items-center justify-center">
|
||||
<BranchIcon width={12} height={12} color="white" />
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"font-normal text-white text-sm leading-5 truncate",
|
||||
hasBranch && "max-w-[70px]",
|
||||
)}
|
||||
title={buttonText}
|
||||
>
|
||||
{buttonText}
|
||||
</div>
|
||||
<div className="w-3 h-3 flex items-center justify-center flex-shrink-0">
|
||||
<BranchIcon width={12} height={12} color="white" />
|
||||
</div>
|
||||
<div
|
||||
className="font-normal text-white text-sm leading-5 truncate"
|
||||
title={buttonText}
|
||||
>
|
||||
{buttonText}
|
||||
</div>
|
||||
|
||||
{hasBranch && <GitExternalLinkIcon />}
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -33,32 +33,27 @@ export function GitControlBarRepoButton({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
"group flex flex-row items-center justify-between gap-2 pl-2.5 pr-2.5 py-1 rounded-[100px] w-fit flex-shrink-0 max-w-[170px] truncate relative",
|
||||
"group flex flex-row items-center justify-between gap-2 pl-2.5 pr-2.5 py-1 rounded-[100px] flex-1 truncate relative",
|
||||
hasRepository
|
||||
? "border border-[#525252] bg-transparent hover:border-[#454545] cursor-pointer"
|
||||
: "border border-[rgba(71,74,84,0.50)] bg-transparent cursor-not-allowed min-w-[170px]",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center justify-start">
|
||||
<div className="w-3 h-3 flex items-center justify-center">
|
||||
{hasRepository ? (
|
||||
<GitProviderIcon
|
||||
gitProvider={gitProvider as Provider}
|
||||
className="w-3 h-3 inline-flex"
|
||||
/>
|
||||
) : (
|
||||
<RepoForkedIcon width={12} height={12} color="white" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"font-normal text-white text-sm leading-5 truncate",
|
||||
hasRepository && "max-w-[100px]",
|
||||
)}
|
||||
title={buttonText}
|
||||
>
|
||||
{buttonText}
|
||||
</div>
|
||||
<div className="w-3 h-3 flex items-center justify-center flex-shrink-0">
|
||||
{hasRepository ? (
|
||||
<GitProviderIcon
|
||||
gitProvider={gitProvider as Provider}
|
||||
className="w-3 h-3 inline-flex"
|
||||
/>
|
||||
) : (
|
||||
<RepoForkedIcon width={12} height={12} color="white" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="font-normal text-white text-sm leading-5 truncate flex-1 min-w-0"
|
||||
title={buttonText}
|
||||
>
|
||||
{buttonText}
|
||||
</div>
|
||||
{hasRepository && <GitExternalLinkIcon />}
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface WaitingForRuntimeMessageProps {
|
||||
className?: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export function WaitingForRuntimeMessage({
|
||||
className,
|
||||
testId,
|
||||
}: WaitingForRuntimeMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid={testId}
|
||||
className={cn(
|
||||
"w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{t("DIFF_VIEWER$WAITING_FOR_RUNTIME")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -57,18 +57,13 @@ export function ConversationMain() {
|
||||
<PanelGroup
|
||||
direction="horizontal"
|
||||
className="grow h-full min-h-0 min-w-0"
|
||||
autoSaveId="react-resizable-panels:layout"
|
||||
>
|
||||
<Panel
|
||||
defaultSize={50}
|
||||
minSize={30}
|
||||
maxSize={80}
|
||||
className="overflow-hidden bg-base"
|
||||
>
|
||||
<Panel minSize={30} maxSize={80} className="overflow-hidden bg-base">
|
||||
<ChatInterfaceWrapper isRightPanelShown={isRightPanelShown} />
|
||||
</Panel>
|
||||
<PanelResizeHandle className="cursor-ew-resize" />
|
||||
<Panel
|
||||
defaultSize={50}
|
||||
minSize={20}
|
||||
maxSize={70}
|
||||
className="flex flex-col overflow-hidden"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
import JupyterIcon from "#/icons/jupyter.svg?react";
|
||||
import TerminalIcon from "#/icons/terminal.svg?react";
|
||||
import GlobeIcon from "#/icons/globe.svg?react";
|
||||
@@ -15,6 +16,7 @@ import { VSCodeTooltipContent } from "./vscode-tooltip-content";
|
||||
import {
|
||||
setHasRightPanelToggled,
|
||||
setSelectedTab,
|
||||
setIsRightPanelShown,
|
||||
type ConversationTab,
|
||||
} from "#/state/conversation-slice";
|
||||
import { RootState } from "#/store";
|
||||
@@ -28,10 +30,30 @@ export function ConversationTabs() {
|
||||
(state: RootState) => state.conversation,
|
||||
);
|
||||
|
||||
// Persist selectedTab and isRightPanelShown in localStorage
|
||||
const [persistedSelectedTab, setPersistedSelectedTab] =
|
||||
useLocalStorage<ConversationTab | null>(
|
||||
"conversation-selected-tab",
|
||||
"editor",
|
||||
);
|
||||
|
||||
const [persistedIsRightPanelShown, setPersistedIsRightPanelShown] =
|
||||
useLocalStorage<boolean>("conversation-right-panel-shown", true);
|
||||
|
||||
const onTabChange = (value: ConversationTab | null) => {
|
||||
dispatch(setSelectedTab(value));
|
||||
// Persist the selected tab to localStorage
|
||||
setPersistedSelectedTab(value);
|
||||
};
|
||||
|
||||
// Initialize Redux state from localStorage on component mount
|
||||
useEffect(() => {
|
||||
// Initialize selectedTab from localStorage if available
|
||||
dispatch(setSelectedTab(persistedSelectedTab));
|
||||
dispatch(setIsRightPanelShown(persistedIsRightPanelShown));
|
||||
dispatch(setHasRightPanelToggled(persistedIsRightPanelShown));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePanelVisibilityChange = () => {
|
||||
if (isRightPanelShown) {
|
||||
@@ -51,11 +73,13 @@ export function ConversationTabs() {
|
||||
if (selectedTab === tab && isRightPanelShown) {
|
||||
// If clicking the same active tab, close the drawer
|
||||
dispatch(setHasRightPanelToggled(false));
|
||||
setPersistedIsRightPanelShown(false);
|
||||
} else {
|
||||
// If clicking a different tab or drawer is closed, open drawer and select tab
|
||||
onTabChange(tab);
|
||||
if (!isRightPanelShown) {
|
||||
dispatch(setHasRightPanelToggled(true));
|
||||
setPersistedIsRightPanelShown(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
|
||||
export function useUrlSearch(inputValue: string, provider: Provider) {
|
||||
const [urlSearchResults, setUrlSearchResults] = useState<GitRepository[]>([]);
|
||||
@@ -16,7 +16,7 @@ export function useUrlSearch(inputValue: string, provider: Provider) {
|
||||
|
||||
setIsUrlSearchLoading(true);
|
||||
try {
|
||||
const repositories = await OpenHands.searchGitRepositories(
|
||||
const repositories = await GitService.searchGitRepositories(
|
||||
repoName,
|
||||
3,
|
||||
provider,
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function GuideMessage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="px-4 md:px-0 w-full flex items-center justify-center">
|
||||
<div className="w-fit flex items-center justify-center gap-1 px-[15px] rounded-[12px] bg-[#454545] leading-5 text-white text-[15px] font-normal md:h-9.5 m-1">
|
||||
<div className="pb-1 md:py-0">
|
||||
<span className="">{t("HOME$GUIDE_MESSAGE_TITLE")} </span>
|
||||
<a
|
||||
href="https://docs.all-hands.dev/usage/getting-started"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="underline">{t("COMMON$CLICK_HERE")}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { GuideMessage } from "./guide-message";
|
||||
|
||||
export function HomeHeader() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<header className="flex flex-col items-center">
|
||||
<GuideMessage />
|
||||
<div className="mt-12 flex flex-col gap-4 items-center">
|
||||
<div className="h-[80px] flex items-center">
|
||||
<span className="text-[32px] text-white font-bold leading-5">
|
||||
{t("HOME$LETS_START_BUILDING")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function GuideMessage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="w-fit flex flex-col md:flex-row items-start md:items-center justify-center gap-1 rounded-[12px] bg-[#454545] leading-5 text-white text-[15px] font-normal m-1 md:h-9.5 px-4 pb-1 md:px-[15px] md:py-0">
|
||||
<span className="">{t("HOME$GUIDE_MESSAGE_TITLE")} </span>
|
||||
<a
|
||||
href="https://docs.all-hands.dev/usage/getting-started"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="underline">{t("COMMON$CLICK_HERE")}</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
export function HomeHeaderTitle() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="h-[80px] flex items-center">
|
||||
<Typography.H1>{t("HOME$LETS_START_BUILDING")}</Typography.H1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { GuideMessage } from "./guide-message";
|
||||
import { HomeHeaderTitle } from "./home-header-title";
|
||||
|
||||
export function HomeHeader() {
|
||||
return (
|
||||
<header className="flex flex-col items-center gap-12">
|
||||
<GuideMessage />
|
||||
<HomeHeaderTitle />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bott
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import JupyterLargeIcon from "#/icons/jupyter-large.svg?react";
|
||||
import { WaitingForRuntimeMessage } from "../chat/waiting-for-runtime-message";
|
||||
|
||||
interface JupyterEditorProps {
|
||||
maxWidth: number;
|
||||
@@ -28,11 +29,7 @@ export function JupyterEditor({ maxWidth }: JupyterEditorProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isRuntimeInactive && (
|
||||
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
|
||||
{t("DIFF_VIEWER$WAITING_FOR_RUNTIME")}
|
||||
</div>
|
||||
)}
|
||||
{isRuntimeInactive && <WaitingForRuntimeMessage />}
|
||||
{!isRuntimeInactive && cells.length > 0 && (
|
||||
<div className="flex-1 h-full flex flex-col" style={{ maxWidth }}>
|
||||
<div
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RootState } from "#/store";
|
||||
import { useTerminal } from "#/hooks/use-terminal";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { WaitingForRuntimeMessage } from "../chat/waiting-for-runtime-message";
|
||||
|
||||
function Terminal() {
|
||||
const { commands } = useSelector((state: RootState) => state.cmd);
|
||||
@@ -12,19 +12,13 @@ function Terminal() {
|
||||
|
||||
const isRuntimeInactive = RUNTIME_INACTIVE_STATES.includes(curAgentState);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ref = useTerminal({
|
||||
commands,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col rounded-xl">
|
||||
{isRuntimeInactive && (
|
||||
<div className="w-full flex items-center text-center justify-center text-2xl text-tertiary-light pt-16">
|
||||
{t("DIFF_VIEWER$WAITING_FOR_RUNTIME")}
|
||||
</div>
|
||||
)}
|
||||
{isRuntimeInactive && <WaitingForRuntimeMessage className="pt-16" />}
|
||||
|
||||
<div className="flex-1 min-h-0 p-4">
|
||||
<div
|
||||
|
||||
@@ -9,7 +9,7 @@ import GitHubLogo from "#/assets/branding/github-logo.svg?react";
|
||||
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
|
||||
import BitbucketLogo from "#/assets/branding/bitbucket-logo.svg?react";
|
||||
import { useAuthUrl } from "#/hooks/use-auth-url";
|
||||
import { GetConfigResponse } from "#/api/open-hands.types";
|
||||
import { GetConfigResponse } from "#/api/option-service/option.types";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
interface AuthModalProps {
|
||||
|
||||
@@ -13,7 +13,7 @@ import posthog from "posthog-js";
|
||||
import "./i18n";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import store from "./store";
|
||||
import OpenHands from "./api/open-hands";
|
||||
import OptionService from "./api/option-service/option-service.api";
|
||||
import { displayErrorToast } from "./utils/custom-toast-handlers";
|
||||
import { queryClient } from "./query-client-config";
|
||||
|
||||
@@ -25,7 +25,7 @@ function PosthogInit() {
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const config = await OpenHands.getConfig();
|
||||
const config = await OptionService.getConfig();
|
||||
setPosthogClientKey(config.POSTHOG_CLIENT_KEY);
|
||||
} catch (error) {
|
||||
displayErrorToast("Error fetching PostHog client key");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import { MCPSSEServer, MCPStdioServer, MCPSHTTPServer } from "#/types/settings";
|
||||
|
||||
type MCPServerType = "sse" | "stdio" | "shttp";
|
||||
@@ -57,7 +57,7 @@ export function useAddMcpServer() {
|
||||
mcp_config: newConfig,
|
||||
};
|
||||
|
||||
await OpenHands.saveSettings(apiSettings);
|
||||
await SettingsService.saveSettings(apiSettings);
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate the settings query to trigger a refetch
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import { MCPConfig } from "#/types/settings";
|
||||
|
||||
export function useDeleteMcpServer() {
|
||||
@@ -27,7 +27,7 @@ export function useDeleteMcpServer() {
|
||||
mcp_config: newConfig,
|
||||
};
|
||||
|
||||
await OpenHands.saveSettings(apiSettings);
|
||||
await SettingsService.saveSettings(apiSettings);
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate the settings query to trigger a refetch
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import posthog from "posthog-js";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import AuthService from "#/api/auth-service/auth-service.api";
|
||||
import { useConfig } from "../query/use-config";
|
||||
import { clearLoginData } from "#/utils/local-storage";
|
||||
|
||||
@@ -9,7 +9,7 @@ export const useLogout = () => {
|
||||
const { data: config } = useConfig();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => OpenHands.logout(config?.APP_MODE ?? "oss"),
|
||||
mutationFn: () => AuthService.logout(config?.APP_MODE ?? "oss"),
|
||||
onSuccess: async () => {
|
||||
queryClient.removeQueries({ queryKey: ["tasks"] });
|
||||
queryClient.removeQueries({ queryKey: ["settings"] });
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import posthog from "posthog-js";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { PostSettings, PostApiSettings } from "#/types/settings";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import { PostSettings } from "#/types/settings";
|
||||
import { PostApiSettings } from "#/settings-service/settings.types";
|
||||
import { useSettings } from "../query/use-settings";
|
||||
|
||||
const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
|
||||
@@ -36,7 +37,7 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
|
||||
settings.GIT_USER_EMAIL?.trim() || DEFAULT_SETTINGS.GIT_USER_EMAIL,
|
||||
};
|
||||
|
||||
await OpenHands.saveSettings(apiSettings);
|
||||
await SettingsService.saveSettings(apiSettings);
|
||||
};
|
||||
|
||||
export const useSaveSettings = () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import { MCPSSEServer, MCPStdioServer, MCPSHTTPServer } from "#/types/settings";
|
||||
|
||||
type MCPServerType = "sse" | "stdio" | "shttp";
|
||||
@@ -59,7 +59,7 @@ export function useUpdateMcpServer() {
|
||||
mcp_config: newConfig,
|
||||
};
|
||||
|
||||
await OpenHands.saveSettings(apiSettings);
|
||||
await SettingsService.saveSettings(apiSettings);
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate the settings query to trigger a refetch
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
|
||||
const fetchAiConfigOptions = async () => ({
|
||||
models: await OpenHands.getModels(),
|
||||
agents: await OpenHands.getAgents(),
|
||||
securityAnalyzers: await OpenHands.getSecurityAnalyzers(),
|
||||
models: await OptionService.getModels(),
|
||||
agents: await OptionService.getAgents(),
|
||||
securityAnalyzers: await OptionService.getSecurityAnalyzers(),
|
||||
});
|
||||
|
||||
export const useAIConfigOptions = () =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useIsAuthed } from "./use-is-authed";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import { useUserProviders } from "../use-user-providers";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { shouldUseInstallationRepos } from "#/utils/utils";
|
||||
@@ -13,7 +13,7 @@ export const useAppInstallations = (selectedProvider: Provider | null) => {
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["installations", providers || [], selectedProvider],
|
||||
queryFn: () => OpenHands.getUserInstallationIds(selectedProvider!),
|
||||
queryFn: () => GitService.getUserInstallationIds(selectedProvider!),
|
||||
enabled:
|
||||
userIsAuthenticated &&
|
||||
!!selectedProvider &&
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
|
||||
|
||||
export const useConfig = () => {
|
||||
@@ -7,7 +7,7 @@ export const useConfig = () => {
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["config"],
|
||||
queryFn: OpenHands.getConfig,
|
||||
queryFn: OptionService.getConfig,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes,
|
||||
enabled: !isOnTosPage,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useUserProviders } from "../use-user-providers";
|
||||
import { useAppInstallations } from "./use-app-installations";
|
||||
import { GitRepository } from "../../types/git";
|
||||
import { Provider } from "../../types/settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import { shouldUseInstallationRepos } from "#/utils/utils";
|
||||
|
||||
interface UseGitRepositoriesOptions {
|
||||
@@ -60,7 +60,7 @@ export function useGitRepositories(options: UseGitRepositoriesOptions) {
|
||||
throw new Error("Missing installation list");
|
||||
}
|
||||
|
||||
return OpenHands.retrieveInstallationRepositories(
|
||||
return GitService.retrieveInstallationRepositories(
|
||||
provider,
|
||||
installationIndex || 0,
|
||||
installations,
|
||||
@@ -69,7 +69,7 @@ export function useGitRepositories(options: UseGitRepositoriesOptions) {
|
||||
);
|
||||
}
|
||||
|
||||
return OpenHands.retrieveUserGitRepositories(
|
||||
return GitService.retrieveUserGitRepositories(
|
||||
provider,
|
||||
pageParam as number,
|
||||
pageSize,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useConfig } from "./use-config";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import UserService from "#/api/user-service/user-service.api";
|
||||
import { useShouldShowUserFeatures } from "#/hooks/use-should-show-user-features";
|
||||
|
||||
export const useGitUser = () => {
|
||||
@@ -13,7 +13,7 @@ export const useGitUser = () => {
|
||||
|
||||
const user = useQuery({
|
||||
queryKey: ["user"],
|
||||
queryFn: OpenHands.getGitUser,
|
||||
queryFn: UserService.getUser,
|
||||
enabled: shouldFetchUser,
|
||||
retry: false,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAppInstallations } from "./use-app-installations";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useUserProviders } from "../use-user-providers";
|
||||
import { Provider } from "#/types/settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import { shouldUseInstallationRepos } from "#/utils/utils";
|
||||
|
||||
export const useInstallationRepositories = (
|
||||
@@ -31,7 +31,7 @@ export const useInstallationRepositories = (
|
||||
throw new Error("Missing installation list");
|
||||
}
|
||||
|
||||
return OpenHands.retrieveInstallationRepositories(
|
||||
return GitService.retrieveInstallationRepositories(
|
||||
selectedProvider!,
|
||||
installationIndex || 0,
|
||||
installations,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import AuthService from "#/api/auth-service/auth-service.api";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
|
||||
|
||||
@@ -15,7 +15,7 @@ export const useIsAuthed = () => {
|
||||
queryFn: async () => {
|
||||
try {
|
||||
// If in OSS mode or authentication succeeds, return true
|
||||
await OpenHands.authenticate(appMode!);
|
||||
await AuthService.authenticate(appMode!);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// If it's a 401 error, return false (not authenticated)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import MicroagentManagementService from "#/ui/microagent-management-service/microagent-management-service.api";
|
||||
|
||||
export const useMicroagentManagementConversations = (
|
||||
selectedRepository: string,
|
||||
@@ -16,7 +16,7 @@ export const useMicroagentManagementConversations = (
|
||||
selectedRepository,
|
||||
],
|
||||
queryFn: () =>
|
||||
OpenHands.getMicroagentManagementConversations(
|
||||
MicroagentManagementService.getMicroagentManagementConversations(
|
||||
selectedRepository,
|
||||
pageId,
|
||||
limit,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery, useInfiniteQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import { Branch, PaginatedBranchesResponse } from "#/types/git";
|
||||
|
||||
export const useRepositoryBranches = (repository: string | null) =>
|
||||
@@ -7,7 +7,7 @@ export const useRepositoryBranches = (repository: string | null) =>
|
||||
queryKey: ["repository", repository, "branches"],
|
||||
queryFn: async () => {
|
||||
if (!repository) return [];
|
||||
const response = await OpenHands.getRepositoryBranches(repository);
|
||||
const response = await GitService.getRepositoryBranches(repository);
|
||||
// Ensure we return an array even if the response is malformed
|
||||
return Array.isArray(response.branches) ? response.branches : [];
|
||||
},
|
||||
@@ -31,7 +31,7 @@ export const useRepositoryBranchesPaginated = (
|
||||
total_count: 0,
|
||||
};
|
||||
}
|
||||
return OpenHands.getRepositoryBranches(
|
||||
return GitService.getRepositoryBranches(
|
||||
repository,
|
||||
pageParam as number,
|
||||
perPage,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
|
||||
export const useRepositoryMicroagentContent = (
|
||||
owner: string,
|
||||
@@ -10,7 +10,7 @@ export const useRepositoryMicroagentContent = (
|
||||
useQuery({
|
||||
queryKey: ["repository", "microagent", "content", owner, repo, filePath],
|
||||
queryFn: () =>
|
||||
OpenHands.getRepositoryMicroagentContent(owner, repo, filePath),
|
||||
GitService.getRepositoryMicroagentContent(owner, repo, filePath),
|
||||
enabled: !!owner && !!repo && !!filePath,
|
||||
staleTime: cacheDisabled ? 0 : 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: cacheDisabled ? 0 : 1000 * 60 * 15, // 15 minutes
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
|
||||
export const useRepositoryMicroagents = (
|
||||
owner: string,
|
||||
@@ -8,7 +8,7 @@ export const useRepositoryMicroagents = (
|
||||
) =>
|
||||
useQuery({
|
||||
queryKey: ["repository", "microagents", owner, repo],
|
||||
queryFn: () => OpenHands.getRepositoryMicroagents(owner, repo),
|
||||
queryFn: () => GitService.getRepositoryMicroagents(owner, repo),
|
||||
enabled: !!owner && !!repo,
|
||||
staleTime: cacheDisabled ? 0 : 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: cacheDisabled ? 0 : 1000 * 60 * 15, // 15 minutes
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import { Branch } from "#/types/git";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
@@ -21,7 +21,7 @@ export function useSearchBranches(
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!repository || !query) return [];
|
||||
return OpenHands.searchRepositoryBranches(
|
||||
return GitService.searchRepositoryBranches(
|
||||
repository,
|
||||
query,
|
||||
perPage,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
export function useSearchRepositories(
|
||||
@@ -11,7 +11,7 @@ export function useSearchRepositories(
|
||||
return useQuery({
|
||||
queryKey: ["repositories", "search", query, selectedProvider, pageSize],
|
||||
queryFn: () =>
|
||||
OpenHands.searchGitRepositories(
|
||||
GitService.searchGitRepositories(
|
||||
query,
|
||||
pageSize,
|
||||
selectedProvider || undefined,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
|
||||
import { Settings } from "#/types/settings";
|
||||
import { useIsAuthed } from "./use-is-authed";
|
||||
|
||||
const getSettingsQueryFn = async (): Promise<Settings> => {
|
||||
const apiSettings = await OpenHands.getSettings();
|
||||
const apiSettings = await SettingsService.getSettings();
|
||||
|
||||
return {
|
||||
LLM_MODEL: apiSettings.llm_model,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useUserProviders } from "../use-user-providers";
|
||||
import { Provider } from "#/types/settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
import { shouldUseInstallationRepos } from "#/utils/utils";
|
||||
|
||||
export const useUserRepositories = (selectedProvider: Provider | null) => {
|
||||
@@ -12,7 +12,7 @@ export const useUserRepositories = (selectedProvider: Provider | null) => {
|
||||
const repos = useInfiniteQuery({
|
||||
queryKey: ["repositories", providers || [], selectedProvider],
|
||||
queryFn: async ({ pageParam }) =>
|
||||
OpenHands.retrieveUserGitRepositories(selectedProvider!, pageParam, 30),
|
||||
GitService.retrieveUserGitRepositories(selectedProvider!, pageParam, 30),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage,
|
||||
enabled:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { generateAuthUrl } from "#/utils/generate-auth-url";
|
||||
import { GetConfigResponse } from "#/api/open-hands.types";
|
||||
import { GetConfigResponse } from "#/api/option-service/option.types";
|
||||
|
||||
interface UseAuthUrlConfig {
|
||||
appMode: GetConfigResponse["APP_MODE"] | null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAuthUrl } from "./use-auth-url";
|
||||
import { GetConfigResponse } from "#/api/open-hands.types";
|
||||
import { GetConfigResponse } from "#/api/option-service/option.types";
|
||||
|
||||
interface UseGitHubAuthUrlConfig {
|
||||
appMode: GetConfigResponse["APP_MODE"] | null;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { delay, http, HttpResponse } from "msw";
|
||||
import {
|
||||
GetConfigResponse,
|
||||
Conversation,
|
||||
ResultSet,
|
||||
} from "#/api/open-hands.types";
|
||||
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 { ApiSettings, PostApiSettings, Provider } from "#/types/settings";
|
||||
import { Provider } from "#/types/settings";
|
||||
import {
|
||||
ApiSettings,
|
||||
PostApiSettings,
|
||||
} from "#/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";
|
||||
|
||||
@@ -46,11 +46,6 @@ function AppContent() {
|
||||
useDocumentTitleFromState();
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log(
|
||||
`isFetched: ${isFetched}, conversation: ${conversation}, isAuthed: ${isAuthed}`,
|
||||
);
|
||||
console.log("conversation: ", conversation);
|
||||
console.log(`conversation status: " ${conversation?.status}`);
|
||||
if (isFetched && !conversation && isAuthed) {
|
||||
displayErrorToast(
|
||||
"This conversation does not exist, or you do not have permission to access it.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { PrefetchPageLinks } from "react-router";
|
||||
import { HomeHeader } from "#/components/features/home/home-header";
|
||||
import { HomeHeader } from "#/components/features/home/home-header/home-header";
|
||||
import { RepoConnector } from "#/components/features/home/repo-connector";
|
||||
import { TaskSuggestions } from "#/components/features/home/tasks/task-suggestions";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { queryClient } from "#/query-client-config";
|
||||
import { GetConfigResponse } from "#/api/open-hands.types";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { GetConfigResponse } from "#/api/option-service/option.types";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import { MicroagentManagementContent } from "#/components/features/microagent-management/microagent-management-content";
|
||||
import { ConversationSubscriptionsProvider } from "#/context/conversation-subscriptions-provider";
|
||||
import { EventHandler } from "#/wrapper/event-handler";
|
||||
@@ -8,7 +8,7 @@ import { EventHandler } from "#/wrapper/event-handler";
|
||||
export const clientLoader = async () => {
|
||||
let config = queryClient.getQueryData<GetConfigResponse>(["config"]);
|
||||
if (!config) {
|
||||
config = await OpenHands.getConfig();
|
||||
config = await OptionService.getConfig();
|
||||
queryClient.setQueryData<GetConfigResponse>(["config"], config);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import { cn } from "#/utils/utils";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { Route } from "./+types/settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import OptionService from "#/api/option-service/option-service.api";
|
||||
import { queryClient } from "#/query-client-config";
|
||||
import { GetConfigResponse } from "#/api/open-hands.types";
|
||||
import { GetConfigResponse } from "#/api/option-service/option.types";
|
||||
import { useSubscriptionAccess } from "#/hooks/query/use-subscription-access";
|
||||
|
||||
const SAAS_ONLY_PATHS = [
|
||||
@@ -42,7 +42,7 @@ export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => {
|
||||
|
||||
let config = queryClient.getQueryData<GetConfigResponse>(["config"]);
|
||||
if (!config) {
|
||||
config = await OpenHands.getConfig();
|
||||
config = await OptionService.getConfig();
|
||||
queryClient.setQueryData<GetConfigResponse>(["config"], config);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { RootState } from "#/store";
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { useVSCodeUrl } from "#/hooks/query/use-vscode-url";
|
||||
import { VSCODE_IN_NEW_TAB } from "#/utils/feature-flags";
|
||||
import { WaitingForRuntimeMessage } from "#/components/features/chat/waiting-for-runtime-message";
|
||||
|
||||
function VSCodeTab() {
|
||||
const { t } = useTranslation();
|
||||
@@ -39,20 +40,8 @@ function VSCodeTab() {
|
||||
}
|
||||
};
|
||||
|
||||
if (isRuntimeInactive) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
|
||||
{t("DIFF_VIEWER$WAITING_FOR_RUNTIME")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
|
||||
{t("DIFF_VIEWER$WAITING_FOR_RUNTIME")}
|
||||
</div>
|
||||
);
|
||||
if (isRuntimeInactive || isLoading) {
|
||||
return <WaitingForRuntimeMessage />;
|
||||
}
|
||||
|
||||
if (error || (data && data.error) || !data?.url || iframeError) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { openHands } from "../api/open-hands-axios";
|
||||
import { ApiSettings, PostApiSettings } from "./settings.types";
|
||||
|
||||
/**
|
||||
* Settings service for managing application settings
|
||||
*/
|
||||
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");
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
const data = await openHands.post("/api/settings", settings);
|
||||
return data.status === 200;
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsService;
|
||||
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
};
|
||||
|
||||
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 })[];
|
||||
};
|
||||
};
|
||||
@@ -117,8 +117,6 @@ export const conversationSlice = createSlice({
|
||||
},
|
||||
// Reset conversation state (useful for cleanup)
|
||||
resetConversationState: (state) => {
|
||||
state.selectedTab = "editor";
|
||||
state.isRightPanelShown = true;
|
||||
state.shouldHideSuggestions = false;
|
||||
},
|
||||
setHasRightPanelToggled: (state, action) => {
|
||||
|
||||
@@ -63,47 +63,9 @@ export type Settings = {
|
||||
GIT_USER_EMAIL?: string;
|
||||
};
|
||||
|
||||
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 | MCPSSEServer)[];
|
||||
stdio_servers: MCPStdioServer[];
|
||||
shttp_servers: (string | MCPSHTTPServer)[];
|
||||
};
|
||||
email?: string;
|
||||
email_verified?: boolean;
|
||||
git_user_name?: string;
|
||||
git_user_email?: string;
|
||||
};
|
||||
|
||||
export type PostSettings = Settings & {
|
||||
user_consents_to_analytics: boolean | null;
|
||||
llm_api_key?: string | null;
|
||||
search_api_key?: string;
|
||||
mcp_config?: MCPConfig;
|
||||
};
|
||||
|
||||
export type PostApiSettings = ApiSettings & {
|
||||
user_consents_to_analytics: boolean | null;
|
||||
search_api_key?: string;
|
||||
mcp_config?: MCPConfig;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { openHands } from "#/api/open-hands-axios";
|
||||
import { Conversation, ResultSet } from "#/api/open-hands.types";
|
||||
|
||||
class MicroagentManagementService {
|
||||
/**
|
||||
* Get conversations for microagent management
|
||||
* @param selectedRepository The selected repository
|
||||
* @param pageId Optional page ID for pagination
|
||||
* @param limit Maximum number of conversations to return
|
||||
* @returns List of conversations
|
||||
*/
|
||||
static async getMicroagentManagementConversations(
|
||||
selectedRepository: string,
|
||||
pageId?: string,
|
||||
limit: number = 100,
|
||||
): Promise<Conversation[]> {
|
||||
const params: Record<string, string | number> = {
|
||||
limit,
|
||||
selected_repository: selectedRepository,
|
||||
};
|
||||
|
||||
if (pageId) {
|
||||
params.page_id = pageId;
|
||||
}
|
||||
|
||||
const { data } = await openHands.get<ResultSet<Conversation>>(
|
||||
"/api/microagent-management/conversations",
|
||||
{ params },
|
||||
);
|
||||
return data.results;
|
||||
}
|
||||
}
|
||||
|
||||
export default MicroagentManagementService;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
const typographyVariants = cva("", {
|
||||
variants: {
|
||||
variant: {
|
||||
h1: "text-[32px] text-white font-bold leading-5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "h1",
|
||||
},
|
||||
});
|
||||
|
||||
interface TypographyProps extends VariantProps<typeof typographyVariants> {
|
||||
className?: string;
|
||||
testId?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Typography({
|
||||
variant,
|
||||
className,
|
||||
testId,
|
||||
children,
|
||||
}: TypographyProps) {
|
||||
const Tag = variant as keyof React.JSX.IntrinsicElements;
|
||||
|
||||
return (
|
||||
<Tag
|
||||
data-testid={testId}
|
||||
className={cn(typographyVariants({ variant }), className)}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
// Export individual heading components for convenience
|
||||
export function H1({
|
||||
className,
|
||||
testId,
|
||||
children,
|
||||
}: Omit<TypographyProps, "variant">) {
|
||||
return (
|
||||
<Typography variant="h1" className={className} testId={testId}>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
// Attach H1 to Typography for the expected API
|
||||
Typography.H1 = H1;
|
||||
@@ -120,7 +120,7 @@ class GitHubResolverMixin(GitHubMixinBase):
|
||||
'first': 50,
|
||||
}
|
||||
if after_cursor:
|
||||
threads_variables['after'] = after_cursor
|
||||
threads_variables['after'] = after_cursor # type: ignore[unreachable]
|
||||
|
||||
threads_data = await self.execute_graphql_query(
|
||||
get_review_threads_graphql_query, threads_variables
|
||||
@@ -167,7 +167,7 @@ class GitHubResolverMixin(GitHubMixinBase):
|
||||
comments_variables['threadId'] = thread_id
|
||||
comments_variables['page'] = 50
|
||||
if after_cursor:
|
||||
comments_variables['after'] = after_cursor
|
||||
comments_variables['after'] = after_cursor # type: ignore[unreachable]
|
||||
|
||||
thread_comments_data = await self.execute_graphql_query(
|
||||
get_thread_comments_graphql_query, comments_variables
|
||||
|
||||
@@ -358,6 +358,11 @@ class ActionExecutor:
|
||||
|
||||
INIT_COMMANDS.append(no_pager_cmd)
|
||||
|
||||
# Hack: for some reason when you set the openhands user to anything but root, tmux changes out
|
||||
# of the mount directory on the first invocation.
|
||||
if self.user_id != 0:
|
||||
INIT_COMMANDS.append(f'cd {self._initial_cwd}')
|
||||
|
||||
logger.info(f'Initializing by running {len(INIT_COMMANDS)} bash commands...')
|
||||
for command in INIT_COMMANDS:
|
||||
action = CmdRunAction(command=command)
|
||||
|
||||
@@ -206,7 +206,9 @@ class BashSession:
|
||||
# else:
|
||||
window_command = _shell_command
|
||||
|
||||
logger.debug(f'Initializing bash session with command: {window_command}')
|
||||
logger.debug(
|
||||
f'Initializing bash session in {self.work_dir} with command: {window_command}'
|
||||
)
|
||||
session_name = f'openhands-{self.username}-{uuid.uuid4()}'
|
||||
self.session = self.server.new_session(
|
||||
session_name=session_name,
|
||||
@@ -331,6 +333,9 @@ class BashSession:
|
||||
|
||||
# Update the current working directory if it has changed
|
||||
if metadata.working_dir != self._cwd and metadata.working_dir:
|
||||
logger.debug(
|
||||
f'directory_changed: {self._cwd}; {metadata.working_dir}; {command}'
|
||||
)
|
||||
self._cwd = metadata.working_dir
|
||||
|
||||
logger.debug(f'COMMAND OUTPUT: {pane_content}')
|
||||
@@ -598,8 +603,12 @@ class BashSession:
|
||||
logger.debug(
|
||||
f'PANE CONTENT GOT after {time.time() - _start_time:.2f} seconds'
|
||||
)
|
||||
logger.debug(f'BEGIN OF PANE CONTENT: {cur_pane_output.split("\n")[:10]}')
|
||||
logger.debug(f'END OF PANE CONTENT: {cur_pane_output.split("\n")[-10:]}')
|
||||
cur_pane_lines = cur_pane_output.split('\n')
|
||||
if len(cur_pane_lines) <= 20:
|
||||
logger.debug('PANE_CONTENT: {cur_pane_output}')
|
||||
else:
|
||||
logger.debug(f'BEGIN OF PANE CONTENT: {cur_pane_lines[:10]}')
|
||||
logger.debug(f'END OF PANE CONTENT: {cur_pane_lines[-10:]}')
|
||||
ps1_matches = CmdOutputMetadata.matches_ps1_metadata(cur_pane_output)
|
||||
current_ps1_count = len(ps1_matches)
|
||||
|
||||
|
||||
@@ -567,7 +567,8 @@ class DockerNestedConversationManager(ConversationManager):
|
||||
env_vars['SERVE_FRONTEND'] = '0'
|
||||
env_vars['RUNTIME'] = 'local'
|
||||
# TODO: In the long term we may come up with a more secure strategy for user management within the nested runtime.
|
||||
env_vars['USER'] = 'root'
|
||||
env_vars['USER'] = 'openhands' if config.run_as_openhands else 'root'
|
||||
env_vars['SANDBOX_USER_ID'] = str(config.sandbox.user_id)
|
||||
env_vars['SESSION_API_KEY'] = self._get_session_api_key_for_conversation(sid)
|
||||
# We need to be able to specify the nested conversation id within the nested runtime
|
||||
env_vars['ALLOW_SET_CONVERSATION_ID'] = '1'
|
||||
|
||||
@@ -103,16 +103,12 @@ async def connect(connection_id: str, environ: dict) -> None:
|
||||
):
|
||||
continue
|
||||
elif isinstance(event, AgentStateChangedObservation):
|
||||
logger.debug(
|
||||
f'oh_event: AgentStateChangedObservation {event.agent_state}'
|
||||
)
|
||||
agent_state_changed = event
|
||||
else:
|
||||
await sio.emit('oh_event', event_to_dict(event), to=connection_id)
|
||||
|
||||
# Send the agent state changed event last if we have one
|
||||
if agent_state_changed:
|
||||
logger.debug(f'sending AgentStateChangedObservation {event.agent_state}')
|
||||
await sio.emit(
|
||||
'oh_event', event_to_dict(agent_state_changed), to=connection_id
|
||||
)
|
||||
@@ -125,9 +121,6 @@ async def connect(connection_id: str, environ: dict) -> None:
|
||||
user_id, conversation_id, providers_set
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f'conversation manager type {conversation_manager.__class__.__name__}'
|
||||
)
|
||||
agent_loop_info = await conversation_manager.join_conversation(
|
||||
conversation_id,
|
||||
connection_id,
|
||||
|
||||
@@ -570,7 +570,7 @@ async def stop_conversation(
|
||||
status='ok',
|
||||
conversation_id=conversation_id,
|
||||
message='Conversation stopped successfully',
|
||||
conversation_status=ConversationStatus.STOPPED,
|
||||
conversation_status=conversation_status,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
|
||||
Reference in New Issue
Block a user