From dd8a44231f88fac94adcd7c0488ae16e98880145 Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Mon, 4 May 2026 21:27:45 +0700 Subject: [PATCH] fix(frontend): remove SaaS and enterprise terminology (#88) * fix: remove SaaS and enterprise terminology * refactor: remove unrelated file --- .env.sample | 1 - AGENTS.md | 8 +- DEVELOPMENT.md | 3 - __tests__/MSW.md | 16 +- __tests__/api/option-service.test.ts | 5 +- __tests__/api/secrets-service.test.ts | 1 - .../components/chat/chat-interface.test.tsx | 2 +- .../chat/expandable-message.test.tsx | 34 - .../chat/message-display-continuity.test.tsx | 2 +- .../conversation/conversation-name.test.tsx | 289 +--- .../features/home/homepage-cta.test.tsx | 159 -- .../features/home/task-suggestions.test.tsx | 2 +- .../sdk-settings/sdk-section-page.test.tsx | 4 +- __tests__/components/user-actions.test.tsx | 2 +- .../chat/event-message-plan-preview.test.tsx | 2 +- .../chat/event-message-think-action.test.tsx | 2 +- .../mutation/use-add-git-providers.test.tsx | 1 - __tests__/hooks/use-git-user.test.tsx | 8 - .../hooks/use-settings-nav-items.test.tsx | 6 - __tests__/posthog-tracking.test.tsx | 158 -- __tests__/routes/launch.test.tsx | 2 +- __tests__/routes/root-layout-refetch.test.tsx | 3 - __tests__/routes/settings.test.tsx | 9 - __tests__/task-suggestions.test.tsx | 4 +- __tests__/utils/local-storage.test.ts | 61 - __tests__/utils/sdk-settings-schema.test.ts | 8 +- global.d.ts | 24 - package.json | 5 +- playwright.config.ts | 2 +- src/api/README.md | 8 +- src/api/agent-server-compatibility.ts | 2 +- src/api/config-service/config-service.api.ts | 18 +- .../conversation-service.api.ts | 13 +- .../v1-conversation-service.api.ts | 70 +- src/api/event-service/event-service.api.ts | 8 +- src/api/git-service/git-service.api.ts | 16 +- src/api/git-service/v1-git-service.api.ts | 8 +- src/api/open-hands-axios.ts | 15 +- src/api/option-service/option-service.api.ts | 6 - src/api/option-service/option.types.ts | 8 - .../pending-message-service.api.ts | 13 +- .../sandbox-service/sandbox-service.api.ts | 3 +- src/api/secrets-service.ts | 16 +- src/api/shared-conversation-service.api.ts | 7 +- src/api/typescript-client.ts | 3 +- .../features/chat/error-message-banner.tsx | 20 +- .../features/chat/expandable-message.tsx | 31 +- .../context-menu/context-menu-container.tsx | 2 +- .../conversation-repo-link.tsx | 3 +- .../conversation-name-context-menu.tsx | 56 - .../conversation/conversation-name.tsx | 6 - .../git-provider-dropdown.tsx | 2 - .../git-repo-dropdown/git-repo-dropdown.tsx | 33 +- .../git-repo-dropdown/use-repository-data.tsx | 1 - src/components/features/home/homepage-cta.tsx | 76 - .../features/home/tasks/task-suggestions.tsx | 5 +- .../configure-azure-devops-anchor.tsx | 37 - src/components/features/sidebar/sidebar.tsx | 2 - src/components/shared/navigation-link.tsx | 111 +- src/constants/onboarding.tsx | 78 - .../conversation-websocket-context.tsx | 54 +- src/hooks/mutation/use-create-conversation.ts | 3 +- src/hooks/mutation/use-refresh-llm-api-key.ts | 23 - src/hooks/mutation/use-submit-onboarding.ts | 41 - .../use-update-conversation-public-flag.ts | 70 - src/hooks/query/use-app-installations.ts | 4 +- src/hooks/query/use-git-repositories.ts | 4 +- src/hooks/query/use-git-user.ts | 5 +- src/hooks/query/use-llm-api-key.ts | 51 - src/hooks/query/use-settings.ts | 2 +- src/hooks/use-app-mode.ts | 43 - src/hooks/use-auth-url.ts | 20 - .../use-conversation-name-context-menu.ts | 46 - src/hooks/use-github-auth-url.ts | 14 - src/hooks/use-is-creating-conversation.ts | 2 +- src/hooks/use-tracking.ts | 90 +- src/i18n/translation.json | 1462 ----------------- src/library-env.d.ts | 24 - src/mocks/billing-handlers.ts | 22 - src/mocks/conversation-handlers.ts | 33 +- src/mocks/git-repository-handlers.ts | 5 +- src/mocks/handlers.ts | 2 - src/mocks/settings-handlers.ts | 13 - src/routes/app-settings.tsx | 61 - src/routes/home.tsx | 14 - src/routes/llm-settings.tsx | 171 +- src/routes/mcp-settings.tsx | 110 +- src/types/runtime-status.ts | 1 - src/types/settings.ts | 1 - src/ui/credits-chip.tsx | 30 - src/utils/error-handler.ts | 8 - src/utils/feature-flags.ts | 1 - src/utils/generate-auth-url.ts | 44 - src/utils/git-status-mapper.ts | 4 +- src/utils/local-storage.ts | 78 - src/utils/mutate-with-toast.ts | 2 +- src/utils/reo.ts | 106 -- src/utils/settings-utils.ts | 5 +- src/utils/utils.ts | 11 +- 99 files changed, 359 insertions(+), 3821 deletions(-) delete mode 100644 __tests__/components/features/home/homepage-cta.test.tsx delete mode 100644 __tests__/posthog-tracking.test.tsx delete mode 100644 src/components/features/home/homepage-cta.tsx delete mode 100644 src/components/features/settings/git-settings/configure-azure-devops-anchor.tsx delete mode 100644 src/constants/onboarding.tsx delete mode 100644 src/hooks/mutation/use-refresh-llm-api-key.ts delete mode 100644 src/hooks/mutation/use-submit-onboarding.ts delete mode 100644 src/hooks/mutation/use-update-conversation-public-flag.ts delete mode 100644 src/hooks/query/use-llm-api-key.ts delete mode 100644 src/hooks/use-app-mode.ts delete mode 100644 src/hooks/use-auth-url.ts delete mode 100644 src/hooks/use-github-auth-url.ts delete mode 100644 src/mocks/billing-handlers.ts delete mode 100644 src/ui/credits-chip.tsx delete mode 100644 src/utils/generate-auth-url.ts delete mode 100644 src/utils/reo.ts diff --git a/.env.sample b/.env.sample index 8e83b4a8b1..445b473ffe 100644 --- a/.env.sample +++ b/.env.sample @@ -17,5 +17,4 @@ VITE_INSECURE_SKIP_VERIFY="false" # Skip TLS certificate verification for proxie # Mocking / test helpers VITE_MOCK_API="false" # Enable/disable API mocking with MSW -VITE_MOCK_SAAS="false" # Simulate SaaS mode in development # VITE_GITHUB_TOKEN="" # GitHub token for repository access (used in some tests) diff --git a/AGENTS.md b/AGENTS.md index 4e6a223d0b..11584f732b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ - `VITE_WORKER_URLS` as a comma-separated list of browser worker URLs if you want the Browser tab to probe exposed app hosts. - `VITE_ENABLE_BROWSER_TOOLS=false` to omit `BrowserToolSet` from new conversation payloads. - Default working-dir fallback is now the relative path `workspace/project` (exported as `DEFAULT_WORKING_DIR` from `src/api/agent-server-config.ts`); git-path heuristics and the default PLAN preview path should reuse that constant instead of hardcoding `/workspace/project`. -- The UI keeps most OpenHands routes/layout intact, but SaaS/org/billing/integration behavior is intentionally hidden or stubbed via the fabricated OSS config because there is no separate app backend. +- The UI keeps most OpenHands routes/layout intact, but hosted-only behavior (org, account management, integrations) has been removed via the fabricated OSS config because there is no separate app backend. - Verification command: `npm run typecheck && npm run build`. - GitHub automation now includes `.github/workflows/ci.yml` for `npm ci`, `npm test`, and `npm run build`, plus `.github/dependabot.yml` with weekly npm/github-actions updates gated by a 7-day cooldown. - Direct `dependencies` and `devDependencies` in `package.json` are exact-pinned (no caret ranges); reproducible installs should use the committed `package-lock.json` plus `npm ci`, and targeted transitive fixes still belong in `overrides`. @@ -27,10 +27,10 @@ - Shared TypeScript-client adapters live in `src/api/typescript-client.ts`; prefer those helpers for agent-server-backed REST/workspace/event/VS Code calls before falling back to `open-hands-axios`. - Local verification/build gotchas: - `npm run typecheck` assumes generated translation types exist; run `npm run make-i18n` first if `src/i18n/declaration.ts` is missing. -- Phase-1 OSS cleanup removed SaaS-only auth/org/billing/onboarding/payment/invitation codepaths, routes, and tests. Keep `integrations`, `git-settings`, `secrets`, MCP settings, and other local/self-hosted flows intact when simplifying OSS behavior. -- When merging main into the Phase-1 OSS cleanup branch, keep the new agent-server compatibility bootstrap in `src/root.tsx`, but do not reintroduce SaaS invitation cleanup or enterprise CTA chrome in the OSS user menu; the OSS account menu should just render settings links plus Docs. +- The OSS cleanup removed hosted-only auth, org, account, onboarding, and invitation codepaths, routes, and tests. Keep `integrations`, `git-settings`, `secrets`, MCP settings, and other local/self-hosted flows intact when simplifying OSS behavior. +- When merging main into this branch, keep the new agent-server compatibility bootstrap in `src/root.tsx`, but do not reintroduce hosted-only invitation cleanup or marketing CTA chrome in the OSS user menu; the OSS account menu should just render settings links plus Docs. -- During the Phase-1 OSS cleanup audit, the runtime SaaS removals held up, but route-level regression coverage for still-active OSS settings pages had been deleted too aggressively. Keep focused tests for local/self-hosted screens like `app-settings`, `llm-settings`, `git-settings`, `mcp-settings`, and `secrets-settings` even when stripping SaaS-only code. +- During the OSS cleanup audit, the runtime removals held up, but route-level regression coverage for still-active OSS settings pages had been deleted too aggressively. Keep focused tests for local/self-hosted screens like `app-settings`, `llm-settings`, `git-settings`, `mcp-settings`, and `secrets-settings` even when stripping hosted-only code. - `npm run dev:mock` needs MSW handlers for the direct agent-server routes used by the adapted frontend, not the original OpenHands mock paths. Key routes that must stay covered are: - bootstrap/model loading: `/server_info`, `/api/llm/models/verified`, `/api/llm/providers` diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 60f43a9ec8..0714901f27 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -49,8 +49,6 @@ If you want to run the frontend without a live backend, use: ```sh npm run dev:mock -# or -npm run dev:mock:saas ``` ## Build and test @@ -108,7 +106,6 @@ You can create a `.env` file in the project directory with these variables based | `VITE_WORKER_URLS` | Optional comma-separated worker/app URLs for the Browser tab | - | | `VITE_ENABLE_BROWSER_TOOLS` | Set to `false` to omit `BrowserToolSet` from new conversation payloads | `true` | | `VITE_MOCK_API` | Enable/disable API mocking with MSW | `false` | -| `VITE_MOCK_SAAS` | Simulate SaaS mode in development | `false` | | `VITE_USE_TLS` | Use HTTPS/WSS for the Vite proxy target | `false` | | `VITE_FRONTEND_PORT` | Port to run the frontend application | `3001` | | `VITE_INSECURE_SKIP_VERIFY` | Skip TLS certificate verification for proxied backend requests | `false` | diff --git a/__tests__/MSW.md b/__tests__/MSW.md index 828c2c6902..8cd0afb5f7 100644 --- a/__tests__/MSW.md +++ b/__tests__/MSW.md @@ -13,7 +13,7 @@ The same mock handlers work in both environments, so you write them once and reu ## Relevant Files - `src/mocks/handlers.ts` - Main handler registry that combines all domain handlers -- `src/mocks/*-handlers.ts` - Domain-specific handlers (auth, billing, conversation, etc.) +- `src/mocks/*-handlers.ts` - Domain-specific handlers (auth, conversation, etc.) - `src/mocks/browser.ts` - Browser setup for development mode - `src/mocks/node.ts` - Node.js setup for tests - `vitest.setup.ts` - Global test setup with MSW lifecycle hooks @@ -25,21 +25,9 @@ The same mock handlers work in both environments, so you write them once and reu ```sh # Run with API mocking enabled npm run dev:mock - -# Run with API mocking + SaaS mode simulation -npm run dev:mock:saas ``` -These commands set `VITE_MOCK_API=true` which activates the MSW Service Worker to intercept requests. - -> [!NOTE] -> **OpenHands vs SaaS Mode** -> -> OpenHands runs in two modes: -> - **OpenHands mode**: For local/self-hosted deployments where users provide their own LLM API keys and configure git providers manually -> - **SaaS mode**: For the cloud offering with billing, managed API keys, and OAuth-based GitHub integration -> -> Use `dev:mock:saas` when working on SaaS-specific features like billing, API key management, or subscription flows. +This command sets `VITE_MOCK_API=true` which activates the MSW Service Worker to intercept requests. ## Writing Tests diff --git a/__tests__/api/option-service.test.ts b/__tests__/api/option-service.test.ts index d02673781e..7db1b1c92f 100644 --- a/__tests__/api/option-service.test.ts +++ b/__tests__/api/option-service.test.ts @@ -12,8 +12,6 @@ describe("OptionService", () => { it("returns config in mock mode without a live backend", async () => { const config = await OptionService.getConfig(); - expect(config.app_mode).toBe("oss"); - expect(config.feature_flags.deployment_mode).toBe("self_hosted"); expect(config.feature_flags.hide_integrations_page).toBe(false); expect(config.updated_at).toBeTruthy(); }); @@ -60,9 +58,8 @@ describe("OptionService", () => { ); await expect(OptionService.getConfig()).resolves.toMatchObject({ - app_mode: "oss", feature_flags: expect.objectContaining({ - deployment_mode: "self_hosted", + hide_integrations_page: false, }), }); }); diff --git a/__tests__/api/secrets-service.test.ts b/__tests__/api/secrets-service.test.ts index fb588c98a7..a9a6b365b9 100644 --- a/__tests__/api/secrets-service.test.ts +++ b/__tests__/api/secrets-service.test.ts @@ -12,7 +12,6 @@ const buildProviders = ( bitbucket_data_center: { token: "", host: null }, azure_devops: { token: "", host: null }, forgejo: { token: "", host: null }, - enterprise_sso: { token: "", host: null }, ...overrides, }); diff --git a/__tests__/components/chat/chat-interface.test.tsx b/__tests__/components/chat/chat-interface.test.tsx index 52a6fbcd98..21919a1394 100644 --- a/__tests__/components/chat/chat-interface.test.tsx +++ b/__tests__/components/chat/chat-interface.test.tsx @@ -127,7 +127,7 @@ describe("ChatInterface - Chat Suggestions", () => { }); (useConfig as unknown as ReturnType).mockReturnValue({ - data: { app_mode: "local" }, + data: {}, }); (useGetTrajectory as unknown as ReturnType).mockReturnValue({ mutate: vi.fn(), diff --git a/__tests__/components/chat/expandable-message.test.tsx b/__tests__/components/chat/expandable-message.test.tsx index 96f5f8ee4a..b058cc56ab 100644 --- a/__tests__/components/chat/expandable-message.test.tsx +++ b/__tests__/components/chat/expandable-message.test.tsx @@ -1,9 +1,7 @@ import { describe, expect, it, vi } from "vitest"; 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 OptionService from "#/api/option-service/option-service.api"; vi.mock("react-i18next", async () => { const actual = await vi.importActual("react-i18next"); @@ -111,36 +109,4 @@ describe("ExpandableMessage", () => { expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument(); }); - it("should render the out of credits message when the user is out of credits", async () => { - const getConfigSpy = vi.spyOn(OptionService, "getConfig"); - // @ts-expect-error - partial mock for testing - getConfigSpy.mockResolvedValue({ - app_mode: "saas", - feature_flags: { - enable_billing: true, - hide_llm_settings: false, - enable_jira: false, - enable_jira_dc: false, - enable_linear: false, - hide_users_page: false, - hide_billing_page: false, - hide_integrations_page: false, - }, - }); - const RouterStub = createRoutesStub([ - { - Component: () => ( - - ), - path: "/", - }, - ]); - - renderWithProviders(); - await screen.findByTestId("out-of-credits"); - }); }); diff --git a/__tests__/components/chat/message-display-continuity.test.tsx b/__tests__/components/chat/message-display-continuity.test.tsx index 1ff8b4fc70..9e35e5e045 100644 --- a/__tests__/components/chat/message-display-continuity.test.tsx +++ b/__tests__/components/chat/message-display-continuity.test.tsx @@ -124,7 +124,7 @@ describe("ChatInterface – message display continuity (spec 3.1)", () => { }); (useConfig as unknown as ReturnType).mockReturnValue({ - data: { app_mode: "local" }, + data: {}, }); (useGetTrajectory as unknown as ReturnType).mockReturnValue({ mutate: vi.fn(), diff --git a/__tests__/components/features/conversation/conversation-name.test.tsx b/__tests__/components/features/conversation/conversation-name.test.tsx index d3f82427cf..8f0b321078 100644 --- a/__tests__/components/features/conversation/conversation-name.test.tsx +++ b/__tests__/components/features/conversation/conversation-name.test.tsx @@ -31,9 +31,7 @@ const { }, })), useConfigMock: vi.fn(() => ({ - data: { - app_mode: "oss", - }, + data: {}, })), })); @@ -579,288 +577,3 @@ describe("ConversationNameContextMenu", () => { }); }); -describe("ConversationNameContextMenu - Share Link Functionality", () => { - const mockWriteText = vi.fn().mockResolvedValue(undefined); - - const mockOnCopyShareLink = vi.fn(); - const mockOnTogglePublic = vi.fn(); - const mockOnClose = vi.fn(); - - const defaultProps = { - onClose: mockOnClose, - onTogglePublic: mockOnTogglePublic, - onCopyShareLink: mockOnCopyShareLink, - shareUrl: "https://example.com/shared/conversations/test-id", - }; - - vi.mock("#/hooks/mutation/use-update-conversation-public-flag", () => ({ - useUpdateConversationPublicFlag: () => ({ - mutate: vi.fn(), - }), - })); - - beforeAll(() => { - // Mock navigator.clipboard - Object.defineProperty(navigator, "clipboard", { - value: { - writeText: mockWriteText, - readText: vi.fn(), - }, - writable: true, - configurable: true, - }); - }); - - beforeEach(() => { - mockWriteText.mockClear(); - mockDisplaySuccessToast.mockClear(); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it("should display copy and open buttons when conversation is public", () => { - // Arrange - useActiveConversationMock.mockReturnValue({ - data: { - conversation_id: "test-id", - title: "Test Conversation", - status: "STOPPED", - conversation_version: "V1" as const, - public: true, - } as Conversation, - }); - - useConfigMock.mockReturnValue({ - data: { - app_mode: "saas", - }, - }); - - // Act - renderWithProviders(); - - // Assert - expect(screen.getByTestId("copy-share-link-button")).toBeInTheDocument(); - expect(screen.getByTestId("open-share-link-button")).toBeInTheDocument(); - }); - - it("should not display share buttons when conversation is not public", () => { - // Arrange - useActiveConversationMock.mockReturnValue({ - data: { - conversation_id: "test-id", - title: "Test Conversation", - status: "STOPPED", - conversation_version: "V1" as const, - public: false, - } as Conversation, - }); - - useConfigMock.mockReturnValue({ - data: { - app_mode: "saas", - }, - }); - - // Act - renderWithProviders(); - - // Assert - expect( - screen.queryByTestId("copy-share-link-button"), - ).not.toBeInTheDocument(); - expect( - screen.queryByTestId("open-share-link-button"), - ).not.toBeInTheDocument(); - }); - - it("should call copy handler when copy button is clicked", async () => { - // Arrange - const user = userEvent.setup(); - const shareUrl = "https://example.com/shared/conversations/test-id"; - - useActiveConversationMock.mockReturnValue({ - data: { - conversation_id: "test-id", - title: "Test Conversation", - status: "STOPPED", - conversation_version: "V1" as const, - public: true, - } as Conversation, - }); - - useConfigMock.mockReturnValue({ - data: { - app_mode: "saas", - }, - }); - - renderWithProviders( - , - ); - - const copyButton = screen.getByTestId("copy-share-link-button"); - - // Act - await user.click(copyButton); - - // Assert - expect(mockOnCopyShareLink).toHaveBeenCalledTimes(1); - }); - - it("should have correct attributes for open share link button", () => { - // Arrange - const shareUrl = "https://example.com/shared/conversations/test-id"; - - useActiveConversationMock.mockReturnValue({ - data: { - conversation_id: "test-id", - title: "Test Conversation", - status: "STOPPED", - conversation_version: "V1" as const, - public: true, - } as Conversation, - }); - - useConfigMock.mockReturnValue({ - data: { - app_mode: "saas", - }, - }); - - renderWithProviders( - , - ); - - const openButton = screen.getByTestId("open-share-link-button"); - - // Assert - expect(openButton).toHaveAttribute("href", shareUrl); - expect(openButton).toHaveAttribute("target", "_blank"); - expect(openButton).toHaveAttribute("rel", "noopener noreferrer"); - }); - - it("should display correct tooltips for share buttons", () => { - // Arrange - useActiveConversationMock.mockReturnValue({ - data: { - conversation_id: "test-id", - title: "Test Conversation", - status: "STOPPED", - conversation_version: "V1" as const, - public: true, - } as Conversation, - }); - - useConfigMock.mockReturnValue({ - data: { - app_mode: "saas", - }, - }); - - renderWithProviders(); - - // Assert - const copyButton = screen.getByTestId("copy-share-link-button"); - const openButton = screen.getByTestId("open-share-link-button"); - - expect(copyButton).toHaveAttribute("title", "Copy to Clipboard"); - expect(openButton).toHaveAttribute("title", "Open in New Tab"); - }); - - describe("Integration with ConversationName component", () => { - beforeEach(() => { - // Default mocks for public V1 conversation in SAAS mode - useActiveConversationMock.mockReturnValue({ - data: { - conversation_id: "test-conversation-id", - title: "Test Conversation", - status: "STOPPED", - conversation_version: "V1" as const, - public: true, - } as Conversation, - }); - - useConfigMock.mockReturnValue({ - data: { - app_mode: "saas", - }, - }); - }); - - it("should copy share URL to clipboard and show success toast when copy button is clicked through ConversationName", async () => { - // Arrange - const user = userEvent.setup(); - const expectedUrl = - "http://localhost:3000/shared/conversations/test-conversation-id"; - - // Ensure navigator.clipboard is properly mocked - if (!navigator.clipboard) { - Object.defineProperty(navigator, "clipboard", { - value: { - writeText: mockWriteText, - readText: vi.fn(), - }, - writable: true, - configurable: true, - }); - } else { - vi.spyOn(navigator.clipboard, "writeText").mockImplementation( - mockWriteText, - ); - } - - renderConversationNameWithRouter(); - - // Open context menu by clicking ellipsis - const ellipsisButton = screen.getByRole("button", { hidden: true }); - await user.click(ellipsisButton); - - // Wait for context menu to appear and find share publicly button - const sharePubliclyButton = await screen.findByTestId( - "share-publicly-button", - ); - expect(sharePubliclyButton).toBeInTheDocument(); - - // Find copy button - const copyButton = screen.getByTestId("copy-share-link-button"); - - // Act - await user.click(copyButton); - - // Assert - clipboard.writeText is async, so we need to wait - await waitFor( - () => { - expect(mockWriteText).toHaveBeenCalledWith(expectedUrl); - expect(mockDisplaySuccessToast).toHaveBeenCalledWith( - "Link copied to clipboard", - ); - }, - { timeout: 2000, container: document.body }, - ); - }); - - it("should show both copy and open buttons when conversation is public", async () => { - // Arrange - const user = userEvent.setup(); - - renderConversationNameWithRouter(); - - // Act - open context menu - const ellipsisButton = screen.getByRole("button", { hidden: true }); - await user.click(ellipsisButton); - - // Wait for context menu - const sharePubliclyButton = await screen.findByTestId( - "share-publicly-button", - ); - - // Assert - expect(sharePubliclyButton).toBeInTheDocument(); - expect(screen.getByTestId("copy-share-link-button")).toBeInTheDocument(); - expect(screen.getByTestId("open-share-link-button")).toBeInTheDocument(); - }); - }); -}); diff --git a/__tests__/components/features/home/homepage-cta.test.tsx b/__tests__/components/features/home/homepage-cta.test.tsx deleted file mode 100644 index f44f6bd93b..0000000000 --- a/__tests__/components/features/home/homepage-cta.test.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { describe, expect, it, vi, beforeEach } from "vitest"; -import { HomepageCTA } from "#/components/features/home/homepage-cta"; - -// Mock the translation function -vi.mock("react-i18next", async () => { - const actual = await vi.importActual("react-i18next"); - return { - ...actual, - useTranslation: () => ({ - t: (key: string) => { - const translations: Record = { - "CTA$ENTERPRISE_TITLE": "Get OpenHands for Enterprise", - "CTA$ENTERPRISE_DESCRIPTION": - "Cloud allows you to access OpenHands anywhere and coordinate with your team like never before", - "CTA$LEARN_MORE": "Learn More", - }; - return translations[key] || key; - }, - i18n: { language: "en" }, - }), - }; -}); - -// Mock local storage -vi.mock("#/utils/local-storage", () => ({ - setCTADismissed: vi.fn(), -})); - -// Mock useTracking hook -const mockTrackSaasSelfhostedInquiry = vi.fn(); -vi.mock("#/hooks/use-tracking", () => ({ - useTracking: () => ({ - trackSaasSelfhostedInquiry: mockTrackSaasSelfhostedInquiry, - }), -})); - -import { setCTADismissed } from "#/utils/local-storage"; - -describe("HomepageCTA", () => { - const mockSetShouldShowCTA = vi.fn(); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - const renderHomepageCTA = () => { - return render(); - }; - - describe("rendering", () => { - it("renders the enterprise title", () => { - renderHomepageCTA(); - expect( - screen.getByText("Get OpenHands for Enterprise"), - ).toBeInTheDocument(); - }); - - it("renders the enterprise description", () => { - renderHomepageCTA(); - expect( - screen.getByText(/Cloud allows you to access OpenHands anywhere/), - ).toBeInTheDocument(); - }); - - it("renders the Learn More link", () => { - renderHomepageCTA(); - const link = screen.getByRole("link", { name: "Learn More" }); - expect(link).toBeInTheDocument(); - }); - - it("renders the close button with correct aria-label", () => { - renderHomepageCTA(); - expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument(); - }); - }); - - describe("close button behavior", () => { - it("calls setCTADismissed with 'homepage' when close button is clicked", async () => { - const user = userEvent.setup(); - renderHomepageCTA(); - - const closeButton = screen.getByRole("button", { name: "Close" }); - await user.click(closeButton); - - expect(setCTADismissed).toHaveBeenCalledWith("homepage"); - }); - - it("calls setShouldShowCTA with false when close button is clicked", async () => { - const user = userEvent.setup(); - renderHomepageCTA(); - - const closeButton = screen.getByRole("button", { name: "Close" }); - await user.click(closeButton); - - expect(mockSetShouldShowCTA).toHaveBeenCalledWith(false); - }); - - it("calls both setCTADismissed and setShouldShowCTA in order", async () => { - const user = userEvent.setup(); - const callOrder: string[] = []; - - vi.mocked(setCTADismissed).mockImplementation(() => { - callOrder.push("setCTADismissed"); - }); - mockSetShouldShowCTA.mockImplementation(() => { - callOrder.push("setShouldShowCTA"); - }); - - renderHomepageCTA(); - - const closeButton = screen.getByRole("button", { name: "Close" }); - await user.click(closeButton); - - expect(callOrder).toEqual(["setCTADismissed", "setShouldShowCTA"]); - }); - }); - - describe("Learn More link behavior", () => { - it("calls trackSaasSelfhostedInquiry with location 'home_page' when clicked", async () => { - const user = userEvent.setup(); - renderHomepageCTA(); - - const learnMoreLink = screen.getByRole("link", { name: "Learn More" }); - await user.click(learnMoreLink); - - expect(mockTrackSaasSelfhostedInquiry).toHaveBeenCalledWith({ - location: "home_page", - }); - }); - - it("has correct href and target attributes", () => { - renderHomepageCTA(); - - const learnMoreLink = screen.getByRole("link", { name: "Learn More" }); - expect(learnMoreLink).toHaveAttribute( - "href", - "https://openhands.dev/enterprise/", - ); - expect(learnMoreLink).toHaveAttribute("target", "_blank"); - expect(learnMoreLink).toHaveAttribute("rel", "noopener noreferrer"); - }); - }); - - describe("accessibility", () => { - it("close button is focusable", () => { - renderHomepageCTA(); - const closeButton = screen.getByRole("button", { name: "Close" }); - expect(closeButton).not.toHaveAttribute("tabindex", "-1"); - }); - - it("Learn More link is focusable", () => { - renderHomepageCTA(); - const learnMoreLink = screen.getByRole("link", { name: "Learn More" }); - expect(learnMoreLink).not.toHaveAttribute("tabindex", "-1"); - }); - }); -}); diff --git a/__tests__/components/features/home/task-suggestions.test.tsx b/__tests__/components/features/home/task-suggestions.test.tsx index 4bfe083e97..1dffe4c284 100644 --- a/__tests__/components/features/home/task-suggestions.test.tsx +++ b/__tests__/components/features/home/task-suggestions.test.tsx @@ -30,7 +30,7 @@ vi.mock("#/hooks/query/use-is-authed", () => ({ vi.mock("#/hooks/query/use-config", () => ({ useConfig: () => ({ - data: { app_mode: "saas" }, + data: {}, isLoading: false, }), })); diff --git a/__tests__/components/features/settings/sdk-settings/sdk-section-page.test.tsx b/__tests__/components/features/settings/sdk-settings/sdk-section-page.test.tsx index e574b03598..44fb0f7114 100644 --- a/__tests__/components/features/settings/sdk-settings/sdk-section-page.test.tsx +++ b/__tests__/components/features/settings/sdk-settings/sdk-section-page.test.tsx @@ -83,7 +83,7 @@ function renderSdkSectionPage( }); mockUseConfig.mockReturnValue({ - data: { app_mode: "oss" }, + data: {}, isLoading: false, }); mockUseSearchParams.mockReturnValue([{ get: () => null }, vi.fn()]); @@ -98,7 +98,7 @@ function renderSdkSectionPage( beforeEach(() => { vi.restoreAllMocks(); mockUseConfig.mockReturnValue({ - data: { app_mode: "oss" }, + data: {}, isLoading: false, }); mockUseSearchParams.mockReturnValue([{ get: () => null }, vi.fn()]); diff --git a/__tests__/components/user-actions.test.tsx b/__tests__/components/user-actions.test.tsx index d35b75adbc..4e067641b8 100644 --- a/__tests__/components/user-actions.test.tsx +++ b/__tests__/components/user-actions.test.tsx @@ -14,7 +14,7 @@ vi.mock("#/hooks/use-settings-nav-items", () => ({ })); describe("UserActions", () => { - it("shows the OSS user menu on hover without SaaS-only actions", async () => { + it("shows the OSS user menu on hover without hosted-only actions", async () => { const user = userEvent.setup(); render( diff --git a/__tests__/components/v1/chat/event-message-plan-preview.test.tsx b/__tests__/components/v1/chat/event-message-plan-preview.test.tsx index 60b1dc1cee..dac093f88c 100644 --- a/__tests__/components/v1/chat/event-message-plan-preview.test.tsx +++ b/__tests__/components/v1/chat/event-message-plan-preview.test.tsx @@ -12,7 +12,7 @@ import { // Mock useConfig vi.mock("#/hooks/query/use-config", () => ({ useConfig: () => ({ - data: { APP_MODE: "saas" }, + data: {}, }), })); diff --git a/__tests__/components/v1/chat/event-message-think-action.test.tsx b/__tests__/components/v1/chat/event-message-think-action.test.tsx index 0f8ee73383..4d4fce038b 100644 --- a/__tests__/components/v1/chat/event-message-think-action.test.tsx +++ b/__tests__/components/v1/chat/event-message-think-action.test.tsx @@ -10,7 +10,7 @@ import { renderWithProviders } from "test-utils"; // Mock useConfig vi.mock("#/hooks/query/use-config", () => ({ useConfig: () => ({ - data: { APP_MODE: "saas" }, + data: {}, }), })); diff --git a/__tests__/hooks/mutation/use-add-git-providers.test.tsx b/__tests__/hooks/mutation/use-add-git-providers.test.tsx index f979d768b1..e9a025b59d 100644 --- a/__tests__/hooks/mutation/use-add-git-providers.test.tsx +++ b/__tests__/hooks/mutation/use-add-git-providers.test.tsx @@ -23,7 +23,6 @@ const buildProviders = ( bitbucket_data_center: { token: "", host: null }, azure_devops: { token: "", host: null }, forgejo: { token: "", host: null }, - enterprise_sso: { token: "", host: null }, ...overrides, }); diff --git a/__tests__/hooks/use-git-user.test.tsx b/__tests__/hooks/use-git-user.test.tsx index 62fb3152b4..5344ffdf23 100644 --- a/__tests__/hooks/use-git-user.test.tsx +++ b/__tests__/hooks/use-git-user.test.tsx @@ -5,11 +5,9 @@ import React from "react"; import { useGitUser } from "#/hooks/query/use-git-user"; import UserService from "#/api/user-service/user-service.api"; import * as useShouldShowUserFeaturesModule from "#/hooks/use-should-show-user-features"; -import * as useConfigModule from "#/hooks/query/use-config"; import { AxiosError } from "axios"; vi.mock("#/hooks/use-should-show-user-features"); -vi.mock("#/hooks/query/use-config"); vi.mock("#/api/user-service/user-service.api"); const identifyMock = vi.fn(); @@ -25,11 +23,6 @@ describe("useGitUser", () => { vi.clearAllMocks(); vi.mocked(useShouldShowUserFeaturesModule.useShouldShowUserFeatures).mockReturnValue(true); - vi.mocked(useConfigModule.useConfig).mockReturnValue({ - data: { app_mode: "oss" }, - isLoading: false, - error: null, - } as any); }); const createWrapper = () => { @@ -88,7 +81,6 @@ describe("useGitUser", () => { name: "The Octocat", email: "octocat@example.com", user: "octocat", - mode: "oss", }); }); }); diff --git a/__tests__/hooks/use-settings-nav-items.test.tsx b/__tests__/hooks/use-settings-nav-items.test.tsx index dbea6996fa..c0a4b0a947 100644 --- a/__tests__/hooks/use-settings-nav-items.test.tsx +++ b/__tests__/hooks/use-settings-nav-items.test.tsx @@ -13,28 +13,22 @@ vi.mock("#/hooks/query/use-config", () => ({ const createConfig = ( feature_flags: Partial = {}, ): WebClientConfig => ({ - app_mode: "oss", posthog_client_key: null, feature_flags: { - enable_billing: false, hide_llm_settings: false, enable_jira: false, enable_jira_dc: false, enable_linear: false, hide_users_page: true, - hide_billing_page: true, hide_integrations_page: false, - deployment_mode: "self_hosted", ...feature_flags, }, providers_configured: [], maintenance_start_time: null, - auth_url: null, recaptcha_site_key: null, faulty_models: [], error_message: null, updated_at: new Date().toISOString(), - github_app_slug: null, }); describe("useSettingsNavItems", () => { diff --git a/__tests__/posthog-tracking.test.tsx b/__tests__/posthog-tracking.test.tsx deleted file mode 100644 index d6fff267aa..0000000000 --- a/__tests__/posthog-tracking.test.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { - describe, - it, - expect, - beforeAll, - afterAll, - afterEach, - vi, -} from "vitest"; -import { screen, waitFor, render, cleanup } from "@testing-library/react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { - createMockAgentErrorEvent, - createMockConversationErrorEvent, -} from "#/mocks/mock-ws-helpers"; -import { ConversationWebSocketProvider } from "#/contexts/conversation-websocket-context"; -import { conversationWebSocketTestSetup } from "./helpers/msw-websocket-setup"; -import { ConnectionStatusComponent } from "./helpers/websocket-test-components"; - -// Mock the tracking function -const mockTrackCreditLimitReached = vi.fn(); - -// Mock useTracking hook -vi.mock("#/hooks/use-tracking", () => ({ - useTracking: () => ({ - trackCreditLimitReached: mockTrackCreditLimitReached, - trackLoginButtonClick: vi.fn(), - trackConversationCreated: vi.fn(), - trackPushButtonClick: vi.fn(), - trackPullButtonClick: vi.fn(), - trackCreatePrButtonClick: vi.fn(), - trackGitProviderConnected: vi.fn(), - trackUserSignupCompleted: vi.fn(), - trackCreditsPurchased: vi.fn(), - }), -})); - -// Mock useActiveConversation hook -vi.mock("#/hooks/query/use-active-conversation", () => ({ - useActiveConversation: () => ({ - data: null, - isLoading: false, - error: null, - }), -})); - -// MSW WebSocket mock setup -const { wsLink, server: mswServer } = conversationWebSocketTestSetup(); - -beforeAll(() => { - // The global MSW server from vitest.setup.ts is already running - // We just need to start our WebSocket-specific server - mswServer.listen({ onUnhandledRequest: "bypass" }); -}); - -afterEach(() => { - // Clear all mocks before each test - mockTrackCreditLimitReached.mockClear(); - mswServer.resetHandlers(); - // Clean up any React components - cleanup(); -}); - -afterAll(async () => { - // Close the WebSocket MSW server - mswServer.close(); - - // Give time for any pending WebSocket connections to close. This is very important to prevent serious memory leaks - await new Promise((resolve) => { - setTimeout(resolve, 500); - }); -}); - -// Helper function to render components with all necessary providers -function renderWithProviders( - children: React.ReactNode, - conversationId = "test-conversation-123", - conversationUrl = "http://localhost:3000/api/conversations/test-conversation-123", -) { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { retry: false }, - mutations: { retry: false }, - }, - }); - - return render( - - - {children} - - , - ); -} - -describe("PostHog Analytics Tracking", () => { - describe("Credit Limit Tracking", () => { - it("should NOT track credit_limit_reached for non-budget errors", async () => { - // Create a regular error without budget/credit keywords - const mockRegularErrorEvent = createMockAgentErrorEvent({ - error: "Failed to execute command: Permission denied", - }); - - mswServer.use( - wsLink.addEventListener("connection", ({ client, server }) => { - server.connect(); - client.send(JSON.stringify(mockRegularErrorEvent)); - }), - ); - - renderWithProviders(); - - // Wait for connection and error to be processed - await waitFor(() => { - expect(screen.getByTestId("connection-state")).toHaveTextContent( - "OPEN", - ); - }); - - // Verify that credit_limit_reached was NOT tracked - expect(mockTrackCreditLimitReached).not.toHaveBeenCalled(); - }); - - it("should track credit_limit_reached when ConversationErrorEvent contains budget error", async () => { - const mockBudgetConversationError = createMockConversationErrorEvent({ - detail: - "Budget has been exceeded! Current cost: 18.51, Max budget: 18.24", - }); - - mswServer.use( - wsLink.addEventListener("connection", ({ client, server }) => { - server.connect(); - client.send(JSON.stringify(mockBudgetConversationError)); - }), - ); - - renderWithProviders(); - - await waitFor(() => { - expect(screen.getByTestId("connection-state")).toHaveTextContent( - "OPEN", - ); - }); - - await waitFor(() => { - expect(mockTrackCreditLimitReached).toHaveBeenCalledWith( - expect.objectContaining({ - conversationId: "test-conversation-123", - }), - ); - }); - }); - }); -}); diff --git a/__tests__/routes/launch.test.tsx b/__tests__/routes/launch.test.tsx index 3f2ad57dfd..ed9cc413c8 100644 --- a/__tests__/routes/launch.test.tsx +++ b/__tests__/routes/launch.test.tsx @@ -29,7 +29,7 @@ vi.mock("#/hooks/query/use-is-authed", () => ({ })); vi.mock("#/hooks/query/use-config", () => ({ - useConfig: () => ({ data: { APP_MODE: "saas" } }), + useConfig: () => ({ data: {} }), })); function renderLaunchRoute(searchParams: string) { diff --git a/__tests__/routes/root-layout-refetch.test.tsx b/__tests__/routes/root-layout-refetch.test.tsx index cfbc640275..31d42e501b 100644 --- a/__tests__/routes/root-layout-refetch.test.tsx +++ b/__tests__/routes/root-layout-refetch.test.tsx @@ -21,13 +21,11 @@ vi.mock("#/hooks/query/use-config", () => ({ })); const DEFAULT_FEATURE_FLAGS = { - enable_billing: false, hide_llm_settings: false, enable_jira: false, enable_jira_dc: false, enable_linear: false, hide_users_page: false, - hide_billing_page: false, hide_integrations_page: false, }; @@ -64,7 +62,6 @@ describe("MainApp - Auth refetch behavior", () => { }); useConfigMock.mockReturnValue({ data: { - app_mode: "saas", github_client_id: "test-client-id", feature_flags: DEFAULT_FEATURE_FLAGS, }, diff --git a/__tests__/routes/settings.test.tsx b/__tests__/routes/settings.test.tsx index a0ae3b0c9d..98dfcf0504 100644 --- a/__tests__/routes/settings.test.tsx +++ b/__tests__/routes/settings.test.tsx @@ -22,42 +22,33 @@ describe("settings route", () => { it("prefers OSS fallback routes only", () => { expect( getFirstAvailablePath({ - enable_billing: false, hide_llm_settings: true, enable_jira: false, enable_jira_dc: false, enable_linear: false, hide_users_page: true, - hide_billing_page: true, hide_integrations_page: false, - deployment_mode: "self_hosted", }), ).toBe("/settings/mcp"); }); it("redirects hidden OSS settings pages to the first available route", async () => { vi.spyOn(OptionService, "getConfig").mockResolvedValue({ - app_mode: "oss", posthog_client_key: null, feature_flags: { - enable_billing: false, hide_llm_settings: true, enable_jira: false, enable_jira_dc: false, enable_linear: false, hide_users_page: true, - hide_billing_page: true, hide_integrations_page: false, - deployment_mode: "self_hosted", }, providers_configured: [], maintenance_start_time: null, - auth_url: null, recaptcha_site_key: null, faulty_models: [], error_message: null, updated_at: new Date().toISOString(), - github_app_slug: null, }); const response = (await clientLoader({ diff --git a/__tests__/task-suggestions.test.tsx b/__tests__/task-suggestions.test.tsx index bda826e09c..930e078b98 100644 --- a/__tests__/task-suggestions.test.tsx +++ b/__tests__/task-suggestions.test.tsx @@ -38,7 +38,7 @@ describe("TaskSuggestions empty states", () => { it("OSS mode + no providers → shows Git provider empty state", () => { (useConfig as any).mockReturnValue({ - data: { app_mode: "oss" }, + data: {}, }); (useUserProviders as any).mockReturnValue({ @@ -58,7 +58,7 @@ describe("TaskSuggestions empty states", () => { it("OSS mode + providers exist but no tasks → shows no tasks message", () => { (useConfig as any).mockReturnValue({ - data: { app_mode: "oss" }, + data: {}, }); (useUserProviders as any).mockReturnValue({ diff --git a/__tests__/utils/local-storage.test.ts b/__tests__/utils/local-storage.test.ts index 69be6899bc..5d62656f47 100644 --- a/__tests__/utils/local-storage.test.ts +++ b/__tests__/utils/local-storage.test.ts @@ -5,8 +5,6 @@ import { setLoginMethod, getLoginMethod, clearLoginData, - setCTADismissed, - isCTADismissed, } from "#/utils/local-storage"; describe("local-storage utilities", () => { @@ -31,9 +29,6 @@ describe("local-storage utilities", () => { setLoginMethod(LoginMethod.AZURE_DEVOPS); expect(localStorage.getItem(LOCAL_STORAGE_KEYS.LOGIN_METHOD)).toBe("azure_devops"); - setLoginMethod(LoginMethod.ENTERPRISE_SSO); - expect(localStorage.getItem(LOCAL_STORAGE_KEYS.LOGIN_METHOD)).toBe("enterprise_sso"); - setLoginMethod(LoginMethod.BITBUCKET_DATA_CENTER); expect(localStorage.getItem(LOCAL_STORAGE_KEYS.LOGIN_METHOD)).toBe("bitbucket_data_center"); }); @@ -81,60 +76,4 @@ describe("local-storage utilities", () => { }); }); }); - - describe("CTA utilities", () => { - describe("isCTADismissed", () => { - it("returns false when CTA has not been dismissed", () => { - expect(isCTADismissed("homepage")).toBe(false); - }); - - it("returns true when CTA has been dismissed", () => { - localStorage.setItem("homepage-cta-dismissed", "true"); - expect(isCTADismissed("homepage")).toBe(true); - }); - - it("returns false when storage value is not 'true'", () => { - localStorage.setItem("homepage-cta-dismissed", "false"); - expect(isCTADismissed("homepage")).toBe(false); - - localStorage.setItem("homepage-cta-dismissed", "invalid"); - expect(isCTADismissed("homepage")).toBe(false); - }); - }); - - describe("setCTADismissed", () => { - it("sets the CTA as dismissed in local storage", () => { - setCTADismissed("homepage"); - expect(localStorage.getItem("homepage-cta-dismissed")).toBe("true"); - }); - - it("generates correct key for homepage location", () => { - setCTADismissed("homepage"); - expect(localStorage.getItem("homepage-cta-dismissed")).toBe("true"); - }); - }); - - describe("storage key format", () => { - it("uses the correct key format: {location}-cta-dismissed", () => { - setCTADismissed("homepage"); - - // Verify key exists with correct format - expect(localStorage.getItem("homepage-cta-dismissed")).toBe("true"); - - // Verify other keys don't exist - expect(localStorage.getItem("cta-dismissed")).toBeNull(); - expect(localStorage.getItem("homepage")).toBeNull(); - }); - }); - - describe("persistence", () => { - it("dismissed state persists across multiple reads", () => { - setCTADismissed("homepage"); - - expect(isCTADismissed("homepage")).toBe(true); - expect(isCTADismissed("homepage")).toBe(true); - expect(isCTADismissed("homepage")).toBe(true); - }); - }); - }); }); diff --git a/__tests__/utils/sdk-settings-schema.test.ts b/__tests__/utils/sdk-settings-schema.test.ts index 7e49677c48..d5c490935f 100644 --- a/__tests__/utils/sdk-settings-schema.test.ts +++ b/__tests__/utils/sdk-settings-schema.test.ts @@ -241,7 +241,7 @@ describe("sdk settings schema helpers", () => { "critic.enabled": true, "llm.api_key": "new-key", "llm.litellm_extra_body": JSON.stringify( - { metadata: { tier: "enterprise" } }, + { metadata: { tier: "sample" } }, null, 2, ), @@ -258,7 +258,7 @@ describe("sdk settings schema helpers", () => { critic: { enabled: true }, llm: { api_key: "new-key", - litellm_extra_body: { metadata: { tier: "enterprise" } }, + litellm_extra_body: { metadata: { tier: "sample" } }, }, }); }); @@ -289,7 +289,7 @@ describe("sdk settings schema helpers", () => { "critic.enabled": true, "critic.mode": "all_actions", "llm.litellm_extra_body": JSON.stringify( - { metadata: { tier: "enterprise" } }, + { metadata: { tier: "sample" } }, null, 2, ), @@ -329,7 +329,7 @@ describe("sdk settings schema helpers", () => { llm: { model: "anthropic/claude-sonnet-4-20250514", timeout: 90, - litellm_extra_body: { metadata: { tier: "enterprise" } }, + litellm_extra_body: { metadata: { tier: "sample" } }, }, critic: { enabled: true, mode: "all_actions" }, }); diff --git a/global.d.ts b/global.d.ts index c75305b980..587fcc28af 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,29 +1,5 @@ interface Window { - __APP_MODE__?: "saas" | "oss"; __GITHUB_CLIENT_ID__?: string | null; - Reo?: { - init: (config: { clientID: string }) => void; - identify: (identity: { - username: string; - type: "github" | "email"; - other_identities?: Array<{ - username: string; - type: "github" | "email"; - }>; - firstname?: string; - lastname?: string; - company?: string; - }) => void; - }; - grecaptcha?: { - enterprise: { - ready: (callback: () => void) => void; - execute: ( - siteKey: string, - options: { action: string }, - ) => Promise; - }; - }; } declare module "postcss-prefix-selector" { diff --git a/package.json b/package.json index 1ff47fac5f..ace700d549 100644 --- a/package.json +++ b/package.json @@ -54,10 +54,9 @@ "dev": "npm run dev:safe", "dev:safe": "node scripts/dev-safe.mjs", "dev:frontend": "npm run make-i18n && cross-env VITE_MOCK_API=false react-router dev", - "dev:mock": "npm run make-i18n && cross-env VITE_MOCK_API=true VITE_MOCK_SAAS=false react-router dev", - "dev:mock:saas": "npm run make-i18n && cross-env VITE_MOCK_API=true VITE_MOCK_SAAS=true react-router dev", + "dev:mock": "npm run make-i18n && cross-env VITE_MOCK_API=true react-router dev", "build": "npm run build:app", - "build:mock": "npm run make-i18n && cross-env VITE_MOCK_API=true VITE_MOCK_SAAS=false react-router build", + "build:mock": "npm run make-i18n && cross-env VITE_MOCK_API=true react-router build", "start": "npx sirv-cli build/ --single", "test": "npm run make-i18n && vitest run", "test:e2e": "playwright test", diff --git a/playwright.config.ts b/playwright.config.ts index 0ca603e0ca..4cd811ade2 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -75,7 +75,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: "npm run dev:mock:saas -- --port 3001", + command: "npm run dev:mock -- --port 3001", url: "http://localhost:3001/", reuseExistingServer: !process.env.CI, }, diff --git a/src/api/README.md b/src/api/README.md index 54acad17fc..1337c5a1f1 100644 --- a/src/api/README.md +++ b/src/api/README.md @@ -93,7 +93,7 @@ export const useFeature = (id: string) => { | Item | Convention | Example | |------|------------|---------| -| Directory | `feature-service/` | `billing-service/` | -| Service file | `feature-service.api.ts` | `billing-service.api.ts` | -| Types file | `feature.types.ts` | `billing.types.ts` | -| Export name | `featureService` | `billingService` | +| Directory | `feature-service/` | `secrets-service/` | +| Service file | `feature-service.api.ts` | `secrets-service.api.ts` | +| Types file | `feature.types.ts` | `secrets.types.ts` | +| Export name | `featureService` | `secretsService` | diff --git a/src/api/agent-server-compatibility.ts b/src/api/agent-server-compatibility.ts index a6d0fd8607..60b836a2a0 100644 --- a/src/api/agent-server-compatibility.ts +++ b/src/api/agent-server-compatibility.ts @@ -1,5 +1,5 @@ -import { createServerClient, type ServerInfo } from "#/api/typescript-client"; import { HttpError } from "@openhands/typescript-client/client/http-client"; +import { createServerClient, type ServerInfo } from "#/api/typescript-client"; export const MINIMUM_SUPPORTED_AGENT_SERVER_VERSION = "1.17.0"; const AGENT_SERVER_INFO_TIMEOUT_MS = 5000; diff --git a/src/api/config-service/config-service.api.ts b/src/api/config-service/config-service.api.ts index 8a3b30f8f9..489d1cee1e 100644 --- a/src/api/config-service/config-service.api.ts +++ b/src/api/config-service/config-service.api.ts @@ -8,13 +8,18 @@ import type { SearchProvidersParams, } from "./config-service.types"; -function filterByQuery(items: T[], query?: string): T[] { +function filterByQuery( + items: T[], + query?: string, +): T[] { if (!query) { return items; } const normalizedQuery = query.toLowerCase(); - return items.filter((item) => item.name.toLowerCase().includes(normalizedQuery)); + return items.filter((item) => + item.name.toLowerCase().includes(normalizedQuery), + ); } function filterByVerified( @@ -47,7 +52,9 @@ class ConfigService { ]); const provider = params.provider__eq ?? null; - const verifiedNames = new Set(provider ? (verifiedByProvider?.[provider] ?? []) : []); + const verifiedNames = new Set( + provider ? (verifiedByProvider?.[provider] ?? []) : [], + ); const verifiedItems: LLMModel[] = [...verifiedNames].map((name) => ({ provider, name, @@ -96,7 +103,10 @@ class ConfigService { })); const items = limitItems( - filterByVerified(filterByQuery(providerItems, params.query), params.verified__eq), + filterByVerified( + filterByQuery(providerItems, params.query), + params.verified__eq, + ), params.limit, ); diff --git a/src/api/conversation-service/conversation-service.api.ts b/src/api/conversation-service/conversation-service.api.ts index 14577838a1..8ac7af8ca0 100644 --- a/src/api/conversation-service/conversation-service.api.ts +++ b/src/api/conversation-service/conversation-service.api.ts @@ -4,7 +4,11 @@ import { GetTrajectoryResponse, FileUploadSuccessResponse, } from "../open-hands.types"; -import { createRemoteEventsList, createRemoteWorkspace, createVSCodeClient } from "../typescript-client"; +import { + createRemoteEventsList, + createRemoteWorkspace, + createVSCodeClient, +} from "../typescript-client"; import { V1AppConversation } from "./v1-conversation-service.types"; class ConversationService { @@ -38,8 +42,11 @@ class ConversationService { static async getVSCodeUrl( _conversationId: string, ): Promise { - const vscode_url = await createVSCodeClient(this.getClientOverrides()).getUrl({ - baseUrl: typeof window !== "undefined" ? window.location.origin : undefined, + const vscode_url = await createVSCodeClient( + this.getClientOverrides(), + ).getUrl({ + baseUrl: + typeof window !== "undefined" ? window.location.origin : undefined, workspaceDir: getAgentServerWorkingDir(), }); diff --git a/src/api/conversation-service/v1-conversation-service.api.ts b/src/api/conversation-service/v1-conversation-service.api.ts index 4f4b3d9cf2..d055f134e8 100644 --- a/src/api/conversation-service/v1-conversation-service.api.ts +++ b/src/api/conversation-service/v1-conversation-service.api.ts @@ -29,7 +29,6 @@ import type { V1AppConversationPage, V1AppConversationStartRequest, V1AppConversationStartTask, - V1AppConversationStartTaskPage, V1RuntimeConversationInfo, V1SendMessageRequest, V1SendMessageResponse, @@ -40,10 +39,13 @@ class V1ConversationService { conversationId: string, message: V1SendMessageRequest, ): Promise { - await createHttpClient().post(`/api/conversations/${conversationId}/events`, { - ...message, - run: true, - }); + await createHttpClient().post( + `/api/conversations/${conversationId}/events`, + { + ...message, + run: true, + }, + ); return message; } @@ -74,7 +76,7 @@ class V1ConversationService { "/api/conversations", payload, ); - const data = response.data; + const { data } = response; return { id: data.id, @@ -113,7 +115,8 @@ class V1ConversationService { sessionApiKey?: string | null, ): Promise { const vscode_url = await createVSCodeClient({ sessionApiKey }).getUrl({ - baseUrl: typeof window !== "undefined" ? window.location.origin : undefined, + baseUrl: + typeof window !== "undefined" ? window.location.origin : undefined, workspaceDir: getAgentServerWorkingDir(), }); @@ -125,10 +128,9 @@ class V1ConversationService { _conversationUrl: string | null | undefined, sessionApiKey?: string | null, ): Promise<{ success: boolean }> { - const response = await createHttpClient({ sessionApiKey }).post<{ success: boolean }>( - `/api/conversations/${conversationId}/pause`, - {}, - ); + const response = await createHttpClient({ sessionApiKey }).post<{ + success: boolean; + }>(`/api/conversations/${conversationId}/pause`, {}); return response.data; } @@ -139,10 +141,9 @@ class V1ConversationService { question: string, sessionApiKey?: string | null, ): Promise<{ response: string }> { - const response = await createHttpClient({ sessionApiKey }).post<{ response: string }>( - `/api/conversations/${conversationId}/ask_agent`, - { question }, - ); + const response = await createHttpClient({ sessionApiKey }).post<{ + response: string; + }>(`/api/conversations/${conversationId}/ask_agent`, { question }); return response.data; } @@ -152,10 +153,9 @@ class V1ConversationService { _conversationUrl: string | null | undefined, sessionApiKey?: string | null, ): Promise<{ success: boolean }> { - const response = await createHttpClient({ sessionApiKey }).post<{ success: boolean }>( - `/api/conversations/${conversationId}/run`, - {}, - ); + const response = await createHttpClient({ sessionApiKey }).post<{ + success: boolean; + }>(`/api/conversations/${conversationId}/run`, {}); return response.data; } @@ -165,12 +165,13 @@ class V1ConversationService { ): Promise<(V1AppConversation | null)[]> { if (ids.length === 0) return []; - const response = await createHttpClient().get<(DirectConversationInfo | null)[]>( - "/api/conversations", - { params: { ids } }, - ); + const response = await createHttpClient().get< + (DirectConversationInfo | null)[] + >("/api/conversations", { params: { ids } }); - return response.data.map((item) => (item ? toV1AppConversation(item) : null)); + return response.data.map((item) => + item ? toV1AppConversation(item) : null, + ); } static async uploadFile( @@ -226,7 +227,9 @@ class V1ConversationService { } static async getSkills(conversationId: string): Promise { - const [conversation] = await this.batchGetAppConversations([conversationId]); + const [conversation] = await this.batchGetAppConversations([ + conversationId, + ]); return loadSkillsForConversation(conversation); } @@ -242,7 +245,7 @@ class V1ConversationService { const response = await createHttpClient({ sessionApiKey }).get< DirectConversationInfo & { stats?: V1RuntimeConversationInfo["stats"] } >(`/api/conversations/${conversationId}`); - const data = response.data; + const { data } = response; return { id: data.id, @@ -260,7 +263,8 @@ class V1ConversationService { cache_read_tokens: data.metrics.accumulated_token_usage.cache_read_tokens ?? 0, cache_write_tokens: - data.metrics.accumulated_token_usage.cache_write_tokens ?? 0, + data.metrics.accumulated_token_usage.cache_write_tokens ?? + 0, context_window: data.metrics.accumulated_token_usage.context_window ?? 0, per_turn_token: @@ -271,7 +275,9 @@ class V1ConversationService { : null, created_at: data.created_at, updated_at: data.updated_at, - status: (data.execution_status as V1RuntimeConversationInfo["status"]) ?? "idle", + status: + (data.execution_status as V1RuntimeConversationInfo["status"]) ?? + "idle", stats: data.stats ?? { usage_to_metrics: {} }, }; } @@ -310,8 +316,12 @@ class V1ConversationService { conversationId: string, title: string, ): Promise { - await createHttpClient().patch(`/api/conversations/${conversationId}`, { title }); - const [conversation] = await this.batchGetAppConversations([conversationId]); + await createHttpClient().patch(`/api/conversations/${conversationId}`, { + title, + }); + const [conversation] = await this.batchGetAppConversations([ + conversationId, + ]); return conversation as V1AppConversation; } } diff --git a/src/api/event-service/event-service.api.ts b/src/api/event-service/event-service.api.ts index f172d26f94..9d4f43e6e8 100644 --- a/src/api/event-service/event-service.api.ts +++ b/src/api/event-service/event-service.api.ts @@ -12,7 +12,9 @@ class EventService { request: ConfirmationResponseRequest, sessionApiKey?: string | null, ): Promise { - const response = await createHttpClient({ sessionApiKey }).post( + const response = await createHttpClient({ + sessionApiKey, + }).post( `/api/conversations/${conversationId}/events/respond_to_confirmation`, request, ); @@ -33,7 +35,9 @@ class EventService { limit = 100, sessionApiKey?: string | null, ) { - const page = await createRemoteEventsList(conversationId, { sessionApiKey }).search({ + const page = await createRemoteEventsList(conversationId, { + sessionApiKey, + }).search({ limit, }); diff --git a/src/api/git-service/git-service.api.ts b/src/api/git-service/git-service.api.ts index c8626c4652..8656de694c 100644 --- a/src/api/git-service/git-service.api.ts +++ b/src/api/git-service/git-service.api.ts @@ -51,7 +51,13 @@ class GitService { pageId?: string, limit = 30, ): Promise { - return this.getRepositoryBranches(repository, provider, query, pageId, limit); + return this.getRepositoryBranches( + repository, + provider, + query, + pageId, + limit, + ); } static async getUserInstallations( @@ -63,12 +69,16 @@ class GitService { } static async getGitChanges(_conversationId: string): Promise { - const changes = await createRemoteWorkspace().gitChanges(getAgentServerWorkingDir()); + const changes = await createRemoteWorkspace().gitChanges( + getAgentServerWorkingDir(), + ); return changes.map((change) => ({ path: change.path, status: mapAnyGitStatusToV0Status( - String(change.status) as Parameters[0], + String(change.status) as Parameters< + typeof mapAnyGitStatusToV0Status + >[0], ), })); } diff --git a/src/api/git-service/v1-git-service.api.ts b/src/api/git-service/v1-git-service.api.ts index eceddf462f..2a5d96054f 100644 --- a/src/api/git-service/v1-git-service.api.ts +++ b/src/api/git-service/v1-git-service.api.ts @@ -14,12 +14,16 @@ class V1GitService { }).gitChanges(path); if (!Array.isArray(changes)) { - throw new Error("Invalid response from runtime - runtime may be unavailable"); + throw new Error( + "Invalid response from runtime - runtime may be unavailable", + ); } return changes.map((change) => ({ status: mapAnyGitStatusToV0Status( - String(change.status) as Parameters[0], + String(change.status) as Parameters< + typeof mapAnyGitStatusToV0Status + >[0], ), path: change.path, })); diff --git a/src/api/open-hands-axios.ts b/src/api/open-hands-axios.ts index 67d2208f1e..638a77d048 100644 --- a/src/api/open-hands-axios.ts +++ b/src/api/open-hands-axios.ts @@ -1,7 +1,16 @@ -import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from "axios"; -import { getAgentServerBaseUrl, getAgentServerHeaders } from "./agent-server-config"; +import axios, { + AxiosError, + AxiosResponse, + InternalAxiosRequestConfig, +} from "axios"; +import { + getAgentServerBaseUrl, + getAgentServerHeaders, +} from "./agent-server-config"; -function serializeParams(params: Record | URLSearchParams): string { +function serializeParams( + params: Record | URLSearchParams, +): string { if (params instanceof URLSearchParams) { return params.toString(); } diff --git a/src/api/option-service/option-service.api.ts b/src/api/option-service/option-service.api.ts index 567a402dd4..4bd606a3ac 100644 --- a/src/api/option-service/option-service.api.ts +++ b/src/api/option-service/option-service.api.ts @@ -34,27 +34,21 @@ class OptionService { await ensureCompatibleAgentServer(); return { - app_mode: "oss", posthog_client_key: null, feature_flags: { - enable_billing: false, hide_llm_settings: false, enable_jira: false, enable_jira_dc: false, enable_linear: false, hide_users_page: true, - hide_billing_page: true, hide_integrations_page: false, - deployment_mode: "self_hosted", }, providers_configured: [], maintenance_start_time: null, - auth_url: null, recaptcha_site_key: null, faulty_models: [], error_message: null, updated_at: new Date().toISOString(), - github_app_slug: null, }; } } diff --git a/src/api/option-service/option.types.ts b/src/api/option-service/option.types.ts index d9440eda27..9853a3846c 100644 --- a/src/api/option-service/option.types.ts +++ b/src/api/option-service/option.types.ts @@ -1,7 +1,5 @@ import { Provider } from "#/types/settings"; -export type DeploymentMode = "cloud" | "self_hosted"; - /** * Structured response from ``GET /api/options/models``. * @@ -20,27 +18,21 @@ export interface ModelsResponse { } export interface WebClientFeatureFlags { - enable_billing: boolean; hide_llm_settings: boolean; enable_jira: boolean; enable_jira_dc: boolean; enable_linear: boolean; hide_users_page: boolean; - hide_billing_page: boolean; hide_integrations_page: boolean; - deployment_mode?: DeploymentMode; } export interface WebClientConfig { - app_mode: "saas" | "oss"; posthog_client_key: string | null; feature_flags: WebClientFeatureFlags; providers_configured: Provider[]; maintenance_start_time: string | null; - auth_url: string | null; recaptcha_site_key: string | null; faulty_models: string[]; error_message: string | null; updated_at: string; - github_app_slug: string | null; } diff --git a/src/api/pending-message-service/pending-message-service.api.ts b/src/api/pending-message-service/pending-message-service.api.ts index 560a2dad1c..3efbc01532 100644 --- a/src/api/pending-message-service/pending-message-service.api.ts +++ b/src/api/pending-message-service/pending-message-service.api.ts @@ -9,11 +9,14 @@ class PendingMessageService { conversationId: string, message: QueuePendingMessageRequest, ): Promise { - await createHttpClient().post(`/api/conversations/${conversationId}/events`, { - ...message, - role: "user", - run: true, - }); + await createHttpClient().post( + `/api/conversations/${conversationId}/events`, + { + ...message, + role: "user", + run: true, + }, + ); return { id: `${conversationId}:${Date.now()}`, diff --git a/src/api/sandbox-service/sandbox-service.api.ts b/src/api/sandbox-service/sandbox-service.api.ts index d1266d9b29..65b49af686 100644 --- a/src/api/sandbox-service/sandbox-service.api.ts +++ b/src/api/sandbox-service/sandbox-service.api.ts @@ -14,7 +14,8 @@ export class SandboxService { static async batchGetSandboxes( ids: string[], ): Promise<(V1SandboxInfo | null)[]> { - const conversations = await V1ConversationService.batchGetAppConversations(ids); + const conversations = + await V1ConversationService.batchGetAppConversations(ids); return conversations.map((conversation) => conversation ? createSandboxInfo(conversation) : null, ); diff --git a/src/api/secrets-service.ts b/src/api/secrets-service.ts index f1dfc00805..c855791de5 100644 --- a/src/api/secrets-service.ts +++ b/src/api/secrets-service.ts @@ -35,7 +35,11 @@ const readStoredGitProviders = (): StoredGitProviderTokens => { return Object.fromEntries( Object.entries(parsed).flatMap(([provider, value]) => { - if (!(provider in ProviderOptions) || !value || typeof value !== "object") { + if ( + !(provider in ProviderOptions) || + !value || + typeof value !== "object" + ) { return []; } @@ -74,14 +78,20 @@ const writeStoredGitProviders = (providers: StoredGitProviderTokens) => { return; } - window.localStorage.setItem(GIT_PROVIDER_STORAGE_KEY, JSON.stringify(providers)); + window.localStorage.setItem( + GIT_PROVIDER_STORAGE_KEY, + JSON.stringify(providers), + ); }; const buildProviderTokensSet = ( providers: StoredGitProviderTokens, ): Partial> => Object.fromEntries( - Object.entries(providers).map(([provider, value]) => [provider, value?.host ?? null]), + Object.entries(providers).map(([provider, value]) => [ + provider, + value?.host ?? null, + ]), ) as Partial>; export class SecretsService { diff --git a/src/api/shared-conversation-service.api.ts b/src/api/shared-conversation-service.api.ts index e982cfa026..f444069fc1 100644 --- a/src/api/shared-conversation-service.api.ts +++ b/src/api/shared-conversation-service.api.ts @@ -27,10 +27,9 @@ export const sharedConversationService = { async getSharedConversation( conversationId: string, ): Promise { - const response = await createHttpClient().get<(SharedConversation | null)[]>( - "/api/shared-conversations", - { params: { ids: conversationId } }, - ); + const response = await createHttpClient().get< + (SharedConversation | null)[] + >("/api/shared-conversations", { params: { ids: conversationId } }); return response.data[0] || null; }, diff --git a/src/api/typescript-client.ts b/src/api/typescript-client.ts index 2c9c060879..7a25e85f7a 100644 --- a/src/api/typescript-client.ts +++ b/src/api/typescript-client.ts @@ -5,7 +5,6 @@ import { SkillsClient, VSCodeClient, } from "@openhands/typescript-client/clients"; -export type { ServerInfo } from "@openhands/typescript-client"; import { HttpClient } from "@openhands/typescript-client/client/http-client"; import { RemoteEventsList } from "@openhands/typescript-client/events/remote-events-list"; import { RemoteWorkspace } from "@openhands/typescript-client/workspace/remote-workspace"; @@ -16,6 +15,8 @@ import { getAgentServerWorkingDir, } from "./agent-server-config"; +export type { ServerInfo } from "@openhands/typescript-client"; + interface TypeScriptClientOverrides { host?: string; apiKey?: string | null; diff --git a/src/components/features/chat/error-message-banner.tsx b/src/components/features/chat/error-message-banner.tsx index e9b7caaa0e..219981ab62 100644 --- a/src/components/features/chat/error-message-banner.tsx +++ b/src/components/features/chat/error-message-banner.tsx @@ -1,7 +1,6 @@ import React from "react"; import { Trans, useTranslation } from "react-i18next"; import { X } from "lucide-react"; -import { NavigationLink } from "#/components/shared/navigation-link"; import { I18nKey } from "#/i18n/declaration"; import { cn } from "#/utils/utils"; @@ -39,24 +38,7 @@ export function ErrorMessageBanner({ )} data-testid="error-message-banner-content" > - {isI18nKey ? ( - - link - - ), - }} - /> - ) : ( - message - )} + {isI18nKey ? : message} {shouldShowToggle && ( diff --git a/src/components/features/chat/expandable-message.tsx b/src/components/features/chat/expandable-message.tsx index 7299b01f91..837f6046d0 100644 --- a/src/components/features/chat/expandable-message.tsx +++ b/src/components/features/chat/expandable-message.tsx @@ -1,13 +1,10 @@ import { useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; -import { useConfig } from "#/hooks/query/use-config"; -import { I18nKey } from "#/i18n/declaration"; import ArrowDown from "#/icons/angle-down-solid.svg?react"; import ArrowUp from "#/icons/angle-up-solid.svg?react"; import CheckCircle from "#/icons/check-circle-solid.svg?react"; import { OpenHandsAction } from "#/types/core/actions"; import { OpenHandsObservation } from "#/types/core/observations"; -import { NavigationLink } from "#/components/shared/navigation-link"; import { cn } from "#/utils/utils"; import { MarkdownRenderer } from "../markdown/markdown-renderer"; import { MonoComponent } from "./mono-component"; @@ -35,8 +32,7 @@ export function ExpandableMessage({ observation, action, }: ExpandableMessageProps) { - const { data: config } = useConfig(); - const { t, i18n } = useTranslation("openhands"); + const { i18n } = useTranslation("openhands"); const [showDetails, setShowDetails] = useState(true); const [details, setDetails] = useState(message); const [translationId, setTranslationId] = useState(id); @@ -93,31 +89,6 @@ export function ExpandableMessage({ const statusIconClasses = "h-4 w-4 ml-2 inline"; - if ( - config?.feature_flags?.enable_billing && - config?.app_mode === "saas" && - id === I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS - ) { - return ( -
-
-
- {t(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS)} -
- - {t(I18nKey.BILLING$CLICK_TO_TOP_UP)} - -
-
- ); - } - return (
> = { bitbucket_data_center: FaBitbucket, github: FaGithub, gitlab: FaGitlab, - enterprise_sso: FaUserShield, }; export function ConversationRepoLink({ diff --git a/src/components/features/conversation/conversation-name-context-menu.tsx b/src/components/features/conversation/conversation-name-context-menu.tsx index 6a6cc2d85c..720f7fe441 100644 --- a/src/components/features/conversation/conversation-name-context-menu.tsx +++ b/src/components/features/conversation/conversation-name-context-menu.tsx @@ -6,8 +6,6 @@ import { ContextMenu } from "#/ui/context-menu"; import { ContextMenuListItem } from "../context-menu/context-menu-list-item"; import { Divider } from "#/ui/divider"; import { I18nKey } from "#/i18n/declaration"; -import { useActiveConversation } from "#/hooks/query/use-active-conversation"; -import { useConfig } from "#/hooks/query/use-config"; import EditIcon from "#/icons/u-edit.svg?react"; import RobotIcon from "#/icons/u-robot.svg?react"; @@ -16,8 +14,6 @@ import DownloadIcon from "#/icons/u-download.svg?react"; import CreditCardIcon from "#/icons/u-credit-card.svg?react"; import CloseIcon from "#/icons/u-close.svg?react"; import DeleteIcon from "#/icons/u-delete.svg?react"; -import LinkIcon from "#/icons/link-external.svg?react"; -import CopyIcon from "#/icons/copy.svg?react"; import { ConversationNameContextMenuIconText } from "./conversation-name-context-menu-icon-text"; import { CONTEXT_MENU_ICON_TEXT_CLASSNAME } from "#/utils/constants"; @@ -35,10 +31,7 @@ interface ConversationNameContextMenuProps { onShowAgentTools?: (event: React.MouseEvent) => void; onShowSkills?: (event: React.MouseEvent) => void; onShowHooks?: (event: React.MouseEvent) => void; - onTogglePublic?: (event: React.MouseEvent) => void; onDownloadConversation?: (event: React.MouseEvent) => void; - onCopyShareLink?: (event: React.MouseEvent) => void; - shareUrl?: string; position?: "top" | "bottom"; } @@ -51,19 +44,13 @@ export function ConversationNameContextMenu({ onShowAgentTools, onShowSkills, onShowHooks, - onTogglePublic, onDownloadConversation, - onCopyShareLink, - shareUrl, position = "bottom", }: ConversationNameContextMenuProps) { const isMobile = useBreakpoint(); const { t } = useTranslation("openhands"); const ref = useClickOutsideElement(onClose); - const { data: conversation } = useActiveConversation(); - const { data: config } = useConfig(); - const shouldShowPublicSharing = config?.app_mode === "saas" && onTogglePublic; const hasTools = Boolean(onShowAgentTools || onShowSkills || onShowHooks); const hasInfo = Boolean(onDisplayCost); const hasControl = Boolean(onStop || onDelete); @@ -164,49 +151,6 @@ export function ConversationNameContextMenu({ )} - {shouldShowPublicSharing && ( - -
-
- - {t(I18nKey.CONVERSATION$SHARE_PUBLICLY)} -
- {conversation?.public && shareUrl && onCopyShareLink && ( - - )} -
-
- )} - {onStop && ( { - if ( - !config?.github_app_slug || - provider !== ProviderOptions.github || - config.app_mode !== "saas" - ) - return null; - - const githubHref = `https://github.com/apps/${config.github_app_slug}/installations/new`; - - return ( - { - // Prevent downshift from closing the menu when clicking the sticky footer - e.preventDefault(); - e.stopPropagation(); - }} - > - {t(I18nKey.HOME$ADD_GITHUB_REPOS)} - - ); - }, [provider, config, t]); - return (
@@ -374,7 +344,6 @@ export function GitRepoDropdown({ renderItem={renderItem} renderEmptyState={renderEmptyState} stickyTopItem={stickyTopItem} - stickyFooterItem={stickyFooterItem} testId="git-repo-dropdown-menu" numberOfRecentItems={recentRepositories.length} itemKey={(repo) => repo.id} diff --git a/src/components/features/home/git-repo-dropdown/use-repository-data.tsx b/src/components/features/home/git-repo-dropdown/use-repository-data.tsx index 679374cfd1..8cd5a4a16f 100644 --- a/src/components/features/home/git-repo-dropdown/use-repository-data.tsx +++ b/src/components/features/home/git-repo-dropdown/use-repository-data.tsx @@ -83,7 +83,6 @@ export function useRepositoryData( ]); // Auto-load more repositories when there aren't enough items to create a scrollable dropdown - // This is particularly important for SaaS mode with installations that might have very few repos useEffect(() => { const shouldAutoLoad = !disabled && diff --git a/src/components/features/home/homepage-cta.tsx b/src/components/features/home/homepage-cta.tsx deleted file mode 100644 index 66499c94ef..0000000000 --- a/src/components/features/home/homepage-cta.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Dispatch, SetStateAction } from "react"; -import { Card } from "#/ui/card"; -import { CardTitle } from "#/ui/card-title"; -import { Typography } from "#/ui/typography"; -import { cn } from "#/utils/utils"; -import { I18nKey } from "#/i18n/declaration"; -import { setCTADismissed } from "#/utils/local-storage"; -import { useTracking } from "#/hooks/use-tracking"; -import CloseIcon from "#/icons/close.svg?react"; - -interface HomepageCTAProps { - setShouldShowCTA: Dispatch>; -} - -export function HomepageCTA({ setShouldShowCTA }: HomepageCTAProps) { - const { t } = useTranslation("openhands"); - const { trackSaasSelfhostedInquiry } = useTracking(); - - const handleClose = () => { - setCTADismissed("homepage"); - setShouldShowCTA(false); - }; - - const handleLearnMoreClick = () => { - trackSaasSelfhostedInquiry({ location: "home_page" }); - }; - - return ( - - - -
-
- - {t(I18nKey.CTA$ENTERPRISE_TITLE)} - - - - {t(I18nKey.CTA$ENTERPRISE_DESCRIPTION)} - -
- - - {t(I18nKey.CTA$LEARN_MORE)} - -
-
- ); -} diff --git a/src/components/features/home/tasks/task-suggestions.tsx b/src/components/features/home/tasks/task-suggestions.tsx index aa0465df00..d40ae1d1d4 100644 --- a/src/components/features/home/tasks/task-suggestions.tsx +++ b/src/components/features/home/tasks/task-suggestions.tsx @@ -7,7 +7,6 @@ import { TaskSuggestionsSkeleton } from "./task-suggestions-skeleton"; import { cn, getDisplayedTaskGroups, getTotalTaskCount } from "#/utils/utils"; import { I18nKey } from "#/i18n/declaration"; import { GitRepository } from "#/types/git"; -import { useConfig } from "#/hooks/query/use-config"; import { useUserProviders } from "#/hooks/use-user-providers"; import { Typography } from "#/ui/typography"; @@ -19,12 +18,10 @@ export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) { const { t } = useTranslation("openhands"); const [isExpanded, setIsExpanded] = useState(false); - const { data: config } = useConfig(); const { data: tasks, isLoading } = useSuggestedTasks(); const { providers } = useUserProviders(); - const isOSS = config?.app_mode === "oss"; - const hasNoProviders = isOSS && providers.length === 0; + const hasNoProviders = providers.length === 0; const suggestedTasks = filterFor ? tasks?.filter( diff --git a/src/components/features/settings/git-settings/configure-azure-devops-anchor.tsx b/src/components/features/settings/git-settings/configure-azure-devops-anchor.tsx deleted file mode 100644 index 4bdb41c332..0000000000 --- a/src/components/features/settings/git-settings/configure-azure-devops-anchor.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { I18nKey } from "#/i18n/declaration"; -import { useConfig } from "#/hooks/query/use-config"; -import { useAuthUrl } from "#/hooks/use-auth-url"; -import { BrandButton } from "../brand-button"; - -export function ConfigureAzureDevOpsAnchor() { - const { t } = useTranslation("openhands"); - const { data: config } = useConfig(); - - const authUrl = useAuthUrl({ - appMode: config?.app_mode ?? null, - identityProvider: "azure_devops", - authUrl: config?.auth_url, - }); - - const handleOAuthFlow = () => { - if (!authUrl) { - return; - } - - window.location.href = authUrl; - }; - - return ( -
- - {t(I18nKey.AZURE_DEVOPS$CONNECT_ACCOUNT)} - -
- ); -} diff --git a/src/components/features/sidebar/sidebar.tsx b/src/components/features/sidebar/sidebar.tsx index a2600b0dac..5dcee0a514 100644 --- a/src/components/features/sidebar/sidebar.tsx +++ b/src/components/features/sidebar/sidebar.tsx @@ -49,7 +49,6 @@ export function Sidebar() { "Something went wrong while fetching settings. Please reload the page.", ); } else if ( - config?.app_mode === "oss" && settingsErrorStatus === 404 && !config?.feature_flags?.hide_llm_settings ) { @@ -60,7 +59,6 @@ export function Sidebar() { isFetchingSettings, settingsIsError, settingsErrorStatus, - config?.app_mode, config?.feature_flags?.hide_llm_settings, ]); diff --git a/src/components/shared/navigation-link.tsx b/src/components/shared/navigation-link.tsx index ee9e472ba9..4456ef2e9f 100644 --- a/src/components/shared/navigation-link.tsx +++ b/src/components/shared/navigation-link.tsx @@ -8,11 +8,10 @@ interface NavigationLinkClassNameState { isActive: boolean; } -export interface NavigationLinkProps - extends Omit< - React.AnchorHTMLAttributes, - "className" | "href" - > { +export interface NavigationLinkProps extends Omit< + React.AnchorHTMLAttributes, + "className" | "href" +> { to: string; replace?: boolean; end?: boolean; @@ -40,54 +39,56 @@ function isPathActive(currentPath: string, to: string, end: boolean) { export const NavigationLink = React.forwardRef< HTMLAnchorElement, NavigationLinkProps ->(function NavigationLink( - { - to, - replace = false, - end = false, - onClick, - className, - children, - target, - rel, - ...props +>( + ( + { + to, + replace = false, + end = false, + onClick, + className, + children, + target, + rel, + ...props + }, + ref, + ) => { + const { currentPath, navigate } = useNavigation(); + const isActive = isPathActive(currentPath, to, end); + + const resolvedClassName = + typeof className === "function" ? className({ isActive }) : className; + + const handleClick = (event: React.MouseEvent) => { + onClick?.(event); + + if ( + event.defaultPrevented || + event.button !== 0 || + isModifiedEvent(event) || + target === "_blank" + ) { + return; + } + + event.preventDefault(); + navigate(to, { replace } satisfies NavigationOptions); + }; + + return ( + + {children} + + ); }, - ref, -) { - const { currentPath, navigate } = useNavigation(); - const isActive = isPathActive(currentPath, to, end); - - const resolvedClassName = - typeof className === "function" ? className({ isActive }) : className; - - const handleClick = (event: React.MouseEvent) => { - onClick?.(event); - - if ( - event.defaultPrevented || - event.button !== 0 || - isModifiedEvent(event) || - target === "_blank" - ) { - return; - } - - event.preventDefault(); - navigate(to, { replace } satisfies NavigationOptions); - }; - - return ( - - {children} - - ); -}); +); diff --git a/src/constants/onboarding.tsx b/src/constants/onboarding.tsx deleted file mode 100644 index 50801382c7..0000000000 --- a/src/constants/onboarding.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { I18nKey } from "#/i18n/declaration"; - -export type OnboardingAppMode = "cloud" | "self-hosted" | "oss"; - -interface BaseOnboardingQuestion { - id: string; - app_mode: OnboardingAppMode[]; - questionKey: I18nKey; - subtitleKey?: I18nKey; -} - -interface InputQuestion extends BaseOnboardingQuestion { - type: "input"; - inputOptions: { key: I18nKey; id: string }[]; -} - -interface SingleSelectQuestion extends BaseOnboardingQuestion { - type: "single"; - answerOptions: { key: I18nKey; id: string }[]; -} - -interface MultiSelectQuestion extends BaseOnboardingQuestion { - type: "multi"; - answerOptions: { key: I18nKey; id: string }[]; -} - -export type OnboardingQuestion = - | InputQuestion - | SingleSelectQuestion - | MultiSelectQuestion; - -export const ONBOARDING_FORM: OnboardingQuestion[] = [ - { - id: "use_case", - type: "multi", - app_mode: ["cloud", "self-hosted"], - questionKey: I18nKey.ONBOARDING$USE_CASE_QUESTION, - subtitleKey: I18nKey.ONBOARDING$SELECT_MULTIPLE_SUBTITLE, - answerOptions: [ - { key: I18nKey.ONBOARDING$USE_CASE_NEW_FEATURES, id: "new_features" }, - { - key: I18nKey.ONBOARDING$USE_CASE_APP_FROM_SCRATCH, - id: "app_from_scratch", - }, - { key: I18nKey.ONBOARDING$USE_CASE_FIXING_BUGS, id: "fixing_bugs" }, - { key: I18nKey.ONBOARDING$USE_CASE_REFACTORING, id: "refactoring" }, - { - key: I18nKey.ONBOARDING$USE_CASE_AUTOMATING_TASKS, - id: "automating_tasks", - }, - { key: I18nKey.ONBOARDING$USE_CASE_NOT_SURE, id: "not_sure" }, - ], - }, - { - id: "role", - type: "single", - app_mode: ["cloud"], - questionKey: I18nKey.ONBOARDING$ROLE_QUESTION, - subtitleKey: I18nKey.ONBOARDING$SELECT_ONE_SUBTITLE, - answerOptions: [ - { - key: I18nKey.ONBOARDING$ROLE_SOFTWARE_ENGINEER, - id: "software_engineer", - }, - { - key: I18nKey.ONBOARDING$ROLE_ENGINEERING_MANAGER, - id: "engineering_manager", - }, - { key: I18nKey.ONBOARDING$ROLE_CTO_FOUNDER, id: "cto_founder" }, - { - key: I18nKey.ONBOARDING$ROLE_PRODUCT_OPERATIONS, - id: "product_operations", - }, - { key: I18nKey.ONBOARDING$ROLE_STUDENT_HOBBYIST, id: "student_hobbyist" }, - { key: I18nKey.ONBOARDING$ROLE_OTHER, id: "other" }, - ], - }, -]; diff --git a/src/contexts/conversation-websocket-context.tsx b/src/contexts/conversation-websocket-context.tsx index 59aa241fc3..ad512502f4 100644 --- a/src/contexts/conversation-websocket-context.tsx +++ b/src/contexts/conversation-websocket-context.tsx @@ -46,11 +46,9 @@ import type { import EventService from "#/api/event-service/event-service.api"; import PendingMessageService from "#/api/pending-message-service/pending-message-service.api"; import { useConversationStore } from "#/stores/conversation-store"; -import { isBudgetOrCreditError, trackError } from "#/utils/error-handler"; -import { useTracking } from "#/hooks/use-tracking"; +import { trackError } from "#/utils/error-handler"; import { useReadConversationFile } from "#/hooks/mutation/use-read-conversation-file"; import useMetricsStore from "#/stores/metrics-store"; -import { I18nKey } from "#/i18n/declaration"; import { useConversationHistory } from "#/hooks/query/use-conversation-history"; import { setConversationState } from "#/utils/conversation-local-storage"; @@ -108,7 +106,6 @@ export function ConversationWebSocketProvider({ const { removeOptimisticUserMessage } = useOptimisticUserMessageStore(); const { setExecutionStatus } = useV1ConversationStateStore(); const { appendInput, appendOutput } = useCommandStore(); - const { trackCreditLimitReached } = useTracking(); // History loading state - separate per connection const [isLoadingHistoryMain, setIsLoadingHistoryMain] = useState(true); @@ -139,24 +136,9 @@ export function ConversationWebSocketProvider({ const isPlanFilePath = (path: string | null): boolean => path?.toUpperCase().endsWith("PLAN.MD") ?? false; - // Helper to handle error clearing logic for non-error events. - // Budget/credit errors persist until an agent event proves the LLM is working. - const handleNonErrorEvent = useCallback( - (event: { source?: string }) => { - const currentError = useErrorMessageStore.getState().errorMessage; - const isBudgetError = - currentError === I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS; - const isAgentEvent = event.source === "agent"; - - // Budget errors persist until agent proves LLM is working - if (isBudgetError && !isAgentEvent) { - return; // Keep budget error visible - } - - removeErrorMessage(); - }, - [removeErrorMessage], - ); + const handleNonErrorEvent = useCallback(() => { + removeErrorMessage(); + }, [removeErrorMessage]); // Helper function to update metrics from stats event const updateMetricsFromStats = useCallback( @@ -390,16 +372,9 @@ export function ConversationWebSocketProvider({ }, posthog, }); - if (isBudgetOrCreditError(errorEvent.detail)) { - setErrorMessage(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS); - trackCreditLimitReached({ - conversationId: conversationId || "unknown", - }); - } else { - setErrorMessage(errorEvent.detail); - } + setErrorMessage(errorEvent.detail); } else { - handleNonErrorEvent(event); + handleNonErrorEvent(); } // Track credit limit reached if AgentErrorEvent has budget-related error @@ -492,7 +467,6 @@ export function ConversationWebSocketProvider({ isLoadingHistoryMain, expectedEventCountMain, setErrorMessage, - removeErrorMessage, removeOptimisticUserMessage, queryClient, conversationId, @@ -500,7 +474,7 @@ export function ConversationWebSocketProvider({ appendInput, appendOutput, updateMetricsFromStats, - trackCreditLimitReached, + handleNonErrorEvent, posthog, ], ); @@ -547,16 +521,9 @@ export function ConversationWebSocketProvider({ }, posthog, }); - if (isBudgetOrCreditError(errorEvent.detail)) { - setErrorMessage(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS); - trackCreditLimitReached({ - conversationId: conversationId || "unknown", - }); - } else { - setErrorMessage(errorEvent.detail); - } + setErrorMessage(errorEvent.detail); } else { - handleNonErrorEvent(event); + handleNonErrorEvent(); } // Handle AgentErrorEvent specifically @@ -672,7 +639,6 @@ export function ConversationWebSocketProvider({ isLoadingHistoryPlanning, expectedEventCountPlanning, setErrorMessage, - removeErrorMessage, removeOptimisticUserMessage, queryClient, subConversations, @@ -683,7 +649,7 @@ export function ConversationWebSocketProvider({ readConversationFile, setPlanContent, updateMetricsFromStats, - trackCreditLimitReached, + handleNonErrorEvent, posthog, ], ); diff --git a/src/hooks/mutation/use-create-conversation.ts b/src/hooks/mutation/use-create-conversation.ts index c27f58b8a6..c4d8839601 100644 --- a/src/hooks/mutation/use-create-conversation.ts +++ b/src/hooks/mutation/use-create-conversation.ts @@ -61,8 +61,7 @@ export const useCreateConversation = () => { ); return { - conversation_id: - conversation.app_conversation_id || conversation.id, + conversation_id: conversation.app_conversation_id || conversation.id, session_api_key: null, url: conversation.agent_server_url, is_v1: true, diff --git a/src/hooks/mutation/use-refresh-llm-api-key.ts b/src/hooks/mutation/use-refresh-llm-api-key.ts deleted file mode 100644 index 11a112e182..0000000000 --- a/src/hooks/mutation/use-refresh-llm-api-key.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { openHands } from "#/api/open-hands-axios"; -import { - LLM_API_KEY_QUERY_KEY, - LlmApiKeyResponse, -} from "#/hooks/query/use-llm-api-key"; - -export function useRefreshLlmApiKey() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: async () => { - const { data } = await openHands.post( - "/api/keys/llm/byor/refresh", - ); - return data; - }, - onSuccess: () => { - // Invalidate the LLM API key query to trigger a refetch - queryClient.invalidateQueries({ queryKey: [LLM_API_KEY_QUERY_KEY] }); - }, - }); -} diff --git a/src/hooks/mutation/use-submit-onboarding.ts b/src/hooks/mutation/use-submit-onboarding.ts deleted file mode 100644 index de922a88e9..0000000000 --- a/src/hooks/mutation/use-submit-onboarding.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useNavigate } from "react-router"; -import { openHands } from "#/api/open-hands-axios"; -import { displayErrorToast } from "#/utils/custom-toast-handlers"; - -type SubmitOnboardingArgs = { - selections: Record; -}; - -export const useSubmitOnboarding = () => { - const navigate = useNavigate(); - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: async ({ selections }: SubmitOnboardingArgs) => { - // Mark onboarding as complete - await openHands.post("/api/complete_onboarding"); - return { selections }; - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["settings"] }); - - const finalRedirectUrl = "/"; - // Check if the redirect URL is an external URL (starts with http or https) - if ( - finalRedirectUrl.startsWith("http://") || - finalRedirectUrl.startsWith("https://") - ) { - // For external URLs, redirect using window.location - window.location.href = finalRedirectUrl; - } else { - // For internal routes, use navigate - navigate(finalRedirectUrl); - } - }, - onError: (error) => { - displayErrorToast(error.message); - window.location.href = "/"; - }, - }); -}; diff --git a/src/hooks/mutation/use-update-conversation-public-flag.ts b/src/hooks/mutation/use-update-conversation-public-flag.ts deleted file mode 100644 index 405d78ac30..0000000000 --- a/src/hooks/mutation/use-update-conversation-public-flag.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useTranslation } from "react-i18next"; -import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api"; -import { I18nKey } from "#/i18n/declaration"; -import { - displaySuccessToast, - displayErrorToast, -} from "#/utils/custom-toast-handlers"; - -export const useUpdateConversationPublicFlag = () => { - const queryClient = useQueryClient(); - const { t } = useTranslation("openhands"); - - return useMutation({ - mutationFn: (variables: { conversationId: string; isPublic: boolean }) => - V1ConversationService.updateConversationPublicFlag( - variables.conversationId, - variables.isPublic, - ), - onMutate: async (variables) => { - // Cancel any outgoing refetches - await queryClient.cancelQueries({ - queryKey: ["user", "conversation", variables.conversationId], - }); - - // Snapshot the previous value - const previousConversation = queryClient.getQueryData([ - "user", - "conversation", - variables.conversationId, - ]); - - // Optimistically update the conversation - queryClient.setQueryData( - ["user", "conversation", variables.conversationId], - (old: unknown) => - old && typeof old === "object" - ? { ...old, public: variables.isPublic } - : old, - ); - - return { previousConversation }; - }, - onError: (err, variables, context) => { - // Rollback on error - if (context?.previousConversation) { - queryClient.setQueryData( - ["user", "conversation", variables.conversationId], - context.previousConversation, - ); - } - displayErrorToast( - t(I18nKey.CONVERSATION$FAILED_TO_UPDATE_PUBLIC_SHARING), - ); - }, - onSuccess: () => { - displaySuccessToast(t(I18nKey.CONVERSATION$PUBLIC_SHARING_UPDATED)); - }, - onSettled: (data, error, variables) => { - // Always refetch after error or success - queryClient.invalidateQueries({ - queryKey: ["user", "conversation", variables.conversationId], - }); - // Also invalidate the conversations list to update any cached data - queryClient.invalidateQueries({ - queryKey: ["user", "conversations"], - }); - }, - }); -}; diff --git a/src/hooks/query/use-app-installations.ts b/src/hooks/query/use-app-installations.ts index 2a3fa259a2..2b35c94623 100644 --- a/src/hooks/query/use-app-installations.ts +++ b/src/hooks/query/use-app-installations.ts @@ -1,5 +1,4 @@ import { useQuery } from "@tanstack/react-query"; -import { useConfig } from "./use-config"; import { useIsAuthed } from "./use-is-authed"; import GitService from "#/api/git-service/git-service.api"; import { useUserProviders } from "../use-user-providers"; @@ -10,7 +9,6 @@ import { shouldUseInstallationRepos } from "#/utils/utils"; * Get the first page of app installations for the provider given. */ export const useAppInstallations = (selectedProvider: Provider | null) => { - const { data: config } = useConfig(); const { data: userIsAuthenticated } = useIsAuthed(); const { providers } = useUserProviders(); @@ -20,7 +18,7 @@ export const useAppInstallations = (selectedProvider: Provider | null) => { enabled: userIsAuthenticated && !!selectedProvider && - shouldUseInstallationRepos(selectedProvider, config?.app_mode), + shouldUseInstallationRepos(selectedProvider), staleTime: 1000 * 60 * 5, // 5 minutes gcTime: 1000 * 60 * 15, // 15 minutes }); diff --git a/src/hooks/query/use-git-repositories.ts b/src/hooks/query/use-git-repositories.ts index 3b2fbcc2b4..546155bb69 100644 --- a/src/hooks/query/use-git-repositories.ts +++ b/src/hooks/query/use-git-repositories.ts @@ -1,5 +1,4 @@ import { useInfiniteQuery, InfiniteData } from "@tanstack/react-query"; -import { useConfig } from "./use-config"; import { useUserProviders } from "../use-user-providers"; import { useAppInstallations } from "./use-app-installations"; import { RepositoryPage } from "../../types/git"; @@ -20,12 +19,11 @@ type Cursor = InstallationCursor | UserCursor; export function useGitRepositories(options: UseGitRepositoriesOptions) { const { provider, pageSize = 30, enabled = true } = options; const { providers } = useUserProviders(); - const { data: config } = useConfig(); const { data: page } = useAppInstallations(provider); const installations = page?.items; const useInstallationRepos = provider - ? shouldUseInstallationRepos(provider, config?.app_mode) + ? shouldUseInstallationRepos(provider) : false; const repos = useInfiniteQuery< diff --git a/src/hooks/query/use-git-user.ts b/src/hooks/query/use-git-user.ts index 2ff523c1a5..8df6877654 100644 --- a/src/hooks/query/use-git-user.ts +++ b/src/hooks/query/use-git-user.ts @@ -1,13 +1,11 @@ import { useQuery } from "@tanstack/react-query"; import React from "react"; import { usePostHog } from "posthog-js/react"; -import { useConfig } from "./use-config"; import UserService from "#/api/user-service/user-service.api"; import { useShouldShowUserFeatures } from "#/hooks/use-should-show-user-features"; export const useGitUser = () => { const posthog = usePostHog(); - const { data: config } = useConfig(); const shouldFetchUser = useShouldShowUserFeatures(); const user = useQuery({ @@ -26,10 +24,9 @@ export const useGitUser = () => { name: user.data.name, email: user.data.email, user: user.data.login, - mode: config?.app_mode || "oss", }); } - }, [config?.app_mode, posthog, user.data]); + }, [posthog, user.data]); return user; }; diff --git a/src/hooks/query/use-llm-api-key.ts b/src/hooks/query/use-llm-api-key.ts deleted file mode 100644 index 0970ee98e8..0000000000 --- a/src/hooks/query/use-llm-api-key.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { AxiosError } from "axios"; -import { openHands } from "#/api/open-hands-axios"; -import { useConfig } from "./use-config"; - -export const LLM_API_KEY_QUERY_KEY = "llm-api-key"; - -export interface LlmApiKeyResponse { - key: string | null; -} - -export interface LlmApiKeyError { - isPaymentRequired: boolean; - message?: string; -} - -export function useLlmApiKey() { - const { data: config } = useConfig(); - - const query = useQuery({ - queryKey: [LLM_API_KEY_QUERY_KEY], - enabled: config?.app_mode === "saas", - queryFn: async () => { - const { data } = - await openHands.get("/api/keys/llm/byor"); - return data; - }, - staleTime: 1000 * 60 * 5, // 5 minutes - gcTime: 1000 * 60 * 15, // 15 minutes - retry: (failureCount, error) => { - // Don't retry on 402 Payment Required - if (error instanceof AxiosError && error.response?.status === 402) { - return false; - } - return failureCount < 3; - }, - // Disable global error toast - we handle 402 errors in the UI - meta: { disableToast: true }, - }); - - // Check if the error is a 402 Payment Required - const isPaymentRequired = - query.error instanceof AxiosError && query.error.response?.status === 402; - - return { - data: query.data, - error: query.error, - isLoading: query.isLoading, - isPaymentRequired, - }; -} diff --git a/src/hooks/query/use-settings.ts b/src/hooks/query/use-settings.ts index f495dcdccd..b9b8033a1a 100644 --- a/src/hooks/query/use-settings.ts +++ b/src/hooks/query/use-settings.ts @@ -12,7 +12,7 @@ import { parseMcpConfig } from "#/utils/mcp-config"; export const getErrorStatus = (error: unknown): number | undefined => { if (typeof error === "object" && error !== null && "status" in error) { - const status = (error as { status?: unknown }).status; + const { status } = error as { status?: unknown }; if (typeof status === "number") { return status; } diff --git a/src/hooks/use-app-mode.ts b/src/hooks/use-app-mode.ts deleted file mode 100644 index b2ba9e83ae..0000000000 --- a/src/hooks/use-app-mode.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useMemo } from "react"; -import { useConfig } from "#/hooks/query/use-config"; - -/** - * Hook that provides boolean checks for app mode deployment mode. - * - * App Mode (app_mode): - * - "oss": Open source version running locally/self-hosted - * - "saas": All-Hands managed SaaS version - * - * Deployment Mode (deployment_mode): - * - "cloud": Enterprise customers running on All-Hands managed infrastructure (*.all-hands.dev, *.openhands.ai) - * - "self_hosted": Enterprise customers running on their own infrastructure - * - * Note: SaaS mode can have either cloud or self_hosted deployment mode. - */ -export function useAppMode() { - const { data: config } = useConfig(); - - return useMemo(() => { - const appMode = config?.app_mode; - const deploymentMode = config?.feature_flags?.deployment_mode; - - return { - // App Mode checks - isOss: appMode === "oss", - isSaas: appMode === "saas", - - // Deployment Mode checks - isCloud: deploymentMode === "cloud", - isSelfHosted: deploymentMode === "self_hosted", - - /** Enterprise checks */ - isEnterpriseSelfHosted: - appMode === "saas" && deploymentMode === "self_hosted", - isEnterpriseCloud: appMode === "saas" && deploymentMode === "cloud", - - // Raw values (for cases where the actual value is needed) - appMode, - deploymentMode, - }; - }, [config?.app_mode, config?.feature_flags?.deployment_mode]); -} diff --git a/src/hooks/use-auth-url.ts b/src/hooks/use-auth-url.ts deleted file mode 100644 index daf8db8229..0000000000 --- a/src/hooks/use-auth-url.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { generateAuthUrl } from "#/utils/generate-auth-url"; -import { WebClientConfig } from "#/api/option-service/option.types"; - -interface UseAuthUrlConfig { - appMode: WebClientConfig["app_mode"] | null; - identityProvider: string; - authUrl?: WebClientConfig["auth_url"]; -} - -export const useAuthUrl = (config: UseAuthUrlConfig) => { - if (config.appMode === "saas") { - return generateAuthUrl( - config.identityProvider, - new URL(window.location.href), - config.authUrl, - ); - } - - return null; -}; diff --git a/src/hooks/use-conversation-name-context-menu.ts b/src/hooks/use-conversation-name-context-menu.ts index 0f330a1502..6af151050a 100644 --- a/src/hooks/use-conversation-name-context-menu.ts +++ b/src/hooks/use-conversation-name-context-menu.ts @@ -1,15 +1,10 @@ -import { useTranslation } from "react-i18next"; import React from "react"; import { useNavigation } from "#/context/navigation-context"; import useMetricsStore from "#/stores/metrics-store"; import { useDeleteConversation } from "./mutation/use-delete-conversation"; import { useUnifiedPauseConversationSandbox } from "./mutation/use-unified-stop-conversation"; -import { useUpdateConversationPublicFlag } from "./mutation/use-update-conversation-public-flag"; -import { displaySuccessToast } from "#/utils/custom-toast-handlers"; -import { I18nKey } from "#/i18n/declaration"; import { useEventStore } from "#/stores/use-event-store"; -import { useActiveConversation } from "./query/use-active-conversation"; import { useDownloadConversation } from "./use-download-conversation"; import { adaptSystemMessage, @@ -30,13 +25,10 @@ export function useConversationNameContextMenu({ showOptions = false, onContextMenuToggle, }: UseConversationNameContextMenuProps) { - const { t } = useTranslation("openhands"); const { conversationId: currentConversationId, navigate } = useNavigation(); const events = useEventStore((state) => state.events); const { mutate: deleteConversation } = useDeleteConversation(); const { mutate: stopConversation } = useUnifiedPauseConversationSandbox(); - const { mutate: updatePublicFlag } = useUpdateConversationPublicFlag(); - const { data: conversation } = useActiveConversation(); const metrics = useMetricsStore(); const [metricsModalVisible, setMetricsModalVisible] = React.useState(false); @@ -131,41 +123,6 @@ export function useConversationNameContextMenu({ onContextMenuToggle?.(false); }; - const handleTogglePublic = (event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - - if (conversationId && conversation) { - // Toggle the current public state - const newPublicState = !conversation.public; - updatePublicFlag({ - conversationId, - isPublic: newPublicState, - }); - } - // Don't close menu - let user see the toggle state change - }; - - const shareUrl = React.useMemo(() => { - if (conversationId) { - return `${window.location.origin}/shared/conversations/${conversationId}`; - } - return ""; - }, [conversationId]); - - const handleCopyShareLink = (event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - - if (!shareUrl) { - onContextMenuToggle?.(false); - return; - } - - navigator.clipboard.writeText(shareUrl); - displaySuccessToast(t(I18nKey.CONVERSATION$LINK_COPIED)); - }; - return { // Handlers handleDelete, @@ -176,9 +133,6 @@ export function useConversationNameContextMenu({ handleShowAgentTools, handleShowSkills, handleShowHooks, - handleTogglePublic, - handleCopyShareLink, - shareUrl, handleConfirmDelete, handleConfirmStop, diff --git a/src/hooks/use-github-auth-url.ts b/src/hooks/use-github-auth-url.ts deleted file mode 100644 index 922070df47..0000000000 --- a/src/hooks/use-github-auth-url.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useAuthUrl } from "./use-auth-url"; -import { WebClientConfig } from "#/api/option-service/option.types"; - -interface UseGitHubAuthUrlConfig { - appMode: WebClientConfig["app_mode"] | null; - authUrl?: WebClientConfig["auth_url"]; -} - -export const useGitHubAuthUrl = (config: UseGitHubAuthUrlConfig) => - useAuthUrl({ - appMode: config.appMode, - identityProvider: "github", - authUrl: config.authUrl, - }); diff --git a/src/hooks/use-is-creating-conversation.ts b/src/hooks/use-is-creating-conversation.ts index 8e25154802..e491169cb9 100644 --- a/src/hooks/use-is-creating-conversation.ts +++ b/src/hooks/use-is-creating-conversation.ts @@ -7,7 +7,7 @@ export const useIsCreatingConversation = () => { mutationKey: ["create-conversation"], }); - const isNavigating = navigation.isNavigating; + const { isNavigating } = navigation; const hasPendingMutations = numberOfPendingMutations > 0; return hasPendingMutations || isNavigating; diff --git a/src/hooks/use-tracking.ts b/src/hooks/use-tracking.ts index 63fcbe2d24..cdd49a0798 100644 --- a/src/hooks/use-tracking.ts +++ b/src/hooks/use-tracking.ts @@ -1,21 +1,17 @@ import { usePostHog } from "posthog-js/react"; -import { useConfig } from "./query/use-config"; import { useSettings } from "./query/use-settings"; import { Provider } from "#/types/settings"; /** * Hook that provides tracking functions with automatic data collection - * from available hooks (config, settings, etc.) + * from available hooks (settings, etc.) */ export const useTracking = () => { const posthog = usePostHog(); - const { data: config } = useConfig(); const { data: settings } = useSettings(); // Common properties included in all tracking events const commonProperties = { - app_surface: config?.app_mode || "unknown", - plan_tier: null, current_url: window.location.href, user_email: settings?.email || settings?.git_user_email || null, }; @@ -74,84 +70,6 @@ export const useTracking = () => { }); }; - const trackCreditsPurchased = ({ - amountUsd, - stripeSessionId, - }: { - amountUsd: number; - stripeSessionId: string; - }) => { - posthog.capture("credits_purchased", { - amount_usd: amountUsd, - stripe_session_id: stripeSessionId, - ...commonProperties, - }); - }; - - const trackCreditLimitReached = ({ - conversationId, - }: { - conversationId: string; - }) => { - posthog.capture("credit_limit_reached", { - conversation_id: conversationId, - ...commonProperties, - }); - }; - - const trackAddTeamMembersButtonClick = () => { - posthog.capture("exp_add_team_members", { - ...commonProperties, - }); - }; - - const trackOnboardingCompleted = ({ - role, - orgSize, - useCase, - }: { - role?: string; - orgSize?: string; - useCase?: string[]; - }) => { - posthog.capture("onboarding_completed", { - role, - org_size: orgSize, - use_case: useCase, - ...commonProperties, - }); - }; - - const trackSaasSelfhostedInquiry = ({ location }: { location: string }) => { - posthog.capture("saas_selfhosted_inquiry", { - location, - ...commonProperties, - }); - }; - - const trackEnterpriseLeadFormSubmitted = ({ - requestType, - name, - company, - email, - message, - }: { - requestType: "saas" | "self-hosted"; - name: string; - company: string; - email: string; - message: string; - }) => { - posthog.capture("enterprise_lead_form_submitted", { - request_type: requestType, - name, - company, - email, - message, - ...commonProperties, - }); - }; - return { trackLoginButtonClick, trackConversationCreated, @@ -160,11 +78,5 @@ export const useTracking = () => { trackCreatePrButtonClick, trackGitProviderConnected, trackUserSignupCompleted, - trackCreditsPurchased, - trackCreditLimitReached, - trackAddTeamMembersButtonClick, - trackOnboardingCompleted, - trackSaasSelfhostedInquiry, - trackEnterpriseLeadFormSubmitted, }; }; diff --git a/src/i18n/translation.json b/src/i18n/translation.json index 07a35ad120..8d078de06e 100644 --- a/src/i18n/translation.json +++ b/src/i18n/translation.json @@ -4793,23 +4793,6 @@ "uk": "Agent Server", "ca": "Agent Server" }, - "SETTINGS$NAV_BILLING": { - "en": "Billing", - "ja": "請求", - "zh-CN": "账单", - "zh-TW": "帳單", - "ko-KR": "결제", - "no": "Fakturering", - "it": "Fatturazione", - "pt": "Faturamento", - "es": "Facturación", - "ar": "الفواتير", - "fr": "Facturation", - "tr": "Faturalama", - "de": "Abrechnung", - "uk": "Виставлення рахунків", - "ca": "Facturació" - }, "SETTINGS$NAV_SECRETS": { "en": "Secrets", "ja": "シークレット", @@ -10420,23 +10403,6 @@ "uk": "Оновити ключ API", "ca": "Actualitza la clau d'API" }, - "SETTINGS$LLM_API_KEY_PAYWALL_MESSAGE": { - "en": "Purchase at least $10 in credits to get access to OpenHands LLM key for use with OpenHands CLI and SDK.", - "zh-CN": "购买至少10美元的积分,即可获得OpenHands LLM密钥,用于OpenHands CLI和SDK。", - "zh-TW": "購買至少10美元的積分,即可獲得OpenHands LLM金鑰,用於OpenHands CLI和SDK。", - "de": "Kaufen Sie mindestens $10 an Guthaben, um Zugang zum OpenHands LLM-Schlüssel für die Verwendung mit OpenHands CLI und SDK zu erhalten.", - "ko-KR": "OpenHands CLI 및 SDK에서 사용할 OpenHands LLM 키에 액세스하려면 최소 $10의 크레딧을 구매하세요.", - "no": "Kjøp minst $10 i kreditter for å få tilgang til OpenHands LLM-nøkkel for bruk med OpenHands CLI og SDK.", - "it": "Acquista almeno $10 di crediti per ottenere l'accesso alla chiave LLM OpenHands per l'uso con OpenHands CLI e SDK.", - "pt": "Compre pelo menos $10 em créditos para obter acesso à chave LLM OpenHands para uso com OpenHands CLI e SDK.", - "es": "Compre al menos $10 en créditos para obtener acceso a la clave LLM de OpenHands para usar con OpenHands CLI y SDK.", - "ar": "اشترِ ما لا يقل عن 10 دولارات من الرصيد للحصول على مفتاح LLM OpenHands لاستخدامه مع OpenHands CLI و SDK.", - "fr": "Achetez au moins 10$ de crédits pour accéder à la clé LLM OpenHands à utiliser avec OpenHands CLI et SDK.", - "tr": "OpenHands CLI ve SDK ile kullanmak için OpenHands LLM Anahtarına erişim sağlamak için en az 10$ kredi satın alın.", - "ja": "OpenHands CLIおよびSDKで使用するOpenHands LLMキーにアクセスするには、少なくとも$10のクレジットを購入してください。", - "uk": "Придбайте кредитів мінімум на $10, щоб отримати доступ до ключа LLM OpenHands для використання з OpenHands CLI та SDK.", - "ca": "Compreu almenys 10 $ en crèdits per accedir a la clau LLM d'OpenHands per a la CLI i l'SDK d'OpenHands." - }, "SETTINGS$LLM_API_KEY_BUY_NOW": { "en": "Buy Now", "zh-CN": "立即购买", @@ -10794,40 +10760,6 @@ "uk": "вкладці OpenHands Cloud.", "ca": "pestanya d'OpenHands Cloud." }, - "SETTINGS$LLM_BILLING_INFO": { - "en": "LLM usage is billed at the providers' rates with no markup.", - "ja": "LLMの使用料金は、プロバイダーの料金でマークアップなしで請求されます。", - "zh-CN": "LLM使用费用按提供商费率计费,无加价。", - "zh-TW": "LLM使用費用按提供商費率計費,無加價。", - "ko-KR": "LLM 사용료는 제공업체 요금으로 마크업 없이 청구됩니다。", - "no": "LLM-bruk faktureres til leverandørenes priser uten påslag.", - "it": "L'utilizzo di LLM viene fatturato alle tariffe dei fornitori senza ricarico.", - "pt": "O uso de LLM é cobrado nas tarifas dos provedores sem markup.", - "es": "El uso de LLM se factura a las tarifas de los proveedores sin recargo.", - "ar": "يتم فوترة استخدام LLM بأسعار المزودين بدون زيادة.", - "fr": "L'utilisation de LLM est facturée aux tarifs des fournisseurs sans majoration.", - "tr": "LLM kullanımı, sağlayıcıların oranlarında ek ücret olmadan faturalandırılır.", - "de": "LLM-Nutzung wird zu Anbieterpreisen ohne Aufschlag abgerechnet.", - "uk": "Використання LLM оплачується за тарифами провайдерів без надбавки.", - "ca": "L'ús del LLM es factura a les tarifes dels proveïdors sense cap markup." - }, - "SETTINGS$SEE_PRICING_DETAILS": { - "en": "See pricing details.", - "ja": "価格詳細を見る。", - "zh-CN": "查看价格详情。", - "zh-TW": "查看價格詳情。", - "ko-KR": "가격 세부정보 보기。", - "no": "Se prisdetaljer.", - "it": "Vedi dettagli sui prezzi.", - "pt": "Ver detalhes de preços.", - "es": "Ver detalles de precios.", - "ar": "انظر تفاصيل الأسعار.", - "fr": "Voir les détails de prix.", - "tr": "Fiyat ayrıntılarını gör.", - "de": "Preisdetails anzeigen.", - "uk": "Переглянути деталі цін.", - "ca": "Consulteu els detalls dels preus." - }, "SETTINGS$CREATE_API_KEY": { "en": "Create API Key", "uk": "Створити API ключ", @@ -12001,23 +11933,6 @@ "uk": "Запит не вдалося виконати через внутрішню помилку сервера.", "ca": "La sol·licitud ha fallat amb un error intern del servidor." }, - "STATUS$ERROR_LLM_OUT_OF_CREDITS": { - "en": "You're out of OpenHands Credits. Add funds", - "ja": "OpenHandsクレジットが不足しています。資金を追加", - "zh-CN": "您的OpenHands点数已用完。添加资金", - "zh-TW": "您的OpenHands點數已用完。添加資金", - "ko-KR": "OpenHands 크레딧이 소진되었습니다. 자금 추가", - "no": "Du er tom for OpenHands-kreditter. Legg til midler", - "it": "Hai esaurito i crediti OpenHands. Aggiungi fondi", - "pt": "Você está sem créditos OpenHands. Adicionar fundos", - "es": "Te has quedado sin créditos de OpenHands. Añadir fondos", - "ar": "لقد نفدت رصيدك من OpenHands. إضافة رصيد", - "fr": "Vous n'avez plus de crédits OpenHands. Ajouter des fonds", - "tr": "OpenHands kredileriniz tükendi. Bakiye ekle", - "de": "Ihre OpenHands-Guthaben sind aufgebraucht. Guthaben hinzufügen", - "uk": "У вас закінчилися кредити OpenHands. Додати кошти", - "ca": "No teniu crèdits d'OpenHands. Afegiu fons" - }, "STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION": { "en": "Content policy violation. The output was blocked by content filtering policy.", "ja": "コンテンツポリシー違反。出力はコンテンツフィルタリングポリシーによってブロックされました。", @@ -13191,244 +13106,6 @@ "uk": "Експорт розмови", "ca": "Exporta la conversa" }, - "BILLING$CLICK_TO_TOP_UP": { - "en": "Add funds to Your Account", - "ja": "アカウントに資金を追加", - "zh-CN": "为您的账户充值", - "zh-TW": "為您的帳戶充值", - "ko-KR": "계정에 자금 추가", - "no": "Legg til midler på kontoen din", - "it": "Aggiungi fondi al tuo account", - "pt": "Adicionar fundos à sua conta", - "es": "Añadir fondos a tu cuenta", - "ar": "إضافة رصيد إلى حسابك", - "fr": "Ajouter des fonds à votre compte", - "tr": "Hesabınıza bakiye ekleyin", - "de": "Guthaben zu Ihrem Konto hinzufügen", - "uk": "Додайте кошти до свого облікового запису", - "ca": "Afegiu fons al vostre compte" - }, - "BILLING$YOUVE_GOT_50": { - "en": "You've got $50 in free OpenHands credits", - "ja": "OpenHandsの無料クレジット$50を獲得しました", - "zh-CN": "您获得了 $50 的 OpenHands 免费额度", - "zh-TW": "您獲得了 $50 的 OpenHands 免費額度", - "ko-KR": "OpenHands 무료 크레딧 $50를 받았습니다", - "no": "Du har fått $50 i gratis OpenHands-kreditter", - "it": "Hai ottenuto $50 in crediti gratuiti OpenHands", - "pt": "Você ganhou $50 em créditos gratuitos OpenHands", - "es": "Has recibido $50 en créditos gratuitos de OpenHands", - "ar": "لديك 50$ من رصيد OpenHands المجاني", - "fr": "Vous avez reçu $50 de crédits OpenHands gratuits", - "tr": "OpenHands'de $50 ücretsiz kredi kazandınız", - "de": "Sie haben $50 in kostenlosen OpenHands-Guthaben erhalten", - "uk": "Ви отримали 50 доларів у безкоштовних кредитах OpenHands", - "ca": "Teniu 50 $ en crèdits gratuïts d'OpenHands" - }, - "BILLING$ERROR_WHILE_CREATING_SESSION": { - "en": "Error occurred while setting up your payment session. Please try again later.", - "ja": "お支払いセッションの設定中にエラーが発生しました。後ほど再度お試しください。", - "zh-CN": "设置支付会话时发生错误。请稍后再试。", - "zh-TW": "設置支付會話時發生錯誤。請稍後再試。", - "ko-KR": "결제 세션 설정 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.", - "no": "Det oppstod en feil under oppsett av betalingsøkten. Vennligst prøv igjen senere.", - "it": "Si è verificato un errore durante la configurazione della sessione di pagamento. Si prega di riprovare più tardi.", - "pt": "Ocorreu um erro ao configurar sua sessão de pagamento. Por favor, tente novamente mais tarde.", - "es": "Se produjo un error al configurar tu sesión de pago. Por favor, inténtalo de nuevo más tarde.", - "ar": "حدث خطأ أثناء إعداد جلسة الدفع الخاصة بك. يرجى المحاولة مرة أخرى لاحقًا.", - "fr": "Une erreur s'est produite lors de la configuration de votre session de paiement. Veuillez réessayer plus tard.", - "tr": "Ödeme oturumunuz kurulurken bir hata oluştu. Lütfen daha sonra tekrar deneyin.", - "de": "Beim Einrichten Ihrer Zahlungssitzung ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.", - "uk": "Під час налаштування сеансу оплати сталася помилка. Будь ласка, спробуйте пізніше.", - "ca": "S'ha produït un error en configurar la sessió de pagament. Torneu-ho a intentar més tard." - }, - "BILLING$CLAIM_YOUR_50": { - "en": "Add a credit card with Stripe to claim your $50. We won't charge you without asking first!", - "ja": "Stripeでクレジットカードを追加して$50を獲得。事前の確認なしで請求することはありません!", - "zh-CN": "添加 Stripe 信用卡以领取 $50。我们不会在未经您同意的情况下收费!", - "zh-TW": "添加 Stripe 信用卡以領取 $50。我們不會在未經您同意的情況下收費!", - "ko-KR": "Stripe에 신용카드를 추가하여 $50를 받으세요. 사전 동의 없이 요금이 청구되지 않습니다!", - "no": "Legg til et kredittkort med Stripe for å få $50. Vi belaster deg ikke uten å spørre først!", - "it": "Aggiungi una carta di credito con Stripe per ottenere $50. Non ti addebiteremo nulla senza chiedere prima!", - "pt": "Adicione um cartão de crédito com Stripe para receber $50. Não cobraremos sem perguntar primeiro!", - "es": "Añade una tarjeta de crédito con Stripe para reclamar tus $50. ¡No te cobraremos sin preguntarte primero!", - "ar": "أضف بطاقة ائتمان مع Stripe للحصول على 50$. لن نقوم بالخصم دون إذن مسبق!", - "fr": "Ajoutez une carte de crédit avec Stripe pour obtenir 50$. Nous ne vous facturerons pas sans vous demander d'abord !", - "tr": "50$ almak için Stripe ile kredi kartı ekleyin. Önce sormadan ücret almayacağız!", - "de": "Fügen Sie eine Kreditkarte mit Stripe hinzu, um $50 zu erhalten. Wir belasten Sie nicht ohne vorherige Zustimmung!", - "uk": "Додайте кредитну картку до Stripe, щоб отримати свої 50 доларів. Ми не стягуватимемо з вас плату без попереднього запиту!", - "ca": "Afegiu una targeta de crèdit amb Stripe per reclamar els vostres 50 $. No us cobrarem sense demanar-vos-ho primer!" - }, - "BILLING$POWERED_BY": { - "en": "Powered by", - "ja": "提供:", - "zh-CN": "技术支持:", - "zh-TW": "技術支援:", - "ko-KR": "제공: ", - "no": "Drevet av", - "it": "Offerto da", - "pt": "Oferecido por", - "es": "Ofrecido por", - "ar": "مشغل بواسطة", - "fr": "Propulsé par", - "tr": "Tarafından desteklenmektedir", - "de": "Bereitgestellt von", - "uk": "Працює на базі", - "ca": "Impulsat per" - }, - "BILLING$PROCEED_TO_STRIPE": { - "en": "Add Billing Info", - "ja": "請求情報を追加", - "zh-CN": "添加账单信息", - "zh-TW": "添加帳單資訊", - "ko-KR": "결제 정보 추가", - "no": "Legg til betalingsinformasjon", - "it": "Aggiungi informazioni di fatturazione", - "pt": "Adicionar informações de pagamento", - "es": "Añadir información de facturación", - "ar": "إضافة معلومات الفواتير", - "fr": "Ajouter les informations de facturation", - "tr": "Fatura Bilgisi Ekle", - "de": "Zahlungsinformationen hinzufügen", - "uk": "Додати платіжну інформацію", - "ca": "Afegeix informació de facturació" - }, - "BILLING$YOURE_IN": { - "en": "You're in! You can start using your $10 in free credits now.", - "ja": "登録完了!$10分の無料クレジットを今すぐご利用いただけます。", - "zh-CN": "您已加入!现在可以开始使用$10的免费额度了。", - "zh-TW": "您已加入!現在可以開始使用$10的免費額度了。", - "ko-KR": "가입 완료! 지금 바로 $10 상당의 무료 크레딧을 사용하실 수 있습니다.", - "no": "Du er med! Du kan begynne å bruke dine $10 i gratis kreditter nå.", - "it": "Ci sei! Puoi iniziare a utilizzare i tuoi $10 in crediti gratuiti ora.", - "pt": "Você está dentro! Você pode começar a usar seus $10 em créditos gratuitos agora.", - "es": "¡Ya estás dentro! Puedes empezar a usar tus $10 en créditos gratuitos ahora.", - "ar": "أنت معنا! يمكنك البدء في استخدام رصيدك المجاني البالغ 10 دولارًا الآن.", - "fr": "C'est fait ! Vous pouvez commencer à utiliser vos 10 $ de crédits gratuits maintenant.", - "tr": "Başardın! Şimdi $10 değerindeki ücretsiz kredilerini kullanmaya başlayabilirsin.", - "de": "Du bist dabei! Du kannst jetzt deine $10 an kostenlosen Guthaben nutzen.", - "uk": "Готово! Ви можете почати використовувати свої безкоштовні кредити на суму 10 доларів США вже зараз.", - "ca": "Ja hi sou! Podeu començar a fer servir els vostres 10 $ en crèdits gratuïts ara." - }, - "PAYMENT$ADD_FUNDS": { - "en": "Add Funds", - "ja": "資金を追加", - "zh-CN": "添加资金", - "zh-TW": "添加資金", - "ko-KR": "자금 추가", - "no": "Legg til midler", - "it": "Aggiungi fondi", - "pt": "Adicionar fundos", - "es": "Añadir fondos", - "ar": "إضافة أموال", - "fr": "Ajouter des fonds", - "tr": "Bakiye Ekle", - "de": "Guthaben hinzufügen", - "uk": "Додати кошти", - "ca": "Afegeix fons" - }, - "PAYMENT$ADD_CREDIT": { - "en": "Add credit", - "ja": "クレジットを追加", - "zh-CN": "添加信用", - "zh-TW": "添加信用", - "ko-KR": "크레딧 추가", - "no": "Legg til kreditt", - "it": "Aggiungi credito", - "pt": "Adicionar crédito", - "es": "Añadir crédito", - "ar": "إضافة رصيد", - "fr": "Ajouter du crédit", - "tr": "Kredi ekle", - "de": "Guthaben hinzufügen", - "uk": "Додати кредит", - "ca": "Afegeix crèdit" - }, - "PAYMENT$MANAGE_CREDITS": { - "en": "Manage Credits", - "ja": "クレジットを管理", - "zh-CN": "管理信用", - "zh-TW": "管理信用", - "ko-KR": "크레딧 관리", - "no": "Administrer kreditt", - "it": "Gestisci crediti", - "pt": "Gerenciar créditos", - "es": "Administrar créditos", - "ar": "إدارة الرصيد", - "fr": "Gérer les crédits", - "tr": "Kredileri yönet", - "de": "Guthaben verwalten", - "uk": "Керування кредитами", - "ca": "Gestiona els crèdits" - }, - "PAYMENT$CANCEL_SUBSCRIPTION": { - "en": "Cancel Subscription", - "ja": "サブスクリプションをキャンセル", - "zh-CN": "取消订阅", - "zh-TW": "取消訂閱", - "ko-KR": "구독 취소", - "no": "Avbryt abonnement", - "it": "Annulla abbonamento", - "pt": "Cancelar assinatura", - "es": "Cancelar suscripción", - "ar": "إلغاء الاشتراك", - "fr": "Annuler l'abonnement", - "tr": "Aboneliği iptal et", - "de": "Abonnement kündigen", - "uk": "Скасувати підписку", - "ca": "Cancel·la la subscripció" - }, - "PAYMENT$CANCEL_SUBSCRIPTION_TITLE": { - "en": "Cancel Subscription", - "ja": "サブスクリプションのキャンセル", - "zh-CN": "取消订阅", - "zh-TW": "取消訂閱", - "ko-KR": "구독 취소", - "no": "Avbryt abonnement", - "it": "Annulla abbonamento", - "pt": "Cancelar assinatura", - "es": "Cancelar suscripción", - "ar": "إلغاء الاشتراك", - "fr": "Annuler l'abonnement", - "tr": "Aboneliği iptal et", - "de": "Abonnement kündigen", - "uk": "Скасувати підписку", - "ca": "Cancel·la la subscripció" - }, - "PAYMENT$SUBSCRIPTION_CANCELLED": { - "en": "Subscription cancelled successfully", - "ja": "サブスクリプションが正常にキャンセルされました", - "zh-CN": "订阅已成功取消", - "zh-TW": "訂閱已成功取消", - "ko-KR": "구독이 성공적으로 취소되었습니다", - "no": "Abonnement avbrutt", - "it": "Abbonamento annullato con successo", - "pt": "Assinatura cancelada com sucesso", - "es": "Suscripción cancelada exitosamente", - "ar": "تم إلغاء الاشتراك بنجاح", - "fr": "Abonnement annulé avec succès", - "tr": "Abonelik başarıyla iptal edildi", - "de": "Abonnement erfolgreich gekündigt", - "uk": "Підписку успішно скасовано", - "ca": "La subscripció s'ha cancel·lat correctament" - }, - "PAYMENT$NEXT_BILLING_DATE": { - "en": "Next billing date: {{date}}", - "ja": "次回請求日: {{date}}", - "zh-CN": "下次计费日期: {{date}}", - "zh-TW": "下次計費日期: {{date}}", - "ko-KR": "다음 결제일: {{date}}", - "no": "Neste faktureringsdato: {{date}}", - "it": "Prossima data di fatturazione: {{date}}", - "pt": "Próxima data de cobrança: {{date}}", - "es": "Próxima fecha de facturación: {{date}}", - "ar": "تاريخ الفوترة التالية: {{date}}", - "fr": "Prochaine date de facturation: {{date}}", - "tr": "Sonraki fatura tarihi: {{date}}", - "de": "Nächstes Abrechnungsdatum: {{date}}", - "uk": "Наступна дата виставлення рахунку: {{date}}", - "ca": "Propera data de facturació: {{date}}" - }, "WAITLIST$IF_NOT_JOINED": { "en": "If you haven't already joined the waitlist, please do so to get access.", "ja": "まだウェイトリストに参加していない場合は、アクセスを得るために参加してください。", @@ -13480,74 +13157,6 @@ "uk": "Майже готово!", "ca": "Ja quasi hi sou!" }, - "PAYMENT$SUCCESS": { - "en": "Payment successful", - "ja": "支払い成功", - "zh-CN": "支付成功", - "zh-TW": "支付成功", - "ko-KR": "결제 성공", - "no": "Betaling vellykket", - "it": "Pagamento riuscito", - "pt": "Pagamento bem-sucedido", - "es": "Pago exitoso", - "ar": "تمت عملية الدفع بنجاح", - "fr": "Paiement réussi", - "tr": "Ödeme başarılı", - "de": "Zahlung erfolgreich", - "uk": "Оплата успішна", - "ca": "Pagament realitzat correctament" - }, - "PAYMENT$CANCELLED": { - "en": "Payment cancelled", - "ja": "支払いがキャンセルされました", - "zh-CN": "支付已取消", - "zh-TW": "支付已取消", - "ko-KR": "결제 취소됨", - "no": "Betaling avbrutt", - "it": "Pagamento annullato", - "pt": "Pagamento cancelado", - "es": "Pago cancelado", - "ar": "تم إلغاء الدفع", - "fr": "Paiement annulé", - "tr": "Ödeme iptal edildi", - "de": "Zahlung abgebrochen", - "uk": "Платіж скасовано", - "ca": "Pagament cancel·lat" - }, - "SUBSCRIPTION$SUCCESS": { - "en": "Subscription successful! You now have access to premium features.", - "ja": "サブスクリプションが成功しました!プレミアム機能にアクセスできるようになりました。", - "zh-CN": "订阅成功!您现在可以访问高级功能。", - "zh-TW": "訂閱成功!您現在可以訪問高級功能。", - "ko-KR": "구독 성공! 이제 프리미엄 기능에 액세스할 수 있습니다.", - "no": "Abonnement vellykket! Du har nå tilgang til premium-funksjoner.", - "it": "Abbonamento riuscito! Ora hai accesso alle funzionalità premium.", - "pt": "Assinatura bem-sucedida! Agora você tem acesso aos recursos premium.", - "es": "¡Suscripción exitosa! Ahora tienes acceso a las funciones premium.", - "ar": "نجح الاشتراك! لديك الآن إمكانية الوصول إلى الميزات المميزة.", - "fr": "Abonnement réussi ! Vous avez maintenant accès aux fonctionnalités premium.", - "tr": "Abonelik başarılı! Artık premium özelliklere erişiminiz var.", - "de": "Abonnement erfolgreich! Sie haben jetzt Zugang zu Premium-Funktionen.", - "uk": "Підписка успішна! Тепер у вас є доступ до преміум-функцій.", - "ca": "Subscripció realitzada correctament! Ara teniu accés a les funcions premium." - }, - "SUBSCRIPTION$FAILURE": { - "en": "Something went wrong, please try again later.", - "ja": "何かが間違っていました。後でもう一度お試しください。", - "zh-CN": "出了点问题,请稍后再试。", - "zh-TW": "出了點問題,請稍後再試。", - "ko-KR": "문제가 발생했습니다. 나중에 다시 시도해 주세요.", - "no": "Noe gikk galt, prøv igjen senere.", - "it": "Qualcosa è andato storto, riprova più tardi.", - "pt": "Algo deu errado, tente novamente mais tarde.", - "es": "Algo salió mal, inténtalo de nuevo más tarde.", - "ar": "حدث خطأ ما، يرجى المحاولة مرة أخرى لاحقاً.", - "fr": "Quelque chose s'est mal passé, veuillez réessayer plus tard.", - "tr": "Bir şeyler ters gitti, lütfen daha sonra tekrar deneyin.", - "de": "Etwas ist schief gelaufen, bitte versuchen Sie es später erneut.", - "uk": "Щось пішло не так, спробуйте ще раз пізніше.", - "ca": "Alguna cosa ha anat malament, torneu-ho a intentar més tard." - }, "SERVED_APP$TITLE": { "en": "Served Application", "ja": "提供されたアプリケーション", @@ -14024,23 +13633,6 @@ "uk": "Увійти за допомогою Bitbucket Data Center", "ca": "Inicia sessió amb Bitbucket Data Center" }, - "ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO": { - "en": "Login with Enterprise SSO", - "ja": "エンタープライズSSOでログイン", - "zh-CN": "使用企业SSO登录", - "zh-TW": "使用企業SSO登入", - "ko-KR": "엔터프라이즈 SSO로 로그인", - "de": "Mit Enterprise SSO anmelden", - "no": "Logg inn med Enterprise SSO", - "it": "Accedi con Enterprise SSO", - "pt": "Entrar com Enterprise SSO", - "es": "Iniciar sesión con Enterprise SSO", - "ar": "تسجيل الدخول باستخدام Enterprise SSO", - "fr": "Se connecter avec Enterprise SSO", - "tr": "Enterprise SSO ile giriş yap", - "uk": "Увійти за допомогою Enterprise SSO", - "ca": "Inicia sessió amb SSO empresarial" - }, "AUTH$SIGN_IN_WITH_IDENTITY_PROVIDER": { "en": "Log in to OpenHands", "ja": "IDプロバイダーでサインイン", @@ -16166,108 +15758,6 @@ "uk": "Це AI-курировані пропозиції завдань, які допоможуть вам розпочати з поширеними діяльностями розробки та найкращими практиками для вашого репозиторію.", "ca": "Aquests són suggeriments de tasques seleccionats per IA per ajudar-vos a començar amb activitats de desenvolupament habituals i les millors pràctiques per al vostre repositori." }, - "PAYMENT$SPECIFY_AMOUNT_USD": { - "en": "Specify an amount in USD to add - min $10", - "ja": "追加するUSD金額を指定してください - 最小$10", - "zh-CN": "指定要添加的美元金额 - 最少$10", - "zh-TW": "指定要新增的美元金額 - 最少$10", - "ko-KR": "추가할 USD 금액을 지정하세요 - 최소 $10", - "no": "Spesifiser et beløp i USD å legge til - min $10", - "it": "Specifica un importo in USD da aggiungere - min $10", - "pt": "Especifique um valor em USD para adicionar - mín $10", - "es": "Especifique una cantidad en USD para agregar - mín $10", - "ar": "حدد مبلغًا بالدولار الأمريكي لإضافته - الحد الأدنى 10 دولارات", - "fr": "Spécifiez un montant en USD à ajouter - min 10 $", - "tr": "Eklenecek USD tutarını belirtin - min $10", - "de": "Geben Sie einen USD-Betrag zum Hinzufügen an - min $10", - "uk": "Вкажіть суму в доларах США для додавання - мін $10", - "ca": "Especifiqueu un import en USD per afegir - mínim 10 $" - }, - "PAYMENT$ERROR_INVALID_NUMBER": { - "en": "Please enter a valid number", - "ja": "有効な数値を入力してください", - "zh-CN": "请输入有效数字", - "zh-TW": "請輸入有效數字", - "ko-KR": "유효한 숫자를 입력하세요", - "no": "Vennligst skriv inn et gyldig tall", - "it": "Inserisci un numero valido", - "pt": "Por favor, insira um número válido", - "es": "Por favor, ingrese un número válido", - "ar": "يرجى إدخال رقم صحيح", - "fr": "Veuillez entrer un nombre valide", - "tr": "Lütfen geçerli bir sayı girin", - "de": "Bitte geben Sie eine gültige Zahl ein", - "uk": "Будь ласка, введіть дійсне число", - "ca": "Introduïu un número vàlid" - }, - "PAYMENT$ERROR_NEGATIVE_AMOUNT": { - "en": "Amount cannot be negative", - "ja": "金額は負の値にできません", - "zh-CN": "金额不能为负数", - "zh-TW": "金額不能為負數", - "ko-KR": "금액은 음수일 수 없습니다", - "no": "Beløpet kan ikke være negativt", - "it": "L'importo non può essere negativo", - "pt": "O valor não pode ser negativo", - "es": "El monto no puede ser negativo", - "ar": "لا يمكن أن يكون المبلغ سالبًا", - "fr": "Le montant ne peut pas être négatif", - "tr": "Tutar negatif olamaz", - "de": "Der Betrag darf nicht negativ sein", - "uk": "Сума не може бути від'ємною", - "ca": "L'import no pot ser negatiu" - }, - "PAYMENT$ERROR_MINIMUM_AMOUNT": { - "en": "Minimum amount is $10", - "ja": "最小金額は$10です", - "zh-CN": "最低金额为$10", - "zh-TW": "最低金額為$10", - "ko-KR": "최소 금액은 $10입니다", - "no": "Minimumsbeløpet er $10", - "it": "L'importo minimo è $10", - "pt": "O valor mínimo é $10", - "es": "El monto mínimo es $10", - "ar": "الحد الأدنى للمبلغ هو 10 دولارات", - "fr": "Le montant minimum est de 10 $", - "tr": "Minimum tutar $10'dur", - "de": "Der Mindestbetrag beträgt 10 $", - "uk": "Мінімальна сума становить $10", - "ca": "L'import mínim és de $10" - }, - "PAYMENT$ERROR_MAXIMUM_AMOUNT": { - "en": "Maximum amount is $25,000", - "ja": "最大金額は$25,000です", - "zh-CN": "最高金额为$25,000", - "zh-TW": "最高金額為$25,000", - "ko-KR": "최대 금액은 $25,000입니다", - "no": "Maksimalbeløpet er $25,000", - "it": "L'importo massimo è $25,000", - "pt": "O valor máximo é $25,000", - "es": "El monto máximo es $25,000", - "ar": "الحد الأقصى للمبلغ هو 25,000 دولار", - "fr": "Le montant maximum est de 25 000 $", - "tr": "Maksimum tutar $25,000'dur", - "de": "Der Höchstbetrag beträgt 25.000 $", - "uk": "Максимальна сума становить $25,000", - "ca": "L'import màxim és de $25.000" - }, - "PAYMENT$ERROR_MUST_BE_WHOLE_NUMBER": { - "en": "Amount must be a whole number", - "ja": "金額は整数である必要があります", - "zh-CN": "金额必须是整数", - "zh-TW": "金額必須是整數", - "ko-KR": "금액은 정수여야 합니다", - "no": "Beløpet må være et heltall", - "it": "L'importo deve essere un numero intero", - "pt": "O valor deve ser um número inteiro", - "es": "El monto debe ser un número entero", - "ar": "يجب أن يكون المبلغ رقمًا صحيحًا", - "fr": "Le montant doit être un nombre entier", - "tr": "Tutar tam sayı olmalıdır", - "de": "Der Betrag muss eine ganze Zahl sein", - "uk": "Сума повинна бути цілим числом", - "ca": "L'import ha de ser un número enter" - }, "GIT$BITBUCKET_TOKEN_HELP_LINK": { "en": "Bitbucket token help link", "ja": "Bitbucketトークンヘルプリンク", @@ -19702,40 +19192,6 @@ "uk": "В процесі", "ca": "En curs" }, - "SETTINGS$UPGRADE_BANNER_MESSAGE": { - "en": "Access LLM settings when you upgrade your plan", - "ja": "プランをアップグレードするとLLM設定にアクセスできます", - "zh-CN": "升级计划后即可访问LLM设置", - "zh-TW": "升級計劃後即可訪問LLM設置", - "ko-KR": "플랜을 업그레이드하면 LLM 설정에 액세스할 수 있습니다", - "no": "Få tilgang til LLM-innstillinger når du oppgraderer planen din", - "it": "Accedi alle impostazioni LLM quando aggiorni il tuo piano", - "pt": "Acesse as configurações do LLM ao atualizar seu plano", - "es": "Accede a la configuración de LLM cuando actualices tu plan", - "ar": "الوصول إلى إعدادات LLM عند ترقية خطتك", - "fr": "Accédez aux paramètres LLM lorsque vous mettez à niveau votre plan", - "tr": "Planınızı yükselttiğinizde LLM ayarlarına erişin", - "de": "Zugriff auf LLM-Einstellungen beim Upgrade Ihres Plans", - "uk": "Отримайте доступ до налаштувань LLM, коли оновите свій план", - "ca": "Accediu a la configuració del LLM quan actualitzeu el vostre pla" - }, - "SETTINGS$UPGRADE_BUTTON": { - "en": "Upgrade", - "ja": "アップグレード", - "zh-CN": "升级", - "zh-TW": "升級", - "ko-KR": "업그레이드", - "no": "Oppgrader", - "it": "Aggiorna", - "pt": "Atualizar", - "es": "Actualizar", - "ar": "ترقية", - "fr": "Mettre à niveau", - "tr": "Yükselt", - "de": "Upgrade", - "uk": "Оновити", - "ca": "Actualitza" - }, "SETTINGS$PRO_PILL": { "en": "Pro", "ja": "プロ", @@ -21657,125 +21113,6 @@ "uk": "Ще не впевнений", "ca": "Encara no ho sé" }, - "ONBOARDING$ROLE_QUESTION": { - "en": "What's your role?", - "ja": "あなたの役割は?", - "zh-CN": "您的角色是什么?", - "zh-TW": "您的角色是什麼?", - "ko-KR": "귀하의 역할은 무엇입니까?", - "no": "Hva er din rolle?", - "ar": "ما هو دورك؟", - "de": "Was ist Ihre Rolle?", - "fr": "Quel est votre rôle ?", - "it": "Qual è il tuo ruolo?", - "pt": "Qual é o seu papel?", - "es": "¿Cuál es tu rol?", - "tr": "Rolünüz nedir?", - "uk": "Яка ваша роль?", - "ca": "Quin és el vostre rol?" - }, - "ONBOARDING$ROLE_SOFTWARE_ENGINEER": { - "en": "Software engineer / developer", - "ja": "ソフトウェアエンジニア / 開発者", - "zh-CN": "软件工程师 / 开发者", - "zh-TW": "軟體工程師 / 開發者", - "ko-KR": "소프트웨어 엔지니어 / 개발자", - "no": "Programvareingeniør / utvikler", - "ar": "مهندس برمجيات / مطور", - "de": "Softwareentwickler / Entwickler", - "fr": "Ingénieur logiciel / développeur", - "it": "Ingegnere software / sviluppatore", - "pt": "Engenheiro de software / desenvolvedor", - "es": "Ingeniero de software / desarrollador", - "tr": "Yazılım mühendisi / geliştirici", - "uk": "Програмний інженер / розробник", - "ca": "Enginyer/a de programari / desenvolupador/a" - }, - "ONBOARDING$ROLE_ENGINEERING_MANAGER": { - "en": "Engineering manager / tech lead", - "ja": "エンジニアリングマネージャー / テックリード", - "zh-CN": "工程经理 / 技术负责人", - "zh-TW": "工程經理 / 技術負責人", - "ko-KR": "엔지니어링 매니저 / 테크 리드", - "no": "Ingeniørsjef / teknisk leder", - "ar": "مدير هندسة / قائد تقني", - "de": "Engineering Manager / Tech Lead", - "fr": "Responsable ingénierie / tech lead", - "it": "Engineering manager / tech lead", - "pt": "Gerente de engenharia / tech lead", - "es": "Gerente de ingeniería / tech lead", - "tr": "Mühendislik müdürü / teknik lider", - "uk": "Менеджер з розробки / технічний лідер", - "ca": "Responsable d'enginyeria / líder tècnic/a" - }, - "ONBOARDING$ROLE_CTO_FOUNDER": { - "en": "CTO / founder", - "ja": "CTO / 創業者", - "zh-CN": "CTO / 创始人", - "zh-TW": "CTO / 創辦人", - "ko-KR": "CTO / 창업자", - "no": "CTO / grunnlegger", - "ar": "مدير التكنولوجيا / مؤسس", - "de": "CTO / Gründer", - "fr": "CTO / fondateur", - "it": "CTO / fondatore", - "pt": "CTO / fundador", - "es": "CTO / fundador", - "tr": "CTO / kurucu", - "uk": "CTO / засновник", - "ca": "CTO / fundador/a" - }, - "ONBOARDING$ROLE_PRODUCT_OPERATIONS": { - "en": "Product or operations role", - "ja": "プロダクトまたはオペレーションの役割", - "zh-CN": "产品或运营角色", - "zh-TW": "產品或營運角色", - "ko-KR": "제품 또는 운영 역할", - "no": "Produkt- eller driftsrolle", - "ar": "دور المنتج أو العمليات", - "de": "Produkt- oder Betriebsrolle", - "fr": "Rôle produit ou opérations", - "it": "Ruolo prodotto o operazioni", - "pt": "Função de produto ou operações", - "es": "Rol de producto u operaciones", - "tr": "Ürün veya operasyon rolü", - "uk": "Роль продукту або операцій", - "ca": "Rol de producte o operacions" - }, - "ONBOARDING$ROLE_STUDENT_HOBBYIST": { - "en": "Student / hobbyist", - "ja": "学生 / 趣味", - "zh-CN": "学生 / 爱好者", - "zh-TW": "學生 / 愛好者", - "ko-KR": "학생 / 취미", - "no": "Student / hobbyist", - "ar": "طالب / هاوٍ", - "de": "Student / Hobbyist", - "fr": "Étudiant / amateur", - "it": "Studente / hobbista", - "pt": "Estudante / hobbyista", - "es": "Estudiante / aficionado", - "tr": "Öğrenci / hobi", - "uk": "Студент / хобіст", - "ca": "Estudiant / aficionat/da" - }, - "ONBOARDING$ROLE_OTHER": { - "en": "Other", - "ja": "その他", - "zh-CN": "其他", - "zh-TW": "其他", - "ko-KR": "기타", - "no": "Annet", - "ar": "أخرى", - "de": "Andere", - "fr": "Autre", - "it": "Altro", - "pt": "Outro", - "es": "Otro", - "tr": "Diğer", - "uk": "Інше", - "ca": "Altre" - }, "ONBOARDING$NEXT_BUTTON": { "en": "Next", "ja": "次へ", @@ -21963,57 +21300,6 @@ "uk": "Створення нової розмови...", "ca": "S'està creant una nova conversa..." }, - "CTA$ENTERPRISE": { - "en": "Enterprise", - "ja": "エンタープライズ", - "zh-CN": "企业版", - "zh-TW": "企業版", - "ko-KR": "엔터프라이즈", - "no": "Bedrift", - "ar": "المؤسسات", - "de": "Unternehmen", - "fr": "Entreprise", - "it": "Azienda", - "pt": "Empresarial", - "es": "Empresa", - "tr": "Kurumsal", - "uk": "Підприємство", - "ca": "Empresa" - }, - "CTA$ENTERPRISE_DEPLOY": { - "en": "Deploy OpenHands on your own infrastructure. Full control over data, compliance, and security.", - "ja": "独自のインフラストラクチャにOpenHandsをデプロイ。データ、コンプライアンス、セキュリティを完全にコントロール。", - "zh-CN": "在您自己的基础设施上部署 OpenHands。完全控制数据、合规性和安全性。", - "zh-TW": "在您自己的基礎設施上部署 OpenHands。完全控制資料、合規性和安全性。", - "ko-KR": "자체 인프라에 OpenHands를 배포하세요. 데이터, 규정 준수 및 보안을 완벽하게 제어합니다.", - "no": "Distribuer OpenHands på din egen infrastruktur. Full kontroll over data, samsvar og sikkerhet.", - "ar": "انشر OpenHands على بنيتك التحتية الخاصة. تحكم كامل في البيانات والامتثال والأمان.", - "de": "Stellen Sie OpenHands auf Ihrer eigenen Infrastruktur bereit. Volle Kontrolle über Daten, Compliance und Sicherheit.", - "fr": "Déployez OpenHands sur votre propre infrastructure. Contrôle total sur les données, la conformité et la sécurité.", - "it": "Distribuisci OpenHands sulla tua infrastruttura. Controllo completo su dati, conformità e sicurezza.", - "pt": "Implante o OpenHands em sua própria infraestrutura. Controle total sobre dados, conformidade e segurança.", - "es": "Implemente OpenHands en su propia infraestructura. Control total sobre datos, cumplimiento y seguridad.", - "tr": "OpenHands'i kendi altyapınızda dağıtın. Veri, uyumluluk ve güvenlik üzerinde tam kontrol.", - "uk": "Розгорніть OpenHands на власній інфраструктурі. Повний контроль над даними, відповідністю та безпекою.", - "ca": "Desplegeu OpenHands a la vostra pròpia infraestructura. Control total sobre dades, compliment i seguretat." - }, - "CTA$FEATURE_ON_PREMISES": { - "en": "On-premises or private cloud", - "ja": "オンプレミスまたはプライベートクラウド", - "zh-CN": "本地部署或私有云", - "zh-TW": "本地部署或私有雲", - "ko-KR": "온프레미스 또는 프라이빗 클라우드", - "no": "Lokalt eller privat sky", - "ar": "محلي أو سحابة خاصة", - "de": "On-Premises oder Private Cloud", - "fr": "Sur site ou cloud privé", - "it": "On-premise o cloud privato", - "pt": "Local ou nuvem privada", - "es": "Local o nube privada", - "tr": "Şirket içi veya özel bulut", - "uk": "Локально або приватна хмара", - "ca": "Local o núvol privat" - }, "CTA$FEATURE_DATA_CONTROL": { "en": "Full data control", "ja": "完全なデータ管理", @@ -22065,159 +21351,6 @@ "uk": "Виділені варіанти підтримки", "ca": "Opcions de suport dedicat" }, - "ENTERPRISE$SELF_HOSTED": { - "en": "Self-Hosted", - "ja": "セルフホスト", - "zh-CN": "自托管", - "zh-TW": "自託管", - "ko-KR": "셀프 호스팅", - "no": "Selvhosted", - "ar": "مستضاف ذاتياً", - "de": "Selbst gehostet", - "fr": "Auto-hébergé", - "it": "Self-hosted", - "pt": "Auto-hospedado", - "es": "Autoalojado", - "tr": "Kendi Sunucunuzda", - "uk": "Самостійний хостинг", - "ca": "Allotjament propi" - }, - "ENTERPRISE$TITLE": { - "en": "OpenHands Enterprise", - "ja": "OpenHands Enterprise", - "zh-CN": "OpenHands 企业版", - "zh-TW": "OpenHands 企業版", - "ko-KR": "OpenHands 엔터프라이즈", - "no": "OpenHands Enterprise", - "ar": "OpenHands للمؤسسات", - "de": "OpenHands Enterprise", - "fr": "OpenHands Enterprise", - "it": "OpenHands Enterprise", - "pt": "OpenHands Enterprise", - "es": "OpenHands Enterprise", - "tr": "OpenHands Kurumsal", - "uk": "OpenHands Enterprise", - "ca": "OpenHands Enterprise" - }, - "ENTERPRISE$DESCRIPTION": { - "en": "Complete data control with your own self-hosted AI development platform.", - "ja": "独自のセルフホストAI開発プラットフォームで完全なデータ管理を実現。", - "zh-CN": "通过自托管AI开发平台实现完全的数据控制。", - "zh-TW": "透過自託管AI開發平台實現完全的資料控制。", - "ko-KR": "셀프 호스팅 AI 개발 플랫폼으로 완벽한 데이터 제어를 실현하세요.", - "no": "Fullstendig datakontroll med din egen selvhostede AI-utviklingsplattform.", - "ar": "تحكم كامل في البيانات مع منصة تطوير الذكاء الاصطناعي المستضافة ذاتياً.", - "de": "Vollständige Datenkontrolle mit Ihrer eigenen selbst gehosteten KI-Entwicklungsplattform.", - "fr": "Contrôle total des données avec votre propre plateforme de développement IA auto-hébergée.", - "it": "Controllo completo dei dati con la tua piattaforma di sviluppo AI self-hosted.", - "pt": "Controle completo de dados com sua própria plataforma de desenvolvimento de IA auto-hospedada.", - "es": "Control completo de datos con tu propia plataforma de desarrollo de IA autoalojada.", - "tr": "Kendi barındırdığınız yapay zeka geliştirme platformuyla tam veri kontrolü.", - "uk": "Повний контроль над даними з власною самостійно розміщеною платформою розробки ШІ.", - "ca": "Control total de les dades amb la vostra pròpia plataforma de desenvolupament d'IA allotjada." - }, - "ENTERPRISE$FEATURE_DATA_PRIVACY": { - "en": "Full data privacy & control", - "ja": "完全なデータプライバシーと管理", - "zh-CN": "完全的数据隐私和控制", - "zh-TW": "完全的資料隱私和控制", - "ko-KR": "완벽한 데이터 프라이버시 및 제어", - "no": "Full datapersonvern og kontroll", - "ar": "خصوصية وتحكم كامل في البيانات", - "de": "Vollständiger Datenschutz und Kontrolle", - "fr": "Confidentialité et contrôle complets des données", - "it": "Privacy e controllo completo dei dati", - "pt": "Privacidade e controle total de dados", - "es": "Privacidad y control total de datos", - "tr": "Tam veri gizliliği ve kontrolü", - "uk": "Повна конфіденційність та контроль даних", - "ca": "Privacitat i control total de les dades" - }, - "ENTERPRISE$FEATURE_DEPLOYMENT": { - "en": "Custom deployment options", - "ja": "カスタムデプロイオプション", - "zh-CN": "自定义部署选项", - "zh-TW": "自訂部署選項", - "ko-KR": "맞춤형 배포 옵션", - "no": "Tilpassede distribusjonsalternativer", - "ar": "خيارات نشر مخصصة", - "de": "Individuelle Bereitstellungsoptionen", - "fr": "Options de déploiement personnalisées", - "it": "Opzioni di distribuzione personalizzate", - "pt": "Opções de implantação personalizadas", - "es": "Opciones de despliegue personalizadas", - "tr": "Özel dağıtım seçenekleri", - "uk": "Налаштовані варіанти розгортання", - "ca": "Opcions de desplegament personalitzades" - }, - "ENTERPRISE$FEATURE_SSO": { - "en": "SSO & enterprise auth", - "ja": "SSOとエンタープライズ認証", - "zh-CN": "SSO和企业认证", - "zh-TW": "SSO和企業認證", - "ko-KR": "SSO 및 엔터프라이즈 인증", - "no": "SSO og bedriftsautentisering", - "ar": "تسجيل دخول موحد ومصادقة المؤسسات", - "de": "SSO und Unternehmensauthentifizierung", - "fr": "SSO et authentification d'entreprise", - "it": "SSO e autenticazione aziendale", - "pt": "SSO e autenticação empresarial", - "es": "SSO y autenticación empresarial", - "tr": "SSO ve kurumsal kimlik doğrulama", - "uk": "SSO та корпоративна автентифікація", - "ca": "SSO i autenticació empresarial" - }, - "ENTERPRISE$FEATURE_SUPPORT": { - "en": "Dedicated support", - "ja": "専用サポート", - "zh-CN": "专属支持", - "zh-TW": "專屬支援", - "ko-KR": "전담 지원", - "no": "Dedikert støtte", - "ar": "دعم مخصص", - "de": "Dedizierter Support", - "fr": "Support dédié", - "it": "Supporto dedicato", - "pt": "Suporte dedicado", - "es": "Soporte dedicado", - "tr": "Özel destek", - "uk": "Виділена підтримка", - "ca": "Suport dedicat" - }, - "ENTERPRISE$LEARN_MORE": { - "en": "Learn More", - "ja": "詳細を見る", - "zh-CN": "了解更多", - "zh-TW": "了解更多", - "ko-KR": "더 알아보기", - "no": "Les mer", - "ar": "اعرف المزيد", - "de": "Mehr erfahren", - "fr": "En savoir plus", - "it": "Scopri di più", - "pt": "Saiba mais", - "es": "Más información", - "tr": "Daha Fazla Bilgi", - "uk": "Дізнатися більше", - "ca": "Més informació" - }, - "ENTERPRISE$LEARN_MORE_ARIA": { - "en": "Learn more about OpenHands Enterprise (opens in new window)", - "ja": "OpenHands Enterpriseの詳細を見る(新しいウィンドウで開く)", - "zh-CN": "了解更多关于 OpenHands 企业版的信息(在新窗口中打开)", - "zh-TW": "了解更多關於 OpenHands 企業版的資訊(在新視窗中開啟)", - "ko-KR": "OpenHands 엔터프라이즈에 대해 더 알아보기 (새 창에서 열림)", - "no": "Les mer om OpenHands Enterprise (åpnes i nytt vindu)", - "ar": "اعرف المزيد عن OpenHands Enterprise (يفتح في نافذة جديدة)", - "de": "Erfahren Sie mehr über OpenHands Enterprise (öffnet in neuem Fenster)", - "fr": "En savoir plus sur OpenHands Enterprise (s'ouvre dans une nouvelle fenêtre)", - "it": "Scopri di più su OpenHands Enterprise (si apre in una nuova finestra)", - "pt": "Saiba mais sobre OpenHands Enterprise (abre em nova janela)", - "es": "Más información sobre OpenHands Enterprise (abre en nueva ventana)", - "tr": "OpenHands Enterprise hakkında daha fazla bilgi edinin (yeni pencerede açılır)", - "uk": "Дізнатися більше про OpenHands Enterprise (відкривається в новому вікні)", - "ca": "Més informació sobre OpenHands Enterprise (s'obre en una finestra nova)" - }, "DEVICE$SUCCESS_TITLE": { "en": "Success!", "ja": "成功!", @@ -22575,40 +21708,6 @@ "uk": "Будь ласка, увійдіть, щоб авторизувати свій пристрій.", "ca": "Inicieu sessió per autoritzar el vostre dispositiu." }, - "CTA$ENTERPRISE_TITLE": { - "en": "Get OpenHands For Enterprise", - "ja": "エンタープライズ向けOpenHandsを入手", - "zh-CN": "获取企业版 OpenHands", - "zh-TW": "獲取企業版 OpenHands", - "ko-KR": "기업용 OpenHands 받기", - "no": "Få OpenHands for bedrift", - "it": "Ottieni OpenHands per Enterprise", - "pt": "Obtenha o OpenHands para Enterprise", - "es": "Obtén OpenHands para Empresas", - "ar": "احصل على OpenHands للمؤسسات", - "fr": "Obtenez OpenHands pour Entreprise", - "tr": "Kurumsal OpenHands'i Edinin", - "de": "OpenHands für Unternehmen", - "uk": "Отримайте OpenHands для підприємств", - "ca": "Obteniu OpenHands per a empreses" - }, - "CTA$ENTERPRISE_DESCRIPTION": { - "en": "Cloud allows you to access OpenHands anywhere and coordinate with your team like never before.", - "ja": "クラウドを使用すると、どこからでもOpenHandsにアクセスし、チームとこれまでにない方法で連携できます。", - "zh-CN": "云端让您可以随时随地访问 OpenHands,并以前所未有的方式与团队协作。", - "zh-TW": "雲端讓您可以隨時隨地存取 OpenHands,並以前所未有的方式與團隊協作。", - "ko-KR": "클라우드를 통해 어디서나 OpenHands에 접속하고 팀과 이전과는 다른 방식으로 협업할 수 있습니다.", - "no": "Cloud lar deg få tilgang til OpenHands hvor som helst og koordinere med teamet ditt som aldri før.", - "it": "Cloud ti permette di accedere a OpenHands ovunque e coordinare con il tuo team come mai prima d'ora.", - "pt": "O Cloud permite que você acesse o OpenHands de qualquer lugar e coordene com sua equipe como nunca antes.", - "es": "Cloud le permite acceder a OpenHands desde cualquier lugar y coordinar con su equipo como nunca antes.", - "ar": "يتيح لك Cloud الوصول إلى OpenHands من أي مكان والتنسيق مع فريقك بشكل لم يسبق له مثيل.", - "fr": "Cloud vous permet d'accéder à OpenHands n'importe où et de coordonner avec votre équipe comme jamais auparavant.", - "tr": "Cloud, OpenHands'e her yerden erişmenizi ve ekibinizle daha önce hiç olmadığı gibi koordinasyon sağlamanızı mümkün kılar.", - "de": "Cloud ermöglicht Ihnen den Zugriff auf OpenHands von überall und die Koordination mit Ihrem Team wie nie zuvor.", - "uk": "Cloud дозволяє отримати доступ до OpenHands будь-де та координувати роботу з вашою командою як ніколи раніше.", - "ca": "El núvol us permet accedir a OpenHands des de qualsevol lloc i coordinar-vos amb el vostre equip com mai abans." - }, "CTA$LEARN_MORE": { "en": "Learn more", "ja": "詳細を見る", @@ -22626,567 +21725,6 @@ "uk": "Дізнатися більше", "ca": "Més informació" }, - "ENTERPRISE$GET_OPENHANDS_TITLE": { - "en": "Get OpenHands for Enterprise", - "ja": "OpenHands Enterpriseを入手", - "zh-CN": "获取 OpenHands 企业版", - "zh-TW": "取得 OpenHands 企業版", - "ko-KR": "OpenHands 엔터프라이즈 시작하기", - "no": "Få OpenHands for bedrifter", - "ar": "احصل على OpenHands للمؤسسات", - "de": "OpenHands für Unternehmen", - "fr": "Obtenez OpenHands pour les entreprises", - "it": "Ottieni OpenHands per le aziende", - "pt": "Obtenha o OpenHands para empresas", - "es": "Obtener OpenHands para empresas", - "ca": "Obteniu OpenHands per a empreses", - "tr": "Kurumsal OpenHands Edinin", - "uk": "Отримайте OpenHands для підприємств" - }, - "ENTERPRISE$GET_OPENHANDS_SUBTITLE": { - "en": "Cloud allows you to access OpenHands anywhere and coordinate with your team like never before.", - "ja": "クラウドを使用すると、どこからでもOpenHandsにアクセスし、チームとこれまでにない方法で連携できます。", - "zh-CN": "云端让您可以随时随地访问 OpenHands,并以前所未有的方式与团队协作。", - "zh-TW": "雲端讓您可以隨時隨地存取 OpenHands,並以前所未有的方式與團隊協作。", - "ko-KR": "클라우드를 통해 어디서나 OpenHands에 접속하고 팀과 원활하게 협업하세요.", - "no": "Skyen lar deg få tilgang til OpenHands hvor som helst og koordinere med teamet ditt som aldri før.", - "ar": "يتيح لك السحاب الوصول إلى OpenHands من أي مكان والتنسيق مع فريقك بشكل غير مسبوق.", - "de": "Mit der Cloud können Sie von überall auf OpenHands zugreifen und sich wie nie zuvor mit Ihrem Team abstimmen.", - "fr": "Le cloud vous permet d'accéder à OpenHands n'importe où et de coordonner avec votre équipe comme jamais auparavant.", - "it": "Il cloud ti permette di accedere a OpenHands ovunque e coordinare con il tuo team come mai prima d'ora.", - "pt": "A nuvem permite que você acesse o OpenHands de qualquer lugar e coordene com sua equipe como nunca antes.", - "es": "La nube le permite acceder a OpenHands desde cualquier lugar y coordinar con su equipo como nunca antes.", - "ca": "El núvol us permet accedir a OpenHands des de qualsevol lloc i coordinar-vos amb el vostre equip com mai abans.", - "tr": "Bulut, OpenHands'e her yerden erişmenizi ve ekibinizle daha önce hiç olmadığı gibi koordinasyon sağlamanızı mümkün kılar.", - "uk": "Хмара дозволяє отримати доступ до OpenHands будь-де та координувати роботу з вашою командою як ніколи раніше." - }, - "ENTERPRISE$SAAS_TITLE": { - "en": "Enterprise SaaS", - "ja": "エンタープライズSaaS", - "zh-CN": "企业 SaaS", - "zh-TW": "企業 SaaS", - "ko-KR": "엔터프라이즈 SaaS", - "no": "Enterprise SaaS", - "ar": "SaaS للمؤسسات", - "de": "Enterprise SaaS", - "fr": "SaaS Entreprise", - "it": "SaaS Enterprise", - "pt": "SaaS Empresarial", - "es": "SaaS Empresarial", - "ca": "SaaS Empresarial", - "tr": "Kurumsal SaaS", - "uk": "Корпоративний SaaS" - }, - "ENTERPRISE$SAAS_DESCRIPTION": { - "en": "Access OpenHands in the cloud. No setup required—start coding with your team from anywhere.", - "ja": "クラウドでOpenHandsにアクセス。セットアップ不要—どこからでもチームとコーディングを開始できます。", - "zh-CN": "在云端访问 OpenHands。无需设置——随时随地与团队开始编码。", - "zh-TW": "在雲端存取 OpenHands。無需設定——隨時隨地與團隊開始編碼。", - "ko-KR": "클라우드에서 OpenHands에 접속하세요. 설정 없이 어디서나 팀과 코딩을 시작하세요.", - "no": "Få tilgang til OpenHands i skyen. Ingen oppsett nødvendig—begynn å kode med teamet ditt fra hvor som helst.", - "ar": "الوصول إلى OpenHands في السحاب. لا حاجة للإعداد—ابدأ البرمجة مع فريقك من أي مكان.", - "de": "Greifen Sie auf OpenHands in der Cloud zu. Keine Einrichtung erforderlich—beginnen Sie sofort mit Ihrem Team zu programmieren.", - "fr": "Accédez à OpenHands dans le cloud. Aucune configuration requise—commencez à coder avec votre équipe de n'importe où.", - "it": "Accedi a OpenHands nel cloud. Nessuna configurazione richiesta—inizia a programmare con il tuo team da ovunque.", - "pt": "Acesse o OpenHands na nuvem. Sem configuração necessária—comece a programar com sua equipe de qualquer lugar.", - "es": "Acceda a OpenHands en la nube. Sin configuración requerida—comience a programar con su equipo desde cualquier lugar.", - "ca": "Accediu a OpenHands al núvol. Sense configuració necessària—comenceu a programar amb el vostre equip des de qualsevol lloc.", - "tr": "OpenHands'e bulutta erişin. Kurulum gerekmez—ekibinizle her yerden kodlamaya başlayın.", - "uk": "Отримайте доступ до OpenHands у хмарі. Налаштування не потрібне—починайте кодувати з командою з будь-якого місця." - }, - "ENTERPRISE$SAAS_FEATURE_NO_INFRASTRUCTURE": { - "en": "No infrastructure to manage", - "ja": "管理するインフラストラクチャなし", - "zh-CN": "无需管理基础设施", - "zh-TW": "無需管理基礎設施", - "ko-KR": "인프라 관리 불필요", - "no": "Ingen infrastruktur å administrere", - "ar": "لا حاجة لإدارة البنية التحتية", - "de": "Keine Infrastruktur zu verwalten", - "fr": "Aucune infrastructure à gérer", - "it": "Nessuna infrastruttura da gestire", - "pt": "Sem infraestrutura para gerenciar", - "es": "Sin infraestructura que gestionar", - "ca": "Sense infraestructura per gestionar", - "tr": "Yönetilecek altyapı yok", - "uk": "Не потрібно керувати інфраструктурою" - }, - "ENTERPRISE$SAAS_FEATURE_SSO": { - "en": "SSO and team management", - "ja": "SSOとチーム管理", - "zh-CN": "SSO 和团队管理", - "zh-TW": "SSO 和團隊管理", - "ko-KR": "SSO 및 팀 관리", - "no": "SSO og teamadministrasjon", - "ar": "تسجيل دخول موحد وإدارة الفريق", - "de": "SSO und Teamverwaltung", - "fr": "SSO et gestion d'équipe", - "it": "SSO e gestione del team", - "pt": "SSO e gerenciamento de equipe", - "es": "SSO y gestión de equipos", - "ca": "SSO i gestió d'equips", - "tr": "SSO ve ekip yönetimi", - "uk": "SSO та управління командою" - }, - "ENTERPRISE$SAAS_FEATURE_ACCESS_ANYWHERE": { - "en": "Access from any device", - "ja": "どのデバイスからでもアクセス", - "zh-CN": "从任何设备访问", - "zh-TW": "從任何裝置存取", - "ko-KR": "모든 기기에서 접속", - "no": "Tilgang fra enhver enhet", - "ar": "الوصول من أي جهاز", - "de": "Zugriff von jedem Gerät", - "fr": "Accès depuis n'importe quel appareil", - "it": "Accesso da qualsiasi dispositivo", - "pt": "Acesso de qualquer dispositivo", - "es": "Acceso desde cualquier dispositivo", - "ca": "Accés des de qualsevol dispositiu", - "tr": "Herhangi bir cihazdan erişim", - "uk": "Доступ з будь-якого пристрою" - }, - "ENTERPRISE$SAAS_FEATURE_AUTO_UPDATES": { - "en": "Automatic updates and security", - "ja": "自動更新とセキュリティ", - "zh-CN": "自动更新和安全", - "zh-TW": "自動更新和安全", - "ko-KR": "자동 업데이트 및 보안", - "no": "Automatiske oppdateringer og sikkerhet", - "ar": "تحديثات تلقائية وأمان", - "de": "Automatische Updates und Sicherheit", - "fr": "Mises à jour automatiques et sécurité", - "it": "Aggiornamenti automatici e sicurezza", - "pt": "Atualizações automáticas e segurança", - "es": "Actualizaciones automáticas y seguridad", - "ca": "Actualitzacions automàtiques i seguretat", - "tr": "Otomatik güncellemeler ve güvenlik", - "uk": "Автоматичні оновлення та безпека" - }, - "ENTERPRISE$SELF_HOSTED_TITLE": { - "en": "Self-hosted", - "ja": "セルフホスト", - "zh-CN": "自托管", - "zh-TW": "自託管", - "ko-KR": "셀프 호스팅", - "no": "Selvhosted", - "ar": "استضافة ذاتية", - "de": "Selbst gehostet", - "fr": "Auto-hébergé", - "it": "Self-hosted", - "pt": "Auto-hospedado", - "es": "Autoalojado", - "ca": "Allotjament propi", - "tr": "Kendi sunucunuzda", - "uk": "Самостійний хостинг" - }, - "ENTERPRISE$SELF_HOSTED_DESCRIPTION": { - "en": "Deploy OpenHands on your own infrastructure. Full control over your data, compliance, and security. Ideal for enterprises that require on-premises or private cloud deployment.", - "ja": "独自のインフラストラクチャにOpenHandsをデプロイ。データ、コンプライアンス、セキュリティを完全にコントロール。オンプレミスまたはプライベートクラウドの展開を必要とする企業に最適です。", - "zh-CN": "在您自己的基础设施上部署 OpenHands。完全控制您的数据、合规性和安全性。适合需要本地或私有云部署的企业。", - "zh-TW": "在您自己的基礎設施上部署 OpenHands。完全控制您的資料、合規性和安全性。適合需要本地或私有雲部署的企業。", - "ko-KR": "자체 인프라에 OpenHands를 배포하세요. 데이터, 규정 준수 및 보안을 완벽하게 제어합니다. 온프레미스 또는 프라이빗 클라우드 배포가 필요한 기업에 적합합니다.", - "no": "Distribuer OpenHands på din egen infrastruktur. Full kontroll over dine data, samsvar og sikkerhet. Ideelt for bedrifter som krever lokal eller privat sky-distribusjon.", - "ar": "انشر OpenHands على البنية التحتية الخاصة بك. تحكم كامل في بياناتك والامتثال والأمان. مثالي للمؤسسات التي تتطلب نشرًا محليًا أو سحابة خاصة.", - "de": "Stellen Sie OpenHands auf Ihrer eigenen Infrastruktur bereit. Volle Kontrolle über Ihre Daten, Compliance und Sicherheit. Ideal für Unternehmen, die eine lokale oder private Cloud-Bereitstellung benötigen.", - "fr": "Déployez OpenHands sur votre propre infrastructure. Contrôle total sur vos données, la conformité et la sécurité. Idéal pour les entreprises nécessitant un déploiement sur site ou en cloud privé.", - "it": "Distribuisci OpenHands sulla tua infrastruttura. Controllo completo sui tuoi dati, conformità e sicurezza. Ideale per le aziende che richiedono un deployment on-premises o su cloud privato.", - "pt": "Implante o OpenHands em sua própria infraestrutura. Controle total sobre seus dados, conformidade e segurança. Ideal para empresas que necessitam de implantação local ou em nuvem privada.", - "es": "Implemente OpenHands en su propia infraestructura. Control total sobre sus datos, cumplimiento y seguridad. Ideal para empresas que requieren implementación local o en nube privada.", - "ca": "Desplegueu OpenHands a la vostra pròpia infraestructura. Control total sobre les vostres dades, compliment i seguretat. Ideal per a empreses que requereixen desplegament local o en núvol privat.", - "tr": "OpenHands'i kendi altyapınızda dağıtın. Verileriniz, uyumluluk ve güvenlik üzerinde tam kontrol. Şirket içi veya özel bulut dağıtımı gerektiren işletmeler için idealdir.", - "uk": "Розгорніть OpenHands на власній інфраструктурі. Повний контроль над вашими даними, відповідністю та безпекою. Ідеально для підприємств, які потребують локального або приватного хмарного розгортання." - }, - "ENTERPRISE$SELF_HOSTED_CARD_DESCRIPTION": { - "en": "Deploy OpenHands on your own infrastructure. Full control over data, compliance, and security.", - "ja": "独自のインフラストラクチャにOpenHandsをデプロイ。データ、コンプライアンス、セキュリティを完全にコントロール。", - "zh-CN": "在您自己的基础设施上部署 OpenHands。完全控制数据、合规性和安全性。", - "zh-TW": "在您自己的基礎設施上部署 OpenHands。完全控制資料、合規性和安全性。", - "ko-KR": "자체 인프라에 OpenHands를 배포하세요. 데이터, 규정 준수 및 보안을 완벽하게 제어합니다.", - "no": "Distribuer OpenHands på din egen infrastruktur. Full kontroll over data, samsvar og sikkerhet.", - "ar": "انشر OpenHands على البنية التحتية الخاصة بك. تحكم كامل في البيانات والامتثال والأمان.", - "de": "Stellen Sie OpenHands auf Ihrer eigenen Infrastruktur bereit. Volle Kontrolle über Daten, Compliance und Sicherheit.", - "fr": "Déployez OpenHands sur votre propre infrastructure. Contrôle total sur les données, la conformité et la sécurité.", - "it": "Distribuisci OpenHands sulla tua infrastruttura. Controllo completo su dati, conformità e sicurezza.", - "pt": "Implante o OpenHands em sua própria infraestrutura. Controle total sobre dados, conformidade e segurança.", - "es": "Implemente OpenHands en su propia infraestructura. Control total sobre datos, cumplimiento y seguridad.", - "ca": "Desplegueu OpenHands a la vostra pròpia infraestructura. Control total sobre dades, compliment i seguretat.", - "tr": "OpenHands'i kendi altyapınızda dağıtın. Veriler, uyumluluk ve güvenlik üzerinde tam kontrol.", - "uk": "Розгорніть OpenHands на власній інфраструктурі. Повний контроль над даними, відповідністю та безпекою." - }, - "ENTERPRISE$SELF_HOSTED_FEATURE_ON_PREMISES": { - "en": "On-premises or private cloud", - "ja": "オンプレミスまたはプライベートクラウド", - "zh-CN": "本地或私有云", - "zh-TW": "本地或私有雲", - "ko-KR": "온프레미스 또는 프라이빗 클라우드", - "no": "Lokalt eller privat sky", - "ar": "محلي أو سحابة خاصة", - "de": "Lokal oder private Cloud", - "fr": "Sur site ou cloud privé", - "it": "On-premises o cloud privato", - "pt": "Local ou nuvem privada", - "es": "Local o nube privada", - "ca": "Local o núvol privat", - "tr": "Yerel veya özel bulut", - "uk": "Локально або приватна хмара" - }, - "ENTERPRISE$SELF_HOSTED_FEATURE_DATA_CONTROL": { - "en": "Full data control", - "ja": "完全なデータ制御", - "zh-CN": "完全数据控制", - "zh-TW": "完全資料控制", - "ko-KR": "완전한 데이터 제어", - "no": "Full datakontroll", - "ar": "تحكم كامل في البيانات", - "de": "Vollständige Datenkontrolle", - "fr": "Contrôle total des données", - "it": "Controllo completo dei dati", - "pt": "Controle total de dados", - "es": "Control total de datos", - "ca": "Control total de dades", - "tr": "Tam veri kontrolü", - "uk": "Повний контроль даних" - }, - "ENTERPRISE$SELF_HOSTED_FEATURE_COMPLIANCE": { - "en": "Custom compliance requirements", - "ja": "カスタムコンプライアンス要件", - "zh-CN": "自定义合规要求", - "zh-TW": "自訂合規要求", - "ko-KR": "맞춤 규정 준수 요구사항", - "no": "Tilpassede samsvarskrav", - "ar": "متطلبات امتثال مخصصة", - "de": "Individuelle Compliance-Anforderungen", - "fr": "Exigences de conformité personnalisées", - "it": "Requisiti di conformità personalizzati", - "pt": "Requisitos de conformidade personalizados", - "es": "Requisitos de cumplimiento personalizados", - "ca": "Requisits de compliment personalitzats", - "tr": "Özel uyumluluk gereksinimleri", - "uk": "Індивідуальні вимоги відповідності" - }, - "ENTERPRISE$SELF_HOSTED_FEATURE_SUPPORT": { - "en": "Dedicated support options", - "ja": "専用サポートオプション", - "zh-CN": "专属支持选项", - "zh-TW": "專屬支援選項", - "ko-KR": "전담 지원 옵션", - "no": "Dedikerte støttealternativer", - "ar": "خيارات دعم مخصصة", - "de": "Dedizierte Support-Optionen", - "fr": "Options de support dédiées", - "it": "Opzioni di supporto dedicate", - "pt": "Opções de suporte dedicado", - "es": "Opciones de soporte dedicado", - "ca": "Opcions de suport dedicat", - "tr": "Özel destek seçenekleri", - "uk": "Виділені варіанти підтримки" - }, - "ENTERPRISE$FORM_SAAS_TITLE": { - "en": "Learn more about Enterprise SaaS", - "ja": "Enterprise SaaSについて詳しく", - "zh-CN": "了解更多企业SaaS", - "zh-TW": "了解更多企業SaaS", - "ko-KR": "Enterprise SaaS 자세히 알아보기", - "no": "Lær mer om Enterprise SaaS", - "ar": "تعرف على المزيد حول Enterprise SaaS", - "de": "Erfahren Sie mehr über Enterprise SaaS", - "fr": "En savoir plus sur Enterprise SaaS", - "it": "Scopri di più su Enterprise SaaS", - "pt": "Saiba mais sobre Enterprise SaaS", - "es": "Conozca más sobre Enterprise SaaS", - "ca": "Més informació sobre Enterprise SaaS", - "tr": "Enterprise SaaS hakkında daha fazla bilgi edinin", - "uk": "Дізнайтеся більше про Enterprise SaaS" - }, - "ENTERPRISE$FORM_SAAS_SUBTITLE": { - "en": "Tell us about your team and we'll help you get started.", - "ja": "チームについて教えてください。開始をお手伝いします。", - "zh-CN": "告诉我们您的团队情况,我们将帮助您开始使用。", - "zh-TW": "告訴我們您的團隊情況,我們將幫助您開始使用。", - "ko-KR": "팀에 대해 알려주시면 시작을 도와드리겠습니다.", - "no": "Fortell oss om teamet ditt, så hjelper vi deg i gang.", - "ar": "أخبرنا عن فريقك وسنساعدك على البدء.", - "de": "Erzählen Sie uns von Ihrem Team und wir helfen Ihnen beim Einstieg.", - "fr": "Parlez-nous de votre équipe et nous vous aiderons à démarrer.", - "it": "Raccontaci del tuo team e ti aiuteremo a iniziare.", - "pt": "Conte-nos sobre sua equipe e ajudaremos você a começar.", - "es": "Cuéntenos sobre su equipo y le ayudaremos a comenzar.", - "ca": "Expliqueu-nos sobre el vostre equip i us ajudarem a començar.", - "tr": "Bize ekibinizden bahsedin, başlamanıza yardımcı olalım.", - "uk": "Розкажіть нам про свою команду, і ми допоможемо вам почати." - }, - "ENTERPRISE$FORM_SELF_HOSTED_TITLE": { - "en": "Learn more about Self-hosted", - "ja": "セルフホストについて詳しく", - "zh-CN": "了解更多自托管", - "zh-TW": "了解更多自託管", - "ko-KR": "셀프 호스팅 자세히 알아보기", - "no": "Lær mer om selvhostet", - "ar": "تعرف على المزيد حول الاستضافة الذاتية", - "de": "Erfahren Sie mehr über Self-hosted", - "fr": "En savoir plus sur l'auto-hébergement", - "it": "Scopri di più su Self-hosted", - "pt": "Saiba mais sobre auto-hospedagem", - "es": "Conozca más sobre auto-alojamiento", - "ca": "Més informació sobre auto-allotjament", - "tr": "Self-hosted hakkında daha fazla bilgi edinin", - "uk": "Дізнайтеся більше про самостійний хостинг" - }, - "ENTERPRISE$FORM_SELF_HOSTED_SUBTITLE": { - "en": "Tell us about your needs and we'll be in touch.", - "ja": "ご要望をお聞かせください。ご連絡いたします。", - "zh-CN": "告诉我们您的需求,我们会与您联系。", - "zh-TW": "告訴我們您的需求,我們會與您聯繫。", - "ko-KR": "귀하의 요구 사항을 알려주시면 연락드리겠습니다.", - "no": "Fortell oss om dine behov, så tar vi kontakt.", - "ar": "أخبرنا عن احتياجاتك وسنتواصل معك.", - "de": "Erzählen Sie uns von Ihren Anforderungen und wir melden uns.", - "fr": "Parlez-nous de vos besoins et nous vous contacterons.", - "it": "Raccontaci le tue esigenze e ti contatteremo.", - "pt": "Conte-nos sobre suas necessidades e entraremos em contato.", - "es": "Cuéntenos sobre sus necesidades y nos pondremos en contacto.", - "ca": "Expliqueu-nos les vostres necessitats i us contactarem.", - "tr": "Bize ihtiyaçlarınızı anlatın, sizinle iletişime geçeceğiz.", - "uk": "Розкажіть нам про ваші потреби, і ми зв'яжемося з вами." - }, - "ENTERPRISE$FORM_NAME_LABEL": { - "en": "Name", - "ja": "名前", - "zh-CN": "姓名", - "zh-TW": "姓名", - "ko-KR": "이름", - "no": "Navn", - "ar": "الاسم", - "de": "Name", - "fr": "Nom", - "it": "Nome", - "pt": "Nome", - "es": "Nombre", - "ca": "Nom", - "tr": "Ad", - "uk": "Ім'я" - }, - "ENTERPRISE$FORM_NAME_PLACEHOLDER": { - "en": "Your name", - "ja": "あなたの名前", - "zh-CN": "您的姓名", - "zh-TW": "您的姓名", - "ko-KR": "이름", - "no": "Ditt navn", - "ar": "اسمك", - "de": "Ihr Name", - "fr": "Votre nom", - "it": "Il tuo nome", - "pt": "Seu nome", - "es": "Su nombre", - "ca": "El vostre nom", - "tr": "Adınız", - "uk": "Ваше ім'я" - }, - "ENTERPRISE$FORM_EMAIL_LABEL": { - "en": "Email address", - "ja": "メールアドレス", - "zh-CN": "电子邮件地址", - "zh-TW": "電子郵件地址", - "ko-KR": "이메일 주소", - "no": "E-postadresse", - "ar": "عنوان البريد الإلكتروني", - "de": "E-Mail-Adresse", - "fr": "Adresse e-mail", - "it": "Indirizzo email", - "pt": "Endereço de e-mail", - "es": "Dirección de correo electrónico", - "ca": "Adreça de correu electrònic", - "tr": "E-posta adresi", - "uk": "Електронна адреса" - }, - "ENTERPRISE$FORM_EMAIL_PLACEHOLDER": { - "en": "name@company.com", - "ja": "name@company.com", - "zh-CN": "name@company.com", - "zh-TW": "name@company.com", - "ko-KR": "name@company.com", - "no": "name@company.com", - "ar": "name@company.com", - "de": "name@company.com", - "fr": "nom@entreprise.com", - "it": "nome@azienda.com", - "pt": "nome@empresa.com", - "es": "nombre@empresa.com", - "ca": "nom@empresa.com", - "tr": "isim@sirket.com", - "uk": "ім'я@компанія.com" - }, - "ENTERPRISE$FORM_COMPANY_LABEL": { - "en": "Company name", - "ja": "会社名", - "zh-CN": "公司名称", - "zh-TW": "公司名稱", - "ko-KR": "회사명", - "no": "Selskapsnavn", - "ar": "اسم الشركة", - "de": "Firmenname", - "fr": "Nom de l'entreprise", - "it": "Nome azienda", - "pt": "Nome da empresa", - "es": "Nombre de la empresa", - "ca": "Nom de l'empresa", - "tr": "Şirket adı", - "uk": "Назва компанії" - }, - "ENTERPRISE$FORM_COMPANY_PLACEHOLDER": { - "en": "Your company", - "ja": "あなたの会社", - "zh-CN": "您的公司", - "zh-TW": "您的公司", - "ko-KR": "회사명", - "no": "Ditt selskap", - "ar": "شركتك", - "de": "Ihr Unternehmen", - "fr": "Votre entreprise", - "it": "La tua azienda", - "pt": "Sua empresa", - "es": "Su empresa", - "ca": "La vostra empresa", - "tr": "Şirketiniz", - "uk": "Ваша компанія" - }, - "ENTERPRISE$FORM_MESSAGE_LABEL": { - "en": "Custom message", - "ja": "カスタムメッセージ", - "zh-CN": "自定义消息", - "zh-TW": "自訂訊息", - "ko-KR": "맞춤 메시지", - "no": "Tilpasset melding", - "ar": "رسالة مخصصة", - "de": "Benutzerdefinierte Nachricht", - "fr": "Message personnalisé", - "it": "Messaggio personalizzato", - "pt": "Mensagem personalizada", - "es": "Mensaje personalizado", - "ca": "Missatge personalitzat", - "tr": "Özel mesaj", - "uk": "Користувацьке повідомлення" - }, - "ENTERPRISE$FORM_MESSAGE_SAAS_PLACEHOLDER": { - "en": "Tell us about your team and use case...", - "ja": "チームと使用目的について教えてください...", - "zh-CN": "告诉我们您的团队和用例...", - "zh-TW": "告訴我們您的團隊和用例...", - "ko-KR": "팀과 사용 사례에 대해 알려주세요...", - "no": "Fortell oss om teamet ditt og brukstilfelle...", - "ar": "أخبرنا عن فريقك وحالة الاستخدام...", - "de": "Erzählen Sie uns von Ihrem Team und Anwendungsfall...", - "fr": "Parlez-nous de votre équipe et cas d'utilisation...", - "it": "Raccontaci del tuo team e caso d'uso...", - "pt": "Conte-nos sobre sua equipe e caso de uso...", - "es": "Cuéntenos sobre su equipo y caso de uso...", - "ca": "Expliqueu-nos sobre el vostre equip i cas d'ús...", - "tr": "Bize ekibiniz ve kullanım durumunuz hakkında bilgi verin...", - "uk": "Розкажіть нам про свою команду та варіант використання..." - }, - "ENTERPRISE$FORM_MESSAGE_SELF_HOSTED_PLACEHOLDER": { - "en": "Tell us about your deployment needs...", - "ja": "デプロイメントのニーズについて教えてください...", - "zh-CN": "告诉我们您的部署需求...", - "zh-TW": "告訴我們您的部署需求...", - "ko-KR": "배포 요구 사항에 대해 알려주세요...", - "no": "Fortell oss om dine distribusjonsbehov...", - "ar": "أخبرنا عن احتياجات النشر الخاصة بك...", - "de": "Erzählen Sie uns von Ihren Bereitstellungsanforderungen...", - "fr": "Parlez-nous de vos besoins de déploiement...", - "it": "Raccontaci le tue esigenze di distribuzione...", - "pt": "Conte-nos sobre suas necessidades de implantação...", - "es": "Cuéntenos sobre sus necesidades de implementación...", - "ca": "Expliqueu-nos les vostres necessitats de desplegament...", - "tr": "Bize dağıtım ihtiyaçlarınızı anlatın...", - "uk": "Розкажіть нам про ваші потреби у розгортанні..." - }, - "ENTERPRISE$FORM_SUBMIT": { - "en": "Submit", - "ja": "送信", - "zh-CN": "提交", - "zh-TW": "提交", - "ko-KR": "제출", - "no": "Send inn", - "ar": "إرسال", - "de": "Absenden", - "fr": "Envoyer", - "it": "Invia", - "pt": "Enviar", - "es": "Enviar", - "ca": "Enviar", - "tr": "Gönder", - "uk": "Надіслати" - }, - "ENTERPRISE$FORM_SUBMITTING": { - "en": "Submitting...", - "ja": "送信中...", - "zh-CN": "提交中...", - "zh-TW": "提交中...", - "ko-KR": "제출 중...", - "no": "Sender inn...", - "ar": "جارٍ الإرسال...", - "de": "Wird gesendet...", - "fr": "Envoi en cours...", - "it": "Invio in corso...", - "pt": "Enviando...", - "es": "Enviando...", - "ca": "Enviant...", - "tr": "Gönderiliyor...", - "uk": "Надсилається..." - }, - "ENTERPRISE$REQUEST_SUBMITTED_TITLE": { - "en": "Request submitted", - "ja": "リクエストが送信されました", - "zh-CN": "请求已提交", - "zh-TW": "請求已提交", - "ko-KR": "요청이 제출되었습니다", - "no": "Forespørsel sendt", - "ar": "تم إرسال الطلب", - "de": "Anfrage gesendet", - "fr": "Demande soumise", - "it": "Richiesta inviata", - "pt": "Solicitação enviada", - "es": "Solicitud enviada", - "ca": "Sol·licitud enviada", - "tr": "İstek gönderildi", - "uk": "Запит надіслано" - }, - "ENTERPRISE$REQUEST_SUBMITTED_DESCRIPTION": { - "en": "Your request has been submitted. We will follow up with next steps shortly.", - "ja": "リクエストが送信されました。次のステップについて、まもなくご連絡いたします。", - "zh-CN": "您的请求已提交。我们将很快与您联系后续步骤。", - "zh-TW": "您的請求已提交。我們將很快與您聯繫後續步驟。", - "ko-KR": "요청이 제출되었습니다. 곧 다음 단계에 대해 연락드리겠습니다.", - "no": "Forespørselen din er sendt. Vi følger opp med neste trinn snart.", - "ar": "تم إرسال طلبك. سنتواصل معك قريباً بشأن الخطوات التالية.", - "de": "Ihre Anfrage wurde gesendet. Wir werden uns in Kürze mit den nächsten Schritten bei Ihnen melden.", - "fr": "Votre demande a été soumise. Nous vous contacterons prochainement pour les prochaines étapes.", - "it": "La tua richiesta è stata inviata. Ti contatteremo a breve con i prossimi passi.", - "pt": "Sua solicitação foi enviada. Entraremos em contato em breve com os próximos passos.", - "es": "Su solicitud ha sido enviada. Nos pondremos en contacto pronto con los próximos pasos.", - "ca": "La seva sol·licitud s'ha enviat. Ens posarem en contacte aviat amb els propers passos.", - "tr": "İsteğiniz gönderildi. En kısa sürede sonraki adımlar hakkında sizinle iletişime geçeceğiz.", - "uk": "Ваш запит надіслано. Ми зв'яжемося з вами найближчим часом щодо наступних кроків." - }, - "ENTERPRISE$DONE_BUTTON": { - "en": "Done", - "ja": "完了", - "zh-CN": "完成", - "zh-TW": "完成", - "ko-KR": "완료", - "no": "Ferdig", - "ar": "تم", - "de": "Fertig", - "fr": "Terminé", - "it": "Fatto", - "pt": "Concluído", - "es": "Hecho", - "ca": "Fet", - "tr": "Tamam", - "uk": "Готово" - }, "COMMON$BACK": { "en": "Back", "ja": "戻る", diff --git a/src/library-env.d.ts b/src/library-env.d.ts index c076513083..1ff0ff3648 100644 --- a/src/library-env.d.ts +++ b/src/library-env.d.ts @@ -2,32 +2,8 @@ /// interface Window { - __APP_MODE__?: "saas" | "oss"; __GITHUB_CLIENT_ID__?: string | null; posthog?: { capture: (event: string, properties?: Record) => void; }; - Reo?: { - init: (config: { clientID: string }) => void; - identify: (identity: { - username: string; - type: "github" | "email"; - other_identities?: Array<{ - username: string; - type: "github" | "email"; - }>; - firstname?: string; - lastname?: string; - company?: string; - }) => void; - }; - grecaptcha?: { - enterprise: { - ready: (callback: () => void) => void; - execute: ( - siteKey: string, - options: { action: string }, - ) => Promise; - }; - }; } diff --git a/src/mocks/billing-handlers.ts b/src/mocks/billing-handlers.ts deleted file mode 100644 index 0af1dbed3f..0000000000 --- a/src/mocks/billing-handlers.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { delay, http, HttpResponse } from "msw"; - -export const BILLING_HANDLERS = [ - http.get("/api/billing/credits", async () => { - await delay(); - return HttpResponse.json({ credits: "100" }); - }), - - http.post("/api/billing/create-checkout-session", async () => { - await delay(); - return HttpResponse.json({ - redirect_url: "https://stripe.com/some-checkout", - }); - }), - - http.post("/api/billing/create-customer-setup-session", async () => { - await delay(); - return HttpResponse.json({ - redirect_url: "https://stripe.com/some-customer-setup", - }); - }), -]; diff --git a/src/mocks/conversation-handlers.ts b/src/mocks/conversation-handlers.ts index bb4527205a..95b70b1a7d 100644 --- a/src/mocks/conversation-handlers.ts +++ b/src/mocks/conversation-handlers.ts @@ -110,23 +110,26 @@ export const CONVERSATION_HANDLERS = [ }); }), - http.patch("/api/conversations/:conversationId", async ({ params, request }) => { - const conversationId = params.conversationId as string; - const conversation = CONVERSATIONS.get(conversationId); + http.patch( + "/api/conversations/:conversationId", + async ({ params, request }) => { + const conversationId = params.conversationId as string; + const conversation = CONVERSATIONS.get(conversationId); - if (conversation) { - const body = (await request.json()) as { title?: string } | null; - if (body?.title) { - CONVERSATIONS.set(conversationId, { - ...conversation, - title: body.title, - updated_at: new Date().toISOString(), - }); - return HttpResponse.json(null, { status: 200 }); + if (conversation) { + const body = (await request.json()) as { title?: string } | null; + if (body?.title) { + CONVERSATIONS.set(conversationId, { + ...conversation, + title: body.title, + updated_at: new Date().toISOString(), + }); + return HttpResponse.json(null, { status: 200 }); + } } - } - return HttpResponse.json(null, { status: 404 }); - }), + return HttpResponse.json(null, { status: 404 }); + }, + ), http.delete("/api/conversations/:conversationId", async ({ params }) => { const conversationId = params.conversationId as string; diff --git a/src/mocks/git-repository-handlers.ts b/src/mocks/git-repository-handlers.ts index e6098f1909..65471df7fe 100644 --- a/src/mocks/git-repository-handlers.ts +++ b/src/mocks/git-repository-handlers.ts @@ -232,8 +232,7 @@ export const GIT_REPOSITORY_HANDLERS = [ http.get("/api/git/changes", async () => HttpResponse.json([])), - http.get( - "/api/git/diff", - async () => HttpResponse.json({ original: "", modified: "" }), + http.get("/api/git/diff", async () => + HttpResponse.json({ original: "", modified: "" }), ), ]; diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 6936b283e9..300deed564 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -1,5 +1,4 @@ import { API_KEYS_HANDLERS } from "./api-keys-handlers"; -import { BILLING_HANDLERS } from "./billing-handlers"; import { FILE_SERVICE_HANDLERS } from "./file-service-handlers"; import { TASK_SUGGESTIONS_HANDLERS } from "./task-suggestions-handlers"; import { SECRETS_HANDLERS } from "./secrets-handlers"; @@ -16,7 +15,6 @@ import { ANALYTICS_HANDLERS } from "./analytics-handlers"; export const handlers = [ ...API_KEYS_HANDLERS, - ...BILLING_HANDLERS, ...FILE_SERVICE_HANDLERS, ...TASK_SUGGESTIONS_HANDLERS, ...SECRETS_HANDLERS, diff --git a/src/mocks/settings-handlers.ts b/src/mocks/settings-handlers.ts index 20b7d80223..236e7d32c3 100644 --- a/src/mocks/settings-handlers.ts +++ b/src/mocks/settings-handlers.ts @@ -41,27 +41,22 @@ const DEFAULT_MODEL = export const createMockWebClientConfig = ( overrides: Partial = {}, ): WebClientConfig => ({ - app_mode: "oss", posthog_client_key: "test-posthog-key", feature_flags: { - enable_billing: false, hide_llm_settings: false, enable_jira: false, enable_jira_dc: false, enable_linear: false, hide_users_page: false, - hide_billing_page: false, hide_integrations_page: false, ...overrides.feature_flags, }, providers_configured: [], maintenance_start_time: null, - auth_url: null, recaptcha_site_key: null, faulty_models: [], error_message: null, updated_at: new Date().toISOString(), - github_app_slug: null, ...overrides, }); @@ -447,29 +442,22 @@ export const SETTINGS_HANDLERS = [ ), http.get("/api/v1/web-client/config", () => { - const mockSaas = import.meta.env.VITE_MOCK_SAAS === "true"; - const config: WebClientConfig = { - app_mode: mockSaas ? "saas" : "oss", posthog_client_key: "fake-posthog-client-key", feature_flags: { - enable_billing: mockSaas, hide_llm_settings: false, enable_jira: false, enable_jira_dc: false, enable_linear: false, hide_users_page: false, - hide_billing_page: false, hide_integrations_page: false, }, providers_configured: [], maintenance_start_time: null, - auth_url: null, recaptcha_site_key: null, faulty_models: [], error_message: null, updated_at: new Date().toISOString(), - github_app_slug: mockSaas ? "openhands" : null, }; return HttpResponse.json(config); @@ -566,5 +554,4 @@ export const SETTINGS_HANDLERS = [ return HttpResponse.json(null, { status: 400 }); }), - ]; diff --git a/src/routes/app-settings.tsx b/src/routes/app-settings.tsx index f12060429b..e002507c37 100644 --- a/src/routes/app-settings.tsx +++ b/src/routes/app-settings.tsx @@ -18,7 +18,6 @@ import { } from "#/utils/custom-toast-handlers"; import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message"; import { AppSettingsInputsSkeleton } from "#/components/features/settings/app-settings/app-settings-inputs-skeleton"; -import { useConfig } from "#/hooks/query/use-config"; import { parseMaxBudgetPerTask } from "#/utils/settings-utils"; import { SandboxGroupingStrategy, @@ -32,7 +31,6 @@ export function AppSettingsScreen() { const { mutate: saveSettings, isPending } = useSaveSettings(); const { data: settings, isLoading } = useSettings(); - const { data: config } = useConfig(); const [languageInputHasChanged, setLanguageInputHasChanged] = React.useState(false); @@ -42,14 +40,6 @@ export function AppSettingsScreen() { soundNotificationsSwitchHasChanged, setSoundNotificationsSwitchHasChanged, ] = React.useState(false); - const [ - proactiveConversationsSwitchHasChanged, - setProactiveConversationsSwitchHasChanged, - ] = React.useState(false); - const [ - solvabilityAnalysisSwitchHasChanged, - setSolvabilityAnalysisSwitchHasChanged, - ] = React.useState(false); const [ sandboxGroupingStrategyHasChanged, setSandboxGroupingStrategyHasChanged, @@ -75,13 +65,6 @@ export function AppSettingsScreen() { const enableSoundNotifications = formData.get("enable-sound-notifications-switch")?.toString() === "on"; - const enableProactiveConversations = - formData.get("enable-proactive-conversations-switch")?.toString() === - "on"; - - const enableSolvabilityAnalysis = - formData.get("enable-solvability-analysis-switch")?.toString() === "on"; - const sandboxGroupingStrategy = selectedSandboxGroupingStrategy || settings?.sandbox_grouping_strategy || @@ -104,8 +87,6 @@ export function AppSettingsScreen() { language, user_consents_to_analytics: enableAnalytics, enable_sound_notifications: enableSoundNotifications, - enable_proactive_conversation_starters: enableProactiveConversations, - enable_solvability_analysis: enableSolvabilityAnalysis, sandbox_grouping_strategy: sandboxGroupingStrategy, max_budget_per_task: maxBudgetPerTask, git_user_name: gitUserName, @@ -124,7 +105,6 @@ export function AppSettingsScreen() { setLanguageInputHasChanged(false); setAnalyticsSwitchHasChanged(false); setSoundNotificationsSwitchHasChanged(false); - setProactiveConversationsSwitchHasChanged(false); setSandboxGroupingStrategyHasChanged(false); setSelectedSandboxGroupingStrategy(null); setMaxBudgetPerTaskHasChanged(false); @@ -159,21 +139,6 @@ export function AppSettingsScreen() { ); }; - const checkIfProactiveConversationsSwitchHasChanged = (checked: boolean) => { - const currentProactiveConversations = - !!settings?.enable_proactive_conversation_starters; - setProactiveConversationsSwitchHasChanged( - checked !== currentProactiveConversations, - ); - }; - - const checkIfSolvabilityAnalysisSwitchHasChanged = (checked: boolean) => { - const currentSolvabilityAnalysis = !!settings?.enable_solvability_analysis; - setSolvabilityAnalysisSwitchHasChanged( - checked !== currentSolvabilityAnalysis, - ); - }; - const handleSandboxGroupingStrategyChange = (key: React.Key | null) => { const newStrategy = key?.toString() as SandboxGroupingStrategy | undefined; setSelectedSandboxGroupingStrategy(newStrategy || null); @@ -203,8 +168,6 @@ export function AppSettingsScreen() { !languageInputHasChanged && !analyticsSwitchHasChanged && !soundNotificationsSwitchHasChanged && - !proactiveConversationsSwitchHasChanged && - !solvabilityAnalysisSwitchHasChanged && !sandboxGroupingStrategyHasChanged && !maxBudgetPerTaskHasChanged && !gitUserNameHasChanged && @@ -245,30 +208,6 @@ export function AppSettingsScreen() { {t(I18nKey.SETTINGS$SOUND_NOTIFICATIONS)} - {config?.app_mode === "saas" && ( - - {t(I18nKey.SETTINGS$PROACTIVE_CONVERSATION_STARTERS)} - - )} - - {config?.app_mode === "saas" && ( - - {t(I18nKey.SETTINGS$SOLVABILITY_ANALYSIS)} - - )} - {ENABLE_SANDBOX_GROUPING() && ( ; function HomeScreen() { - const { isEnterpriseCloud } = useAppMode(); const [selectedRepo, setSelectedRepo] = React.useState( null, ); - const [shouldShowCTA, setShouldShowCTA] = React.useState( - () => !isCTADismissed("homepage"), - ); - return (
- - {isEnterpriseCloud && shouldShowCTA && ( -
- -
- )}
); } diff --git a/src/routes/llm-settings.tsx b/src/routes/llm-settings.tsx index 1b90d82e90..d7e1b9f6de 100644 --- a/src/routes/llm-settings.tsx +++ b/src/routes/llm-settings.tsx @@ -1,22 +1,16 @@ import React from "react"; -import { useSearchParams } from "react-router"; import { useTranslation } from "react-i18next"; import { ModelSelector } from "#/components/shared/modals/settings/model-selector"; import { useAgentSettingsSchema } from "#/hooks/query/use-agent-settings-schema"; import { useSettings } from "#/hooks/query/use-settings"; import { SettingsInput } from "#/components/features/settings/settings-input"; import { HelpLink } from "#/ui/help-link"; -import { useConfig } from "#/hooks/query/use-config"; import { KeyStatusIcon } from "#/components/features/settings/key-status-icon"; import { SdkSectionHeaderProps, SdkSectionPage, } from "#/components/features/settings/sdk-settings/sdk-section-page"; import { I18nKey } from "#/i18n/declaration"; -import { - displayErrorToast, - displaySuccessToast, -} from "#/utils/custom-toast-handlers"; import { Settings, SettingsSchema, SettingsScope } from "#/types/settings"; import { extractModelAndProvider } from "#/utils/extract-model-and-provider"; import { @@ -86,26 +80,13 @@ function OpenHandsApiKeyHelp({ testId }: OpenHandsApiKeyHelpProps) { const { t } = useTranslation("openhands"); return ( - <> - -

- {t(I18nKey.SETTINGS$LLM_BILLING_INFO)}{" "} - - {t(I18nKey.SETTINGS$SEE_PRICING_DETAILS)} - -

- + ); } @@ -115,60 +96,22 @@ export function LlmSettingsScreen({ scope?: SettingsScope; }) { const { t } = useTranslation("openhands"); - const [searchParams, setSearchParams] = useSearchParams(); const { data: settings } = useSettings(scope); const { data: schema } = useAgentSettingsSchema( settings?.agent_settings_schema, ); - const { data: config } = useConfig(); - - const [selectedProvider, setSelectedProvider] = React.useState( - null, - ); - const hasHydratedInitialPersonalSaasViewRef = React.useRef(false); const defaultModel = String( (DEFAULT_SETTINGS.agent_settings?.llm as Record)?.model ?? "", ); - const isSaasMode = config?.app_mode === "saas"; - - React.useEffect(() => { - if (settings?.llm_model) { - const { provider } = extractModelAndProvider(settings.llm_model); - setSelectedProvider(provider || null); - } - }, [settings?.llm_model]); - - React.useEffect(() => { - if (settings && isSaasMode) { - hasHydratedInitialPersonalSaasViewRef.current = true; - } - }, [isSaasMode, settings]); - - React.useEffect(() => { - const checkout = searchParams.get("checkout"); - - if (checkout === "success") { - displaySuccessToast(t(I18nKey.SUBSCRIPTION$SUCCESS)); - setSearchParams({}); - } else if (checkout === "cancel") { - displayErrorToast(t(I18nKey.SUBSCRIPTION$FAILURE)); - setSearchParams({}); - } - }, [searchParams, setSearchParams, t]); - const getInitialView = React.useCallback( ( currentSettings: Settings, filteredSchema: SettingsSchema, ): SettingsView => { - if (isSaasMode && !hasHydratedInitialPersonalSaasViewRef.current) { - return "basic"; - } - const schemaView = inferInitialView(currentSettings, filteredSchema); if (schemaView !== "basic") { return schemaView; @@ -182,7 +125,7 @@ export function LlmSettingsScreen({ return hasCustomBaseUrl ? "all" : "basic"; }, - [isSaasMode], + [], ); const buildHeader = React.useCallback( @@ -193,53 +136,38 @@ export function LlmSettingsScreen({ typeof values["llm.base_url"] === "string" ? values["llm.base_url"] : ""; - const derivedProvider = modelValue - ? extractModelAndProvider(modelValue).provider || null - : null; - const activeProvider = - view === "basic" - ? (selectedProvider ?? derivedProvider) - : derivedProvider; - const shouldUseOpenHandsKey = - isSaasMode && activeProvider === "openhands"; const showOpenHandsApiKeyHelp = modelValue.startsWith("openhands/"); - const renderApiKeyInput = (testId: string, helpTestId: string) => { - if (shouldUseOpenHandsKey) { - return null; - } + const renderApiKeyInput = (testId: string, helpTestId: string) => ( + <> + " : ""} + onChange={(value) => onChange("llm.api_key", value)} + isDisabled={isDisabled} + startContent={ + settings?.llm_api_key_set ? ( + + ) : undefined + } + /> - return ( - <> - " : ""} - onChange={(value) => onChange("llm.api_key", value)} - isDisabled={isDisabled} - startContent={ - settings?.llm_api_key_set ? ( - - ) : undefined - } - /> - - - - ); - }; + + + ); return (
@@ -251,7 +179,6 @@ export function LlmSettingsScreen({ { - setSelectedProvider(provider); const nextModel = buildModelId(provider, model); if (nextModel) { onChange("llm.model", nextModel); @@ -310,7 +237,7 @@ export function LlmSettingsScreen({
); }, - [isSaasMode, defaultModel, selectedProvider, settings?.llm_api_key_set, t], + [defaultModel, settings?.llm_api_key_set, t], ); const buildPayload = React.useCallback( @@ -324,25 +251,7 @@ export function LlmSettingsScreen({ // basePayload is a nested dict (e.g. {llm: {model: "gpt-4"}}) const agentSettings = structuredClone(basePayload); - const modelValue = - typeof context.values["llm.model"] === "string" - ? context.values["llm.model"] - : ""; - const derivedProvider = modelValue - ? extractModelAndProvider(modelValue).provider || null - : null; - const activeProvider = - context.view === "basic" - ? (selectedProvider ?? derivedProvider) - : derivedProvider; - const shouldUseOpenHandsKey = - isSaasMode && activeProvider === "openhands"; - const llm = (agentSettings.llm ?? {}) as Record; - if (shouldUseOpenHandsKey && llm.model !== undefined) { - llm.api_key = ""; - agentSettings.llm = llm; - } if (context.view === "basic") { llm.base_url = getSchemaFieldDefaultValue(schema, "llm.base_url"); @@ -351,7 +260,7 @@ export function LlmSettingsScreen({ return { agent_settings_diff: agentSettings }; }, - [isSaasMode, schema, selectedProvider], + [schema], ); return ( @@ -363,7 +272,7 @@ export function LlmSettingsScreen({ buildPayload={buildPayload} getInitialView={getInitialView} forceShowAdvancedView - allowAllView={!isSaasMode} + allowAllView testId="llm-settings-screen" /> ); diff --git a/src/routes/mcp-settings.tsx b/src/routes/mcp-settings.tsx index 8c42aea4a5..8fb7dd87d3 100644 --- a/src/routes/mcp-settings.tsx +++ b/src/routes/mcp-settings.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from "react"; import { AxiosError } from "axios"; import { useTranslation } from "react-i18next"; import { useSettings } from "#/hooks/query/use-settings"; -import { useConfig } from "#/hooks/query/use-config"; import { useSaveSettings } from "#/hooks/mutation/use-save-settings"; import { useDeleteMcpServer } from "#/hooks/mutation/use-delete-mcp-server"; import { useAddMcpServer } from "#/hooks/mutation/use-add-mcp-server"; @@ -44,7 +43,6 @@ interface MCPServerConfig { export function MCPSettingsScreen() { const { t } = useTranslation("openhands"); const { data: settings, isLoading } = useSettings(); - const { data: config } = useConfig(); const { mutate: saveSettings, isPending: isSavingSearchApiKey } = useSaveSettings(); const { mutate: deleteMcpServer } = useDeleteMcpServer(); @@ -61,8 +59,6 @@ export function MCPSettingsScreen() { useState(false); const [serverToDelete, setServerToDelete] = useState(null); - const isSaasMode = config?.app_mode === "saas"; - const mcpConfig: MCPConfig = parseMcpConfig( settings?.agent_settings?.mcp_config, ); @@ -219,62 +215,58 @@ export function MCPSettingsScreen() { onDelete={handleDeleteClick} /> - {!isSaasMode ? ( -
-
- - {t(I18nKey.SETTINGS$MCP_SEARCH_TITLE)} - - - {t(I18nKey.SETTINGS$MCP_SEARCH_DESCRIPTION)} - +
+
+ {t(I18nKey.SETTINGS$MCP_SEARCH_TITLE)} + + {t(I18nKey.SETTINGS$MCP_SEARCH_DESCRIPTION)} + +
+ +
+ { + setSearchApiKey(value); + setSearchApiKeyDirty(value !== (settings.search_api_key ?? "")); + }} + startContent={ + settings.search_api_key_set ? ( + + ) : undefined + } + /> + + + +
+ + {isSavingSearchApiKey + ? t(I18nKey.SETTINGS$SAVING) + : t(I18nKey.SETTINGS$SAVE_CHANGES)} +
- -
- { - setSearchApiKey(value); - setSearchApiKeyDirty(value !== (settings.search_api_key ?? "")); - }} - startContent={ - settings.search_api_key_set ? ( - - ) : undefined - } - /> - - - -
- - {isSavingSearchApiKey - ? t(I18nKey.SETTINGS$SAVING) - : t(I18nKey.SETTINGS$SAVE_CHANGES)} - -
-
-
- ) : null} +
+
{confirmationModalIsVisible && serverToDelete && ( ) { - return ( -
- {children} -
- ); -} diff --git a/src/utils/error-handler.ts b/src/utils/error-handler.ts index d479853b6d..385881e0ce 100644 --- a/src/utils/error-handler.ts +++ b/src/utils/error-handler.ts @@ -50,11 +50,3 @@ export function showChatError({ status_update: true, }); } - -/** - * Checks if an error message indicates a budget or credit limit issue - */ -export function isBudgetOrCreditError(errorMessage: string): boolean { - const lowerCaseError = errorMessage.toLowerCase(); - return lowerCaseError.includes("budget") || lowerCaseError.includes("credit"); -} diff --git a/src/utils/feature-flags.ts b/src/utils/feature-flags.ts index 5cdd19f9ef..969c421484 100644 --- a/src/utils/feature-flags.ts +++ b/src/utils/feature-flags.ts @@ -12,7 +12,6 @@ export function loadFeatureFlag( } } -export const BILLING_SETTINGS = () => loadFeatureFlag("BILLING_SETTINGS"); export const HIDE_LLM_SETTINGS = () => loadFeatureFlag("HIDE_LLM_SETTINGS"); export const VSCODE_IN_NEW_TAB = () => loadFeatureFlag("VSCODE_IN_NEW_TAB"); export const ENABLE_TRAJECTORY_REPLAY = () => diff --git a/src/utils/generate-auth-url.ts b/src/utils/generate-auth-url.ts deleted file mode 100644 index a5ec936144..0000000000 --- a/src/utils/generate-auth-url.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Generates a URL to redirect to for OAuth authentication - * @param identityProvider The identity provider to use (e.g., "github", "gitlab", "bitbucket", "azure_devops") - * @param requestUrl The URL of the request - * @returns The URL to redirect to for OAuth - */ -export const generateAuthUrl = ( - identityProvider: string, - requestUrl: URL, - authUrl?: string | null, -) => { - // Use HTTPS protocol unless the host is localhost - const protocol = - requestUrl.hostname === "localhost" ? requestUrl.protocol : "https:"; - const redirectUri = `${protocol}//${requestUrl.host}/oauth/keycloak/callback`; - - let finalAuthUrl: string; - - if (authUrl) { - // Ensure https:// is prepended and remove any accidental duplicate slashes - finalAuthUrl = `https://${authUrl.replace(/^https?:\/\//, "")}`; - } else { - finalAuthUrl = requestUrl.hostname - .replace(/(^|\.)staging\.all-hands\.dev$/, "$1auth.staging.all-hands.dev") - .replace(/(^|\.)app\.all-hands\.dev$/, "auth.app.all-hands.dev") - .replace(/(^|\.)localhost$/, "auth.staging.all-hands.dev"); - - // If no replacements matched, prepend "auth." (excluding localhost) - if ( - finalAuthUrl === requestUrl.hostname && - requestUrl.hostname !== "localhost" - ) { - finalAuthUrl = `auth.${requestUrl.hostname}`; - } - - finalAuthUrl = `https://${finalAuthUrl}`; - } - - const scope = "openid email profile"; // OAuth scope - not user-facing - const separator = requestUrl.search ? "&" : "?"; - const cleanHref = requestUrl.href.replace(/\/$/, ""); - const state = `${cleanHref}${separator}login_method=${identityProvider}`; - return `${finalAuthUrl}/realms/allhands/protocol/openid-connect/auth?client_id=allhands&kc_idp_hint=${identityProvider}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}`; -}; diff --git a/src/utils/git-status-mapper.ts b/src/utils/git-status-mapper.ts index 4c150b978f..b739a168fd 100644 --- a/src/utils/git-status-mapper.ts +++ b/src/utils/git-status-mapper.ts @@ -7,7 +7,9 @@ type ClientGitChangeStatus = "added" | "modified" | "deleted" | "renamed"; type SupportedGitStatus = V1GitChangeStatus | ClientGitChangeStatus; -export function mapAnyGitStatusToV0Status(status: SupportedGitStatus): GitChangeStatus { +export function mapAnyGitStatusToV0Status( + status: SupportedGitStatus, +): GitChangeStatus { switch (status) { case "ADDED": case "added": diff --git a/src/utils/local-storage.ts b/src/utils/local-storage.ts index 656f1dd2fc..31453903df 100644 --- a/src/utils/local-storage.ts +++ b/src/utils/local-storage.ts @@ -1,8 +1,6 @@ // Local storage keys export const LOCAL_STORAGE_KEYS = { LOGIN_METHOD: "openhands_login_method", - ENTERPRISE_FORM_SAAS: "openhands_enterprise_form_saas", - ENTERPRISE_FORM_SELF_HOSTED: "openhands_enterprise_form_self_hosted", }; // Login methods @@ -12,7 +10,6 @@ export enum LoginMethod { BITBUCKET = "bitbucket", BITBUCKET_DATA_CENTER = "bitbucket_data_center", AZURE_DEVOPS = "azure_devops", - ENTERPRISE_SSO = "enterprise_sso", } /** @@ -38,78 +35,3 @@ export const getLoginMethod = (): LoginMethod | null => { export const clearLoginData = (): void => { localStorage.removeItem(LOCAL_STORAGE_KEYS.LOGIN_METHOD); }; - -// CTA locations that can be dismissed -export type CTALocation = "homepage"; - -// Generate storage key for a CTA location -const getCTAKey = (location: CTALocation): string => - `${location}-cta-dismissed`; - -/** - * Set a CTA as dismissed in local storage (persists across tabs) - * @param location The CTA location to dismiss - */ -export const setCTADismissed = (location: CTALocation): void => { - localStorage.setItem(getCTAKey(location), "true"); -}; - -/** - * Check if a CTA has been dismissed - * @param location The CTA location to check - * @returns true if dismissed, false otherwise - */ -export const isCTADismissed = (location: CTALocation): boolean => - localStorage.getItem(getCTAKey(location)) === "true"; - -// Enterprise form data types -export type EnterpriseFormType = "saas" | "self-hosted"; - -export interface EnterpriseFormData { - name: string; - company: string; - email: string; - message: string; -} - -const getEnterpriseFormKey = (formType: EnterpriseFormType): string => - formType === "saas" - ? LOCAL_STORAGE_KEYS.ENTERPRISE_FORM_SAAS - : LOCAL_STORAGE_KEYS.ENTERPRISE_FORM_SELF_HOSTED; - -/** - * Save enterprise form data to localStorage - * @param formType The type of form (saas or self-hosted) - * @param data The form data to save - */ -export const saveEnterpriseFormData = ( - formType: EnterpriseFormType, - data: EnterpriseFormData, -): void => { - localStorage.setItem(getEnterpriseFormKey(formType), JSON.stringify(data)); -}; - -/** - * Get enterprise form data from localStorage - * @param formType The type of form (saas or self-hosted) - * @returns The saved form data or null if not found - */ -export const getEnterpriseFormData = ( - formType: EnterpriseFormType, -): EnterpriseFormData | null => { - const data = localStorage.getItem(getEnterpriseFormKey(formType)); - if (!data) return null; - try { - return JSON.parse(data) as EnterpriseFormData; - } catch { - return null; - } -}; - -/** - * Clear enterprise form data from localStorage - * @param formType The type of form (saas or self-hosted) - */ -export const clearEnterpriseFormData = (formType: EnterpriseFormType): void => { - localStorage.removeItem(getEnterpriseFormKey(formType)); -}; diff --git a/src/utils/mutate-with-toast.ts b/src/utils/mutate-with-toast.ts index edd46f349b..4343903797 100644 --- a/src/utils/mutate-with-toast.ts +++ b/src/utils/mutate-with-toast.ts @@ -1,6 +1,6 @@ import toast from "react-hot-toast"; -import i18n from "#/i18n"; import { UseMutationResult } from "@tanstack/react-query"; +import i18n from "#/i18n"; import { TOAST_OPTIONS, displaySuccessToast, diff --git a/src/utils/reo.ts b/src/utils/reo.ts deleted file mode 100644 index b2b8773ec8..0000000000 --- a/src/utils/reo.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Reo.dev tracking service for SaaS mode - * Tracks developer activity and engagement in the product - * Using CDN approach for better TypeScript compatibility - */ - -import EventLogger from "./event-logger"; - -export interface ReoIdentity { - username: string; - type: "github" | "email"; - other_identities?: Array<{ - username: string; - type: "github" | "email"; - }>; - firstname?: string; - lastname?: string; - company?: string; -} - -const REO_CLIENT_ID = "6bac7145b4ee6ec"; - -class ReoService { - private initialized = false; - - private scriptLoaded = false; - - /** - * Load and initialize the Reo.dev tracking script from CDN - */ - async init(): Promise { - if (this.initialized) { - return; - } - - try { - // Load the Reo script dynamically from CDN - await this.loadScript(); - - // Initialize Reo with client ID - if (window.Reo) { - window.Reo.init({ clientID: REO_CLIENT_ID }); - this.initialized = true; - } - } catch (error) { - EventLogger.error(`Failed to initialize Reo.dev tracking: ${error}`); - } - } - - /** - * Load the Reo.dev script from CDN - */ - private loadScript(): Promise { - return new Promise((resolve, reject) => { - if (this.scriptLoaded) { - resolve(); - return; - } - - const script = document.createElement("script"); - script.src = `https://static.reo.dev/${REO_CLIENT_ID}/reo.js`; - script.defer = true; - - script.onload = () => { - this.scriptLoaded = true; - resolve(); - }; - - script.onerror = () => { - reject(new Error("Failed to load Reo.dev script")); - }; - - document.head.appendChild(script); - }); - } - - /** - * Identify a user in Reo.dev tracking - * Should be called after successful login - */ - identify(identity: ReoIdentity): void { - if (!this.initialized) { - EventLogger.warning("Reo.dev not initialized. Call init() first."); - return; - } - - try { - if (window.Reo) { - window.Reo.identify(identity); - } - } catch (error) { - EventLogger.error(`Failed to identify user in Reo.dev: ${error}`); - } - } - - /** - * Check if Reo.dev is initialized - */ - isInitialized(): boolean { - return this.initialized; - } -} - -const reoService = new ReoService(); - -export default reoService; diff --git a/src/utils/settings-utils.ts b/src/utils/settings-utils.ts index 8fc76e4a81..78cbe3ce41 100644 --- a/src/utils/settings-utils.ts +++ b/src/utils/settings-utils.ts @@ -54,7 +54,10 @@ export function isSettingsPageHidden( featureFlags: WebClientFeatureFlags | undefined, ): boolean { if (featureFlags?.hide_llm_settings && path === "/settings") return true; - if (featureFlags?.hide_integrations_page && path === "/settings/integrations") { + if ( + featureFlags?.hide_integrations_page && + path === "/settings/integrations" + ) { return true; } return false; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 0efdb09ad1..e6213bb883 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -161,22 +161,13 @@ export const getFileExtension = (fileName: string): string => { return extension || "FILE"; }; -export const shouldUseInstallationRepos = ( - provider: Provider, - app_mode: "saas" | "oss" | undefined, -) => { +export const shouldUseInstallationRepos = (provider: Provider) => { if (!provider) return false; switch (provider) { case "bitbucket": case "bitbucket_data_center": return true; - case "gitlab": - return false; - case "azure_devops": - return false; - case "github": - return app_mode === "saas"; default: return false; }