diff --git a/Development.md b/Development.md index 8d6e35751879..06bf415a7921 100644 --- a/Development.md +++ b/Development.md @@ -100,7 +100,7 @@ poetry run pytest ./tests/unit/test_*.py To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image. -Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.24-nikolaik` +Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.25-nikolaik` ## Develop inside Docker container diff --git a/README.md b/README.md index 22caad34c99f..51cf52943e94 100644 --- a/README.md +++ b/README.md @@ -43,17 +43,17 @@ See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installatio system requirements and more information. ```bash -docker pull docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik +docker pull docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik docker run -it --rm --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e LOG_ALL_EVENTS=true \ -v /var/run/docker.sock:/var/run/docker.sock \ -v ~/.openhands-state:/.openhands-state \ -p 3000:3000 \ --add-host host.docker.internal:host-gateway \ --name openhands-app \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 + docker.all-hands.dev/all-hands-ai/openhands:0.25 ``` You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)! diff --git a/containers/dev/compose.yml b/containers/dev/compose.yml index 50c8ed04563f..34245f04efaf 100644 --- a/containers/dev/compose.yml +++ b/containers/dev/compose.yml @@ -11,7 +11,7 @@ services: - BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"} - SANDBOX_API_HOSTNAME=host.docker.internal # - - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.24-nikolaik} + - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.25-nikolaik} - SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} - WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace} ports: diff --git a/docker-compose.yml b/docker-compose.yml index 4353b7b6bb5b..6407f78b4030 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: image: openhands:latest container_name: openhands-app-${DATE:-} environment: - - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik} + - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik} #- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user - WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace} ports: diff --git a/docs/DOC_STYLE_GUIDE.md b/docs/DOC_STYLE_GUIDE.md index a55af799b112..93b916b0e85a 100644 --- a/docs/DOC_STYLE_GUIDE.md +++ b/docs/DOC_STYLE_GUIDE.md @@ -46,3 +46,11 @@ docker run -it \ -e THAT=that ... ``` + +### Referring to UI Elements + +When referencing UI elements, use ``. + +Example: +1. Toggle the `Advanced` option +2. Enter your model in the `Custom Model` textbox. diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md index 6a666e91f8d3..c00beec86d72 100644 --- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md +++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md @@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345" ```bash docker run -it \ --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e LLM_API_KEY=$LLM_API_KEY \ @@ -61,7 +61,7 @@ docker run -it \ -v /var/run/docker.sock:/var/run/docker.sock \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 \ + docker.all-hands.dev/all-hands-ai/openhands:0.25 \ python -m openhands.core.cli ``` diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md index a72cd57f0cc1..1151e1e60df8 100644 --- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md +++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md @@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345" ```bash docker run -it \ --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e LLM_API_KEY=$LLM_API_KEY \ @@ -56,6 +56,6 @@ docker run -it \ -v /var/run/docker.sock:/var/run/docker.sock \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 \ + docker.all-hands.dev/all-hands-ai/openhands:0.25 \ python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue ``` diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/installation.mdx b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/installation.mdx index 6a1789214923..79a9bf0acdb7 100644 --- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/installation.mdx +++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/installation.mdx @@ -13,16 +13,16 @@ La façon la plus simple d'exécuter OpenHands est avec Docker. ```bash -docker pull docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik +docker pull docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik docker run -it --rm --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e LOG_ALL_EVENTS=true \ -v /var/run/docker.sock:/var/run/docker.sock \ -p 3000:3000 \ --add-host host.docker.internal:host-gateway \ --name openhands-app \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 + docker.all-hands.dev/all-hands-ai/openhands:0.25 ``` Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action). diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/runtimes.md b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/runtimes.md index 865489d34841..d256032b4c4c 100644 --- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/runtimes.md +++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/runtimes.md @@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands. ``` docker run # ... - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -v /var/run/docker.sock:/var/run/docker.sock \ # ... ``` diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md index 57b95b719570..4aafa581294b 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/cli-mode.md @@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345" ```bash docker run -it \ --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e LLM_API_KEY=$LLM_API_KEY \ @@ -59,7 +59,7 @@ docker run -it \ -v /var/run/docker.sock:/var/run/docker.sock \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 \ + docker.all-hands.dev/all-hands-ai/openhands:0.25 \ python -m openhands.core.cli ``` diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md index 44a4b5bc6f63..a164f3a9ba8b 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/headless-mode.md @@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345" ```bash docker run -it \ --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e LLM_API_KEY=$LLM_API_KEY \ @@ -57,6 +57,6 @@ docker run -it \ -v /var/run/docker.sock:/var/run/docker.sock \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 \ + docker.all-hands.dev/all-hands-ai/openhands:0.25 \ python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue ``` diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/installation.mdx b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/installation.mdx index 2d20773af4bc..4988d8d4c7da 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/installation.mdx +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/installation.mdx @@ -11,16 +11,16 @@ 在 Docker 中运行 OpenHands 是最简单的方式。 ```bash -docker pull docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik +docker pull docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik docker run -it --rm --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e LOG_ALL_EVENTS=true \ -v /var/run/docker.sock:/var/run/docker.sock \ -p 3000:3000 \ --add-host host.docker.internal:host-gateway \ --name openhands-app \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 + docker.all-hands.dev/all-hands-ai/openhands:0.25 ``` 你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands,作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。 diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/runtimes.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/runtimes.md index 5786ce571c81..e2d4bde47a2e 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/runtimes.md +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/runtimes.md @@ -11,7 +11,7 @@ ``` docker run # ... - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -v /var/run/docker.sock:/var/run/docker.sock \ # ... ``` diff --git a/docs/modules/usage/how-to/cli-mode.md b/docs/modules/usage/how-to/cli-mode.md index 612f1590eac9..630f63b9697a 100644 --- a/docs/modules/usage/how-to/cli-mode.md +++ b/docs/modules/usage/how-to/cli-mode.md @@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker: ```bash docker run -it \ --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e LLM_API_KEY=$LLM_API_KEY \ @@ -45,7 +45,7 @@ docker run -it \ -v ~/.openhands-state:/.openhands-state \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 \ + docker.all-hands.dev/all-hands-ai/openhands:0.25 \ python -m openhands.core.cli ``` diff --git a/docs/modules/usage/how-to/gui-mode.md b/docs/modules/usage/how-to/gui-mode.md index 483f8869e9eb..200e4ce3e0dc 100644 --- a/docs/modules/usage/how-to/gui-mode.md +++ b/docs/modules/usage/how-to/gui-mode.md @@ -1,9 +1,6 @@ # GUI Mode -## Introduction - -OpenHands provides a user-friendly Graphical User Interface (GUI) mode for interacting with the AI assistant. -This mode offers an intuitive way to set up the environment, manage settings, and communicate with the AI. +OpenHands provides a Graphical User Interface (GUI) mode for interacting with the AI assistant. ## Installation and Setup @@ -14,104 +11,95 @@ This mode offers an intuitive way to set up the environment, manage settings, an ### Initial Setup -1. Upon first launch, you'll see a settings modal. -2. Select an `LLM Provider` and `LLM Model` from the dropdown menus. +1. Upon first launch, you'll see a settings page. +2. Select an `LLM Provider` and `LLM Model` from the dropdown menus. If the required model does not exist in the list, + toggle `Advanced` options and enter it with the correct prefix in the `Custom Model` text box. 3. Enter the corresponding `API Key` for your chosen provider. -4. Click "Save" to apply the settings. +4. Click `Save Changes` to apply the settings. ### GitHub Token Setup OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it is available. This can happen in two ways: -- **Locally (OSS)**: The user directly inputs their GitHub token. -- **Online (SaaS)**: The token is obtained through GitHub OAuth authentication. - -#### Setting Up a Local GitHub Token - -1. **Generate a Personal Access Token (PAT)**: - - Go to GitHub Settings > Developer Settings > Personal Access Tokens > Tokens (classic). - - Click "Generate new token (classic)". +- **Local Installation**: The user directly inputs their GitHub token. +
+ Setting Up a GitHub Token + 1. **Generate a Personal Access Token (PAT)**: + - On GitHub, go to Settings > Developer Settings > Personal Access Tokens > Tokens (classic). + - Click `Generate new token (classic)`. - Required scopes: - `repo` (Full control of private repositories) - - `workflow` (Update GitHub Action workflows) - - `read:org` (Read organization data) + 2. **Enter Token in OpenHands**: + - Click the Settings button (gear icon). + - Navigate to the `GitHub Settings` section. + - Paste your token in the `GitHub Token` field. + - Click `Save Changes` to apply the changes. +
-2. **Enter Token in OpenHands**: - - Click the Settings button (gear icon) in the top right. - - Navigate to the "GitHub" section. - - Paste your token in the "GitHub Token" field. - - Click "Save" to apply the changes. +
+ Organizational Token Policies -#### Organizational Token Policies + If you're working with organizational repositories, additional setup may be required: -If you're working with organizational repositories, additional setup may be required: - -1. **Check Organization Requirements**: + 1. **Check Organization Requirements**: - Organization admins may enforce specific token policies. - Some organizations require tokens to be created with SSO enabled. - Review your organization's [token policy settings](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization). - -2. **Verify Organization Access**: + 2. **Verify Organization Access**: - Go to your token settings on GitHub. - - Look for the organization under "Organization access". - - If required, click "Enable SSO" next to your organization. + - Look for the organization under `Organization access`. + - If required, click `Enable SSO` next to your organization. - Complete the SSO authorization process. +
+ +
+ Troubleshooting + + Common issues and solutions: + + - **Token Not Recognized**: + - Ensure the token is properly saved in settings. + - Check that the token hasn't expired. + - Verify the token has the required scopes. + - Try regenerating the token. + + - **Organization Access Denied**: + - Check if SSO is required but not enabled. + - Verify organization membership. + - Contact organization admin if token policies are blocking access. -#### OAuth Authentication (Online Mode) + - **Verifying Token Works**: + - The app will show a green checkmark if the token is valid. + - Try accessing a repository to confirm permissions. + - Check the browser console for any error messages. +
-When using OpenHands in online mode, the GitHub OAuth flow: +- **OpenHands Cloud**: The token is obtained through GitHub OAuth authentication. -1. Requests the following permissions: +
+ OAuth Authentication + + When using OpenHands Cloud, the GitHub OAuth flow requests the following permissions: - Repository access (read/write) - Workflow management - Organization read access -2. Authentication steps: - - Click "Sign in with GitHub" when prompted. + To authenticate OpenHands: + - Click `Sign in with GitHub` when prompted. - Review the requested permissions. - Authorize OpenHands to access your GitHub account. - If using an organization, authorize organization access if prompted. - -#### Troubleshooting - -Common issues and solutions: - -- **Token Not Recognized**: - - Ensure the token is properly saved in settings. - - Check that the token hasn't expired. - - Verify the token has the required scopes. - - Try regenerating the token. - -- **Organization Access Denied**: - - Check if SSO is required but not enabled. - - Verify organization membership. - - Contact organization admin if token policies are blocking access. - -- **Verifying Token Works**: - - The app will show a green checkmark if the token is valid. - - Try accessing a repository to confirm permissions. - - Check the browser console for any error messages. - - Use the "Test Connection" button in settings if available. +
### Advanced Settings -1. Toggle `Advanced Options` to access additional settings. +1. Inside the Settings page, toggle `Advanced` options to access additional settings. 2. Use the `Custom Model` text box to manually enter a model if it's not in the list. 3. Specify a `Base URL` if required by your LLM provider. -### Main Interface - -The main interface consists of several key components: - -- **Chat Window**: The central area where you can view the conversation history with the AI assistant. -- **Input Box**: Located at the bottom of the screen, use this to type your messages or commands to the AI. -- **Send Button**: Click this to send your message to the AI. -- **Settings Button**: A gear icon that opens the settings modal, allowing you to adjust your configuration at any time. -- **Workspace Panel**: Displays the files and folders in your workspace, allowing you to navigate and view files, or the agent's past commands or web browsing history. - ### Interacting with the AI -1. Type your question, request, or task description in the input box. +1. Type your prompt in the input box. 2. Click the send button or press Enter to submit your message. 3. The AI will process your input and provide a response in the chat window. 4. You can continue the conversation by asking follow-up questions or providing additional information. diff --git a/docs/modules/usage/how-to/headless-mode.md b/docs/modules/usage/how-to/headless-mode.md index b751dc3000d1..e68b1e494934 100644 --- a/docs/modules/usage/how-to/headless-mode.md +++ b/docs/modules/usage/how-to/headless-mode.md @@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker: ```bash docker run -it \ --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e SANDBOX_USER_ID=$(id -u) \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e LLM_API_KEY=$LLM_API_KEY \ @@ -43,7 +43,7 @@ docker run -it \ -v ~/.openhands-state:/.openhands-state \ --add-host host.docker.internal:host-gateway \ --name openhands-app-$(date +%Y%m%d%H%M%S) \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 \ + docker.all-hands.dev/all-hands-ai/openhands:0.25 \ python -m openhands.core.main -t "write a bash script that prints hi" ``` diff --git a/docs/modules/usage/installation.mdx b/docs/modules/usage/installation.mdx index 6a65befc38f6..610be444fef4 100644 --- a/docs/modules/usage/installation.mdx +++ b/docs/modules/usage/installation.mdx @@ -6,11 +6,14 @@ - Linux - Windows with [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and [Docker Desktop support](https://docs.docker.com/desktop/setup/install/windows-install/#system-requirements) +A system with a modern processor and a minimum of **4GB RAM** is recommended to run OpenHands. + ## Prerequisites
MacOS - ### Docker Desktop + + **Docker Desktop** 1. [Install Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install). 2. Open Docker Desktop, go to `Settings > Advanced` and ensure `Allow the default Docker socket to be used` is enabled. @@ -23,7 +26,7 @@ Tested with Ubuntu 22.04. ::: - ### Docker Desktop + **Docker Desktop** 1. [Install Docker Desktop on Linux](https://docs.docker.com/desktop/setup/install/linux/). @@ -31,12 +34,13 @@
Windows - ### WSL + + **WSL** 1. [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install). 2. Run `wsl --version` in powershell and confirm `Default Version: 2`. - ### Docker Desktop + **Docker Desktop** 1. [Install Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install). 2. Open Docker Desktop, go to `Settings` and confirm the following: @@ -54,17 +58,17 @@ The easiest way to run OpenHands is in Docker. ```bash -docker pull docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik +docker pull docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik docker run -it --rm --pull=always \ - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -e LOG_ALL_EVENTS=true \ -v /var/run/docker.sock:/var/run/docker.sock \ -v ~/.openhands-state:/.openhands-state \ -p 3000:3000 \ --add-host host.docker.internal:host-gateway \ --name openhands-app \ - docker.all-hands.dev/all-hands-ai/openhands:0.24 + docker.all-hands.dev/all-hands-ai/openhands:0.25 ``` You'll find OpenHands running at http://localhost:3000! @@ -76,24 +80,22 @@ or run it on tagged issues with [a github action](https://docs.all-hands.dev/mod ## Setup -Upon launching OpenHands, you'll see a settings modal. You **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`. +Upon launching OpenHands, you'll see a Settings page. You **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`. These can be changed at any time by selecting the `Settings` button (gear icon) in the UI. -If the required `LLM Model` does not exist in the list, you can toggle `Advanced Options` and manually enter it with the correct prefix +If the required model does not exist in the list, you can toggle `Advanced` options and manually enter it with the correct prefix in the `Custom Model` text box. -The `Advanced Options` also allow you to specify a `Base URL` if required. +The `Advanced` options also allow you to specify a `Base URL` if required. -
- settings-modal - settings-modal -
+Now you're ready to [get started with OpenHands](./getting-started). ## Versions -The command above pulls the most recent stable release of OpenHands. You have other options as well: -- For a specific release, use `docker.all-hands.dev/all-hands-ai/openhands:$VERSION`, replacing $VERSION with the version number. -- We use semver, and release major, minor, and patch tags. So `0.9` will automatically point to the latest `0.9.x` release, and `0` will point to the latest `0.x.x` release. -- For the most up-to-date development version, you can use `docker.all-hands.dev/all-hands-ai/openhands:main`. This version is unstable and is recommended for testing or development purposes only. +The [docker command above](./installation#start-the-app) pulls the most recent stable release of OpenHands. You have other options as well: +- For a specific release, replace $VERSION in `openhands:$VERSION` and `runtime:$VERSION`, with the version number. +We use SemVer so `0.9` will automatically point to the latest `0.9.x` release, and `0` will point to the latest `0.x.x` release. +- For the most up-to-date development version, replace $VERSION in `openhands:$VERSION` and `runtime:$VERSION`, with `main`. +This version is unstable and is recommended for testing or development purposes only. You can choose the tag that best suits your needs based on stability requirements and desired features. diff --git a/docs/modules/usage/llms/azure-llms.md b/docs/modules/usage/llms/azure-llms.md index 7046fe7bf536..84f16627ab31 100644 --- a/docs/modules/usage/llms/azure-llms.md +++ b/docs/modules/usage/llms/azure-llms.md @@ -25,7 +25,7 @@ You will need your ChatGPT deployment name which can be found on the deployments <deployment-name> below. ::: -1. Enable `Advanced Options` +1. Enable `Advanced` options 2. Set the following: - `Custom Model` to azure/<deployment-name> - `Base URL` to your Azure API Base URL (e.g. `https://example-endpoint.openai.azure.com`) diff --git a/docs/modules/usage/llms/google-llms.md b/docs/modules/usage/llms/google-llms.md index d89ba389f057..74e9015ffb0a 100644 --- a/docs/modules/usage/llms/google-llms.md +++ b/docs/modules/usage/llms/google-llms.md @@ -10,7 +10,7 @@ OpenHands uses LiteLLM to make calls to Google's chat models. You can find their When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings: - `LLM Provider` to `Gemini` - `LLM Model` to the model you will be using. -If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. gemini/<model-name> like `gemini/gemini-1.5-pro`). +If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. gemini/<model-name> like `gemini/gemini-1.5-pro`). - `API Key` to your Gemini API key ## VertexAI - Google Cloud Platform Configs @@ -27,4 +27,4 @@ VERTEXAI_LOCATION="" Then set the following in the OpenHands UI through the Settings: - `LLM Provider` to `VertexAI` - `LLM Model` to the model you will be using. -If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. vertex_ai/<model-name>). +If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. vertex_ai/<model-name>). diff --git a/docs/modules/usage/llms/groq.md b/docs/modules/usage/llms/groq.md index d484d5e3a4e1..0de104cf1400 100644 --- a/docs/modules/usage/llms/groq.md +++ b/docs/modules/usage/llms/groq.md @@ -8,7 +8,7 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr - `LLM Provider` to `Groq` - `LLM Model` to the model you will be using. [Visit here to see the list of models that Groq hosts](https://console.groq.com/docs/models). If the model is not in the list, toggle -`Advanced Options`, and enter it in `Custom Model` (e.g. groq/<model-name> like `groq/llama3-70b-8192`). +`Advanced` options, and enter it in `Custom Model` (e.g. groq/<model-name> like `groq/llama3-70b-8192`). - `API key` to your Groq API key. To find or create your Groq API Key, [see here](https://console.groq.com/keys). @@ -17,7 +17,7 @@ models that Groq hosts](https://console.groq.com/docs/models). If the model is n The Groq endpoint for chat completion is [mostly OpenAI-compatible](https://console.groq.com/docs/openai). Therefore, you can access Groq models as you would access any OpenAI-compatible endpoint. In the OpenHands UI through the Settings: -1. Enable `Advanced Options` +1. Enable `Advanced` options 2. Set the following: - `Custom Model` to the prefix `openai/` + the model you will be using (e.g. `openai/llama3-70b-8192`) - `Base URL` to `https://api.groq.com/openai/v1` diff --git a/docs/modules/usage/llms/litellm-proxy.md b/docs/modules/usage/llms/litellm-proxy.md index 9178bc5c33ea..21413e0ef191 100644 --- a/docs/modules/usage/llms/litellm-proxy.md +++ b/docs/modules/usage/llms/litellm-proxy.md @@ -8,7 +8,7 @@ To use LiteLLM proxy with OpenHands, you need to: 1. Set up a LiteLLM proxy server (see [LiteLLM documentation](https://docs.litellm.ai/docs/proxy/quick_start)) 2. When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings: - * Enable `Advanced Options` + * Enable `Advanced` options * `Custom Model` to the prefix `litellm_proxy/` + the model you will be using (e.g. `litellm_proxy/anthropic.claude-3-5-sonnet-20241022-v2:0`) * `Base URL` to your LiteLLM proxy URL (e.g. `https://your-litellm-proxy.com`) * `API Key` to your LiteLLM proxy API key diff --git a/docs/modules/usage/llms/llms.md b/docs/modules/usage/llms/llms.md index f4fa118dd02e..c2b08d013491 100644 --- a/docs/modules/usage/llms/llms.md +++ b/docs/modules/usage/llms/llms.md @@ -38,7 +38,7 @@ The following can be set in the OpenHands UI through the Settings: - `LLM Provider` - `LLM Model` - `API Key` -- `Base URL` (through `Advanced Settings`) +- `Base URL` (through `Advanced` settings) There are some settings that may be necessary for some LLMs/providers that cannot be set through the UI. Instead, these can be set through environment variables passed to the [docker run command](/modules/usage/installation#start-the-app) diff --git a/docs/modules/usage/llms/openai-llms.md b/docs/modules/usage/llms/openai-llms.md index 9157c7cac8bb..d0358989691a 100644 --- a/docs/modules/usage/llms/openai-llms.md +++ b/docs/modules/usage/llms/openai-llms.md @@ -8,7 +8,7 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr * `LLM Provider` to `OpenAI` * `LLM Model` to the model you will be using. [Visit here to see a full list of OpenAI models that LiteLLM supports.](https://docs.litellm.ai/docs/providers/openai#openai-chat-completion-models) -If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. openai/<model-name> like `openai/gpt-4o`). +If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. openai/<model-name> like `openai/gpt-4o`). * `API Key` to your OpenAI API key. To find or create your OpenAI Project API Key, [see here](https://platform.openai.com/api-keys). ## Using OpenAI-Compatible Endpoints @@ -18,7 +18,7 @@ Just as for OpenAI Chat completions, we use LiteLLM for OpenAI-compatible endpoi ## Using an OpenAI Proxy If you're using an OpenAI proxy, in the OpenHands UI through the Settings: -1. Enable `Advanced Options` +1. Enable `Advanced` options 2. Set the following: - `Custom Model` to openai/<model-name> (e.g. `openai/gpt-4o` or openai/<proxy-prefix>/<model-name>) - `Base URL` to the URL of your OpenAI proxy diff --git a/docs/modules/usage/llms/openrouter.md b/docs/modules/usage/llms/openrouter.md index 247d0a0558f1..2b5204d26c82 100644 --- a/docs/modules/usage/llms/openrouter.md +++ b/docs/modules/usage/llms/openrouter.md @@ -8,5 +8,5 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr * `LLM Provider` to `OpenRouter` * `LLM Model` to the model you will be using. [Visit here to see a full list of OpenRouter models](https://openrouter.ai/models). -If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. openrouter/<model-name> like `openrouter/anthropic/claude-3.5-sonnet`). +If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. openrouter/<model-name> like `openrouter/anthropic/claude-3.5-sonnet`). * `API Key` to your OpenRouter API key. diff --git a/docs/modules/usage/runtimes.md b/docs/modules/usage/runtimes.md index 740a53b00482..1fb2d0f4236d 100644 --- a/docs/modules/usage/runtimes.md +++ b/docs/modules/usage/runtimes.md @@ -16,7 +16,7 @@ some flags being passed to `docker run` that make this possible: ``` docker run # ... - -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.24-nikolaik \ + -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.25-nikolaik \ -v /var/run/docker.sock:/var/run/docker.sock \ # ... ``` diff --git a/docs/sidebars.ts b/docs/sidebars.ts index da416ac30b91..f71e36a0b571 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -66,7 +66,7 @@ const sidebars: SidebarsConfig = { }, { type: 'doc', - label: 'Github Actions', + label: 'Github Action', id: 'usage/how-to/github-action', }, { diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index a2df79a259a5..6f20f1eb776d 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -23,6 +23,17 @@ export default function Home(): JSX.Element { })} > +
+
+

Most Popular Links

+ +
); } diff --git a/docs/static/img/settings-advanced.png b/docs/static/img/settings-advanced.png deleted file mode 100644 index 43a9cf05ab83..000000000000 Binary files a/docs/static/img/settings-advanced.png and /dev/null differ diff --git a/docs/static/img/settings-screenshot.png b/docs/static/img/settings-screenshot.png deleted file mode 100644 index 987dd8c25570..000000000000 Binary files a/docs/static/img/settings-screenshot.png and /dev/null differ diff --git a/evaluation/benchmarks/EDA/run_infer.py b/evaluation/benchmarks/EDA/run_infer.py index 2d65e1943849..f216a86ff8ca 100644 --- a/evaluation/benchmarks/EDA/run_infer.py +++ b/evaluation/benchmarks/EDA/run_infer.py @@ -9,6 +9,7 @@ EvalMetadata, EvalOutput, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -17,7 +18,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, get_parser, ) @@ -60,17 +60,14 @@ def codeact_user_response_eda(state: State) -> str: def get_config( metadata: EvalMetadata, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'python:3.12-bookworm' config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='python:3.12-bookworm', - enable_auto_lint=False, - use_host_network=False, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/agent_bench/run_infer.py b/evaluation/benchmarks/agent_bench/run_infer.py index 8c1f08b37798..a78e40239548 100644 --- a/evaluation/benchmarks/agent_bench/run_infer.py +++ b/evaluation/benchmarks/agent_bench/run_infer.py @@ -17,6 +17,7 @@ EvalMetadata, EvalOutput, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -25,7 +26,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, parse_arguments, ) @@ -40,21 +40,15 @@ def get_config( metadata: EvalMetadata, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'python:3.12-slim' + config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime=os.environ.get('RUNTIME', 'docker'), max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='python:3.12-slim', - enable_auto_lint=True, - use_host_network=False, - api_key=os.environ.get('ALLHANDS_API_KEY', None), - remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'), - keep_runtime_alive=False, - remote_runtime_init_timeout=3600, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/aider_bench/run_infer.py b/evaluation/benchmarks/aider_bench/run_infer.py index 8045f948d3f9..ae5faadc098b 100644 --- a/evaluation/benchmarks/aider_bench/run_infer.py +++ b/evaluation/benchmarks/aider_bench/run_infer.py @@ -16,6 +16,7 @@ EvalMetadata, EvalOutput, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -24,7 +25,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, load_from_toml, parse_arguments, @@ -47,22 +47,14 @@ def get_config( metadata: EvalMetadata, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'python:3.11-bookworm' config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime=os.environ.get('RUNTIME', 'docker'), max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='python:3.11-bookworm', - enable_auto_lint=True, - use_host_network=False, - timeout=100, - api_key=os.environ.get('ALLHANDS_API_KEY', None), - remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'), - keep_runtime_alive=False, - remote_runtime_init_timeout=1800, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/biocoder/run_infer.py b/evaluation/benchmarks/biocoder/run_infer.py index 20f3dc4870a1..f1c98ed06672 100644 --- a/evaluation/benchmarks/biocoder/run_infer.py +++ b/evaluation/benchmarks/biocoder/run_infer.py @@ -14,6 +14,7 @@ EvalOutput, codeact_user_response, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -22,7 +23,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, parse_arguments, ) @@ -57,18 +57,15 @@ def get_config( metadata: EvalMetadata, ) -> AppConfig: BIOCODER_BENCH_CONTAINER_IMAGE = 'public.ecr.aws/i5g0m1f6/eval_biocoder:v1.0' + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = BIOCODER_BENCH_CONTAINER_IMAGE config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image=BIOCODER_BENCH_CONTAINER_IMAGE, - enable_auto_lint=True, - use_host_network=False, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/bird/run_infer.py b/evaluation/benchmarks/bird/run_infer.py index 02d92aa3ee3e..1c56deb9670c 100644 --- a/evaluation/benchmarks/bird/run_infer.py +++ b/evaluation/benchmarks/bird/run_infer.py @@ -17,6 +17,7 @@ EvalMetadata, EvalOutput, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -25,7 +26,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, parse_arguments, ) @@ -71,17 +71,15 @@ def codeact_user_response(state: State) -> str: def get_config( metadata: EvalMetadata, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'python:3.12-bookworm' + config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='python:3.12-bookworm', - enable_auto_lint=True, - use_host_network=False, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/browsing_delegation/run_infer.py b/evaluation/benchmarks/browsing_delegation/run_infer.py index 164e117e26c0..5f3ee99d7437 100644 --- a/evaluation/benchmarks/browsing_delegation/run_infer.py +++ b/evaluation/benchmarks/browsing_delegation/run_infer.py @@ -10,6 +10,7 @@ EvalMetadata, EvalOutput, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -18,7 +19,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, parse_arguments, ) @@ -36,17 +36,14 @@ def get_config( assert ( metadata.max_iterations == 1 ), 'max_iterations must be 1 for browsing delegation evaluation.' + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'python:3.12-bookworm' config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='python:3.12-bookworm', - enable_auto_lint=False, - use_host_network=False, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, workspace_base=None, workspace_mount_path=None, ) diff --git a/evaluation/benchmarks/commit0_bench/run_infer.py b/evaluation/benchmarks/commit0_bench/run_infer.py index 2e0fc528f7c3..63d394a029d1 100644 --- a/evaluation/benchmarks/commit0_bench/run_infer.py +++ b/evaluation/benchmarks/commit0_bench/run_infer.py @@ -15,6 +15,7 @@ EvalOutput, assert_and_raise, codeact_user_response, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -25,7 +26,6 @@ from openhands.core.config import ( AgentConfig, AppConfig, - SandboxConfig, get_llm_config_arg, get_parser, ) @@ -105,9 +105,7 @@ def get_config( instance: pd.Series, metadata: EvalMetadata, ) -> AppConfig: - # COMMIT0_CONTAINER_IMAGE = 'wentingzhao/' assert USE_INSTANCE_IMAGE - # We use a different instance image for the each instance of commit0 eval repo_name = instance['repo'].split('/')[1] base_container_image = get_instance_docker_image(repo_name) logger.info( @@ -115,28 +113,16 @@ def get_config( f'Please make sure this image exists. ' f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.' ) - # else: - # raise - # base_container_image = SWE_BENCH_CONTAINER_IMAGE - # logger.info(f'Using swe-bench container image: {base_container_image}') + + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = base_container_image config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, max_iterations=metadata.max_iterations, runtime=os.environ.get('RUNTIME', 'docker'), - sandbox=SandboxConfig( - base_container_image=base_container_image, - enable_auto_lint=True, - use_host_network=False, - # large enough timeout, since some testcases take very long to run - timeout=300, - api_key=os.environ.get('ALLHANDS_API_KEY', None), - remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'), - keep_runtime_alive=False, - remote_runtime_init_timeout=3600, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/discoverybench/run_infer.py b/evaluation/benchmarks/discoverybench/run_infer.py index fc5d74b13554..d91d01194d83 100644 --- a/evaluation/benchmarks/discoverybench/run_infer.py +++ b/evaluation/benchmarks/discoverybench/run_infer.py @@ -16,6 +16,7 @@ EvalOutput, codeact_user_response, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -25,7 +26,6 @@ from openhands.core.config import ( AgentConfig, AppConfig, - SandboxConfig, get_llm_config_arg, parse_arguments, ) @@ -62,17 +62,14 @@ def get_config( metadata: EvalMetadata, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'python:3.12-bookworm' config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='python:3.12-bookworm', - enable_auto_lint=True, - use_host_network=False, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/gaia/run_infer.py b/evaluation/benchmarks/gaia/run_infer.py index 2fdab0b2927a..e63026e813e4 100644 --- a/evaluation/benchmarks/gaia/run_infer.py +++ b/evaluation/benchmarks/gaia/run_infer.py @@ -13,6 +13,7 @@ EvalOutput, codeact_user_response, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -21,7 +22,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, get_parser, ) @@ -48,17 +48,14 @@ def get_config( metadata: EvalMetadata, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'python:3.12-bookworm' config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='python:3.12-bookworm', - enable_auto_lint=True, - use_host_network=False, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/gorilla/run_infer.py b/evaluation/benchmarks/gorilla/run_infer.py index d107151fc537..e856fa267c03 100644 --- a/evaluation/benchmarks/gorilla/run_infer.py +++ b/evaluation/benchmarks/gorilla/run_infer.py @@ -11,6 +11,7 @@ EvalOutput, codeact_user_response, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -19,7 +20,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, get_parser, ) @@ -40,17 +40,14 @@ def get_config( metadata: EvalMetadata, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'python:3.12-bookworm' config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='python:3.12-bookworm', - enable_auto_lint=True, - use_host_network=False, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/gpqa/run_infer.py b/evaluation/benchmarks/gpqa/run_infer.py index b92a30b8590f..e297e3fb9ed5 100644 --- a/evaluation/benchmarks/gpqa/run_infer.py +++ b/evaluation/benchmarks/gpqa/run_infer.py @@ -29,6 +29,7 @@ EvalMetadata, EvalOutput, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -37,7 +38,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, get_parser, ) @@ -61,17 +61,14 @@ def get_config( metadata: EvalMetadata, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'python:3.12-bookworm' config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='python:3.12-bookworm', - enable_auto_lint=True, - use_host_network=False, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/humanevalfix/run_infer.py b/evaluation/benchmarks/humanevalfix/run_infer.py index c2cccf90c732..fbf88859b6af 100644 --- a/evaluation/benchmarks/humanevalfix/run_infer.py +++ b/evaluation/benchmarks/humanevalfix/run_infer.py @@ -22,6 +22,7 @@ EvalOutput, codeact_user_response, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -30,7 +31,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, parse_arguments, ) @@ -82,17 +82,14 @@ def get_config( metadata: EvalMetadata, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'python:3.12-bookworm' config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='python:3.12-bookworm', - enable_auto_lint=True, - use_host_network=False, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/logic_reasoning/run_infer.py b/evaluation/benchmarks/logic_reasoning/run_infer.py index e37c5b4ab053..fac82f29f510 100644 --- a/evaluation/benchmarks/logic_reasoning/run_infer.py +++ b/evaluation/benchmarks/logic_reasoning/run_infer.py @@ -9,6 +9,7 @@ EvalOutput, codeact_user_response, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -17,7 +18,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, get_parser, ) @@ -45,18 +45,18 @@ def get_config( metadata: EvalMetadata, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'xingyaoww/od-eval-logic-reasoning:v1.0' + sandbox_config.runtime_extra_deps = ( + '$OH_INTERPRETER_PATH -m pip install scitools-pyke' + ) + config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='xingyaoww/od-eval-logic-reasoning:v1.0', - enable_auto_lint=True, - use_host_network=False, - runtime_extra_deps='$OH_INTERPRETER_PATH -m pip install scitools-pyke', - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/miniwob/run_infer.py b/evaluation/benchmarks/miniwob/run_infer.py index 023cbe9cab8b..55e510818a80 100644 --- a/evaluation/benchmarks/miniwob/run_infer.py +++ b/evaluation/benchmarks/miniwob/run_infer.py @@ -12,6 +12,7 @@ EvalOutput, codeact_user_response, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -21,7 +22,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, parse_arguments, ) @@ -55,23 +55,14 @@ def get_config( metadata: EvalMetadata, env_id: str, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'xingyaoww/od-eval-miniwob:v1.0' config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime=os.environ.get('RUNTIME', 'docker'), max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='xingyaoww/od-eval-miniwob:v1.0', - enable_auto_lint=True, - use_host_network=False, - browsergym_eval_env=env_id, - api_key=os.environ.get('ALLHANDS_API_KEY', None), - remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'), - remote_runtime_init_timeout=1800, - keep_runtime_alive=False, - timeout=120, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/mint/run_infer.py b/evaluation/benchmarks/mint/run_infer.py index 4c356f26d944..bd1a394332c9 100644 --- a/evaluation/benchmarks/mint/run_infer.py +++ b/evaluation/benchmarks/mint/run_infer.py @@ -14,6 +14,7 @@ EvalMetadata, EvalOutput, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -22,7 +23,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, get_parser, ) @@ -103,18 +103,18 @@ def load_incontext_example(task_name: str, with_tool: bool = True): def get_config( metadata: EvalMetadata, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'xingyaoww/od-eval-mint:v1.0' + sandbox_config.runtime_extra_deps = ( + f'$OH_INTERPRETER_PATH -m pip install {" ".join(MINT_DEPENDENCIES)}' + ) + config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='xingyaoww/od-eval-mint:v1.0', - enable_auto_lint=True, - use_host_network=False, - runtime_extra_deps=f'$OH_INTERPRETER_PATH -m pip install {" ".join(MINT_DEPENDENCIES)}', - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/ml_bench/run_infer.py b/evaluation/benchmarks/ml_bench/run_infer.py index c2fcc1ae3e26..5eff173b4600 100644 --- a/evaluation/benchmarks/ml_bench/run_infer.py +++ b/evaluation/benchmarks/ml_bench/run_infer.py @@ -25,6 +25,7 @@ EvalOutput, codeact_user_response, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -33,7 +34,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, get_parser, load_app_config, @@ -77,16 +77,14 @@ def get_config( metadata: EvalMetadata, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'public.ecr.aws/i5g0m1f6/ml-bench' config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='public.ecr.aws/i5g0m1f6/ml-bench', - enable_auto_lint=True, - use_host_network=False, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/scienceagentbench/run_infer.py b/evaluation/benchmarks/scienceagentbench/run_infer.py index 09619fb718a6..fe0cd7ef3a00 100644 --- a/evaluation/benchmarks/scienceagentbench/run_infer.py +++ b/evaluation/benchmarks/scienceagentbench/run_infer.py @@ -11,6 +11,7 @@ EvalOutput, codeact_user_response, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -20,7 +21,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, get_parser, ) @@ -59,22 +59,17 @@ def get_config( metadata: EvalMetadata, instance_id: str, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = ( + 'docker.io/xingyaoww/openhands-eval-scienceagentbench' + ) config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime=os.environ.get('RUNTIME', 'docker'), max_budget_per_task=4, max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='docker.io/xingyaoww/openhands-eval-scienceagentbench', - enable_auto_lint=True, - use_host_network=False, - timeout=300, - api_key=os.environ.get('ALLHANDS_API_KEY', None), - remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'), - keep_runtime_alive=False, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/swe_bench/eval_infer.py b/evaluation/benchmarks/swe_bench/eval_infer.py index be18a36e9ab6..d5d2a81857c6 100644 --- a/evaluation/benchmarks/swe_bench/eval_infer.py +++ b/evaluation/benchmarks/swe_bench/eval_infer.py @@ -1,5 +1,6 @@ import json import os +import subprocess import tempfile import time from functools import partial @@ -21,13 +22,14 @@ from evaluation.utils.shared import ( EvalMetadata, EvalOutput, + get_default_sandbox_config_for_eval, prepare_dataset, reset_logger_for_multiprocessing, run_evaluation, ) from openhands.core.config import ( AppConfig, - SandboxConfig, + LLMConfig, get_parser, ) from openhands.core.logger import openhands_logger as logger @@ -79,22 +81,16 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig: f'Please make sure this image exists. ' f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.' ) + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = base_container_image + sandbox_config.remote_runtime_resource_factor = get_instance_resource_factor( + dataset_name=metadata.dataset, + instance_id=instance['instance_id'], + ) config = AppConfig( run_as_openhands=False, runtime=os.environ.get('RUNTIME', 'docker'), - sandbox=SandboxConfig( - base_container_image=base_container_image, - use_host_network=False, - # large enough timeout, since some testcases take very long to run - timeout=600, - api_key=os.environ.get('ALLHANDS_API_KEY', None), - remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'), - remote_runtime_init_timeout=3600, - remote_runtime_resource_factor=get_instance_resource_factor( - dataset_name=metadata.dataset, - instance_id=instance['instance_id'], - ), - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, @@ -415,13 +411,17 @@ def process_instance( else: # Initialize with a dummy metadata when file doesn't exist metadata = EvalMetadata( - agent_class="dummy_agent", # Placeholder agent class - llm_config=LLMConfig(model="dummy_model"), # Minimal LLM config + agent_class='dummy_agent', # Placeholder agent class + llm_config=LLMConfig(model='dummy_model'), # Minimal LLM config max_iterations=1, # Minimal iterations - eval_output_dir=os.path.dirname(args.input_file), # Use input file dir as output dir + eval_output_dir=os.path.dirname( + args.input_file + ), # Use input file dir as output dir start_time=time.strftime('%Y-%m-%d %H:%M:%S'), # Current time - git_commit=subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8').strip(), # Current commit - dataset=args.dataset # Dataset name from args + git_commit=subprocess.check_output(['git', 'rev-parse', 'HEAD']) + .decode('utf-8') + .strip(), # Current commit + dataset=args.dataset, # Dataset name from args ) # The evaluation harness constrains the signature of `process_instance_func` but we need to diff --git a/evaluation/benchmarks/swe_bench/resource/princeton-nlp__SWE-bench_Verified-test.json b/evaluation/benchmarks/swe_bench/resource/princeton-nlp__SWE-bench_Verified-test.json deleted file mode 100644 index 161ab736da08..000000000000 --- a/evaluation/benchmarks/swe_bench/resource/princeton-nlp__SWE-bench_Verified-test.json +++ /dev/null @@ -1 +0,0 @@ -{"pydata__xarray-6721": 8, "pytest-dev__pytest-7236": 8, "matplotlib__matplotlib-24627": 4, "django__django-15561": 4, "django__django-15098": 4, "django__django-14771": 4, "sympy__sympy-21612": 4, "sympy__sympy-15345": 4, "psf__requests-5414": 4, "astropy__astropy-14508": 2, "django__django-11451": 2, "django__django-11477": 2, "django__django-10880": 2, "django__django-11163": 2, "django__django-11815": 2, "astropy__astropy-14369": 2, "django__django-10097": 2, "django__django-10554": 2, "django__django-12304": 2, "django__django-12325": 2, "django__django-11551": 2, "django__django-11734": 2, "django__django-13109": 2, "django__django-13089": 2, "django__django-13343": 2, "django__django-13363": 2, "django__django-13809": 2, "django__django-13810": 2, "django__django-13786": 2, "django__django-13807": 2, "django__django-14493": 2, "django__django-11820": 2, "django__django-11951": 2, "django__django-11964": 2, "astropy__astropy-14309": 2, "astropy__astropy-14365": 2, "astropy__astropy-12907": 2, "astropy__astropy-14182": 2, "django__django-15161": 2, "django__django-15128": 2, "django__django-14999": 2, "django__django-14915": 2, "django__django-14752": 2, "django__django-14765": 2, "django__django-14089": 2, "django__django-15252": 2, "django__django-15380": 2, "django__django-15382": 2, "django__django-15499": 2, "django__django-15467": 2, "django__django-15280": 2, "django__django-15315": 2, "django__django-15277": 2, "django__django-15268": 2, "django__django-15629": 2, "django__django-15695": 2, "django__django-15732": 2, "django__django-15863": 2, "django__django-16082": 2, "django__django-16145": 2, "django__django-16256": 2, "django__django-16429": 2, "django__django-16454": 2, "django__django-16493": 2, "matplotlib__matplotlib-13989": 2, "matplotlib__matplotlib-20488": 2, "django__django-15503": 2, "django__django-15525": 2, "django__django-15375": 2, "django__django-15278": 2, "matplotlib__matplotlib-21568": 2, "matplotlib__matplotlib-20859": 2, "matplotlib__matplotlib-20826": 2, "matplotlib__matplotlib-20676": 2, "matplotlib__matplotlib-23412": 2, "matplotlib__matplotlib-22719": 2, "matplotlib__matplotlib-23299": 2, "matplotlib__matplotlib-22865": 2, "matplotlib__matplotlib-24149": 2, "matplotlib__matplotlib-24177": 2, "matplotlib__matplotlib-24570": 2, "matplotlib__matplotlib-24637": 2, "matplotlib__matplotlib-24970": 2, "matplotlib__matplotlib-23476": 2, "matplotlib__matplotlib-24026": 2, "matplotlib__matplotlib-23314": 2, "matplotlib__matplotlib-25332": 2, "matplotlib__matplotlib-25311": 2, "matplotlib__matplotlib-25122": 2, "matplotlib__matplotlib-25479": 2, "matplotlib__matplotlib-26342": 2, "psf__requests-2317": 2, "matplotlib__matplotlib-25960": 2, "matplotlib__matplotlib-25775": 2, "pydata__xarray-4356": 2, "pydata__xarray-4075": 2, "pydata__xarray-6461": 2, "pydata__xarray-4687": 2, "pydata__xarray-6599": 2, "pylint-dev__pylint-4661": 2, "django__django-15554": 2, "django__django-15563": 2, "pytest-dev__pytest-5262": 2, "pytest-dev__pytest-10081": 2, "scikit-learn__scikit-learn-12973": 2, "scikit-learn__scikit-learn-13124": 2, "scikit-learn__scikit-learn-13779": 2, "scikit-learn__scikit-learn-14141": 2, "scikit-learn__scikit-learn-13439": 2, "scikit-learn__scikit-learn-13496": 2, "scikit-learn__scikit-learn-15100": 2, "scikit-learn__scikit-learn-25102": 2, "scikit-learn__scikit-learn-25232": 2, "scikit-learn__scikit-learn-25747": 2, "scikit-learn__scikit-learn-26323": 2, "scikit-learn__scikit-learn-9288": 2, "scikit-learn__scikit-learn-14496": 2, "scikit-learn__scikit-learn-14629": 2, "sphinx-doc__sphinx-8265": 2, "sphinx-doc__sphinx-8548": 2, "sphinx-doc__sphinx-8593": 2, "sphinx-doc__sphinx-8595": 2, "sphinx-doc__sphinx-8621": 2, "sphinx-doc__sphinx-8638": 2, "sphinx-doc__sphinx-9229": 2, "sphinx-doc__sphinx-9281": 2, "sphinx-doc__sphinx-9461": 2, "sphinx-doc__sphinx-9591": 2, "sphinx-doc__sphinx-9658": 2, "sphinx-doc__sphinx-9673": 2, "sympy__sympy-12096": 2, "sympy__sympy-12481": 2, "sphinx-doc__sphinx-10323": 2, "sphinx-doc__sphinx-7590": 2, "sympy__sympy-13877": 2, "sympy__sympy-12489": 2, "sympy__sympy-15809": 2, "sympy__sympy-14711": 2, "sympy__sympy-16597": 2, "sympy__sympy-16766": 2, "sympy__sympy-16792": 2, "sympy__sympy-15875": 2, "sympy__sympy-17655": 2, "sympy__sympy-18189": 2, "sympy__sympy-18763": 2, "sympy__sympy-19040": 2, "sympy__sympy-19495": 2, "sympy__sympy-19637": 2, "sympy__sympy-19783": 2, "sympy__sympy-17630": 2, "sympy__sympy-20428": 2, "sympy__sympy-20590": 2, "sympy__sympy-20801": 2, "sympy__sympy-21379": 2, "sympy__sympy-21847": 2, "sympy__sympy-22456": 2, "sympy__sympy-22714": 2, "sympy__sympy-22914": 2, "sympy__sympy-23262": 2, "sympy__sympy-23413": 2, "sympy__sympy-23534": 2, "sympy__sympy-24066": 2, "sympy__sympy-24213": 2, "sympy__sympy-24443": 2, "sympy__sympy-24562": 2, "sympy__sympy-24661": 2} diff --git a/evaluation/benchmarks/swe_bench/run_infer.py b/evaluation/benchmarks/swe_bench/run_infer.py index 5e3f0e6a5bd7..89fe618a6c34 100644 --- a/evaluation/benchmarks/swe_bench/run_infer.py +++ b/evaluation/benchmarks/swe_bench/run_infer.py @@ -18,6 +18,7 @@ EvalOutput, assert_and_raise, codeact_user_response, + get_default_sandbox_config_for_eval, get_metrics, is_fatal_evaluation_error, make_metadata, @@ -30,7 +31,6 @@ from openhands.core.config import ( AgentConfig, AppConfig, - SandboxConfig, get_llm_config_arg, get_parser, ) @@ -122,30 +122,23 @@ def get_config( base_container_image = SWE_BENCH_CONTAINER_IMAGE logger.info(f'Using swe-bench container image: {base_container_image}') + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = base_container_image + sandbox_config.enable_auto_lint = True + sandbox_config.use_host_network = False + # Add platform to the sandbox config to solve issue 4401 + sandbox_config.platform = 'linux/amd64' + sandbox_config.remote_runtime_resource_factor = get_instance_resource_factor( + dataset_name=metadata.dataset, + instance_id=instance['instance_id'], + ) + config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, max_iterations=metadata.max_iterations, runtime=os.environ.get('RUNTIME', 'docker'), - sandbox=SandboxConfig( - base_container_image=base_container_image, - enable_auto_lint=True, - use_host_network=False, - # large enough timeout, since some testcases take very long to run - timeout=300, - # Add platform to the sandbox config to solve issue 4401 - platform='linux/amd64', - api_key=os.environ.get('ALLHANDS_API_KEY', None), - remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'), - keep_runtime_alive=False, - remote_runtime_init_timeout=3600, - remote_runtime_api_timeout=120, - remote_runtime_resource_factor=get_instance_resource_factor( - dataset_name=metadata.dataset, - instance_id=instance['instance_id'], - ), - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, @@ -331,6 +324,22 @@ def complete_runtime( logger.info(action, extra={'msg_type': 'ACTION'}) obs = runtime.run_action(action) logger.info(obs, extra={'msg_type': 'OBSERVATION'}) + + if obs.exit_code == -1: + # The previous command is still running + # We need to kill previous command + logger.info('The previous command is still running, trying to kill it...') + action = CmdRunAction(command='C-c') + obs = runtime.run_action(action) + logger.info(obs, extra={'msg_type': 'OBSERVATION'}) + + # Then run the command again + action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}') + action.set_hard_timeout(600) + logger.info(action, extra={'msg_type': 'ACTION'}) + obs = runtime.run_action(action) + logger.info(obs, extra={'msg_type': 'OBSERVATION'}) + assert_and_raise( isinstance(obs, CmdOutputObservation) and obs.exit_code == 0, f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}', diff --git a/evaluation/benchmarks/the_agent_company/run_infer.py b/evaluation/benchmarks/the_agent_company/run_infer.py index 84fb057ec791..43047fc17f8a 100644 --- a/evaluation/benchmarks/the_agent_company/run_infer.py +++ b/evaluation/benchmarks/the_agent_company/run_infer.py @@ -13,11 +13,11 @@ import yaml from browsing import pre_login +from evaluation.utils.shared import get_default_sandbox_config_for_eval from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, LLMConfig, - SandboxConfig, get_agent_config_arg, get_llm_config_arg, get_parser, @@ -38,6 +38,8 @@ def get_config( llm_config: LLMConfig, agent_config: AgentConfig | None, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = base_container_image config = AppConfig( run_as_openhands=False, max_budget_per_task=4, @@ -45,16 +47,7 @@ def get_config( save_trajectory_path=os.path.join( mount_path_on_host, f'traj_{task_short_name}.json' ), - sandbox=SandboxConfig( - base_container_image=base_container_image, - enable_auto_lint=True, - # using host network to access the host machine from the container - use_host_network=True, - # large enough timeout, since some testcases take very long to run - timeout=300, - api_key=os.environ.get('ALLHANDS_API_KEY', None), - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # we mount trajectories path so that trajectories, generated by OpenHands # controller, can be accessible to the evaluator file in the runtime container workspace_mount_path=mount_path_on_host, diff --git a/evaluation/benchmarks/toolqa/run_infer.py b/evaluation/benchmarks/toolqa/run_infer.py index 45b9febed27b..2fc670e568c6 100644 --- a/evaluation/benchmarks/toolqa/run_infer.py +++ b/evaluation/benchmarks/toolqa/run_infer.py @@ -10,6 +10,7 @@ EvalOutput, codeact_user_response, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -18,7 +19,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, get_parser, ) @@ -41,17 +41,14 @@ def get_config( metadata: EvalMetadata, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'python:3.12-bookworm' config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='python:3.12-bookworm', - enable_auto_lint=True, - use_host_network=False, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/visualwebarena/run_infer.py b/evaluation/benchmarks/visualwebarena/run_infer.py index 8986d3ab8fff..69656e610b02 100644 --- a/evaluation/benchmarks/visualwebarena/run_infer.py +++ b/evaluation/benchmarks/visualwebarena/run_infer.py @@ -11,6 +11,7 @@ EvalMetadata, EvalOutput, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -20,7 +21,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, parse_arguments, ) @@ -55,32 +55,29 @@ def get_config( assert base_url is not None, 'VISUALWEBARENA_BASE_URL must be set' assert openai_api_key is not None, 'OPENAI_API_KEY must be set' assert openai_base_url is not None, 'OPENAI_BASE_URL must be set' + + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'python:3.12-bookworm' + sandbox_config.browsergym_eval_env = env_id + sandbox_config.runtime_startup_env_vars = { + 'BASE_URL': base_url, + 'OPENAI_API_KEY': openai_api_key, + 'OPENAI_BASE_URL': openai_base_url, + 'VWA_CLASSIFIEDS': f'{base_url}:9980', + 'VWA_CLASSIFIEDS_RESET_TOKEN': '4b61655535e7ed388f0d40a93600254c', + 'VWA_SHOPPING': f'{base_url}:7770', + 'VWA_SHOPPING_ADMIN': f'{base_url}:7780/admin', + 'VWA_REDDIT': f'{base_url}:9999', + 'VWA_GITLAB': f'{base_url}:8023', + 'VWA_WIKIPEDIA': f'{base_url}:8888', + 'VWA_HOMEPAGE': f'{base_url}:4399', + } config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='python:3.12-bookworm', - enable_auto_lint=True, - use_host_network=False, - browsergym_eval_env=env_id, - runtime_startup_env_vars={ - 'BASE_URL': base_url, - 'OPENAI_API_KEY': openai_api_key, - 'OPENAI_BASE_URL': openai_base_url, - 'VWA_CLASSIFIEDS': f'{base_url}:9980', - 'VWA_CLASSIFIEDS_RESET_TOKEN': '4b61655535e7ed388f0d40a93600254c', - 'VWA_SHOPPING': f'{base_url}:7770', - 'VWA_SHOPPING_ADMIN': f'{base_url}:7780/admin', - 'VWA_REDDIT': f'{base_url}:9999', - 'VWA_GITLAB': f'{base_url}:8023', - 'VWA_WIKIPEDIA': f'{base_url}:8888', - 'VWA_HOMEPAGE': f'{base_url}:4399', - }, - timeout=300, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/benchmarks/webarena/run_infer.py b/evaluation/benchmarks/webarena/run_infer.py index ad846190d8fe..a9b251b90ae5 100644 --- a/evaluation/benchmarks/webarena/run_infer.py +++ b/evaluation/benchmarks/webarena/run_infer.py @@ -11,6 +11,7 @@ EvalMetadata, EvalOutput, compatibility_for_eval_history_pairs, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -19,7 +20,6 @@ from openhands.controller.state.state import State from openhands.core.config import ( AppConfig, - SandboxConfig, get_llm_config_arg, parse_arguments, ) @@ -50,29 +50,26 @@ def get_config( assert base_url is not None, 'WEBARENA_BASE_URL must be set' assert openai_api_key is not None, 'OPENAI_API_KEY must be set' + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.base_container_image = 'python:3.12-bookworm' + sandbox_config.browsergym_eval_env = env_id + sandbox_config.runtime_startup_env_vars = { + 'BASE_URL': base_url, + 'OPENAI_API_KEY': openai_api_key, + 'SHOPPING': f'{base_url}:7770/', + 'SHOPPING_ADMIN': f'{base_url}:7780/admin', + 'REDDIT': f'{base_url}:9999', + 'GITLAB': f'{base_url}:8023', + 'WIKIPEDIA': f'{base_url}:8888/wikipedia_en_all_maxi_2022-05/A/User:The_other_Kiwix_guy/Landing', + 'MAP': f'{base_url}:3000', + 'HOMEPAGE': f'{base_url}:4399', + } config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime='docker', max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - base_container_image='python:3.12-bookworm', - enable_auto_lint=True, - use_host_network=False, - browsergym_eval_env=env_id, - runtime_startup_env_vars={ - 'BASE_URL': base_url, - 'OPENAI_API_KEY': openai_api_key, - 'SHOPPING': f'{base_url}:7770/', - 'SHOPPING_ADMIN': f'{base_url}:7780/admin', - 'REDDIT': f'{base_url}:9999', - 'GITLAB': f'{base_url}:8023', - 'WIKIPEDIA': f'{base_url}:8888/wikipedia_en_all_maxi_2022-05/A/User:The_other_Kiwix_guy/Landing', - 'MAP': f'{base_url}:3000', - 'HOMEPAGE': f'{base_url}:4399', - }, - remote_runtime_enable_retries=True, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/integration_tests/run_infer.py b/evaluation/integration_tests/run_infer.py index f240d2e2333d..d215b0599bf0 100644 --- a/evaluation/integration_tests/run_infer.py +++ b/evaluation/integration_tests/run_infer.py @@ -8,6 +8,7 @@ from evaluation.utils.shared import ( EvalMetadata, EvalOutput, + get_default_sandbox_config_for_eval, make_metadata, prepare_dataset, reset_logger_for_multiprocessing, @@ -21,7 +22,6 @@ from openhands.core.config import ( AgentConfig, AppConfig, - SandboxConfig, get_llm_config_arg, parse_arguments, ) @@ -43,23 +43,14 @@ def get_config( metadata: EvalMetadata, instance_id: str, ) -> AppConfig: + sandbox_config = get_default_sandbox_config_for_eval() + sandbox_config.platform = 'linux/amd64' config = AppConfig( default_agent=metadata.agent_class, run_as_openhands=False, runtime=os.environ.get('RUNTIME', 'docker'), max_iterations=metadata.max_iterations, - sandbox=SandboxConfig( - # use default base_container_image - enable_auto_lint=True, - use_host_network=False, - timeout=300, - # Add platform to the sandbox config to solve issue 4401 - platform='linux/amd64', - api_key=os.environ.get('ALLHANDS_API_KEY', None), - remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'), - keep_runtime_alive=False, - remote_runtime_init_timeout=3600, - ), + sandbox=sandbox_config, # do not mount workspace workspace_base=None, workspace_mount_path=None, diff --git a/evaluation/utils/shared.py b/evaluation/utils/shared.py index 7035d56e41ef..566fbbd71bb3 100644 --- a/evaluation/utils/shared.py +++ b/evaluation/utils/shared.py @@ -16,7 +16,7 @@ from tqdm import tqdm from openhands.controller.state.state import State -from openhands.core.config import LLMConfig +from openhands.core.config import LLMConfig, SandboxConfig from openhands.core.config.agent_config import AgentConfig from openhands.core.config.condenser_config import ( CondenserConfig, @@ -555,3 +555,18 @@ def get_metrics(state: State) -> dict[str, Any]: metrics = state.metrics.get() if state.metrics else {} metrics['condenser'] = get_condensation_metadata(state) return metrics + + +def get_default_sandbox_config_for_eval() -> SandboxConfig: + return SandboxConfig( + use_host_network=False, + # large enough timeout, since some testcases take very long to run + timeout=300, + api_key=os.environ.get('ALLHANDS_API_KEY', None), + remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'), + keep_runtime_alive=False, + remote_runtime_init_timeout=3600, + remote_runtime_api_timeout=120, + remote_runtime_enable_retries=True, + remote_runtime_class='sysbox', + ) diff --git a/frontend/__tests__/components/features/payment/payment-form.test.tsx b/frontend/__tests__/components/features/payment/payment-form.test.tsx new file mode 100644 index 000000000000..815d0530d103 --- /dev/null +++ b/frontend/__tests__/components/features/payment/payment-form.test.tsx @@ -0,0 +1,166 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest"; +import OpenHands from "#/api/open-hands"; +import { PaymentForm } from "#/components/features/payment/payment-form"; + +describe("PaymentForm", () => { + const getBalanceSpy = vi.spyOn(OpenHands, "getBalance"); + const createCheckoutSessionSpy = vi.spyOn(OpenHands, "createCheckoutSession"); + const getConfigSpy = vi.spyOn(OpenHands, "getConfig"); + + const renderPaymentForm = () => + render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + beforeEach(() => { + // useBalance hook will return the balance only if the APP_MODE is "saas" + getConfigSpy.mockResolvedValue({ + APP_MODE: "saas", + GITHUB_CLIENT_ID: "123", + POSTHOG_CLIENT_KEY: "456", + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should render the users current balance", async () => { + getBalanceSpy.mockResolvedValue("100.50"); + renderPaymentForm(); + + await waitFor(() => { + const balance = screen.getByTestId("user-balance"); + expect(balance).toHaveTextContent("$100.50"); + }); + }); + + it("should render the users current balance to two decimal places", async () => { + getBalanceSpy.mockResolvedValue("100"); + renderPaymentForm(); + + await waitFor(() => { + const balance = screen.getByTestId("user-balance"); + expect(balance).toHaveTextContent("$100.00"); + }); + }); + + test("the user can top-up a specific amount", async () => { + const user = userEvent.setup(); + renderPaymentForm(); + + const topUpInput = await screen.findByTestId("top-up-input"); + await user.type(topUpInput, "50.12"); + + const topUpButton = screen.getByText("Add credit"); + await user.click(topUpButton); + + expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.12); + }); + + it("should round the top-up amount to two decimal places", async () => { + const user = userEvent.setup(); + renderPaymentForm(); + + const topUpInput = await screen.findByTestId("top-up-input"); + await user.type(topUpInput, "50.125456"); + + const topUpButton = screen.getByText("Add credit"); + await user.click(topUpButton); + + expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.13); + }); + + it("should render the payment method link", async () => { + renderPaymentForm(); + + screen.getByTestId("payment-methods-link"); + }); + + it("should disable the top-up button if the user enters an invalid amount", async () => { + const user = userEvent.setup(); + renderPaymentForm(); + + const topUpButton = screen.getByText("Add credit"); + expect(topUpButton).toBeDisabled(); + + const topUpInput = await screen.findByTestId("top-up-input"); + await user.type(topUpInput, " "); + + expect(topUpButton).toBeDisabled(); + }); + + it("should disable the top-up button after submission", async () => { + const user = userEvent.setup(); + renderPaymentForm(); + + const topUpInput = await screen.findByTestId("top-up-input"); + await user.type(topUpInput, "50.12"); + + const topUpButton = screen.getByText("Add credit"); + await user.click(topUpButton); + + expect(topUpButton).toBeDisabled(); + }); + + describe("prevent submission if", () => { + test("user enters a negative amount", async () => { + const user = userEvent.setup(); + renderPaymentForm(); + + const topUpInput = await screen.findByTestId("top-up-input"); + await user.type(topUpInput, "-50.12"); + + const topUpButton = screen.getByText("Add credit"); + await user.click(topUpButton); + + expect(createCheckoutSessionSpy).not.toHaveBeenCalled(); + }); + + test("user enters an empty string", async () => { + const user = userEvent.setup(); + renderPaymentForm(); + + const topUpInput = await screen.findByTestId("top-up-input"); + await user.type(topUpInput, " "); + + const topUpButton = screen.getByText("Add credit"); + await user.click(topUpButton); + + expect(createCheckoutSessionSpy).not.toHaveBeenCalled(); + }); + + test("user enters a non-numeric value", async () => { + const user = userEvent.setup(); + renderPaymentForm(); + + const topUpInput = await screen.findByTestId("top-up-input"); + await user.type(topUpInput, "abc"); + + const topUpButton = screen.getByText("Add credit"); + await user.click(topUpButton); + + expect(createCheckoutSessionSpy).not.toHaveBeenCalled(); + }); + + test("user enters less than the minimum amount", async () => { + const user = userEvent.setup(); + renderPaymentForm(); + + const topUpInput = await screen.findByTestId("top-up-input"); + await user.type(topUpInput, "20"); // test assumes the minimum is 25 + + const topUpButton = screen.getByText("Add credit"); + await user.click(topUpButton); + + expect(createCheckoutSessionSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/frontend/__tests__/components/settings/settings-input.test.tsx b/frontend/__tests__/components/settings/settings-input.test.tsx index 6009a2409e83..c2c6f650679d 100644 --- a/frontend/__tests__/components/settings/settings-input.test.tsx +++ b/frontend/__tests__/components/settings/settings-input.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; +import userEvent from "@testing-library/user-event"; import { SettingsInput } from "#/components/features/settings/settings-input"; describe("SettingsInput", () => { @@ -85,4 +86,24 @@ describe("SettingsInput", () => { expect(screen.getByText("Start Content")).toBeInTheDocument(); }); + + it("should call onChange with the input value", async () => { + const onChangeMock = vi.fn(); + const user = userEvent.setup(); + + render( + , + ); + + const input = screen.getByTestId("test-input"); + await user.type(input, "Test"); + + expect(onChangeMock).toHaveBeenCalledTimes(4); + expect(onChangeMock).toHaveBeenNthCalledWith(4, "Test"); + }); }); diff --git a/frontend/__tests__/routes/home.test.tsx b/frontend/__tests__/routes/home.test.tsx index ec7a24761f60..64232673355e 100644 --- a/frontend/__tests__/routes/home.test.tsx +++ b/frontend/__tests__/routes/home.test.tsx @@ -39,12 +39,12 @@ describe("Home Screen", () => { Component: Home, path: "/", }, + { + Component: SettingsScreen, + path: "/settings", + }, ], }, - { - Component: SettingsScreen, - path: "/settings", - }, ]); afterEach(() => { @@ -96,6 +96,9 @@ describe("Home Screen", () => { const user = userEvent.setup(); renderWithProviders(); + const settingsScreen = screen.queryByTestId("settings-screen"); + expect(settingsScreen).not.toBeInTheDocument(); + const settingsModal = await screen.findByTestId("ai-config-modal"); expect(settingsModal).toBeInTheDocument(); @@ -104,11 +107,11 @@ describe("Home Screen", () => { ); await user.click(advancedSettingsButton); + const settingsScreenAfter = await screen.findByTestId("settings-screen"); + expect(settingsScreenAfter).toBeInTheDocument(); + const settingsModalAfter = screen.queryByTestId("ai-config-modal"); expect(settingsModalAfter).not.toBeInTheDocument(); - - const settingsScreen = await screen.findByTestId("settings-screen"); - expect(settingsScreen).toBeInTheDocument(); }); }); }); diff --git a/frontend/__tests__/routes/settings-with-payment.test.tsx b/frontend/__tests__/routes/settings-with-payment.test.tsx new file mode 100644 index 000000000000..69f9b8c364b1 --- /dev/null +++ b/frontend/__tests__/routes/settings-with-payment.test.tsx @@ -0,0 +1,83 @@ +import { screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createRoutesStub } from "react-router"; +import { renderWithProviders } from "test-utils"; +import OpenHands from "#/api/open-hands"; +import SettingsScreen from "#/routes/settings"; +import { PaymentForm } from "#/components/features/payment/payment-form"; +import * as FeatureFlags from "#/utils/feature-flags"; + +describe("Settings Billing", () => { + const getConfigSpy = vi.spyOn(OpenHands, "getConfig"); + vi.spyOn(FeatureFlags, "BILLING_SETTINGS").mockReturnValue(true); + + const RoutesStub = createRoutesStub([ + { + Component: SettingsScreen, + path: "/settings", + children: [ + { + Component: () => , + path: "/settings/billing", + }, + ], + }, + ]); + + const renderSettingsScreen = () => + renderWithProviders(); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should not render the navbar if OSS mode", async () => { + getConfigSpy.mockResolvedValue({ + APP_MODE: "oss", + GITHUB_CLIENT_ID: "123", + POSTHOG_CLIENT_KEY: "456", + }); + + renderSettingsScreen(); + + await waitFor(() => { + const navbar = screen.queryByTestId("settings-navbar"); + expect(navbar).not.toBeInTheDocument(); + }); + }); + + it("should render the navbar if SaaS mode", async () => { + getConfigSpy.mockResolvedValue({ + APP_MODE: "saas", + GITHUB_CLIENT_ID: "123", + POSTHOG_CLIENT_KEY: "456", + }); + + renderSettingsScreen(); + + await waitFor(() => { + const navbar = screen.getByTestId("settings-navbar"); + within(navbar).getByText("Account"); + within(navbar).getByText("Credits"); + }); + }); + + it("should render the billing settings if clicking the credits item", async () => { + const user = userEvent.setup(); + getConfigSpy.mockResolvedValue({ + APP_MODE: "saas", + GITHUB_CLIENT_ID: "123", + POSTHOG_CLIENT_KEY: "456", + }); + + renderSettingsScreen(); + + const navbar = await screen.findByTestId("settings-navbar"); + const credits = within(navbar).getByText("Credits"); + await user.click(credits); + + const billingSection = await screen.findByTestId("billing-settings"); + within(billingSection).getByText("Manage Credits"); + }); +}); diff --git a/frontend/__tests__/routes/settings.test.tsx b/frontend/__tests__/routes/settings.test.tsx index 2052d6eb9cbe..f307e4fb4b17 100644 --- a/frontend/__tests__/routes/settings.test.tsx +++ b/frontend/__tests__/routes/settings.test.tsx @@ -10,6 +10,7 @@ import * as AdvancedSettingsUtlls from "#/utils/has-advanced-settings-set"; import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers"; import { PostApiSettings } from "#/types/settings"; import * as ConsentHandlers from "#/utils/handle-capture-consent"; +import AccountSettings from "#/routes/account-settings"; const toggleAdvancedSettings = async (user: UserEvent) => { const advancedSwitch = await screen.findByTestId("advanced-settings-switch"); @@ -36,6 +37,7 @@ describe("Settings Screen", () => { { Component: SettingsScreen, path: "/settings", + children: [{ Component: AccountSettings, path: "/settings" }], }, ]); @@ -76,7 +78,8 @@ describe("Settings Screen", () => { }); }); - it("should render an indicator if the GitHub token is not set", async () => { + // TODO: Set a better unset indicator + it.skip("should render an indicator if the GitHub token is not set", async () => { getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, github_token_is_set: false, @@ -97,6 +100,20 @@ describe("Settings Screen", () => { }); }); + it("should set asterik placeholder if the GitHub token is set", async () => { + getSettingsSpy.mockResolvedValue({ + ...MOCK_DEFAULT_USER_SETTINGS, + github_token_is_set: true, + }); + + renderSettingsScreen(); + + await waitFor(() => { + const input = screen.getByTestId("github-token-input"); + expect(input).toHaveProperty("placeholder", "**********"); + }); + }); + it("should render an indicator if the GitHub token is set", async () => { getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, @@ -314,7 +331,8 @@ describe("Settings Screen", () => { // screen.getByTestId("security-analyzer-input"); }); - it("should render an indicator if the LLM API key is not set", async () => { + // TODO: Set a better unset indicator + it.skip("should render an indicator if the LLM API key is not set", async () => { getSettingsSpy.mockResolvedValueOnce({ ...MOCK_DEFAULT_USER_SETTINGS, llm_api_key: null, @@ -443,7 +461,22 @@ describe("Settings Screen", () => { expect(input).toHaveValue("1x (2 core, 8G)"); }); - it("should save the runtime settings when the 'Save Changes' button is clicked", async () => { + it("should always have the runtime input disabled", async () => { + getConfigSpy.mockResolvedValue({ + APP_MODE: "saas", + GITHUB_CLIENT_ID: "123", + POSTHOG_CLIENT_KEY: "456", + }); + + renderSettingsScreen(); + + await toggleAdvancedSettings(userEvent.setup()); + + const input = await screen.findByTestId("runtime-settings-input"); + expect(input).toBeDisabled(); + }); + + it.skip("should save the runtime settings when the 'Save Changes' button is clicked", async () => { const user = userEvent.setup(); getConfigSpy.mockResolvedValue({ APP_MODE: "saas", @@ -665,7 +698,7 @@ describe("Settings Screen", () => { expect(saveSettingsSpy).toHaveBeenCalledWith( expect.objectContaining({ - llm_api_key: undefined, + llm_api_key: "", // empty because it's not set previously github_token: undefined, language: "no", }), @@ -704,7 +737,7 @@ describe("Settings Screen", () => { expect(saveSettingsSpy).toHaveBeenCalledWith( expect.objectContaining({ github_token: undefined, - llm_api_key: undefined, + llm_api_key: "", // empty because it's not set previously llm_model: "openai/gpt-4o", }), ); @@ -869,5 +902,55 @@ describe("Settings Screen", () => { }), ); }); + + it("should send an empty LLM API Key if the user submits an empty string", async () => { + const user = userEvent.setup(); + renderSettingsScreen(); + + const input = await screen.findByTestId("llm-api-key-input"); + expect(input).toHaveValue(""); + + const saveButton = screen.getByText("Save Changes"); + await user.click(saveButton); + + expect(saveSettingsSpy).toHaveBeenCalledWith( + expect.objectContaining({ llm_api_key: "" }), + ); + }); + + it("should not send an empty LLM API Key if the user submits an empty string but already has it set", async () => { + const user = userEvent.setup(); + getSettingsSpy.mockResolvedValue({ + ...MOCK_DEFAULT_USER_SETTINGS, + llm_api_key: "**********", + }); + + renderSettingsScreen(); + + const input = await screen.findByTestId("llm-api-key-input"); + expect(input).toHaveValue(""); + + const saveButton = screen.getByText("Save Changes"); + await user.click(saveButton); + + expect(saveSettingsSpy).toHaveBeenCalledWith( + expect.objectContaining({ llm_api_key: undefined }), + ); + }); + + it("should submit the LLM API Key if it is the first time the user sets it", async () => { + const user = userEvent.setup(); + renderSettingsScreen(); + + const input = await screen.findByTestId("llm-api-key-input"); + await user.type(input, "new-api-key"); + + const saveButton = screen.getByText("Save Changes"); + await user.click(saveButton); + + expect(saveSettingsSpy).toHaveBeenCalledWith( + expect.objectContaining({ llm_api_key: "new-api-key" }), + ); + }); }); }); diff --git a/frontend/__tests__/utils/amount-is-valid.test.ts b/frontend/__tests__/utils/amount-is-valid.test.ts new file mode 100644 index 000000000000..30181e1b48c9 --- /dev/null +++ b/frontend/__tests__/utils/amount-is-valid.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "vitest"; +import { amountIsValid } from "#/utils/amount-is-valid"; + +describe("amountIsValid", () => { + describe("fails", () => { + test("when the amount is negative", () => { + expect(amountIsValid("-5")).toBe(false); + expect(amountIsValid("-25")).toBe(false); + }); + + test("when the amount is zero", () => { + expect(amountIsValid("0")).toBe(false); + }); + + test("when an empty string is passed", () => { + expect(amountIsValid("")).toBe(false); + expect(amountIsValid(" ")).toBe(false); + }); + + test("when a non-numeric value is passed", () => { + expect(amountIsValid("abc")).toBe(false); + expect(amountIsValid("1abc")).toBe(false); + expect(amountIsValid("abc1")).toBe(false); + }); + + test("when an amount less than the minimum is passed", () => { + // test assumes the minimum is 25 + expect(amountIsValid("24")).toBe(false); + expect(amountIsValid("24.99")).toBe(false); + }); + }); +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a261adf741e7..12eb28118046 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "openhands-frontend", - "version": "0.24.0", + "version": "0.25.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openhands-frontend", - "version": "0.24.0", + "version": "0.25.0", "dependencies": { "@heroui/react": "2.6.14", "@monaco-editor/react": "^4.7.0-rc.0", @@ -14,26 +14,28 @@ "@react-router/serve": "^7.1.5", "@react-types/shared": "^3.27.0", "@reduxjs/toolkit": "^2.5.1", - "@tanstack/react-query": "^5.66.0", + "@stripe/react-stripe-js": "^3.1.1", + "@stripe/stripe-js": "^5.5.0", + "@tanstack/react-query": "^5.66.7", "@vitejs/plugin-react": "^4.3.2", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.4.0", "axios": "^1.7.9", "clsx": "^2.1.1", "eslint-config-airbnb-typescript": "^18.0.0", - "framer-motion": "^12.4.2", + "framer-motion": "^12.4.4", "i18next": "^24.2.2", "i18next-browser-languagedetector": "^8.0.3", "i18next-http-backend": "^3.0.2", "isbot": "^5.1.22", - "jose": "^5.9.4", + "jose": "^5.10.0", "monaco-editor": "^0.52.2", - "posthog-js": "^1.217.6", + "posthog-js": "^1.219.3", "react": "^19.0.0", "react-dom": "^19.0.0", "react-highlight": "^0.15.0", "react-hot-toast": "^2.5.1", - "react-i18next": "^15.4.0", + "react-i18next": "^15.4.1", "react-icons": "^5.4.0", "react-markdown": "^9.0.3", "react-redux": "^9.2.0", @@ -41,7 +43,7 @@ "react-syntax-highlighter": "^15.6.1", "react-textarea-autosize": "^8.5.7", "remark-gfm": "^4.0.1", - "sirv-cli": "^3.0.0", + "sirv-cli": "^3.0.1", "socket.io-client": "^4.8.1", "tailwind-merge": "^3.0.1", "vite": "^6.1.0", @@ -66,7 +68,7 @@ "@types/ws": "^8.5.14", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", - "@vitest/coverage-v8": "^3.0.5", + "@vitest/coverage-v8": "^3.0.6", "autoprefixer": "^10.4.20", "cross-env": "^7.0.3", "eslint": "^8.57.0", @@ -84,6 +86,7 @@ "msw": "^2.6.6", "postcss": "^8.5.2", "prettier": "^3.5.1", + "stripe": "^17.5.0", "tailwindcss": "^3.4.17", "typescript": "^5.7.3", "vite-plugin-svgr": "^4.2.0", @@ -5871,6 +5874,29 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@stripe/react-stripe-js": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.1.1.tgz", + "integrity": "sha512-+JzYFgUivVD7koqYV7LmLlt9edDMAwKH7XhZAHFQMo7NeRC+6D2JmQGzp9tygWerzwttwFLlExGp4rAOvD6l9g==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": "^1.44.1 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-5.6.0.tgz", + "integrity": "sha512-w8CEY73X/7tw2KKlL3iOk679V9bWseE4GzNz3zlaYxcTjmcmWOathRb0emgo/QQ3eoNzmq68+2Y2gxluAv3xGw==", + "license": "MIT", + "engines": { + "node": ">=12.16" + } + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -6139,9 +6165,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.66.3", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.3.tgz", - "integrity": "sha512-+2iDxH7UFdtwcry766aJszGmbByQDIzTltJ3oQAZF9bhCxHCIN3yDwHa6qDCZxcpMGvUphCRx/RYJvLbM8mucQ==", + "version": "5.66.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.4.tgz", + "integrity": "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA==", "license": "MIT", "funding": { "type": "github", @@ -6149,12 +6175,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.66.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.3.tgz", - "integrity": "sha512-sWMvxZ5VugPDgD1CzP7f0s9yFvjcXP3FXO5IVV2ndXlYqUCwykU8U69Kk05Qn5UvGRqB/gtj4J7vcTC6vtLHtQ==", + "version": "5.66.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.7.tgz", + "integrity": "sha512-qd3q/tUpF2K1xItfPZddk1k/8pSXnovg41XyCqJgPoyYEirMBtB0sVEVVQ/CsAOngzgWtBPXimVf4q4kM9uO6A==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.66.3" + "@tanstack/query-core": "5.66.4" }, "funding": { "type": "github", @@ -6703,16 +6729,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz", - "integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.1.tgz", + "integrity": "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0" + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/typescript-estree": "8.24.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6727,14 +6753,14 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz", - "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.1.tgz", + "integrity": "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0" + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6745,9 +6771,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz", - "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", + "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", "dev": true, "license": "MIT", "engines": { @@ -6759,14 +6785,14 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz", - "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz", + "integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6786,13 +6812,13 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz", - "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", + "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -6873,9 +6899,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.5.tgz", - "integrity": "sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.6.tgz", + "integrity": "sha512-JRTlR8Bw+4BcmVTICa7tJsxqphAktakiLsAmibVLAWbu1lauFddY/tXeM6sAyl1cgkPuXtpnUgaCPhTdz1Qapg==", "dev": true, "license": "MIT", "dependencies": { @@ -6896,8 +6922,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.0.5", - "vitest": "3.0.5" + "@vitest/browser": "3.0.6", + "vitest": "3.0.6" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -6906,15 +6932,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz", - "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.6.tgz", + "integrity": "sha512-zBduHf/ja7/QRX4HdP1DSq5XrPgdN+jzLOwaTq/0qZjYfgETNFCKf9nOAp2j3hmom3oTbczuUzrzg9Hafh7hNg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", + "@vitest/spy": "3.0.6", + "@vitest/utils": "3.0.6", + "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, "funding": { @@ -6922,13 +6948,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz", - "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.6.tgz", + "integrity": "sha512-KPztr4/tn7qDGZfqlSPQoF2VgJcKxnDNhmfR3VgZ6Fy1bO8T9Fc1stUiTXtqz0yG24VpD00pZP5f8EOFknjNuQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.5", + "@vitest/spy": "3.0.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -6959,9 +6985,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz", - "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.6.tgz", + "integrity": "sha512-Zyctv3dbNL+67qtHfRnUE/k8qxduOamRfAL1BurEIQSyOEFffoMvx2pnDSSbKAAVxY0Ej2J/GH2dQKI0W2JyVg==", "dev": true, "license": "MIT", "dependencies": { @@ -6972,14 +6998,14 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz", - "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.6.tgz", + "integrity": "sha512-JopP4m/jGoaG1+CBqubV/5VMbi7L+NQCJTu1J1Pf6YaUbk7bZtaq5CX7p+8sY64Sjn1UQ1XJparHfcvTTdu9cA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.5", - "pathe": "^2.0.2" + "@vitest/utils": "3.0.6", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" @@ -6993,15 +7019,15 @@ "license": "MIT" }, "node_modules/@vitest/snapshot": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz", - "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.6.tgz", + "integrity": "sha512-qKSmxNQwT60kNwwJHMVwavvZsMGXWmngD023OHSgn873pV0lylK7dwBTfYP7e4URy5NiBCHHiQGA9DHkYkqRqg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.5", + "@vitest/pretty-format": "3.0.6", "magic-string": "^0.30.17", - "pathe": "^2.0.2" + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" @@ -7015,9 +7041,9 @@ "license": "MIT" }, "node_modules/@vitest/spy": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz", - "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.6.tgz", + "integrity": "sha512-HfOGx/bXtjy24fDlTOpgiAEJbRfFxoX3zIGagCqACkFKKZ/TTOE6gYMKXlqecvxEndKFuNHcHqP081ggZ2yM0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7028,14 +7054,14 @@ } }, "node_modules/@vitest/utils": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz", - "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.6.tgz", + "integrity": "sha512-18ktZpf4GQFTbf9jK543uspU03Q2qya7ZGya5yiZ0Gx0nnnalBvd5ZBislbl2EhLjM8A8rt4OilqKG7QwcGkvQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.5", - "loupe": "^3.1.2", + "@vitest/pretty-format": "3.0.6", + "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, "funding": { @@ -8746,9 +8772,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.101", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.101.tgz", - "integrity": "sha512-L0ISiQrP/56Acgu4/i/kfPwWSgrzYZUnQrC0+QPFuhqlLP1Ir7qzPPDVS9BcKIyWTRU8+o6CC8dKw38tSWhYIA==", + "version": "1.5.102", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz", + "integrity": "sha512-eHhqaja8tE/FNpIiBrvBjFV/SSKpyWHLvxuR9dPTdo+3V9ppdLmFB7ZZQ98qNovcngPLYIz0oOBF9P0FfZef5Q==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -10093,9 +10119,9 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, @@ -10198,12 +10224,12 @@ } }, "node_modules/framer-motion": { - "version": "12.4.3", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.3.tgz", - "integrity": "sha512-rsMeO7w3dKyNG09o3cGwSH49iHU+VgDmfSSfsX+wfkO3zDA6WWkh4sUsMXd155YROjZP+7FTIhDrBYfgZeHjKQ==", + "version": "12.4.4", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.4.tgz", + "integrity": "sha512-JWkVwbJBgVkeZHNcnk8ififgwTF+5de9wbJnTLI+g9YqaGo75Xd5uRVDm9FR8chqRDOKcXv/71f40CGescYVmg==", "license": "MIT", "dependencies": { - "motion-dom": "^12.0.0", + "motion-dom": "^12.4.4", "motion-utils": "^12.0.0", "tslib": "^2.4.0" }, @@ -11752,9 +11778,9 @@ } }, "node_modules/jose": { - "version": "5.9.6", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", - "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -12324,7 +12350,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -13503,9 +13528,9 @@ } }, "node_modules/motion-dom": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.0.0.tgz", - "integrity": "sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==", + "version": "12.4.4", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.4.4.tgz", + "integrity": "sha512-D8Kjp8oqUNqxoAVmIlOH+YCMov/4koBAmG4OJs0VWfh18xkQEIsx9+S7yrXyx0XaMBEPtre6e9LiSW2Zs7vIhA==", "license": "MIT", "dependencies": { "motion-utils": "^12.0.0" @@ -13527,9 +13552,9 @@ } }, "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "license": "MIT", "engines": { "node": ">=10" @@ -14534,9 +14559,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.219.0", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.219.0.tgz", - "integrity": "sha512-RnjtcjI4UYTBsjfF4Fs1lICWmGjiqMU9H0fN2ab1BEcDOFL/2m9Fx/1viCxvMiQR8cmgWWpkipJXD0gY7czDOA==", + "version": "1.219.3", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.219.3.tgz", + "integrity": "sha512-oKN4no9RRAptZ86R/MvMjsxQnFAe97rwU2emmTzf/q9ng+7V4nU+APM0ItzrESFtRYx1X8kKtxDUlkujNhfMvw==", "license": "MIT", "dependencies": { "core-js": "^3.38.1", @@ -14552,9 +14577,9 @@ "license": "Apache-2.0" }, "node_modules/preact": { - "version": "10.26.0", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.0.tgz", - "integrity": "sha512-6ugi/Mb7lyV5RA6KlnijFyDLMU253i7L0RRiObIzDoqj59KT9iTeNJbA/YGw6M7jP4vxaab0DOA8DgodTOA6EQ==", + "version": "10.26.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.2.tgz", + "integrity": "sha512-0gNmv4qpS9HaN3+40CLBAnKe0ZfyE4ZWo5xKlC1rVrr0ckkEvJvAQqKaHANdFKsGstoxrY4AItZ7kZSGVoVjgg==", "license": "MIT", "funding": { "type": "opencollective", @@ -14679,7 +14704,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -14691,7 +14715,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/property-information": { @@ -14883,9 +14906,9 @@ } }, "node_modules/react-i18next": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.0.tgz", - "integrity": "sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.1.tgz", + "integrity": "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.0", @@ -15999,9 +16022,9 @@ } }, "node_modules/sirv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", - "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", "license": "MIT", "dependencies": { "@polka/url": "^1.0.0-next.24", @@ -16013,9 +16036,9 @@ } }, "node_modules/sirv-cli": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv-cli/-/sirv-cli-3.0.0.tgz", - "integrity": "sha512-p88yHl8DmTOUJroRiW2o9ezJc/YRLxphBydX2NGQc3naKBA09B3EM4Q/yaN8FYF0e50fRSZP7dyatr72b1u5Jw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv-cli/-/sirv-cli-3.0.1.tgz", + "integrity": "sha512-ICXaF2u6IQhLZ0EXF6nqUF4YODfSQSt+mGykt4qqO5rY+oIiwdg7B8w2PVDBJlQulaS2a3J8666CUoDoAuCGvg==", "license": "MIT", "dependencies": { "console-clear": "^1.1.0", @@ -16572,6 +16595,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-17.6.0.tgz", + "integrity": "sha512-+HB6+SManp0gSRB0dlPmXO+io18krlAe0uimXhhIkL/RG/VIRigkfoM3QDJPkqbuSW0XsA6uzsivNCJU1ELEDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/style-to-object": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", @@ -17692,31 +17729,31 @@ } }, "node_modules/vitest": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz", - "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.6.tgz", + "integrity": "sha512-/iL1Sc5VeDZKPDe58oGK4HUFLhw6b5XdY1MYawjuSaDA4sEfYlY9HnS6aCEG26fX+MgUi7MwlduTBHHAI/OvMA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.5", - "@vitest/mocker": "3.0.5", - "@vitest/pretty-format": "^3.0.5", - "@vitest/runner": "3.0.5", - "@vitest/snapshot": "3.0.5", - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", + "@vitest/expect": "3.0.6", + "@vitest/mocker": "3.0.6", + "@vitest/pretty-format": "^3.0.6", + "@vitest/runner": "3.0.6", + "@vitest/snapshot": "3.0.6", + "@vitest/spy": "3.0.6", + "@vitest/utils": "3.0.6", + "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", - "pathe": "^2.0.2", + "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.5", + "vite-node": "3.0.6", "why-is-node-running": "^2.3.0" }, "bin": { @@ -17732,8 +17769,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.5", - "@vitest/ui": "3.0.5", + "@vitest/browser": "3.0.6", + "@vitest/ui": "3.0.6", "happy-dom": "*", "jsdom": "*" }, @@ -17769,16 +17806,16 @@ "license": "MIT" }, "node_modules/vitest/node_modules/vite-node": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz", - "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.6.tgz", + "integrity": "sha512-s51RzrTkXKJrhNbUzQRsarjmAae7VmMPAsRT7lppVpIg6mK3zGthP9Hgz0YQQKuNcF+Ii7DfYk3Fxz40jRmePw==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", - "pathe": "^2.0.2", + "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { diff --git a/frontend/package.json b/frontend/package.json index 6049777c09df..4fc89d93629d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "openhands-frontend", - "version": "0.24.0", + "version": "0.25.0", "private": true, "type": "module", "engines": { @@ -13,26 +13,28 @@ "@react-router/serve": "^7.1.5", "@react-types/shared": "^3.27.0", "@reduxjs/toolkit": "^2.5.1", - "@tanstack/react-query": "^5.66.0", + "@stripe/react-stripe-js": "^3.1.1", + "@stripe/stripe-js": "^5.5.0", + "@tanstack/react-query": "^5.66.7", "@vitejs/plugin-react": "^4.3.2", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.4.0", "axios": "^1.7.9", "clsx": "^2.1.1", "eslint-config-airbnb-typescript": "^18.0.0", - "framer-motion": "^12.4.2", + "framer-motion": "^12.4.4", "i18next": "^24.2.2", "i18next-browser-languagedetector": "^8.0.3", "i18next-http-backend": "^3.0.2", "isbot": "^5.1.22", - "jose": "^5.9.4", + "jose": "^5.10.0", "monaco-editor": "^0.52.2", - "posthog-js": "^1.217.6", + "posthog-js": "^1.219.3", "react": "^19.0.0", "react-dom": "^19.0.0", "react-highlight": "^0.15.0", "react-hot-toast": "^2.5.1", - "react-i18next": "^15.4.0", + "react-i18next": "^15.4.1", "react-icons": "^5.4.0", "react-markdown": "^9.0.3", "react-redux": "^9.2.0", @@ -40,7 +42,7 @@ "react-syntax-highlighter": "^15.6.1", "react-textarea-autosize": "^8.5.7", "remark-gfm": "^4.0.1", - "sirv-cli": "^3.0.0", + "sirv-cli": "^3.0.1", "socket.io-client": "^4.8.1", "tailwind-merge": "^3.0.1", "vite": "^6.1.0", @@ -49,7 +51,8 @@ }, "scripts": { "dev": "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 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", "build": "npm run make-i18n && npm run typecheck && react-router build", "start": "npx sirv-cli build/ --single", "test": "vitest run", @@ -93,7 +96,7 @@ "@types/ws": "^8.5.14", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", - "@vitest/coverage-v8": "^3.0.5", + "@vitest/coverage-v8": "^3.0.6", "autoprefixer": "^10.4.20", "cross-env": "^7.0.3", "eslint": "^8.57.0", @@ -111,6 +114,7 @@ "msw": "^2.6.6", "postcss": "^8.5.2", "prettier": "^3.5.1", + "stripe": "^17.5.0", "tailwindcss": "^3.4.17", "typescript": "^5.7.3", "vite-plugin-svgr": "^4.2.0", diff --git a/frontend/src/api/open-hands.ts b/frontend/src/api/open-hands.ts index e77d7a5527c2..58abf00956aa 100644 --- a/frontend/src/api/open-hands.ts +++ b/frontend/src/api/open-hands.ts @@ -274,6 +274,23 @@ class OpenHands { return data.status === 200; } + static async createCheckoutSession(amount: number): Promise { + const { data } = await openHands.post( + "/api/billing/create-checkout-session", + { + amount, + }, + ); + return data.redirect_url; + } + + static async getBalance(): Promise { + const { data } = await openHands.get<{ credits: string }>( + "/api/billing/credits", + ); + return data.credits; + } + static async getGitHubUser(): Promise { const response = await openHands.get("/api/github/user"); diff --git a/frontend/src/components/features/context-menu/account-settings-context-menu.tsx b/frontend/src/components/features/context-menu/account-settings-context-menu.tsx index 8be19387f52f..288dd7728705 100644 --- a/frontend/src/components/features/context-menu/account-settings-context-menu.tsx +++ b/frontend/src/components/features/context-menu/account-settings-context-menu.tsx @@ -22,7 +22,7 @@ export function AccountSettingsContextMenu({ {t(I18nKey.ACCOUNT_SETTINGS$LOGOUT)} diff --git a/frontend/src/components/features/payment/payment-form.tsx b/frontend/src/components/features/payment/payment-form.tsx new file mode 100644 index 000000000000..e42a9e7894d0 --- /dev/null +++ b/frontend/src/components/features/payment/payment-form.tsx @@ -0,0 +1,92 @@ +import React from "react"; +import { useCreateStripeCheckoutSession } from "#/hooks/mutation/stripe/use-create-stripe-checkout-session"; +import { useBalance } from "#/hooks/query/use-balance"; +import { cn } from "#/utils/utils"; +import MoneyIcon from "#/icons/money.svg?react"; +import { SettingsInput } from "../settings/settings-input"; +import { BrandButton } from "../settings/brand-button"; +import { HelpLink } from "../settings/help-link"; +import { LoadingSpinner } from "#/components/shared/loading-spinner"; +import { amountIsValid } from "#/utils/amount-is-valid"; + +export function PaymentForm() { + const { data: balance, isLoading } = useBalance(); + const { mutate: addBalance, isPending } = useCreateStripeCheckoutSession(); + + const [buttonIsDisabled, setButtonIsDisabled] = React.useState(true); + + const billingFormAction = async (formData: FormData) => { + const amount = formData.get("top-up-input")?.toString(); + + if (amount?.trim()) { + if (!amountIsValid(amount)) return; + + const float = parseFloat(amount); + addBalance({ amount: Number(float.toFixed(2)) }); + } + + setButtonIsDisabled(true); + }; + + const handleTopUpInputChange = (value: string) => { + setButtonIsDisabled(!amountIsValid(value)); + }; + + return ( +
+

+ Manage Credits +

+ +
+
+ + Balance +
+ {!isLoading && ( + ${Number(balance).toFixed(2)} + )} + {isLoading && } +
+ +
+ + +
+ + Add credit + + {isPending && } +
+
+ + + + ); +} diff --git a/frontend/src/components/features/settings/settings-input.tsx b/frontend/src/components/features/settings/settings-input.tsx index 5362af09aa28..5d737fd991dd 100644 --- a/frontend/src/components/features/settings/settings-input.tsx +++ b/frontend/src/components/features/settings/settings-input.tsx @@ -12,6 +12,7 @@ interface SettingsInputProps { isDisabled?: boolean; startContent?: React.ReactNode; className?: string; + onChange?: (value: string) => void; } export function SettingsInput({ @@ -25,6 +26,7 @@ export function SettingsInput({ isDisabled, startContent, className, + onChange, }: SettingsInputProps) { return (