Compare commits

..

2 Commits

Author SHA1 Message Date
Robert Brennan
4faab09658 Update frontend/src/components/features/chat/expandable-message.tsx 2025-05-12 08:24:03 -04:00
openhands
ba06da2680 Fix: Make error messages collapsed by default 2025-05-12 12:21:02 +00:00
189 changed files with 1425 additions and 4485 deletions

19
.github/.codecov.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
codecov:
notify:
wait_for_ci: true
# our project is large, so 6 builds are typically uploaded. this waits till 5/6
# See https://docs.codecov.com/docs/notifications#section-preventing-notifications-until-after-n-builds
after_n_builds: 5
coverage:
status:
patch:
default:
threshold: 100% # allow patch coverage to be lower than project coverage by any amount
project:
default:
threshold: 5% # allow project coverage to drop at most 5%
comment: false
github_checks:
annotations: false

View File

@@ -42,3 +42,7 @@ jobs:
- name: Run tests and collect coverage
working-directory: ./frontend
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -312,7 +312,11 @@ jobs:
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
RUN_AS_OPENHANDS=false \
poetry run pytest -n 7 -raRs --reruns 2 --reruns-delay 5 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
poetry run pytest -n 7 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# Run unit tests with the Docker runtime Docker images as openhands user
test_runtime_oh:
@@ -377,7 +381,11 @@ jobs:
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
RUN_AS_OPENHANDS=true \
poetry run pytest -n 7 -raRs --reruns 2 --reruns-delay 5 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
poetry run pytest -n 7 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# The two following jobs (named identically) are to check whether all the runtime tests have passed as the
# "All Runtime Tests Passed" is a required job for PRs to merge

View File

@@ -30,12 +30,11 @@ jobs:
run: |
cd frontend
npm install --frozen-lockfile
- name: Lint, TypeScript compilation, and translation checks
- name: Lint and TypeScript compilation
run: |
cd frontend
npm run lint
npm run make-i18n && tsc
npm run check-translation-completeness
# Run lint on the python code
lint-python:

View File

@@ -48,7 +48,11 @@ jobs:
- name: Build Environment
run: make build
- name: Run Tests
run: poetry run pytest --forked -n auto -svv ./tests/unit
run: poetry run pytest --forked -n auto --cov=openhands --cov-report=xml -svv ./tests/unit
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# Run specific Windows python tests
test-on-windows:

View File

@@ -9,7 +9,7 @@ on:
jobs:
trigger-job:
name: Trigger remote eval job
if: ${{ github.event.label.name == 'run-eval-1' || github.event.label.name == 'run-eval-2' || github.event.label.name == 'run-eval-50' || github.event.label.name == 'run-eval-100' }}
if: ${{ github.event.label.name == 'run-eval-xs' || github.event.label.name == 'run-eval-s' || github.event.label.name == 'run-eval-m' }}
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
@@ -26,14 +26,12 @@ jobs:
echo "Repository URL: $REPO_URL"
echo "PR Branch: $PR_BRANCH"
if [[ "${{ github.event.label.name }}" == "run-eval-1" ]]; then
if [[ "${{ github.event.label.name }}" == "run-eval-xs" ]]; then
EVAL_INSTANCES="1"
elif [[ "${{ github.event.label.name }}" == "run-eval-2" ]]; then
EVAL_INSTANCES="2"
elif [[ "${{ github.event.label.name }}" == "run-eval-50" ]]; then
EVAL_INSTANCES="50"
elif [[ "${{ github.event.label.name }}" == "run-eval-100" ]]; then
EVAL_INSTANCES="100"
elif [[ "${{ github.event.label.name }}" == "run-eval-s" ]]; then
EVAL_INSTANCES="5"
elif [[ "${{ github.event.label.name }}" == "run-eval-m" ]]; then
EVAL_INSTANCES="30"
fi
curl -X POST \

View File

@@ -1,3 +1,8 @@
---
name: repo
type: repo
agent: CodeActAgent
---
This repository contains the code for OpenHands, an automated AI software engineer. It has a Python backend
(in the `openhands` directory) and React frontend (in the `frontend` directory).

View File

@@ -1,8 +1,8 @@
# Development Guide
This guide is for people working on OpenHands and editing the source code.
If you wish to contribute your changes, check out the [CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md) on how to clone and setup the project
initially before moving on. Otherwise, you can clone the OpenHands project directly.
If you wish to contribute your changes, check out the [CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md) on how to clone and setup the project initially before moving on.
Otherwise, you can clone the OpenHands project directly.
## Start the Server for Development
@@ -21,8 +21,7 @@ Make sure you have all these dependencies installed before moving on to `make bu
#### Develop without sudo access
If you want to develop without system admin/sudo access to upgrade/install `Python` and/or `NodeJs`, you can use
`conda` or `mamba` to manage the packages for you:
If you want to develop without system admin/sudo access to upgrade/install `Python` and/or `NodeJs`, you can use `conda` or `mamba` to manage the packages for you:
```bash
# Download and install Mamba (a faster version of conda)
@@ -37,8 +36,7 @@ mamba install conda-forge::poetry
### 2. Build and Setup The Environment
Begin by building the project which includes setting up the environment and installing dependencies. This step ensures
that OpenHands is ready to run on your system:
Begin by building the project which includes setting up the environment and installing dependencies. This step ensures that OpenHands is ready to run on your system:
```bash
make build
@@ -47,6 +45,8 @@ make build
### 3. Configuring the Language Model
OpenHands supports a diverse array of Language Models (LMs) through the powerful [litellm](https://docs.litellm.ai) library.
By default, we've chosen Claude Sonnet 3.5 as our go-to model, but the world is your oyster! You can unleash the
potential of any other LM that piques your interest.
To configure the LM of your choice, run:
@@ -54,12 +54,9 @@ To configure the LM of your choice, run:
make setup-config
```
This command will prompt you to enter the LLM API key, model name, and other variables ensuring that OpenHands is
tailored to your specific needs. Note that the model name will apply only when you run headless. If you use the UI,
please set the model in the UI.
This command will prompt you to enter the LLM API key, model name, and other variables ensuring that OpenHands is tailored to your specific needs. Note that the model name will apply only when you run headless. If you use the UI, please set the model in the UI.
Note: If you have previously run OpenHands using the docker command, you may have already set some environmental
variables in your terminal. The final configurations are set from highest to lowest priority:
Note: If you have previously run OpenHands using the docker command, you may have already set some environmental variables in your terminal. The final configurations are set from highest to lowest priority:
Environment variables > config.toml variables > default variables
**Note on Alternative Models:**
@@ -77,15 +74,13 @@ make run
#### Option B: Individual Server Startup
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on
backend-related tasks or configurations.
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on backend-related tasks or configurations.
```bash
make start-backend
```
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related
components or interface enhancements.
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related components or interface enhancements.
```bash
make start-frontend
```
@@ -120,10 +115,10 @@ poetry run pytest ./tests/unit/test_*.py
### 9. Use existing Docker image
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.
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.38-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.37-nikolaik`
## Develop inside Docker container

View File

@@ -3,12 +3,17 @@ These are the procedures and guidelines on how issues are triaged in this repo b
## General
* All issues must be tagged with **enhancement**, **bug** or **troubleshooting/help**.
* Issues may be tagged with what it relates to (**agent quality**, **resolver**, **CLI**, etc.).
* Issues may be tagged with what it relates to (**agent quality**, **frontend**, **resolver**, etc.).
## Severity
* **Low**: Minor issues or affecting single user.
* **Medium**: Affecting multiple users.
* **High**: High visibility issues or affecting many users.
* **Critical**: Affecting all users or potential security issues.
## Effort
* Issues may be estimated with effort required (**small effort**, **medium effort**, **large effort**).
## Difficulty
* Issues with low implementation difficulty may be tagged with **good first issue**.

View File

@@ -51,17 +51,17 @@ system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-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.38
docker.all-hands.dev/all-hands-ai/openhands:0.37
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!

View File

@@ -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.38-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.37-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -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.38-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.37-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:

View File

@@ -37,7 +37,7 @@ Pour exécuter OpenHands en mode CLI avec Docker :
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -46,7 +46,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.38 \
docker.all-hands.dev/all-hands-ai/openhands:0.37 \
python -m openhands.core.cli
```

View File

@@ -34,7 +34,7 @@ Pour exécuter OpenHands en mode Headless avec Docker :
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -44,7 +44,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.38 \
docker.all-hands.dev/all-hands-ai/openhands:0.37 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -58,17 +58,17 @@ Un système avec un processeur moderne et un minimum de **4 Go de RAM** est reco
La façon la plus simple d'exécuter OpenHands est dans Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-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.38
docker.all-hands.dev/all-hands-ai/openhands:0.37
```
Vous trouverez OpenHands en cours d'exécution à l'adresse http://localhost:3000 !

View File

@@ -36,7 +36,7 @@ DockerでOpenHandsをCLIモードで実行するには
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-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.38 \
docker.all-hands.dev/all-hands-ai/openhands:0.37 \
python -m openhands.core.cli
```

View File

@@ -33,7 +33,7 @@ DockerでヘッドレスモードでOpenHandsを実行するには
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-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.38 \
docker.all-hands.dev/all-hands-ai/openhands:0.37 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -58,17 +58,17 @@ OpenHandsを実行するには、最新のプロセッサと最低**4GB RAM**を
OpenHandsを実行する最も簡単な方法はDockerを使用することです。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-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.38
docker.all-hands.dev/all-hands-ai/openhands:0.37
```
OpenHandsは http://localhost:3000 で実行されています!

View File

@@ -37,7 +37,7 @@ Para executar o OpenHands no modo CLI com Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -46,7 +46,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.38 \
docker.all-hands.dev/all-hands-ai/openhands:0.37 \
python -m openhands.core.cli
```

View File

@@ -34,7 +34,7 @@ Para executar o OpenHands em modo Headless com Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -44,7 +44,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.38 \
docker.all-hands.dev/all-hands-ai/openhands:0.37 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -58,17 +58,17 @@
A maneira mais fácil de executar o OpenHands é no Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-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.38
docker.all-hands.dev/all-hands-ai/openhands:0.37
```
Você encontrará o OpenHands rodando em http://localhost:3000!

View File

@@ -36,7 +36,7 @@ poetry run python -m openhands.core.cli
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-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.38 \
docker.all-hands.dev/all-hands-ai/openhands:0.37 \
python -m openhands.core.cli
```

View File

@@ -33,7 +33,7 @@ poetry run python -m openhands.core.main -t "write a bash script that prints hi"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-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.38 \
docker.all-hands.dev/all-hands-ai/openhands:0.37 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -58,17 +58,17 @@
运行 OpenHands 最简单的方法是使用 Docker。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-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.38
docker.all-hands.dev/all-hands-ai/openhands:0.37
```
OpenHands 将在 http://localhost:3000 运行!

View File

@@ -31,7 +31,7 @@ This command opens an interactive prompt where you can type tasks or commands an
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -40,7 +40,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.38 \
docker.all-hands.dev/all-hands-ai/openhands:0.37 \
python -m openhands.cli.main
```

View File

@@ -31,7 +31,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.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -41,7 +41,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.38 \
docker.all-hands.dev/all-hands-ai/openhands:0.37 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -1,181 +0,0 @@
---
sidebar_position: 9
---
# Connecting to the WebSocket
This guide explains how to connect to the OpenHands WebSocket API to receive real-time events and send actions to the agent.
## Overview
OpenHands uses [Socket.IO](https://socket.io/) for WebSocket communication between the client and server. The WebSocket connection allows you to:
1. Receive real-time events from the agent
2. Send user actions to the agent
3. Maintain a persistent connection for ongoing conversations
## Connecting to the WebSocket
### Connection Parameters
When connecting to the WebSocket, you need to provide the following query parameters:
- `conversation_id`: The ID of the conversation you want to join
- `latest_event_id`: The ID of the latest event you've received (use `-1` for a new connection)
- `providers_set`: (Optional) A comma-separated list of provider types
### Connection Example
Here's a basic example of connecting to the WebSocket using JavaScript:
```javascript
import { io } from "socket.io-client";
const socket = io("http://localhost:3000", {
transports: ["websocket"],
query: {
conversation_id: "your-conversation-id",
latest_event_id: -1,
providers_set: "github,gitlab" // Optional
}
});
socket.on("connect", () => {
console.log("Connected to OpenHands WebSocket");
});
socket.on("oh_event", (event) => {
console.log("Received event:", event);
});
socket.on("connect_error", (error) => {
console.error("Connection error:", error);
});
socket.on("disconnect", (reason) => {
console.log("Disconnected:", reason);
});
```
## Sending Actions to the Agent
To send an action to the agent, use the `oh_user_action` event:
```javascript
// Send a user message to the agent
socket.emit("oh_user_action", {
type: "message",
source: "user",
message: "Hello, can you help me with my project?"
});
```
## Receiving Events from the Agent
The server emits events using the `oh_event` event type. Here are some common event types you might receive:
- User messages (`source: "user", type: "message"`)
- Agent messages (`source: "agent", type: "message"`)
- File edits (`action: "edit"`)
- File writes (`action: "write"`)
- Command executions (`action: "run"`)
Example event handler:
```javascript
socket.on("oh_event", (event) => {
if (event.source === "agent" && event.type === "message") {
console.log("Agent says:", event.message);
} else if (event.action === "run") {
console.log("Command executed:", event.args.command);
console.log("Result:", event.result);
}
});
```
## Using Websocat for Testing
[Websocat](https://github.com/vi/websocat) is a command-line tool for interacting with WebSockets. It's useful for testing your WebSocket connection without writing a full client application.
### Installation
```bash
# On macOS
brew install websocat
# On Linux
curl -L https://github.com/vi/websocat/releases/download/v1.11.0/websocat.x86_64-unknown-linux-musl > websocat
chmod +x websocat
sudo mv websocat /usr/local/bin/
```
### Connecting to the WebSocket
```bash
# Connect to the WebSocket and print all received messages
echo "40{}" | \
websocat "ws://localhost:3000/socket.io/?EIO=4&transport=websocket&conversation_id=your-conversation-id&latest_event_id=-1"
```
### Sending a Message
```bash
# Send a message to the agent
echo '42["oh_user_action",{"type":"message","source":"user","message":"Hello, agent!"}]' | \
websocat "ws://localhost:3000/socket.io/?EIO=4&transport=websocket&conversation_id=your-conversation-id&latest_event_id=-1"
```
### Complete Example with Websocat
Here's a complete example of connecting to the WebSocket, sending a message, and receiving events:
```bash
# Start a persistent connection
websocat -v "ws://localhost:3000/socket.io/?EIO=4&transport=websocket&conversation_id=your-conversation-id&latest_event_id=-1"
# In another terminal, send a message
echo '42["oh_user_action",{"type":"message","source":"user","message":"Can you help me with my project?"}]' | \
websocat "ws://localhost:3000/socket.io/?EIO=4&transport=websocket&conversation_id=your-conversation-id&latest_event_id=-1"
```
## Event Structure
Events sent and received through the WebSocket follow a specific structure:
```typescript
interface OpenHandsEvent {
id: string; // Unique event ID
source: string; // "user" or "agent"
timestamp: string; // ISO timestamp
message?: string; // For message events
type?: string; // Event type (e.g., "message")
action?: string; // Action type (e.g., "run", "edit", "write")
args?: any; // Action arguments
result?: any; // Action result
}
```
## Best Practices
1. **Handle Reconnection**: Implement reconnection logic in your client to handle network interruptions.
2. **Track Event IDs**: Store the latest event ID you've received and use it when reconnecting to avoid duplicate events.
3. **Error Handling**: Implement proper error handling for connection errors and failed actions.
4. **Rate Limiting**: Avoid sending too many actions in a short period to prevent overloading the server.
## Troubleshooting
### Connection Issues
- Verify that the OpenHands server is running and accessible
- Check that you're providing the correct conversation ID
- Ensure your WebSocket URL is correctly formatted
### Authentication Issues
- Make sure you have the necessary authentication cookies if required
- Verify that you have permission to access the specified conversation
### Event Handling Issues
- Check that you're correctly parsing the event data
- Verify that your event handlers are properly registered

View File

@@ -58,17 +58,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
The easiest way to run OpenHands is in Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.37-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.38-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.37-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.38
docker.all-hands.dev/all-hands-ai/openhands:0.37
```
You'll find OpenHands running at http://localhost:3000!

View File

@@ -1,7 +1,6 @@
# Azure
OpenHands uses LiteLLM to make calls to Azure's chat models. You can find their documentation on using Azure as a
provider [here](https://docs.litellm.ai/docs/providers/azure).
OpenHands uses LiteLLM to make calls to Azure's chat models. You can find their documentation on using Azure as a provider [here](https://docs.litellm.ai/docs/providers/azure).
## Azure OpenAI Configuration
@@ -19,7 +18,7 @@ docker run -it --pull=always \
...
```
Then in the OpenHands UI Settings under the `LLM` tab:
Then in the OpenHands UI Settings:
:::note
You will need your ChatGPT deployment name which can be found on the deployments page in Azure. This is referenced as

View File

@@ -7,11 +7,10 @@ OpenHands uses LiteLLM to make calls to Google's chat models. You can find their
## Gemini - Google AI Studio Configs
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
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, enable `Advanced` options, and enter it in `Custom Model`
(e.g. gemini/<model-name> like `gemini/gemini-2.0-flash`).
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-2.0-flash`).
- `API Key` to your Gemini API key
## VertexAI - Google Cloud Platform Configs
@@ -25,8 +24,7 @@ VERTEXAI_PROJECT="<your-gcp-project-id>"
VERTEXAI_LOCATION="<your-gcp-location>"
```
Then set the following in the OpenHands UI through the Settings under the `LLM` tab:
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, enable `Advanced` options, and enter it in `Custom Model`
(e.g. vertex_ai/&lt;model-name&gt;).
If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. vertex_ai/&lt;model-name&gt;).

View File

@@ -1,21 +1,22 @@
# Groq
OpenHands uses LiteLLM to make calls to chat models on Groq. You can find their documentation on using Groq as a
provider [here](https://docs.litellm.ai/docs/providers/groq).
OpenHands uses LiteLLM to make calls to chat models on Groq. You can find their documentation on using Groq as a provider [here](https://docs.litellm.ai/docs/providers/groq).
## Configuration
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
- `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,
enable `Advanced` options, and enter it in `Custom Model` (e.g. groq/&lt;model-name&gt; like `groq/llama3-70b-8192`).
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/&lt;model-name&gt; 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).
## Using Groq as an OpenAI-Compatible Endpoint
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 under the `LLM` tab:
would access any OpenAI-compatible endpoint. In the OpenHands UI through the Settings:
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`)

View File

@@ -7,7 +7,7 @@ OpenHands supports using the [LiteLLM proxy](https://docs.litellm.ai/docs/proxy/
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 under the `LLM` tab:
2. When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
* 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`)
@@ -15,7 +15,6 @@ To use LiteLLM proxy with OpenHands, you need to:
## Supported Models
The supported models depend on your LiteLLM proxy configuration. OpenHands supports any model that your LiteLLM proxy
is configured to handle.
The supported models depend on your LiteLLM proxy configuration. OpenHands supports any model that your LiteLLM proxy is configured to handle.
Refer to your LiteLLM proxy configuration for the list of available models and their names.

View File

@@ -11,12 +11,14 @@ OpenHands can connect to any LLM supported by LiteLLM. However, it requires a po
Based on our evaluations of language models for coding tasks (using the SWE-bench dataset), we can provide some
recommendations for model selection. Our latest benchmarking results can be found in [this spreadsheet](https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0).
Based on these findings and community feedback, these are the latest models that have been verified to work reasonably well with OpenHands:
Based on these findings and community feedback, the following models have been verified to work reasonably well with OpenHands:
- [anthropic/claude-3-7-sonnet-20250219](https://www.anthropic.com/api) (recommended)
- [openai/o4-mini](https://openai.com/index/introducing-o3-and-o4-mini/)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
- [openai/o3-mini](https://openai.com/index/openai-o3-mini/)
- [openai/o3](https://openai.com/index/introducing-o3-and-o4-mini/)
- [openai/o4-mini](https://openai.com/index/introducing-o3-and-o4-mini/)
- [all-hands/openhands-lm-32b-v0.1](https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model) -- available through [OpenRouter](https://openrouter.ai/all-hands/openhands-lm-32b-v0.1)
@@ -25,8 +27,8 @@ OpenHands will issue many prompts to the LLM you configure. Most of these LLMs c
limits and monitor usage.
:::
If you have successfully run OpenHands with specific providers, we encourage you to open a PR to share your setup process
to help others using the same provider!
If you have successfully run OpenHands with specific LLMs not in the list, please add them to the verified list. We
also encourage you to open a PR to share your setup process to help others using the same provider and LLM!
For a full list of the providers and models available, please consult the
[litellm documentation](https://docs.litellm.ai/docs/providers).

View File

@@ -75,7 +75,7 @@ Start OpenHands using `make run`.
### Configure OpenHands
Once OpenHands is running, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
Once OpenHands is running, you'll need to set the following in the OpenHands UI through the Settings:
1. Enable `Advanced` options.
2. Set the following:
- `Custom Model` to `openai/<served-model-name>` (e.g. `openai/openhands-lm-32b-v0.1`)

View File

@@ -1,15 +1,14 @@
# OpenAI
OpenHands uses LiteLLM to make calls to OpenAI's chat models. You can find their documentation on using OpenAI as a
provider [here](https://docs.litellm.ai/docs/providers/openai).
OpenHands uses LiteLLM to make calls to OpenAI's chat models. You can find their documentation on using OpenAI as a provider [here](https://docs.litellm.ai/docs/providers/openai).
## Configuration
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
* `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, enable `Advanced` options, and enter it in `Custom Model` (e.g. openai/&lt;model-name&gt; like `openai/gpt-4o`).
If the model is not in the list, toggle `Advanced` options, and enter it in `Custom Model` (e.g. openai/&lt;model-name&gt; 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 +17,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 under the `LLM` tab:
If you're using an OpenAI proxy, in the OpenHands UI through the Settings:
1. Enable `Advanced` options
2. Set the following:
- `Custom Model` to openai/&lt;model-name&gt; (e.g. `openai/gpt-4o` or openai/&lt;proxy-prefix&gt;/&lt;model-name&gt;)

View File

@@ -1,14 +1,12 @@
# OpenRouter
OpenHands uses LiteLLM to make calls to chat models on OpenRouter. You can find their documentation on using
OpenRouter as a provider [here](https://docs.litellm.ai/docs/providers/openrouter).
OpenHands uses LiteLLM to make calls to chat models on OpenRouter. You can find their documentation on using OpenRouter as a provider [here](https://docs.litellm.ai/docs/providers/openrouter).
## Configuration
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
* `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, enable `Advanced` options, and enter it in
`Custom Model` (e.g. openrouter/&lt;model-name&gt; 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/&lt;model-name&gt; like `openrouter/anthropic/claude-3.5-sonnet`).
* `API Key` to your OpenRouter API key.

View File

@@ -13,11 +13,9 @@ or custom tools. MCP is based on the open standard defined at [modelcontextproto
## Configuration
MCP configuration can be defined in:
* The OpenHands UI through the Settings under the `MCP` tab.
* The `config.toml` file under the `[mcp]` section if not using the UI.
MCP configuration is defined in the `[mcp]` section of your `config.toml` file.
### Configuration Example via config.toml
### Configuration Example
```toml
[mcp]
@@ -84,7 +82,7 @@ Stdio servers are configured using an object with the following properties:
When OpenHands starts, it:
1. Reads the MCP configuration.
1. Reads the MCP configuration from `config.toml`.
2. Connects to any configured SSE servers.
3. Starts any configured stdio servers.
4. Registers the tools provided by these servers with the agent.

View File

@@ -1,23 +0,0 @@
# Organization and User Microagents
## Purpose
Organizations and users can define microagents that apply to all repositories belonging to the organization or user.
## Usage
These microagents can be [any type of microagent](./microagents-overview#microagent-types) and will be loaded
accordingly. However, they are applied to all repositories belonging to the organization or user.
Add a `.openhands` repository under the organization or user and create a `microagents` directory and place the
microagents in that directory.
## Example
General microagent file example for organization `Great-Co` located inside the `.openhands` repository:
`microagents/org-microagent.md`:
```
* Use type hints and error boundaries; validate inputs at system boundaries and fail with meaningful error messages.
* Document interfaces and public APIs; use implementation comments only for non-obvious logic.
* Follow the same naming convention for variables, classes, constants, etc. already used in each repository.
```

View File

@@ -7,7 +7,7 @@ They provide expert guidance, automate common tasks, and ensure consistent pract
Currently OpenHands supports the following types of microagents:
- [General Microagents](./microagents-repo): General guidelines for OpenHands about the repository.
- [General Repository Microagents](./microagents-repo): General guidelines for OpenHands about the repository.
- [Keyword-Triggered Microagents](./microagents-keyword): Guidelines activated by specific keywords in prompts.
To customize OpenHands' behavior, create a .openhands/microagents/ directory in the root of your repository and
@@ -24,7 +24,7 @@ Example repository structure:
some-repository/
└── .openhands/
└── microagents/
└── repo.md # General guidelines
└── repo.md # General repository guidelines
└── trigger_this.md # Microagent triggered by specific keywords
└── trigger_that.md # Microagent triggered by specific keywords
```
@@ -34,7 +34,7 @@ some-repository/
Each microagent file may include frontmatter that provides additional information. In some cases, this frontmatter
is required:
| Microagent Type | Required |
|---------------------------------|----------|
| `General Microagents` | No |
| `Keyword-Triggered Microagents` | Yes |
| Microagent Type | Required |
|----------------------------------|----------|
| `General Repository Microagents` | No |
| `Keyword-Triggered Microagents` | Yes |

View File

@@ -1,4 +1,4 @@
# General Microagents
# General Repository Microagents
## Purpose
@@ -20,7 +20,7 @@ Frontmatter should be enclosed in triple dashes (---) and may include the follow
## Example
General microagent file example located at `.openhands/microagents/repo.md`:
General repository microagent file example located at `.openhands/microagents/repo.md`:
```
This project is a TODO application that allows users to track TODO items.
@@ -28,4 +28,4 @@ To set it up, you can run `npm run build`.
Always make sure the tests are passing before committing changes. You can run the tests by running `npm run test`.
```
[See more examples of general microagents here.](https://github.com/All-Hands-AI/OpenHands/tree/main/.openhands/microagents)
[See more examples of general repository microagents here.](https://github.com/All-Hands-AI/OpenHands/tree/main/.openhands/microagents)

View File

@@ -70,7 +70,7 @@ const sidebars: SidebarsConfig = {
},
{
type: 'doc',
label: 'General Microagents',
label: 'General Repository Microagents',
id: 'usage/prompting/microagents-repo',
},
{
@@ -78,11 +78,6 @@ const sidebars: SidebarsConfig = {
label: 'Keyword-Triggered Microagents',
id: 'usage/prompting/microagents-keyword',
},
{
type: 'doc',
label: 'Organization and User Microagents',
id: 'usage/prompting/microagents-org',
},
{
type: 'doc',
label: 'Global Microagents',
@@ -272,11 +267,6 @@ const sidebars: SidebarsConfig = {
label: 'Evaluation',
id: 'usage/how-to/evaluation-harness',
},
{
type: 'doc',
label: 'WebSocket Connection',
id: 'usage/how-to/websocket-connection',
},
],
},
{

View File

@@ -17,7 +17,7 @@ RUN git checkout 4eddc7db6449a5ade3e37285747c8b208cd54ce7
RUN micromamba create -n sci-agent python=3.10 pip setuptools wheel
RUN micromamba run -n sci-agent pip install -r requirements.txt
# Replace all occurrences of conda with micromamba under the /workspace
# Replace all occurence of conda with micromamba under the /workspace
RUN find ./ -type f -exec sed -i 's/conda/micromamba/g' {} \;
# pushd evaluation/scienceagentbench

View File

@@ -63,7 +63,7 @@ to `CodeActAgent`.
default, the script evaluates the entire SWE-bench_Lite test set (300 issues). Note:
in order to use `eval_limit`, you must also set `agent`.
- `max_iter`, e.g. `20`, is the maximum number of iterations for the agent to run. By
default, it is set to 100.
default, it is set to 60.
- `num_workers`, e.g. `3`, is the number of parallel workers to run the evaluation. By
default, it is set to 1.
- `dataset`, a huggingface dataset name. e.g. `princeton-nlp/SWE-bench`, `princeton-nlp/SWE-bench_Lite`, `princeton-nlp/SWE-bench_Verified`, or `princeton-nlp/SWE-bench_Multimodal`, specifies which dataset to evaluate on.
@@ -102,9 +102,9 @@ Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZj
```bash
./evaluation/benchmarks/swe_bench/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [max_iter] [num_workers] [dataset] [dataset_split]
# Example - This runs evaluation on CodeActAgent for 300 instances on "princeton-nlp/SWE-bench_Lite"'s test set, with max 100 iteration per instances, with 16 number of workers running in parallel
# Example - This runs evaluation on CodeActAgent for 300 instances on "princeton-nlp/SWE-bench_Lite"'s test set, with max 30 iteration per instances, with 16 number of workers running in parallel
ALLHANDS_API_KEY="YOUR-API-KEY" RUNTIME=remote SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.eval.all-hands.dev" EVAL_DOCKER_IMAGE_PREFIX="us-central1-docker.pkg.dev/evaluation-092424/swe-bench-images" \
./evaluation/benchmarks/swe_bench/scripts/run_infer.sh llm.eval HEAD CodeActAgent 300 100 16 "princeton-nlp/SWE-bench_Lite" test
./evaluation/benchmarks/swe_bench/scripts/run_infer.sh llm.eval HEAD CodeActAgent 300 30 16 "princeton-nlp/SWE-bench_Lite" test
```
To clean-up all existing runtime you've already started, run:
@@ -176,7 +176,7 @@ Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZj
# Example - This evaluates patches generated by CodeActAgent on Llama-3.1-70B-Instruct-Turbo on "princeton-nlp/SWE-bench_Lite"'s test set, with 16 number of workers running in parallel
ALLHANDS_API_KEY="YOUR-API-KEY" RUNTIME=remote SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.eval.all-hands.dev" EVAL_DOCKER_IMAGE_PREFIX="us-central1-docker.pkg.dev/evaluation-092424/swe-bench-images" \
evaluation/benchmarks/swe_bench/scripts/eval_infer_remote.sh evaluation/evaluation_outputs/outputs/swe-bench-lite/CodeActAgent/Llama-3.1-70B-Instruct-Turbo_maxiter_100_N_v1.9-no-hint/output.jsonl 16 "princeton-nlp/SWE-bench_Lite" "test"
evaluation/benchmarks/swe_bench/scripts/eval_infer_remote.sh evaluation/evaluation_outputs/outputs/swe-bench-lite/CodeActAgent/Llama-3.1-70B-Instruct-Turbo_maxiter_30_N_v1.9-no-hint/output.jsonl 16 "princeton-nlp/SWE-bench_Lite" "test"
```
To clean-up all existing runtimes that you've already started, run:

View File

@@ -714,19 +714,6 @@ def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
subset = dataset[dataset[filter_column].isin(selected_ids)]
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
return subset
if 'selected_repos' in data:
# repos for the swe-bench instances:
# ['astropy/astropy', 'django/django', 'matplotlib/matplotlib', 'mwaskom/seaborn', 'pallets/flask', 'psf/requests', 'pydata/xarray', 'pylint-dev/pylint', 'pytest-dev/pytest', 'scikit-learn/scikit-learn', 'sphinx-doc/sphinx', 'sympy/sympy']
selected_repos = data['selected_repos']
if isinstance(selected_repos, str): selected_repos = [selected_repos]
assert isinstance(selected_repos, list)
logger.info(
f'Filtering {selected_repos} tasks from "selected_repos"...'
)
subset = dataset[dataset["repo"].isin(selected_repos)]
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
return subset
skip_ids = os.environ.get('SKIP_IDS', '').split(',')
if len(skip_ids) > 0:
logger.info(f'Filtering {len(skip_ids)} tasks from "SKIP_IDS"...')

View File

@@ -26,8 +26,8 @@ if [ -z "$AGENT" ]; then
fi
if [ -z "$MAX_ITER" ]; then
echo "MAX_ITER not specified, use default 100"
MAX_ITER=100
echo "MAX_ITER not specified, use default 60"
MAX_ITER=60
fi
if [ -z "$RUN_WITH_BROWSING" ]; then

View File

@@ -2,7 +2,6 @@
echo "Running frontend checks..."
cd frontend
npm run check-unlocalized-strings
npm run check-translation-completeness
npx lint-staged
# Run backend pre-commit

View File

@@ -61,7 +61,7 @@ make build
# Start the application
make run
```
Or to run backend and frontend separately.
Or to run backend and frontend seperately.
```sh
# Start the backend from the root directory

View File

@@ -4,7 +4,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ActionSuggestions } from "#/components/features/chat/action-suggestions";
import OpenHands from "#/api/open-hands";
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
import { ConversationProvider } from "#/context/conversation-context";
// Mock dependencies
vi.mock("posthog-js", () => ({
@@ -39,20 +38,12 @@ vi.mock("react-i18next", () => ({
}),
}));
vi.mock("react-router", () => ({
useParams: () => ({
conversationId: "test-conversation-id",
}),
}));
const renderActionSuggestions = () =>
render(<ActionSuggestions onSuggestionsClick={() => {}} />, {
wrapper: ({ children }) => (
<ConversationProvider>
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
</ConversationProvider>
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
),
});
@@ -74,11 +65,6 @@ describe("ActionSuggestions", () => {
});
it("should render both GitHub buttons when GitHub token is set and repository is selected", async () => {
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
// @ts-expect-error - only required for testing
getConversationSpy.mockResolvedValue({
selected_repository: "test-repo",
});
renderActionSuggestions();
// Find all buttons with data-testid="suggestion"

View File

@@ -20,6 +20,7 @@ describe("AccountSettingsContextMenu", () => {
<AccountSettingsContextMenu
onLogout={onLogoutMock}
onClose={onCloseMock}
isLoggedIn
/>,
);
@@ -34,6 +35,7 @@ describe("AccountSettingsContextMenu", () => {
<AccountSettingsContextMenu
onLogout={onLogoutMock}
onClose={onCloseMock}
isLoggedIn
/>,
);
@@ -43,18 +45,19 @@ describe("AccountSettingsContextMenu", () => {
expect(onLogoutMock).toHaveBeenCalledOnce();
});
test("logout button is always enabled", async () => {
test("onLogout should be disabled if the user is not logged in", async () => {
render(
<AccountSettingsContextMenu
onLogout={onLogoutMock}
onClose={onCloseMock}
isLoggedIn={false}
/>,
);
const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
await user.click(logoutOption);
expect(onLogoutMock).toHaveBeenCalledOnce();
expect(onLogoutMock).not.toHaveBeenCalled();
});
it("should call onClose when clicking outside of the element", async () => {
@@ -62,6 +65,7 @@ describe("AccountSettingsContextMenu", () => {
<AccountSettingsContextMenu
onLogout={onLogoutMock}
onClose={onCloseMock}
isLoggedIn
/>,
);

View File

@@ -43,6 +43,7 @@ describe("HomeHeader", () => {
await userEvent.click(launchButton);
expect(createConversationSpy).toHaveBeenCalledExactlyOnceWith(
"gui",
undefined,
undefined,
undefined,

View File

@@ -173,6 +173,7 @@ describe("RepoConnector", () => {
await userEvent.click(launchButton);
expect(createConversationSpy).toHaveBeenCalledExactlyOnceWith(
"gui",
"rbren/polaris",
"github",
undefined,

View File

@@ -85,6 +85,7 @@ describe("TaskCard", () => {
await userEvent.click(launchButton);
expect(createConversationSpy).toHaveBeenCalledWith(
"suggested_task",
MOCK_RESPOSITORIES[0].full_name,
MOCK_RESPOSITORIES[0].git_provider,
undefined,

View File

@@ -1,59 +0,0 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ApiKeysManager } from "#/components/features/settings/api-keys-manager";
// Mock the react-i18next
vi.mock("react-i18next", async () => {
const actual = await vi.importActual<typeof import("react-i18next")>("react-i18next");
return {
...actual,
useTranslation: () => ({
t: (key: string) => key,
}),
Trans: ({ i18nKey, components }: { i18nKey: string; components: Record<string, React.ReactNode> }) => {
// Simplified Trans component that renders the link
if (i18nKey === "SETTINGS$API_KEYS_DESCRIPTION") {
return (
<span>
API keys allow you to authenticate with the OpenHands API programmatically.
Keep your API keys secure; anyone with your API key can access your account.
For more information on how to use the API, see our {components.a}
</span>
);
}
return <span>{i18nKey}</span>;
},
};
});
// Mock the API keys hook
vi.mock("#/hooks/query/use-api-keys", () => ({
useApiKeys: () => ({
data: [],
isLoading: false,
error: null,
}),
}));
describe("ApiKeysManager", () => {
const renderComponent = () => {
const queryClient = new QueryClient();
return render(
<QueryClientProvider client={queryClient}>
<ApiKeysManager />
</QueryClientProvider>
);
};
it("should render the API documentation link", () => {
renderComponent();
// Find the link to the API documentation
const link = screen.getByRole("link");
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", "https://docs.all-hands.dev/modules/usage/cloud/cloud-api");
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
});
});

View File

@@ -57,7 +57,7 @@ describe("UserActions", () => {
).not.toBeInTheDocument();
});
test("logout button is always enabled", async () => {
test("onLogout should not be called when the user is not logged in", async () => {
render(<UserActions onLogout={onLogoutMock} />);
const userAvatar = screen.getByTestId("user-avatar");
@@ -66,6 +66,6 @@ describe("UserActions", () => {
const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
await user.click(logoutOption);
expect(onLogoutMock).toHaveBeenCalledOnce();
expect(onLogoutMock).not.toHaveBeenCalled();
});
});

View File

@@ -11,6 +11,7 @@ describe("Translations", () => {
<AccountSettingsContextMenu
onLogout={() => {}}
onClose={() => {}}
isLoggedIn
/>,
);
expect(

View File

@@ -48,7 +48,7 @@ describe("Content", () => {
await waitFor(() => {
expect(provider).toHaveValue("Anthropic");
expect(model).toHaveValue("claude-3-7-sonnet-20250219");
expect(model).toHaveValue("claude-3-5-sonnet-20241022");
expect(apiKey).toHaveValue("");
expect(apiKey).toHaveProperty("placeholder", "");
@@ -135,7 +135,7 @@ describe("Content", () => {
);
const condensor = screen.getByTestId("enable-memory-condenser-switch");
expect(model).toHaveValue("anthropic/claude-3-7-sonnet-20250219");
expect(model).toHaveValue("anthropic/claude-3-5-sonnet-20241022");
expect(baseUrl).toHaveValue("");
expect(apiKey).toHaveValue("");
expect(apiKey).toHaveProperty("placeholder", "");
@@ -542,7 +542,7 @@ describe("Form submission", () => {
// select model
await userEvent.click(model);
const modelOption = screen.getByText("claude-3-7-sonnet-20250219");
const modelOption = screen.getByText("claude-3-5-sonnet-20241022");
await userEvent.click(modelOption);
const submitButton = screen.getByTestId("submit-button");
@@ -550,7 +550,7 @@ describe("Form submission", () => {
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
llm_model: "anthropic/claude-3-7-sonnet-20250219",
llm_model: "anthropic/claude-3-5-sonnet-20241022",
llm_base_url: "",
confirmation_mode: false,
}),

View File

@@ -1,565 +0,0 @@
import { render, screen, waitFor, within } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import userEvent from "@testing-library/user-event";
import { createRoutesStub, Outlet } from "react-router";
import SecretsSettingsScreen from "#/routes/secrets-settings";
import { SecretsService } from "#/api/secrets-service";
import { GetSecretsResponse } from "#/api/secrets-service.types";
import OpenHands from "#/api/open-hands";
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
const MOCK_GET_SECRETS_RESPONSE: GetSecretsResponse["custom_secrets"] = [
{
name: "My_Secret_1",
description: "My first secret",
},
{
name: "My_Secret_2",
description: "My second secret",
},
];
const RouterStub = createRoutesStub([
{
Component: Outlet,
path: "/settings",
children: [
{
Component: SecretsSettingsScreen,
path: "/settings/secrets",
},
{
Component: () => <div data-testid="git-settings-screen" />,
path: "/settings/git",
},
],
},
]);
const renderSecretsSettings = () =>
render(<RouterStub initialEntries={["/settings/secrets"]} />, {
wrapper: ({ children }) => (
<QueryClientProvider
client={
new QueryClient({
defaultOptions: { queries: { retry: false } },
})
}
>
{children}
</QueryClientProvider>
),
});
beforeEach(() => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
// @ts-expect-error - only return the config we need
getConfigSpy.mockResolvedValue({
APP_MODE: "oss",
});
});
describe("Content", () => {
it("should render the secrets settings screen", () => {
renderSecretsSettings();
screen.getByTestId("secrets-settings-screen");
});
it("should NOT render a button to connect with git if they havent already in oss", async () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
// @ts-expect-error - only return the config we need
getConfigSpy.mockResolvedValue({
APP_MODE: "oss",
});
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: {},
});
renderSecretsSettings();
expect(getConfigSpy).toHaveBeenCalled();
await waitFor(() => expect(getSecretsSpy).toHaveBeenCalled());
expect(screen.queryByTestId("connect-git-button")).not.toBeInTheDocument();
});
it("should render a button to connect with git if they havent already in saas", async () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
// @ts-expect-error - only return the config we need
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
});
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: {},
});
renderSecretsSettings();
expect(getSecretsSpy).not.toHaveBeenCalled();
await waitFor(() =>
expect(screen.queryByTestId("add-secret-button")).not.toBeInTheDocument(),
);
const button = await screen.findByTestId("connect-git-button");
await userEvent.click(button);
screen.getByTestId("git-settings-screen");
});
it("should render a message if there are no existing secrets", async () => {
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
getSecretsSpy.mockResolvedValue([]);
renderSecretsSettings();
await screen.findByTestId("no-secrets-message");
});
it("should render existing secrets", async () => {
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE);
renderSecretsSettings();
const secrets = await screen.findAllByTestId("secret-item");
expect(secrets).toHaveLength(2);
expect(screen.queryByTestId("no-secrets-message")).not.toBeInTheDocument();
});
});
describe("Secret actions", () => {
it("should create a new secret", async () => {
const createSecretSpy = vi.spyOn(SecretsService, "createSecret");
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
createSecretSpy.mockResolvedValue(true);
renderSecretsSettings();
// render form & hide items
expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument();
const button = await screen.findByTestId("add-secret-button");
await userEvent.click(button);
const secretForm = screen.getByTestId("add-secret-form");
const secrets = screen.queryAllByTestId("secret-item");
expect(screen.queryByTestId("add-secret-button")).not.toBeInTheDocument();
expect(secretForm).toBeInTheDocument();
expect(secrets).toHaveLength(0);
// enter details
const nameInput = within(secretForm).getByTestId("name-input");
const valueInput = within(secretForm).getByTestId("value-input");
const descriptionInput =
within(secretForm).getByTestId("description-input");
const submitButton = within(secretForm).getByTestId("submit-button");
vi.clearAllMocks(); // reset mocks to check for upcoming calls
await userEvent.type(nameInput, "My_Custom_Secret");
await userEvent.type(valueInput, "my-custom-secret-value");
await userEvent.type(descriptionInput, "My custom secret description");
await userEvent.click(submitButton);
// make POST request
expect(createSecretSpy).toHaveBeenCalledWith(
"My_Custom_Secret",
"my-custom-secret-value",
"My custom secret description",
);
// hide form & render items
expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument();
expect(getSecretsSpy).toHaveBeenCalled();
});
it("should edit a secret", async () => {
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
const updateSecretSpy = vi.spyOn(SecretsService, "updateSecret");
getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE);
updateSecretSpy.mockResolvedValue(true);
renderSecretsSettings();
// render edit button within a secret list item
const secrets = await screen.findAllByTestId("secret-item");
const firstSecret = within(secrets[0]);
const editButton = firstSecret.getByTestId("edit-secret-button");
await userEvent.click(editButton);
// render edit form
const editForm = screen.getByTestId("edit-secret-form");
expect(screen.queryByTestId("add-secret-button")).not.toBeInTheDocument();
expect(editForm).toBeInTheDocument();
expect(screen.queryAllByTestId("secret-item")).toHaveLength(0);
// enter details
const nameInput = within(editForm).getByTestId("name-input");
const descriptionInput = within(editForm).getByTestId("description-input");
const submitButton = within(editForm).getByTestId("submit-button");
// should not show value input
const valueInput = within(editForm).queryByTestId("value-input");
expect(valueInput).not.toBeInTheDocument();
expect(nameInput).toHaveValue("My_Secret_1");
expect(descriptionInput).toHaveValue("My first secret");
await userEvent.clear(nameInput);
await userEvent.type(nameInput, "My_Edited_Secret");
await userEvent.clear(descriptionInput);
await userEvent.type(descriptionInput, "My edited secret description");
await userEvent.click(submitButton);
// make POST request
expect(updateSecretSpy).toHaveBeenCalledWith(
"My_Secret_1",
"My_Edited_Secret",
"My edited secret description",
);
// hide form
expect(screen.queryByTestId("edit-secret-form")).not.toBeInTheDocument();
// optimistic update
const updatedSecrets = await screen.findAllByTestId("secret-item");
expect(updatedSecrets).toHaveLength(2);
expect(updatedSecrets[0]).toHaveTextContent(/my_edited_secret/i);
});
it("should be able to cancel the create or edit form", async () => {
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE);
renderSecretsSettings();
// render form & hide items
expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument();
const button = await screen.findByTestId("add-secret-button");
await userEvent.click(button);
const secretForm = screen.getByTestId("add-secret-form");
expect(secretForm).toBeInTheDocument();
// cancel button
const cancelButton = within(secretForm).getByTestId("cancel-button");
await userEvent.click(cancelButton);
expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument();
expect(screen.queryByTestId("add-secret-button")).toBeInTheDocument();
// render edit button within a secret list item
const secrets = await screen.findAllByTestId("secret-item");
const firstSecret = within(secrets[0]);
const editButton = firstSecret.getByTestId("edit-secret-button");
await userEvent.click(editButton);
// render edit form
const editForm = screen.getByTestId("edit-secret-form");
expect(editForm).toBeInTheDocument();
expect(screen.queryAllByTestId("secret-item")).toHaveLength(0);
// cancel button
const cancelEditButton = within(editForm).getByTestId("cancel-button");
await userEvent.click(cancelEditButton);
expect(screen.queryByTestId("edit-secret-form")).not.toBeInTheDocument();
expect(screen.queryAllByTestId("secret-item")).toHaveLength(2);
});
it("should undo the optimistic update if the request fails", async () => {
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
const updateSecretSpy = vi.spyOn(SecretsService, "updateSecret");
getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE);
updateSecretSpy.mockRejectedValue(new Error("Failed to update secret"));
renderSecretsSettings();
// render edit button within a secret list item
const secrets = await screen.findAllByTestId("secret-item");
const firstSecret = within(secrets[0]);
const editButton = firstSecret.getByTestId("edit-secret-button");
await userEvent.click(editButton);
// render edit form
const editForm = screen.getByTestId("edit-secret-form");
expect(editForm).toBeInTheDocument();
expect(screen.queryAllByTestId("secret-item")).toHaveLength(0);
// enter details
const nameInput = within(editForm).getByTestId("name-input");
const submitButton = within(editForm).getByTestId("submit-button");
// should not show value input
const valueInput = within(editForm).queryByTestId("value-input");
expect(valueInput).not.toBeInTheDocument();
await userEvent.clear(nameInput);
await userEvent.type(nameInput, "My_Edited_Secret");
await userEvent.click(submitButton);
// make POST request
expect(updateSecretSpy).toHaveBeenCalledWith(
"My_Secret_1",
"My_Edited_Secret",
"My first secret",
);
// hide form
expect(screen.queryByTestId("edit-secret-form")).not.toBeInTheDocument();
// no optimistic update
const updatedSecrets = await screen.findAllByTestId("secret-item");
expect(updatedSecrets).toHaveLength(2);
expect(updatedSecrets[0]).not.toHaveTextContent(/my edited secret/i);
});
it("should remove the secret from the list after deletion", async () => {
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
const deleteSecretSpy = vi.spyOn(SecretsService, "deleteSecret");
getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE);
deleteSecretSpy.mockResolvedValue(true);
renderSecretsSettings();
// render delete button within a secret list item
const secrets = await screen.findAllByTestId("secret-item");
const secondSecret = within(secrets[1]);
const deleteButton = secondSecret.getByTestId("delete-secret-button");
await userEvent.click(deleteButton);
// confirmation modal
const confirmationModal = screen.getByTestId("confirmation-modal");
const confirmButton =
within(confirmationModal).getByTestId("confirm-button");
await userEvent.click(confirmButton);
// make DELETE request
expect(deleteSecretSpy).toHaveBeenCalledWith("My_Secret_2");
expect(screen.queryByTestId("confirmation-modal")).not.toBeInTheDocument();
// optimistic update
expect(screen.queryAllByTestId("secret-item")).toHaveLength(1);
expect(screen.queryByText("My_Secret_2")).not.toBeInTheDocument();
});
it("should be able to cancel the delete confirmation modal", async () => {
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
const deleteSecretSpy = vi.spyOn(SecretsService, "deleteSecret");
getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE);
deleteSecretSpy.mockResolvedValue(true);
renderSecretsSettings();
// render delete button within a secret list item
const secrets = await screen.findAllByTestId("secret-item");
const secondSecret = within(secrets[1]);
const deleteButton = secondSecret.getByTestId("delete-secret-button");
await userEvent.click(deleteButton);
// confirmation modal
const confirmationModal = screen.getByTestId("confirmation-modal");
const cancelButton = within(confirmationModal).getByTestId("cancel-button");
await userEvent.click(cancelButton);
// no DELETE request
expect(deleteSecretSpy).not.toHaveBeenCalled();
expect(screen.queryByTestId("confirmation-modal")).not.toBeInTheDocument();
expect(screen.queryAllByTestId("secret-item")).toHaveLength(2);
});
it("should revert the optimistic update if the request fails", async () => {
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
const deleteSecretSpy = vi.spyOn(SecretsService, "deleteSecret");
getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE);
deleteSecretSpy.mockRejectedValue(new Error("Failed to delete secret"));
renderSecretsSettings();
// render delete button within a secret list item
const secrets = await screen.findAllByTestId("secret-item");
const secondSecret = within(secrets[1]);
const deleteButton = secondSecret.getByTestId("delete-secret-button");
await userEvent.click(deleteButton);
// confirmation modal
const confirmationModal = screen.getByTestId("confirmation-modal");
const confirmButton =
within(confirmationModal).getByTestId("confirm-button");
await userEvent.click(confirmButton);
// make DELETE request
expect(deleteSecretSpy).toHaveBeenCalledWith("My_Secret_2");
expect(screen.queryByTestId("confirmation-modal")).not.toBeInTheDocument();
// optimistic update
expect(screen.queryAllByTestId("secret-item")).toHaveLength(2);
expect(screen.queryByText("My_Secret_2")).toBeInTheDocument();
});
it("should hide the no items message when in form view", async () => {
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
getSecretsSpy.mockResolvedValue([]);
renderSecretsSettings();
// render form & hide items
expect(screen.queryByTestId("no-secrets-message")).not.toBeInTheDocument();
const button = await screen.findByTestId("add-secret-button");
await userEvent.click(button);
const secretForm = screen.getByTestId("add-secret-form");
expect(secretForm).toBeInTheDocument();
expect(screen.queryByTestId("no-secrets-message")).not.toBeInTheDocument();
});
it("should not allow spaces in secret names", async () => {
const createSecretSpy = vi.spyOn(SecretsService, "createSecret");
renderSecretsSettings();
// render form & hide items
expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument();
const button = await screen.findByTestId("add-secret-button");
await userEvent.click(button);
const secretForm = screen.getByTestId("add-secret-form");
expect(secretForm).toBeInTheDocument();
// enter details
const nameInput = within(secretForm).getByTestId("name-input");
const valueInput = within(secretForm).getByTestId("value-input");
const submitButton = within(secretForm).getByTestId("submit-button");
await userEvent.type(nameInput, "My Custom Secret With Spaces");
await userEvent.type(valueInput, "my-custom-secret-value");
await userEvent.click(submitButton);
// make POST request
expect(createSecretSpy).not.toHaveBeenCalled();
await userEvent.clear(nameInput);
await userEvent.type(nameInput, "MyCustomSecret");
await userEvent.click(submitButton);
expect(createSecretSpy).toHaveBeenCalledWith(
"MyCustomSecret",
"my-custom-secret-value",
undefined,
);
});
it("should not allow existing secret names", async () => {
const createSecretSpy = vi.spyOn(SecretsService, "createSecret");
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE.slice(0, 1));
renderSecretsSettings();
// render form & hide items
expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument();
const button = await screen.findByTestId("add-secret-button");
await userEvent.click(button);
const secretForm = screen.getByTestId("add-secret-form");
expect(secretForm).toBeInTheDocument();
// enter details
const nameInput = within(secretForm).getByTestId("name-input");
const valueInput = within(secretForm).getByTestId("value-input");
const submitButton = within(secretForm).getByTestId("submit-button");
await userEvent.type(nameInput, "My_Secret_1");
await userEvent.type(valueInput, "my-custom-secret-value");
await userEvent.click(submitButton);
// make POST request
expect(createSecretSpy).not.toHaveBeenCalled();
expect(screen.queryByText(/secret already exists/i)).toBeInTheDocument();
await userEvent.clear(nameInput);
await userEvent.type(nameInput, "My_Custom_Secret");
await userEvent.clear(valueInput);
await userEvent.type(valueInput, "my-custom-secret-value");
await userEvent.click(submitButton);
expect(createSecretSpy).toHaveBeenCalledWith(
"My_Custom_Secret",
"my-custom-secret-value",
undefined,
);
expect(
screen.queryByText("SECRETS$SECRET_VALUE_REQUIRED"),
).not.toBeInTheDocument();
});
it("should not submit whitespace secret names or values", async () => {
const createSecretSpy = vi.spyOn(SecretsService, "createSecret");
renderSecretsSettings();
// render form & hide items
expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument();
const button = await screen.findByTestId("add-secret-button");
await userEvent.click(button);
const secretForm = screen.getByTestId("add-secret-form");
expect(secretForm).toBeInTheDocument();
// enter details
const nameInput = within(secretForm).getByTestId("name-input");
const valueInput = within(secretForm).getByTestId("value-input");
const submitButton = within(secretForm).getByTestId("submit-button");
await userEvent.type(nameInput, " ");
await userEvent.type(valueInput, "my-custom-secret-value");
await userEvent.click(submitButton);
// make POST request
expect(createSecretSpy).not.toHaveBeenCalled();
await userEvent.clear(nameInput);
await userEvent.type(nameInput, "My_Custom_Secret");
await userEvent.clear(valueInput);
await userEvent.type(valueInput, " ");
await userEvent.click(submitButton);
expect(createSecretSpy).not.toHaveBeenCalled();
expect(
screen.queryByText("SECRETS$SECRET_VALUE_REQUIRED"),
).toBeInTheDocument();
});
it("should not reset ipout values on an invalid submit", async () => {
const getSecretsSpy = vi.spyOn(SecretsService, "getSecrets");
const createSecretSpy = vi.spyOn(SecretsService, "createSecret");
getSecretsSpy.mockResolvedValue(MOCK_GET_SECRETS_RESPONSE);
renderSecretsSettings();
// render form & hide items
expect(screen.queryByTestId("add-secret-form")).not.toBeInTheDocument();
const button = await screen.findByTestId("add-secret-button");
await userEvent.click(button);
const secretForm = screen.getByTestId("add-secret-form");
expect(secretForm).toBeInTheDocument();
// enter details
const nameInput = within(secretForm).getByTestId("name-input");
const valueInput = within(secretForm).getByTestId("value-input");
const submitButton = within(secretForm).getByTestId("submit-button");
await userEvent.type(nameInput, MOCK_GET_SECRETS_RESPONSE[0].name);
await userEvent.type(valueInput, "my-custom-secret-value");
await userEvent.click(submitButton);
// make POST request
expect(createSecretSpy).not.toHaveBeenCalled();
expect(screen.queryByText(/secret already exists/i)).toBeInTheDocument();
expect(nameInput).toHaveValue(MOCK_GET_SECRETS_RESPONSE[0].name);
expect(valueInput).toHaveValue("my-custom-secret-value");
});
});

View File

@@ -79,7 +79,7 @@ describe("Settings Screen", () => {
};
it("should render the navbar", async () => {
const sectionsToInclude = ["llm", "git", "application", "secrets"];
const sectionsToInclude = ["llm", "git", "application"];
const sectionsToExclude = ["api keys", "credits"];
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
// @ts-expect-error - only return app mode
@@ -110,13 +110,7 @@ describe("Settings Screen", () => {
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
});
const sectionsToInclude = [
"git",
"application",
"credits",
"secrets",
"api keys",
];
const sectionsToInclude = ["git", "application", "credits", "api keys"];
const sectionsToExclude = ["llm"];
renderSettingsScreen();

View File

@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.38.0",
"version": "0.37.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.38.0",
"version": "0.37.0",
"dependencies": {
"@heroui/react": "2.7.8",
"@microlink/react-json-view": "^1.26.1",

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.38.0",
"version": "0.37.0",
"private": true,
"type": "module",
"engines": {
@@ -68,8 +68,7 @@
"lint:fix": "eslint src --ext .ts,.tsx,.js --fix && prettier --write src/**/*.{ts,tsx}",
"prepare": "cd .. && husky frontend/.husky",
"typecheck": "react-router typegen && tsc",
"check-unlocalized-strings": "node scripts/check-unlocalized-strings.cjs",
"check-translation-completeness": "node scripts/check-translation-completeness.cjs"
"check-unlocalized-strings": "node scripts/check-unlocalized-strings.cjs"
},
"lint-staged": {
"src/**/*.{ts,tsx,js}": [

View File

@@ -1,88 +0,0 @@
#!/usr/bin/env node
/**
* Pre-commit hook script to check for translation completeness
* This script ensures that all translation keys have entries for all supported languages
*/
const fs = require('fs');
const path = require('path');
// Load the translation file
const translationJsonPath = path.join(__dirname, '../src/i18n/translation.json');
const translationJson = require(translationJsonPath);
// Load the available languages from the i18n index file
const i18nIndexPath = path.join(__dirname, '../src/i18n/index.ts');
const i18nIndexContent = fs.readFileSync(i18nIndexPath, 'utf8');
// Extract the language codes from the AvailableLanguages array
const languageCodesRegex = /\{ label: "[^"]+", value: "([^"]+)" \}/g;
const supportedLanguageCodes = [];
let match;
while ((match = languageCodesRegex.exec(i18nIndexContent)) !== null) {
supportedLanguageCodes.push(match[1]);
}
// Track missing and extra translations
const missingTranslations = {};
const extraLanguages = {};
let hasErrors = false;
// Check each translation key
Object.entries(translationJson).forEach(([key, translations]) => {
// Get the languages available for this key
const availableLanguages = Object.keys(translations);
// Find missing languages for this key
const missing = supportedLanguageCodes.filter(
(langCode) => !availableLanguages.includes(langCode)
);
if (missing.length > 0) {
missingTranslations[key] = missing;
hasErrors = true;
}
// Find extra languages for this key
const extra = availableLanguages.filter(
(langCode) => !supportedLanguageCodes.includes(langCode)
);
if (extra.length > 0) {
extraLanguages[key] = extra;
hasErrors = true;
}
});
// Generate detailed error message if there are missing translations
if (Object.keys(missingTranslations).length > 0) {
console.error('\x1b[31m%s\x1b[0m', 'ERROR: Missing translations detected');
console.error(`Found ${Object.keys(missingTranslations).length} translation keys with missing languages:`);
Object.entries(missingTranslations).forEach(([key, langs]) => {
console.error(`- Key "${key}" is missing translations for: ${langs.join(', ')}`);
});
console.error('\nPlease add the missing translations before committing.');
}
// Generate detailed error message if there are extra languages
if (Object.keys(extraLanguages).length > 0) {
console.error('\x1b[31m%s\x1b[0m', 'ERROR: Extra languages detected');
console.error(`Found ${Object.keys(extraLanguages).length} translation keys with extra languages not in AvailableLanguages:`);
Object.entries(extraLanguages).forEach(([key, langs]) => {
console.error(`- Key "${key}" has translations for unsupported languages: ${langs.join(', ')}`);
});
console.error('\nPlease remove the extra languages before committing.');
}
// Exit with error code if there are issues
if (hasErrors) {
process.exit(1);
} else {
console.log('\x1b[32m%s\x1b[0m', 'All translation keys have complete language coverage!');
}

View File

@@ -111,8 +111,6 @@ const EXCLUDED_TECHNICAL_STRINGS = [
"GitLab API", // Git provider specific terminology
"Pull Request", // Git provider specific terminology
"GitHub API", // Git provider specific terminology
"add-secret-form", // Test ID for secret form
"edit-secret-form", // Test ID for secret form
];
function isExcludedTechnicalString(str) {

View File

@@ -10,6 +10,7 @@ import {
GetTrajectoryResponse,
GitChangeDiff,
GitChange,
ConversationTrigger,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
import { ApiSettings, PostApiSettings, Provider } from "#/types/settings";
@@ -143,6 +144,7 @@ class OpenHands {
}
static async createConversation(
conversation_trigger: ConversationTrigger = "gui",
selectedRepository?: string,
git_provider?: Provider,
initialUserMsg?: string,
@@ -152,6 +154,7 @@ class OpenHands {
selected_branch?: string,
): Promise<Conversation> {
const body = {
conversation_trigger,
repository: selectedRepository,
git_provider,
selected_branch,

View File

@@ -1,43 +1,8 @@
import { openHands } from "./open-hands-axios";
import {
CustomSecret,
GetSecretsResponse,
POSTProviderTokens,
} from "./secrets-service.types";
import { Provider, ProviderToken } from "#/types/settings";
import { openHands } from "./open-hands-axios";
import { POSTProviderTokens } from "./secrets-service.types";
export class SecretsService {
static async getSecrets() {
const { data } = await openHands.get<GetSecretsResponse>("/api/secrets");
return data.custom_secrets;
}
static async createSecret(name: string, value: string, description?: string) {
const secret: CustomSecret = {
name,
value,
description,
};
const { status } = await openHands.post("/api/secrets", secret);
return status === 201;
}
static async updateSecret(id: string, name: string, description?: string) {
const secret: Omit<CustomSecret, "value"> = {
name,
description,
};
const { status } = await openHands.put(`/api/secrets/${id}`, secret);
return status === 200;
}
static async deleteSecret(id: string) {
const { status } = await openHands.delete<boolean>(`/api/secrets/${id}`);
return status === 200;
}
static async addGitProvider(providers: Record<Provider, ProviderToken>) {
const tokens: POSTProviderTokens = {
provider_tokens: providers,

View File

@@ -1,15 +1,5 @@
import { Provider, ProviderToken } from "#/types/settings";
export type CustomSecret = {
name: string;
value: string;
description?: string;
};
export interface GetSecretsResponse {
custom_secrets: Omit<CustomSecret, "value">[];
}
export interface POSTProviderTokens {
provider_tokens: Record<Provider, ProviderToken>;
}

View File

@@ -1,11 +1,11 @@
import posthog from "posthog-js";
import React from "react";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { SuggestionItem } from "#/components/features/suggestions/suggestion-item";
import type { RootState } from "#/store";
import { I18nKey } from "#/i18n/declaration";
import { useUserProviders } from "#/hooks/use-user-providers";
import { useConversation } from "#/context/conversation-context";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
interface ActionSuggestionsProps {
onSuggestionsClick: (value: string) => void;
@@ -16,13 +16,17 @@ export function ActionSuggestions({
}: ActionSuggestionsProps) {
const { t } = useTranslation();
const { providers } = useUserProviders();
const { conversationId } = useConversation();
const { data: conversation } = useUserConversation(conversationId);
const { selectedRepository } = useSelector(
(state: RootState) => state.initialQuery,
);
const [hasPullRequest, setHasPullRequest] = React.useState(false);
const providersAreSet = providers.length > 0;
const isGitLab = providers.includes("gitlab");
const isGitLab =
selectedRepository !== null &&
selectedRepository.git_provider &&
selectedRepository.git_provider.toLowerCase() === "gitlab";
const pr = isGitLab ? "merge request" : "pull request";
const prShort = isGitLab ? "MR" : "PR";
@@ -41,7 +45,7 @@ export function ActionSuggestions({
return (
<div className="flex flex-col gap-2 mb-2">
{providersAreSet && conversation?.selected_repository && (
{providersAreSet && selectedRepository && (
<div className="flex flex-row gap-2 justify-center w-full">
{!hasPullRequest ? (
<>

View File

@@ -6,7 +6,6 @@ import { cn } from "#/utils/utils";
import { ul, ol } from "../markdown/list";
import { CopyToClipboardButton } from "#/components/shared/buttons/copy-to-clipboard-button";
import { anchor } from "../markdown/anchor";
import { paragraph } from "../markdown/paragraph";
interface ChatMessageProps {
type: "user" | "assistant";
@@ -65,7 +64,6 @@ export function ChatMessage({
ul,
ol,
a: anchor,
p: paragraph,
}}
remarkPlugins={[remarkGfm]}
>

View File

@@ -15,7 +15,6 @@ import { OpenHandsObservation } from "#/types/core/observations";
import { cn } from "#/utils/utils";
import { code } from "../markdown/code";
import { ol, ul } from "../markdown/list";
import { paragraph } from "../markdown/paragraph";
import { MonoComponent } from "./mono-component";
import { PathComponent } from "./path-component";
@@ -43,7 +42,7 @@ export function ExpandableMessage({
}: ExpandableMessageProps) {
const { data: config } = useConfig();
const { t, i18n } = useTranslation();
const [showDetails, setShowDetails] = useState(true);
const [showDetails, setShowDetails] = useState(false);
const [details, setDetails] = useState(message);
const [translationId, setTranslationId] = useState<string | undefined>(id);
const [translationParams, setTranslationParams] = useState<
@@ -197,7 +196,6 @@ export function ExpandableMessage({
code,
ul,
ol,
p: paragraph,
}}
remarkPlugins={[remarkGfm]}
>

View File

@@ -7,11 +7,13 @@ import { I18nKey } from "#/i18n/declaration";
interface AccountSettingsContextMenuProps {
onLogout: () => void;
onClose: () => void;
isLoggedIn: boolean;
}
export function AccountSettingsContextMenu({
onLogout,
onClose,
isLoggedIn,
}: AccountSettingsContextMenuProps) {
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
const { t } = useTranslation();
@@ -22,7 +24,7 @@ export function AccountSettingsContextMenu({
ref={ref}
className="absolute right-full md:left-full -top-1 z-10 w-fit"
>
<ContextMenuListItem onClick={onLogout}>
<ContextMenuListItem onClick={onLogout} isDisabled={!isLoggedIn}>
{t(I18nKey.ACCOUNT_SETTINGS$LOGOUT)}
</ContextMenuListItem>
</ContextMenu>

View File

@@ -28,7 +28,7 @@ export function HomeHeader() {
testId="header-launch-button"
variant="primary"
type="button"
onClick={() => createConversation({})}
onClick={() => createConversation({ conversation_trigger: "gui" })}
isDisabled={isCreatingConversation}
>
{!isCreatingConversation && "Launch from Scratch"}

View File

@@ -30,8 +30,6 @@ export function RepositorySelectionForm({
const [selectedBranch, setSelectedBranch] = React.useState<Branch | null>(
null,
);
// Add a ref to track if the branch was manually cleared by the user
const branchManuallyClearedRef = React.useRef<boolean>(false);
const {
data: repositories,
isLoading: isLoadingRepositories,
@@ -54,14 +52,13 @@ export function RepositorySelectionForm({
const debouncedSearchQuery = useDebounce(searchQuery, 300);
const { data: searchedRepos } = useSearchRepositories(debouncedSearchQuery);
// Auto-select main or master branch if it exists, but only if the branch wasn't manually cleared
// Auto-select main or master branch if it exists
React.useEffect(() => {
if (
branches &&
branches.length > 0 &&
!selectedBranch &&
!isLoadingBranches &&
!branchManuallyClearedRef.current // Only auto-select if not manually cleared
!isLoadingBranches
) {
// Look for main or master branch
const mainBranch = branches.find((branch) => branch.name === "main");
@@ -74,7 +71,7 @@ export function RepositorySelectionForm({
setSelectedBranch(masterBranch);
}
}
}, [branches, isLoadingBranches, selectedBranch]);
}, [branches, selectedBranch, isLoadingBranches]);
// We check for isSuccess because the app might require time to render
// into the new conversation screen after the conversation is created.
@@ -100,14 +97,11 @@ export function RepositorySelectionForm({
if (selectedRepo) onRepoSelection(selectedRepo.full_name);
setSelectedRepository(selectedRepo || null);
setSelectedBranch(null); // Reset branch selection when repo changes
branchManuallyClearedRef.current = false; // Reset the flag when repo changes
};
const handleBranchSelection = (key: React.Key | null) => {
const selectedBranchObj = branches?.find((branch) => branch.name === key);
setSelectedBranch(selectedBranchObj || null);
// Reset the manually cleared flag when a branch is explicitly selected
branchManuallyClearedRef.current = false;
};
const handleRepoInputChange = (value: string) => {
@@ -122,15 +116,8 @@ export function RepositorySelectionForm({
};
const handleBranchInputChange = (value: string) => {
// Clear the selected branch if the input is empty or contains only whitespace
// This fixes the issue where users can't delete the entire default branch name
if (value === "" || value.trim() === "") {
if (value === "") {
setSelectedBranch(null);
// Set the flag to indicate that the branch was manually cleared
branchManuallyClearedRef.current = true;
} else {
// Reset the flag when the user starts typing again
branchManuallyClearedRef.current = false;
}
};
@@ -213,6 +200,7 @@ export function RepositorySelectionForm({
onClick={() =>
createConversation({
selectedRepository,
conversation_trigger: "gui",
selected_branch: selectedBranch?.name,
})
}

View File

@@ -40,6 +40,7 @@ export function TaskCard({ task }: TaskCardProps) {
const repo = getRepo(task.repo, task.git_provider);
return createConversation({
conversation_trigger: "suggested_task",
selectedRepository: repo,
suggested_task: task,
});

View File

@@ -4,7 +4,6 @@ import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { JupyterLine } from "#/utils/parse-cell-content";
import { paragraph } from "../markdown/paragraph";
interface JupyterCellOutputProps {
lines: JupyterLine[];
@@ -26,12 +25,7 @@ export function JupyterCellOutput({ lines }: JupyterCellOutputProps) {
if (line.type === "image") {
return (
<div key={index}>
<Markdown
components={{
p: paragraph,
}}
urlTransform={(value: string) => value}
>
<Markdown urlTransform={(value: string) => value}>
{line.content}
</Markdown>
</div>

View File

@@ -1,11 +0,0 @@
import React from "react";
import { ExtraProps } from "react-markdown";
// Custom component to render <p> in markdown with bottom padding
export function paragraph({
children,
}: React.ClassAttributes<HTMLParagraphElement> &
React.HTMLAttributes<HTMLParagraphElement> &
ExtraProps) {
return <p className="pb-[10px] last:pb-0">{children}</p>;
}

View File

@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { useTranslation, Trans } from "react-i18next";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { BrandButton } from "#/components/features/settings/brand-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
@@ -64,21 +64,7 @@ export function ApiKeysManager() {
</div>
<p className="text-sm text-gray-300">
<Trans
i18nKey={I18nKey.SETTINGS$API_KEYS_DESCRIPTION}
components={{
a: (
<a
href="https://docs.all-hands.dev/modules/usage/cloud/cloud-api"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:underline"
>
API documentation
</a>
),
}}
/>
{t(I18nKey.SETTINGS$API_KEYS_DESCRIPTION)}
</p>
{isLoading && (

View File

@@ -51,9 +51,10 @@ export function GitHubTokenInput({
placeholder="github.com"
defaultValue={githubHostSet || undefined}
startContent={
githubHostSet &&
githubHostSet.trim() !== "" && (
githubHostSet && githubHostSet.trim() !== "" ? (
<KeyStatusIcon testId="gh-set-host-indicator" isSet />
) : (
<KeyStatusIcon testId="gh-set-host-indicator" isSet={false} />
)
}
/>

View File

@@ -51,9 +51,10 @@ export function GitLabTokenInput({
placeholder="gitlab.com"
defaultValue={gitlabHostSet || undefined}
startContent={
gitlabHostSet &&
gitlabHostSet.trim() !== "" && (
gitlabHostSet && gitlabHostSet.trim() !== "" ? (
<KeyStatusIcon testId="gl-set-host-indicator" isSet />
) : (
<KeyStatusIcon testId="gl-set-host-indicator" isSet={false} />
)
}
/>

View File

@@ -1,202 +0,0 @@
import { useQueryClient } from "@tanstack/react-query";
import React from "react";
import { useTranslation } from "react-i18next";
import { useCreateSecret } from "#/hooks/mutation/use-create-secret";
import { useUpdateSecret } from "#/hooks/mutation/use-update-secret";
import { SettingsInput } from "../settings-input";
import { cn } from "#/utils/utils";
import { BrandButton } from "../brand-button";
import { useGetSecrets } from "#/hooks/query/use-get-secrets";
import { GetSecretsResponse } from "#/api/secrets-service.types";
import { OptionalTag } from "../optional-tag";
interface SecretFormProps {
mode: "add" | "edit";
selectedSecret: string | null;
onCancel: () => void;
}
export function SecretForm({
mode,
selectedSecret,
onCancel,
}: SecretFormProps) {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { data: secrets } = useGetSecrets();
const { mutate: createSecret } = useCreateSecret();
const { mutate: updateSecret } = useUpdateSecret();
const [error, setError] = React.useState<string | null>(null);
const secretDescription =
(mode === "edit" &&
selectedSecret &&
secrets
?.find((secret) => secret.name === selectedSecret)
?.description?.trim()) ||
"";
const handleCreateSecret = (
name: string,
value: string,
description?: string,
) => {
createSecret(
{ name, value, description },
{
onSettled: onCancel,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["secrets"] });
},
},
);
};
const updateSecretOptimistically = (
oldName: string,
name: string,
description?: string,
) => {
queryClient.setQueryData<GetSecretsResponse["custom_secrets"]>(
["secrets"],
(oldSecrets) => {
if (!oldSecrets) return [];
return oldSecrets.map((secret) => {
if (secret.name === oldName) {
return {
...secret,
name,
description,
};
}
return secret;
});
},
);
};
const revertOptimisticUpdate = () => {
queryClient.invalidateQueries({ queryKey: ["secrets"] });
};
const handleEditSecret = (
secretToEdit: string,
name: string,
description?: string,
) => {
updateSecretOptimistically(secretToEdit, name, description);
updateSecret(
{ secretToEdit, name, description },
{
onSettled: onCancel,
onError: revertOptimisticUpdate,
},
);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const name = formData.get("secret-name")?.toString();
const value = formData.get("secret-value")?.toString().trim();
const description = formData.get("secret-description")?.toString();
if (name) {
setError(null);
const isNameAlreadyUsed = secrets?.some(
(secret) => secret.name === name && secret.name !== selectedSecret,
);
if (isNameAlreadyUsed) {
setError("Secret already exists");
return;
}
if (mode === "add") {
if (!value) {
setError(t("SECRETS$SECRET_VALUE_REQUIRED"));
return;
}
handleCreateSecret(name, value, description || undefined);
} else if (mode === "edit" && selectedSecret) {
handleEditSecret(selectedSecret, name, description || undefined);
}
}
};
const formTestId = mode === "add" ? "add-secret-form" : "edit-secret-form";
return (
<form
data-testid={formTestId}
onSubmit={handleSubmit}
className="flex flex-col items-start gap-6"
>
<SettingsInput
testId="name-input"
name="secret-name"
type="text"
label="Name"
className="w-[350px]"
required
defaultValue={mode === "edit" && selectedSecret ? selectedSecret : ""}
placeholder="e.g. OpenAI_API_Key"
pattern="^\S*$"
/>
{error && <p className="text-red-500 text-sm">{error}</p>}
{mode === "add" && (
<label className="flex flex-col gap-2.5 w-fit">
<span className="text-sm">Value</span>
<textarea
data-testid="value-input"
name="secret-value"
required
className={cn(
"resize-none w-[680px]",
"bg-tertiary border border-[#717888] rounded p-2 placeholder:italic placeholder:text-tertiary-alt",
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
)}
rows={8}
/>
</label>
)}
<label className="flex flex-col gap-2.5 w-fit">
<div className="flex items-center gap-2">
<span className="text-sm">Description</span>
<OptionalTag />
</div>
<input
data-testid="description-input"
name="secret-description"
defaultValue={secretDescription}
className={cn(
"resize-none w-[680px]",
"bg-tertiary border border-[#717888] rounded p-2 placeholder:italic placeholder:text-tertiary-alt",
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
)}
/>
</label>
<div className="flex items-center gap-4">
<BrandButton
testId="cancel-button"
type="button"
variant="secondary"
onClick={onCancel}
>
Cancel
</BrandButton>
<BrandButton testId="submit-button" type="submit" variant="primary">
{mode === "add" && t("SECRETS$ADD_SECRET")}
{mode === "edit" && t("SECRETS$EDIT_SECRET")}
</BrandButton>
</div>
</form>
);
}

View File

@@ -1,63 +0,0 @@
import { FaPencil, FaTrash } from "react-icons/fa6";
export function SecretListItemSkeleton() {
return (
<div className="border-t border-[#717888] last-of-type:border-b max-w-[830px] pr-2.5 py-[13px] flex items-center justify-between">
<div className="flex items-center justify-between w-1/3">
<span className="skeleton h-4 w-1/2" />
<span className="skeleton h-4 w-1/4" />
</div>
<div className="flex items-center gap-8">
<span className="skeleton h-4 w-4" />
<span className="skeleton h-4 w-4" />
</div>
</div>
);
}
interface SecretListItemProps {
title: string;
description?: string;
onEdit: () => void;
onDelete: () => void;
}
export function SecretListItem({
title,
description,
onEdit,
onDelete,
}: SecretListItemProps) {
return (
<tr
data-testid="secret-item"
className="border-t border-[#717888] last-of-type:border-b max-w-[830px] py-[13px] flex w-full items-center"
>
<td className="w-1/4 text-sm text-content-2">{title}</td>
<td className="w-1/2 truncate overflow-hidden whitespace-nowrap text-sm text-content-2 opacity-80 italic">
{description || "-"}
</td>
<td className="w-1/4 flex items-center justify-end gap-4">
<button
data-testid="edit-secret-button"
type="button"
onClick={onEdit}
aria-label={`Edit ${title}`}
>
<FaPencil size={16} />
</button>
<button
data-testid="delete-secret-button"
type="button"
onClick={onDelete}
aria-label={`Delete ${title}`}
>
<FaTrash size={16} />
</button>
</td>
</tr>
);
}

View File

@@ -14,11 +14,9 @@ interface SettingsInputProps {
startContent?: React.ReactNode;
className?: string;
onChange?: (value: string) => void;
required?: boolean;
min?: number;
max?: number;
step?: number;
pattern?: string;
}
export function SettingsInput({
@@ -34,11 +32,9 @@ export function SettingsInput({
startContent,
className,
onChange,
required,
min,
max,
step,
pattern,
}: SettingsInputProps) {
return (
<label className={cn("flex flex-col gap-2.5 w-fit", className)}>
@@ -59,8 +55,6 @@ export function SettingsInput({
min={min}
max={max}
step={step}
required={required}
pattern={pattern}
className={cn(
"bg-tertiary border border-[#717888] h-10 w-full rounded p-2 placeholder:italic placeholder:text-tertiary-alt",
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",

View File

@@ -35,6 +35,7 @@ export function UserActions({ onLogout, user, isLoading }: UserActionsProps) {
{accountContextMenuIsVisible && (
<AccountSettingsContextMenu
isLoggedIn={!!user}
onLogout={handleLogout}
onClose={closeAccountMenu}
/>

View File

@@ -1,45 +0,0 @@
import { BrandButton } from "#/components/features/settings/brand-button";
import { ModalBackdrop } from "./modal-backdrop";
interface ConfirmationModalProps {
text: string;
onConfirm: () => void;
onCancel: () => void;
}
export function ConfirmationModal({
text,
onConfirm,
onCancel,
}: ConfirmationModalProps) {
return (
<ModalBackdrop onClose={onCancel}>
<div
data-testid="confirmation-modal"
className="bg-base-secondary p-4 rounded-xl flex flex-col gap-4 border border-tertiary"
>
<p>{text}</p>
<div className="w-full flex gap-2">
<BrandButton
testId="cancel-button"
type="button"
onClick={onCancel}
variant="secondary"
className="grow"
>
Cancel
</BrandButton>
<BrandButton
testId="confirm-button"
type="button"
onClick={onConfirm}
variant="primary"
className="grow"
>
Confirm
</BrandButton>
</div>
</div>
</ModalBackdrop>
);
}

View File

@@ -52,7 +52,7 @@ export function TaskForm({ ref }: TaskFormProps) {
const formData = new FormData(event.currentTarget);
const q = formData.get("q")?.toString();
createConversation({ q });
createConversation({ q, conversation_trigger: "gui" });
};
return (

View File

@@ -6,6 +6,7 @@ import OpenHands from "#/api/open-hands";
import { setInitialPrompt } from "#/state/initial-query-slice";
import { RootState } from "#/store";
import { GitRepository } from "#/types/git";
import { ConversationTrigger } from "#/api/open-hands.types";
import { SuggestedTask } from "#/components/features/home/tasks/task.types";
export const useCreateConversation = () => {
@@ -20,6 +21,7 @@ export const useCreateConversation = () => {
return useMutation({
mutationKey: ["create-conversation"],
mutationFn: async (variables: {
conversation_trigger: ConversationTrigger;
q?: string;
selectedRepository?: GitRepository | null;
selected_branch?: string;
@@ -28,6 +30,7 @@ export const useCreateConversation = () => {
if (variables.q) dispatch(setInitialPrompt(variables.q));
return OpenHands.createConversation(
variables.conversation_trigger,
variables.selectedRepository
? variables.selectedRepository.full_name
: undefined,

View File

@@ -1,15 +0,0 @@
import { useMutation } from "@tanstack/react-query";
import { SecretsService } from "#/api/secrets-service";
export const useCreateSecret = () =>
useMutation({
mutationFn: ({
name,
value,
description,
}: {
name: string;
value: string;
description?: string;
}) => SecretsService.createSecret(name, value, description),
});

View File

@@ -1,7 +0,0 @@
import { useMutation } from "@tanstack/react-query";
import { SecretsService } from "#/api/secrets-service";
export const useDeleteSecret = () =>
useMutation({
mutationFn: (id: string) => SecretsService.deleteSecret(id),
});

View File

@@ -15,7 +15,6 @@ export const useLogout = () => {
queryClient.removeQueries({ queryKey: ["tasks"] });
queryClient.removeQueries({ queryKey: ["settings"] });
queryClient.removeQueries({ queryKey: ["user"] });
queryClient.removeQueries({ queryKey: ["secrets"] });
posthog.reset();
await navigate("/");

View File

@@ -1,15 +0,0 @@
import { useMutation } from "@tanstack/react-query";
import { SecretsService } from "#/api/secrets-service";
export const useUpdateSecret = () =>
useMutation({
mutationFn: ({
secretToEdit,
name,
description,
}: {
secretToEdit: string;
name: string;
description?: string;
}) => SecretsService.updateSecret(secretToEdit, name, description),
});

View File

@@ -29,7 +29,7 @@ export const useGetGitChanges = () => {
// Latest changes should be on top
React.useEffect(() => {
if (!result.isFetching && result.isSuccess && result.data) {
if (result.data) {
const currentData = result.data;
// If this is new data (not the same reference as before)
@@ -59,11 +59,10 @@ export const useGetGitChanges = () => {
}
}
}
}, [result.isFetching, result.isSuccess, result.data]);
}, [result.data]);
return {
data: orderedChanges,
isLoading: result.isLoading,
isSuccess: result.isSuccess,
isError: result.isError,
error: result.error,

View File

@@ -1,17 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { SecretsService } from "#/api/secrets-service";
import { useUserProviders } from "../use-user-providers";
import { useConfig } from "./use-config";
export const useGetSecrets = () => {
const { data: config } = useConfig();
const { providers } = useUserProviders();
const isOss = config?.APP_MODE === "oss";
return useQuery({
queryKey: ["secrets"],
queryFn: SecretsService.getSecrets,
enabled: isOss || providers.length > 0,
});
};

View File

@@ -1,11 +1,5 @@
// this file generate by script, don't modify it manually!!!
export enum I18nKey {
SECRETS$SECRET_VALUE_REQUIRED = "SECRETS$SECRET_VALUE_REQUIRED",
SECRETS$ADD_SECRET = "SECRETS$ADD_SECRET",
SECRETS$EDIT_SECRET = "SECRETS$EDIT_SECRET",
SECRETS$NO_SECRETS_FOUND = "SECRETS$NO_SECRETS_FOUND",
SECRETS$ADD_NEW_SECRET = "SECRETS$ADD_NEW_SECRET",
SECRETS$CONFIRM_DELETE_KEY = "SECRETS$CONFIRM_DELETE_KEY",
SETTINGS$MCP_TITLE = "SETTINGS$MCP_TITLE",
SETTINGS$MCP_DESCRIPTION = "SETTINGS$MCP_DESCRIPTION",
SETTINGS$NAV_MCP = "SETTINGS$NAV_MCP",
@@ -66,7 +60,6 @@ export enum I18nKey {
SETTINGS$NAV_GIT = "SETTINGS$NAV_GIT",
SETTINGS$NAV_APPLICATION = "SETTINGS$NAV_APPLICATION",
SETTINGS$NAV_CREDITS = "SETTINGS$NAV_CREDITS",
SETTINGS$NAV_SECRETS = "SETTINGS$NAV_SECRETS",
SETTINGS$NAV_API_KEYS = "SETTINGS$NAV_API_KEYS",
SETTINGS$NAV_LLM = "SETTINGS$NAV_LLM",
GIT$MERGE_REQUEST = "GIT$MERGE_REQUEST",

View File

@@ -1,100 +1,4 @@
{
"SECRETS$SECRET_VALUE_REQUIRED": {
"en": "Secret value is required",
"ja": "シークレット値は必須です",
"zh-CN": "密钥值是必需的",
"zh-TW": "密鑰值是必需的",
"ko-KR": "비밀 값이 필요합니다",
"no": "Hemmelig verdi er påkrevd",
"it": "Il valore del segreto è obbligatorio",
"pt": "O valor do segredo é obrigatório",
"es": "El valor del secreto es obligatorio",
"ar": "قيمة السر مطلوبة",
"fr": "La valeur du secret est requise",
"tr": "Gizli değer gereklidir",
"de": "Geheimer Wert ist erforderlich",
"uk": "Значення секрету є обов'язковим"
},
"SECRETS$ADD_SECRET": {
"en": "Add secret",
"ja": "シークレットを追加",
"zh-CN": "添加密钥",
"zh-TW": "添加密鑰",
"ko-KR": "비밀 추가",
"no": "Legg til hemmelighet",
"it": "Aggiungi segreto",
"pt": "Adicionar segredo",
"es": "Añadir secreto",
"ar": "إضافة سر",
"fr": "Ajouter un secret",
"tr": "Gizli ekle",
"de": "Geheimnis hinzufügen",
"uk": "Додати секрет"
},
"SECRETS$EDIT_SECRET": {
"en": "Edit secret",
"ja": "シークレットを編集",
"zh-CN": "编辑密钥",
"zh-TW": "編輯密鑰",
"ko-KR": "비밀 편집",
"no": "Rediger hemmelighet",
"it": "Modifica segreto",
"pt": "Editar segredo",
"es": "Editar secreto",
"ar": "تعديل السر",
"fr": "Modifier le secret",
"tr": "Gizliyi düzenle",
"de": "Geheimnis bearbeiten",
"uk": "Редагувати секрет"
},
"SECRETS$NO_SECRETS_FOUND": {
"en": "No secrets found",
"ja": "シークレットが見つかりません",
"zh-CN": "未找到密钥",
"zh-TW": "未找到密鑰",
"ko-KR": "비밀을 찾을 수 없습니다",
"no": "Ingen hemmeligheter funnet",
"it": "Nessun segreto trovato",
"pt": "Nenhum segredo encontrado",
"es": "No se encontraron secretos",
"ar": "لم يتم العثور على أسرار",
"fr": "Aucun secret trouvé",
"tr": "Gizli bulunamadı",
"de": "Keine Geheimnisse gefunden",
"uk": "Секретів не знайдено"
},
"SECRETS$ADD_NEW_SECRET": {
"en": "Add a new secret",
"ja": "新しいシークレットを追加",
"zh-CN": "添加新密钥",
"zh-TW": "添加新密鑰",
"ko-KR": "새 비밀 추가",
"no": "Legg til en ny hemmelighet",
"it": "Aggiungi un nuovo segreto",
"pt": "Adicionar um novo segredo",
"es": "Añadir un nuevo secreto",
"ar": "إضافة سر جديد",
"fr": "Ajouter un nouveau secret",
"tr": "Yeni bir gizli ekle",
"de": "Neues Geheimnis hinzufügen",
"uk": "Додати новий секрет"
},
"SECRETS$CONFIRM_DELETE_KEY": {
"en": "Are you sure you want to delete this key?",
"ja": "このキーを削除してもよろしいですか?",
"zh-CN": "您确定要删除此密钥吗?",
"zh-TW": "您確定要刪除此密鑰嗎?",
"ko-KR": "이 키를 삭제하시겠습니까?",
"no": "Er du sikker på at du vil slette denne nøkkelen?",
"it": "Sei sicuro di voler eliminare questa chiave?",
"pt": "Tem certeza de que deseja excluir esta chave?",
"es": "¿Está seguro de que desea eliminar esta clave?",
"ar": "هل أنت متأكد أنك تريد حذف هذا المفتاح؟",
"fr": "Êtes-vous sûr de vouloir supprimer cette clé ?",
"tr": "Bu anahtarı silmek istediğinizden emin misiniz?",
"de": "Sind Sie sicher, dass Sie diesen Schlüssel löschen möchten?",
"uk": "Ви впевнені, що хочете видалити цей ключ?"
},
"SETTINGS$MCP_TITLE": {
"en": "Model Context Protocol (MCP)",
"ja": "モデルコンテキストプロトコル (MCP)",
@@ -1055,22 +959,6 @@
"de": "Guthaben",
"uk": "Кредити"
},
"SETTINGS$NAV_SECRETS": {
"en": "Secrets",
"ja": "シークレット",
"zh-CN": "机密",
"zh-TW": "機密",
"ko-KR": "비밀",
"no": "Hemmeligheter",
"it": "Segreti",
"pt": "Segredos",
"es": "Secretos",
"ar": "أسرار",
"fr": "Secrets",
"tr": "Sırları",
"de": "Geheimnisse",
"uk": "Секрети"
},
"SETTINGS$NAV_API_KEYS": {
"en": "API Keys",
"ja": "APIキー",
@@ -1181,6 +1069,7 @@
"fr": "Copier dans le presse-papiers",
"tr": "Panoya kopyala",
"de": "In die Zwischenablage kopieren",
"fa": "کپی به کلیپ‌بورد",
"uk": "Копіювати в буфер обміну"
},
"BUTTON$COPIED": {
@@ -1197,6 +1086,7 @@
"fr": "Copié dans le presse-papiers",
"tr": "Panoya kopyalandı",
"de": "In die Zwischenablage kopiert",
"fa": "در کلیپ‌بورد کپی شد",
"uk": "Copied to clipboard"
},
"APP$TITLE": {
@@ -2268,8 +2158,7 @@
"ar": "مضيف GitHub (اختياري)",
"fr": "Hôte GitHub (optionnel)",
"tr": "GitHub Sunucusu (isteğe bağlı)",
"de": "GitHub-Host (optional)",
"uk": "Хост GitHub (необов'язково)"
"de": "GitHub-Host (optional)"
},
"GITHUB$TOKEN_OPTIONAL": {
"en": "GitHub Token (Optional)",
@@ -2867,33 +2756,13 @@
"en": "Settings not found. Please check your API key",
"es": "Configuraciones no encontradas. Por favor revisa tu API key",
"zh-TW": "找不到設定。請檢查您的 API 金鑰",
"uk": "Налаштування не знайдено. Будь ласка, перевірте свій ключ API.",
"ja": "設定が見つかりません。APIキーを確認してください",
"zh-CN": "未找到设置。请检查您的API密钥",
"ko-KR": "설정을 찾을 수 없습니다. API 키를 확인하세요",
"no": "Innstillinger ikke funnet. Vennligst sjekk API-nøkkelen din",
"ar": "لم يتم العثور على الإعدادات. يرجى التحقق من مفتاح API الخاص بك",
"de": "Einstellungen nicht gefunden. Bitte überprüfen Sie Ihren API-Schlüssel",
"fr": "Paramètres non trouvés. Veuillez vérifier votre clé API",
"it": "Impostazioni non trovate. Controlla la tua chiave API",
"pt": "Configurações não encontradas. Por favor, verifique sua chave API",
"tr": "Ayarlar bulunamadı. Lütfen API anahtarınızı kontrol edin"
"uk": "Налаштування не знайдено. Будь ласка, перевірте свій ключ API."
},
"CONNECT_TO_GITHUB_BY_TOKEN_MODAL$TERMS_OF_SERVICE": {
"en": "terms of service",
"es": "términos de servicio",
"zh-TW": "服務條款",
"uk": "умови обслуговування",
"ja": "利用規約",
"zh-CN": "服务条款",
"ko-KR": "서비스 약관",
"no": "tjenestevilkår",
"ar": "شروط الخدمة",
"de": "Nutzungsbedingungen",
"fr": "conditions d'utilisation",
"it": "termini di servizio",
"pt": "termos de serviço",
"tr": "hizmet şartları"
"uk": "умови обслуговування"
},
"SESSION$SERVER_CONNECTED_MESSAGE": {
"en": "Connected to server",
@@ -2908,8 +2777,7 @@
"ar": "تم الاتصال بالخادم",
"tr": "Sunucuya bağlandı",
"no": "Koblet til server",
"uk": "Підключено до сервера",
"ja": "サーバーに接続しました"
"uk": "Підключено до сервера"
},
"SESSION$SESSION_HANDLING_ERROR_MESSAGE": {
"en": "Error handling message",
@@ -3866,10 +3734,7 @@
"de": "Keine Ergebnisse gefunden.",
"it": "Nessun risultato trovato.",
"pt": "Nenhum resultado encontrado.",
"uk": "Результатів не знайдено.",
"no": "Ingen resultater funnet.",
"ar": "لم يتم العثور على نتائج.",
"tr": "Sonuç bulunamadı."
"uk": "Результатів не знайдено."
},
"GITHUB$LOADING_REPOSITORIES": {
"en": "Loading repositories...",
@@ -4252,8 +4117,7 @@
"ar": "Upload a .json",
"no": "Upload a .json",
"tr": "Upload a .json",
"uk": "Завантажити .json",
"ja": ".jsonをアップロード"
"uk": "Завантажити .json"
},
"LANDING$RECENT_CONVERSATION": {
"en": "jump back to your most recent conversation",
@@ -4381,6 +4245,7 @@
"ar": "أو",
"no": "Eller",
"tr": "veya",
"fa": "یا",
"uk": "Або"
},
"SUGGESTIONS$TEST_COVERAGE": {
@@ -4413,6 +4278,7 @@
"ar": "دمج تلقائي لطلبات سحب Dependabot",
"no": "Auto-flett Dependabot PRs",
"tr": "Otomatik birleştirme",
"fa": "ادغام خودکار درخواست‌های Dependabot",
"uk": "Автоматичне об'єднання Dependabot PR"
},
"CHAT_INTERFACE$AGENT_STOPPED_MESSAGE": {
@@ -5073,323 +4939,83 @@
},
"SETTINGS$API_KEYS": {
"en": "API Keys",
"uk": "API ключі",
"ja": "APIキー",
"zh-CN": "API密钥",
"zh-TW": "API金鑰",
"ko-KR": "API 키",
"no": "API-nøkler",
"ar": "مفاتيح API",
"de": "API-Schlüssel",
"fr": "Clés API",
"it": "Chiavi API",
"pt": "Chaves API",
"es": "Claves API",
"tr": "API Anahtarları"
"uk": "API ключі"
},
"SETTINGS$API_KEYS_DESCRIPTION": {
"en": "API keys allow you to authenticate with the OpenHands API programmatically. Keep your API keys secure; anyone with your API key can access your account. For more information on how to use the API, see our <a>API documentation</a>.",
"ja": "APIキーを使用すると、OpenHands APIにプログラムで認証できます。APIキーは安全に保管してください。APIキーを持っている人は誰でもあなたのアカウントにアクセスできます。APIの使用方法の詳細については、<a>APIドキュメント</a>をご覧ください。",
"zh-CN": "API密钥允许您以编程方式验证OpenHands API。请确保您的API密钥安全任何拥有您API密钥的人都可以访问您的账户。有关如何使用API的更多信息请参阅我们的<a>API文档</a>。",
"zh-TW": "API密鑰允許您以編程方式驗證OpenHands API。請確保您的API密鑰安全任何擁有您API密鑰的人都可以訪問您的賬戶。有關如何使用API的更多信息請參閱我們的<a>API文檔</a>。",
"ko-KR": "API 키를 사용하면 OpenHands API에 프로그래밍 방식으로 인증할 수 있습니다. API 키를 안전하게 보관하세요. API 키가 있는 사람은 누구나 귀하의 계정에 액세스할 수 있습니다. API 사용 방법에 대한 자세한 내용은 <a>API 문서</a>를 참조하세요.",
"no": "API-nøkler lar deg autentisere med OpenHands API programmatisk. Hold API-nøklene dine sikre; alle med API-nøkkelen din kan få tilgang til kontoen din. For mer informasjon om hvordan du bruker API-et, se vår <a>API-dokumentasjon</a>.",
"it": "Le chiavi API ti consentono di autenticarti con l'API OpenHands in modo programmatico. Mantieni le tue chiavi API al sicuro; chiunque abbia la tua chiave API può accedere al tuo account. Per ulteriori informazioni su come utilizzare l'API, consulta la nostra <a>documentazione API</a>.",
"pt": "As chaves de API permitem que você se autentique com a API OpenHands programaticamente. Mantenha suas chaves de API seguras; qualquer pessoa com sua chave de API pode acessar sua conta. Para obter mais informações sobre como usar a API, consulte nossa <a>documentação da API</a>.",
"es": "Las claves API le permiten autenticarse con la API de OpenHands de forma programática. Mantenga sus claves API seguras; cualquier persona con su clave API puede acceder a su cuenta. Para obtener más información sobre cómo usar la API, consulte nuestra <a>documentación de API</a>.",
"ar": "تتيح لك مفاتيح API المصادقة مع واجهة برمجة تطبيقات OpenHands برمجيًا. حافظ على أمان مفاتيح API الخاصة بك؛ يمكن لأي شخص لديه مفتاح API الخاص بك الوصول إلى حسابك. لمزيد من المعلومات حول كيفية استخدام واجهة برمجة التطبيقات، راجع <a>وثائق API</a> الخاصة بنا.",
"fr": "Les clés API vous permettent de vous authentifier auprès de l'API OpenHands par programmation. Gardez vos clés API en sécurité ; toute personne disposant de votre clé API peut accéder à votre compte. Pour plus d'informations sur l'utilisation de l'API, consultez notre <a>documentation API</a>.",
"tr": "API anahtarları, OpenHands API ile programlı olarak kimlik doğrulamanıza olanak tanır. API anahtarlarınızı güvende tutun; API anahtarınıza sahip olan herkes hesabınıza erişebilir. API'nin nasıl kullanılacağı hakkında daha fazla bilgi için <a>API belgelerimize</a> bakın.",
"de": "API-Schlüssel ermöglichen es Ihnen, sich programmatisch bei der OpenHands-API zu authentifizieren. Halten Sie Ihre API-Schlüssel sicher; jeder mit Ihrem API-Schlüssel kann auf Ihr Konto zugreifen. Weitere Informationen zur Verwendung der API finden Sie in unserer <a>API-Dokumentation</a>.",
"uk": "Ключі API дозволяють вам програмно автентифікуватися за допомогою API OpenHands. Зберігайте свої ключі API в безпеці; будь-хто, хто має ваш ключ API, може отримати доступ до вашого облікового запису. Для отримання додаткової інформації про використання API, перегляньте нашу <a>документацію API</a>."
"en": "API keys allow you to authenticate with the OpenHands API programmatically. Keep your API keys secure; anyone with your API key can access your account.",
"uk": "Ключі API дозволяють вам програмно автентифікуватися за допомогою API OpenHands. Зберігайте свої ключі API в безпеці; будь-хто, хто має ваш ключ API, може отримати доступ до вашого облікового запису."
},
"SETTINGS$CREATE_API_KEY": {
"en": "Create API Key",
"uk": "Створити API ключ",
"ja": "APIキーを作成",
"zh-CN": "创建API密钥",
"zh-TW": "創建API金鑰",
"ko-KR": "API 키 생성",
"no": "Opprett API-nøkkel",
"ar": "إنشاء مفتاح API",
"de": "API-Schlüssel erstellen",
"fr": "Créer une clé API",
"it": "Crea chiave API",
"pt": "Criar chave API",
"es": "Crear clave API",
"tr": "API Anahtarı Oluştur"
"uk": "Створити API ключ"
},
"SETTINGS$CREATE_API_KEY_DESCRIPTION": {
"en": "Give your API key a descriptive name to help you identify it later.",
"uk": "Дайте своєму ключу API змістовну назву, щоб ви могли його пізніше ідентифікувати.",
"ja": "後で識別しやすいように、APIキーにわかりやすい名前を付けてください。",
"zh-CN": "为您的API密钥提供一个描述性名称以便您以后能够识别它。",
"zh-TW": "為您的API金鑰提供一個描述性名稱以便您以後能夠識別它。",
"ko-KR": "나중에 식별하는 데 도움이 되도록 API 키에 설명적인 이름을 지정하세요.",
"no": "Gi API-nøkkelen din et beskrivende navn for å hjelpe deg med å identifisere den senere.",
"ar": "أعط مفتاح API الخاص بك اسمًا وصفيًا لمساعدتك في التعرف عليه لاحقًا.",
"de": "Geben Sie Ihrem API-Schlüssel einen beschreibenden Namen, um ihn später leichter identifizieren zu können.",
"fr": "Donnez à votre clé API un nom descriptif pour vous aider à l'identifier ultérieurement.",
"it": "Dai alla tua chiave API un nome descrittivo per aiutarti a identificarla in seguito.",
"pt": "Dê à sua chave API um nome descritivo para ajudá-lo a identificá-la posteriormente.",
"es": "Asigne a su clave API un nombre descriptivo para ayudarle a identificarla más adelante.",
"tr": "API anahtarınıza, daha sonra tanımlamanıza yardımcı olacak açıklayıcı bir isim verin."
"uk": "Дайте своєму ключу API змістовну назву, щоб ви могли його пізніше ідентифікувати."
},
"SETTINGS$DELETE_API_KEY": {
"en": "Delete API Key",
"uk": "Видалити API ключ",
"ja": "APIキーを削除",
"zh-CN": "删除API密钥",
"zh-TW": "刪除API金鑰",
"ko-KR": "API 키 삭제",
"no": "Slett API-nøkkel",
"ar": "حذف مفتاح API",
"de": "API-Schlüssel löschen",
"fr": "Supprimer la clé API",
"it": "Elimina chiave API",
"pt": "Excluir chave API",
"es": "Eliminar clave API",
"tr": "API Anahtarını Sil"
"uk": "Видалити API ключ"
},
"SETTINGS$DELETE_API_KEY_CONFIRMATION": {
"en": "Are you sure you want to delete the API key \"{{name}}\"? This action cannot be undone.",
"uk": "Ви впевнені, що хочете видалити ключ API \"{{name}}\"? Цю дію не можна скасувати.",
"ja": "APIキー\"{{name}}\"を削除してもよろしいですか?この操作は元に戻せません。",
"zh-CN": "您确定要删除API密钥\"{{name}}\"吗?此操作无法撤消。",
"zh-TW": "您確定要刪除API金鑰\"{{name}}\"嗎?此操作無法撤消。",
"ko-KR": "API 키 \"{{name}}\"을(를) 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
"no": "Er du sikker på at du vil slette API-nøkkelen \"{{name}}\"? Denne handlingen kan ikke angres.",
"ar": "هل أنت متأكد أنك تريد حذف مفتاح API \"{{name}}\"؟ لا يمكن التراجع عن هذا الإجراء.",
"de": "Sind Sie sicher, dass Sie den API-Schlüssel \"{{name}}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"fr": "Êtes-vous sûr de vouloir supprimer la clé API \"{{name}}\" ? Cette action ne peut pas être annulée.",
"it": "Sei sicuro di voler eliminare la chiave API \"{{name}}\"? Questa azione non può essere annullata.",
"pt": "Tem certeza de que deseja excluir a chave API \"{{name}}\"? Esta ação não pode ser desfeita.",
"es": "¿Está seguro de que desea eliminar la clave API \"{{name}}\"? Esta acción no se puede deshacer.",
"tr": "\"{{name}}\" API anahtarını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz."
"uk": "Ви впевнені, що хочете видалити ключ API \"{{name}}\"? Цю дію не можна скасувати."
},
"SETTINGS$NO_API_KEYS": {
"en": "You don't have any API keys yet. Create one to get started.",
"uk": "У вас ще немає API-ключів. Створіть один, щоб почати.",
"ja": "まだAPIキーがありません。作成して始めましょう。",
"zh-CN": "您还没有任何API密钥。创建一个以开始使用。",
"zh-TW": "您還沒有任何API金鑰。創建一個以開始使用。",
"ko-KR": "아직 API 키가 없습니다. 시작하려면 하나를 생성하세요.",
"no": "Du har ingen API-nøkler ennå. Opprett en for å komme i gang.",
"ar": "ليس لديك أي مفاتيح API حتى الآن. قم بإنشاء واحد للبدء.",
"de": "Sie haben noch keine API-Schlüssel. Erstellen Sie einen, um zu beginnen.",
"fr": "Vous n'avez pas encore de clés API. Créez-en une pour commencer.",
"it": "Non hai ancora chiavi API. Creane una per iniziare.",
"pt": "Você ainda não tem nenhuma chave API. Crie uma para começar.",
"es": "Aún no tiene ninguna clave API. Cree una para comenzar.",
"tr": "Henüz API anahtarınız yok. Başlamak için bir tane oluşturun."
"uk": "У вас ще немає API-ключів. Створіть один, щоб почати."
},
"SETTINGS$NAME": {
"en": "Name",
"uk": "Назва",
"ja": "名前",
"zh-CN": "名称",
"zh-TW": "名稱",
"ko-KR": "이름",
"no": "Navn",
"ar": "الاسم",
"de": "Name",
"fr": "Nom",
"it": "Nome",
"pt": "Nome",
"es": "Nombre",
"tr": "İsim"
"uk": "Назва"
},
"SETTINGS$KEY_PREFIX": {
"en": "Key Prefix",
"uk": "Префікс ключа",
"ja": "キーのプレフィックス",
"zh-CN": "密钥前缀",
"zh-TW": "金鑰前綴",
"ko-KR": "키 접두사",
"no": "Nøkkelprefix",
"ar": "بادئة المفتاح",
"de": "Schlüsselpräfix",
"fr": "Préfixe de clé",
"it": "Prefisso chiave",
"pt": "Prefixo da chave",
"es": "Prefijo de clave",
"tr": "Anahtar Öneki"
"uk": "Префікс ключа"
},
"SETTINGS$CREATED_AT": {
"en": "Created",
"uk": "Створено",
"ja": "作成日時",
"zh-CN": "创建时间",
"zh-TW": "創建時間",
"ko-KR": "생성됨",
"no": "Opprettet",
"ar": "تم الإنشاء",
"de": "Erstellt",
"fr": "Créé",
"it": "Creato",
"pt": "Criado",
"es": "Creado",
"tr": "Oluşturuldu"
"uk": "Створено"
},
"SETTINGS$LAST_USED": {
"en": "Last Used",
"uk": "Останнє використання",
"ja": "最終使用日時",
"zh-CN": "最后使用时间",
"zh-TW": "最後使用時間",
"ko-KR": "마지막 사용",
"no": "Sist brukt",
"ar": "آخر استخدام",
"de": "Zuletzt verwendet",
"fr": "Dernière utilisation",
"it": "Ultimo utilizzo",
"pt": "Último uso",
"es": "Último uso",
"tr": "Son Kullanım"
"uk": "Останнє використання"
},
"SETTINGS$ACTIONS": {
"en": "Actions",
"uk": "Дії",
"ja": "アクション",
"zh-CN": "操作",
"zh-TW": "操作",
"ko-KR": "작업",
"no": "Handlinger",
"ar": "إجراءات",
"de": "Aktionen",
"fr": "Actions",
"it": "Azioni",
"pt": "Ações",
"es": "Acciones",
"tr": "İşlemler"
"uk": "Дії"
},
"SETTINGS$API_KEY_CREATED": {
"en": "API Key Created",
"uk": "API-ключ створено",
"ja": "APIキーが作成されました",
"zh-CN": "API密钥已创建",
"zh-TW": "API金鑰已創建",
"ko-KR": "API 키 생성됨",
"no": "API-nøkkel opprettet",
"ar": "تم إنشاء مفتاح API",
"de": "API-Schlüssel erstellt",
"fr": "Clé API créée",
"it": "Chiave API creata",
"pt": "Chave API criada",
"es": "Clave API creada",
"tr": "API Anahtarı Oluşturuldu"
"uk": "API-ключ створено"
},
"SETTINGS$API_KEY_DELETED": {
"en": "API key deleted successfully",
"uk": "API-ключ видалено",
"ja": "APIキーが正常に削除されました",
"zh-CN": "API密钥已成功删除",
"zh-TW": "API金鑰已成功刪除",
"ko-KR": "API 키가 성공적으로 삭제되었습니다",
"no": "API-nøkkel slettet",
"ar": "تم حذف مفتاح API بنجاح",
"de": "API-Schlüssel erfolgreich gelöscht",
"fr": "Clé API supprimée avec succès",
"it": "Chiave API eliminata con successo",
"pt": "Chave API excluída com sucesso",
"es": "Clave API eliminada con éxito",
"tr": "API anahtarı başarıyla silindi"
"uk": "API-ключ видалено"
},
"SETTINGS$API_KEY_WARNING": {
"en": "This is the only time your API key will be displayed. Please copy it now and store it securely.",
"uk": "Це єдиний раз, коли буде відображено ваш ключ API. Будь ласка, скопіюйте його зараз і збережіть у безпеці.",
"ja": "これはAPIキーが表示される唯一の機会です。今すぐコピーして安全に保管してください。",
"zh-CN": "这是您的API密钥唯一显示的时间。请立即复制并安全存储。",
"zh-TW": "這是您的API金鑰唯一顯示的時間。請立即複製並安全存儲。",
"ko-KR": "이것은 API 키가 표시되는 유일한 시간입니다. 지금 복사하여 안전하게 보관하세요.",
"no": "Dette er den eneste gangen API-nøkkelen din vil bli vist. Vennligst kopier den nå og lagre den sikkert.",
"ar": "هذه هي المرة الوحيدة التي سيتم فيها عرض مفتاح API الخاص بك. يرجى نسخه الآن وتخزينه بشكل آمن.",
"de": "Dies ist das einzige Mal, dass Ihr API-Schlüssel angezeigt wird. Bitte kopieren Sie ihn jetzt und speichern Sie ihn sicher.",
"fr": "C'est la seule fois que votre clé API sera affichée. Veuillez la copier maintenant et la stocker en toute sécurité.",
"it": "Questa è l'unica volta che la tua chiave API verrà visualizzata. Copiala ora e conservala in modo sicuro.",
"pt": "Esta é a única vez que sua chave API será exibida. Por favor, copie-a agora e armazene-a com segurança.",
"es": "Esta es la única vez que se mostrará su clave API. Por favor, cópiela ahora y guárdela de forma segura.",
"tr": "API anahtarınız yalnızca bu kez görüntülenecektir. Lütfen şimdi kopyalayın ve güvenli bir şekilde saklayın."
"uk": "Це єдиний раз, коли буде відображено ваш ключ API. Будь ласка, скопіюйте його зараз і збережіть у безпеці.."
},
"SETTINGS$API_KEY_COPIED": {
"en": "API key copied to clipboard",
"uk": "Ключ API скопійовано в буфер обміну",
"ja": "APIキーがクリップボードにコピーされました",
"zh-CN": "API密钥已复制到剪贴板",
"zh-TW": "API金鑰已複製到剪貼板",
"ko-KR": "API 키가 클립보드에 복사되었습니다",
"no": "API-nøkkel kopiert til utklippstavlen",
"ar": "تم نسخ مفتاح API إلى الحافظة",
"de": "API-Schlüssel in die Zwischenablage kopiert",
"fr": "Clé API copiée dans le presse-papiers",
"it": "Chiave API copiata negli appunti",
"pt": "Chave API copiada para a área de transferência",
"es": "Clave API copiada al portapapeles",
"tr": "API anahtarı panoya kopyalandı"
"uk": "Ключ API скопійовано в буфер обміну"
},
"SETTINGS$API_KEY_NAME_PLACEHOLDER": {
"en": "My API Key",
"uk": "Мій ключ API",
"ja": "私のAPIキー",
"zh-CN": "我的API密钥",
"zh-TW": "我的API金鑰",
"ko-KR": "내 API 키",
"no": "Min API-nøkkel",
"ar": "مفتاح API الخاص بي",
"de": "Mein API-Schlüssel",
"fr": "Ma clé API",
"it": "La mia chiave API",
"pt": "Minha chave API",
"es": "Mi clave API",
"tr": "API Anahtarım"
"uk": "Мій ключ API"
},
"BUTTON$CREATE": {
"en": "Create",
"uk": "Створити",
"ja": "作成",
"zh-CN": "创建",
"zh-TW": "創建",
"ko-KR": "생성",
"no": "Opprett",
"ar": "إنشاء",
"de": "Erstellen",
"fr": "Créer",
"it": "Crea",
"pt": "Criar",
"es": "Crear",
"tr": "Oluştur"
"uk": "Створити"
},
"BUTTON$DELETE": {
"en": "Delete",
"uk": "Видалити",
"ja": "削除",
"zh-CN": "删除",
"zh-TW": "刪除",
"ko-KR": "삭제",
"no": "Slett",
"ar": "حذف",
"de": "Löschen",
"fr": "Supprimer",
"it": "Elimina",
"pt": "Excluir",
"es": "Eliminar",
"tr": "Sil"
"uk": "Видалити"
},
"BUTTON$COPY_TO_CLIPBOARD": {
"en": "Copy to Clipboard",
"uk": "Копіювати в буфер обміну",
"ja": "クリップボードにコピー",
"zh-CN": "复制到剪贴板",
"zh-TW": "複製到剪貼板",
"ko-KR": "클립보드에 복사",
"no": "Kopier til utklippstavlen",
"ar": "نسخ إلى الحافظة",
"de": "In die Zwischenablage kopieren",
"fr": "Copier dans le presse-papiers",
"it": "Copia negli appunti",
"pt": "Copiar para a área de transferência",
"es": "Copiar al portapapeles",
"tr": "Panoya Kopyala"
"uk": "Копіювати в буфер обміну"
},
"BUTTON$REFRESH": {
"en": "Refresh",
@@ -5409,19 +5035,7 @@
},
"ERROR$REQUIRED_FIELD": {
"en": "This field is required",
"uk": "Це поле обов'язкове",
"ja": "この項目は必須です",
"zh-CN": "此字段为必填项",
"zh-TW": "此欄位為必填項",
"ko-KR": "이 필드는 필수입니다",
"no": "Dette feltet er påkrevd",
"ar": "هذا الحقل مطلوب",
"de": "Dieses Feld ist erforderlich",
"fr": "Ce champ est obligatoire",
"it": "Questo campo è obbligatorio",
"pt": "Este campo é obrigatório",
"es": "Este campo es obligatorio",
"tr": "Bu alan zorunludur"
"uk": "Це поле обов'язкове"
},
"PLANNER$EMPTY_MESSAGE": {
"en": "No plan created.",
@@ -6225,19 +5839,7 @@
},
"AGENT_ERROR$TOO_MANY_CONVERSATIONS": {
"en": "Too many conversations at once.",
"uk": "Занадто багато розмов одночасно.",
"ja": "一度に多すぎる会話があります。",
"zh-CN": "同时进行的对话过多。",
"zh-TW": "同時進行的對話過多。",
"ko-KR": "한 번에 너무 많은 대화가 있습니다.",
"no": "For mange samtaler på en gang.",
"ar": "الكثير من المحادثات في وقت واحد.",
"de": "Zu viele Gespräche auf einmal.",
"fr": "Trop de conversations à la fois.",
"it": "Troppe conversazioni contemporaneamente.",
"pt": "Muitas conversas ao mesmo tempo.",
"es": "Demasiadas conversaciones a la vez.",
"tr": "Aynı anda çok fazla konuşma var."
"uk": "Занадто багато розмов одночасно."
},
"PROJECT_MENU_CARD_CONTEXT_MENU$CONNECT_TO_GITHUB_LABEL": {
"en": "Connect to GitHub",
@@ -6349,6 +5951,7 @@
"pt": "Captura de tela do navegador",
"es": "Captura de pantalla del navegador",
"tr": "Tarayıcı ekran görüntüsü",
"fa": "تصویر صفحه مرورگر",
"uk": "Знімок екрана браузера"
},
"ERROR_TOAST$CLOSE_BUTTON_LABEL": {
@@ -6656,20 +6259,20 @@
"uk": "Результат інструменту MCP: {{action.payload.args.name}}"
},
"OBSERVATION_MESSAGE$RECALL": {
"en": "Microagent ready",
"ja": "マイクロエージェントの準備完了",
"zh-CN": "微代理已就绪",
"zh-TW": "微代理已就緒",
"ko-KR": "마이크로에이전트 준비됨",
"no": "MikroAgent klar",
"it": "Microagent pronto",
"pt": "Microagent pronto",
"es": "Microagent listo",
"ar": "الوكيل المصغر جاهز",
"fr": "Microagent prêt",
"tr": "MikroAjan hazır",
"de": "Microagent bereit",
"uk": "Мікроагент готовий"
"en": "Microagent Activated",
"ja": "マイクロエージェントが有効化されました",
"zh-CN": "微代理已激活",
"zh-TW": "微代理已啟動",
"ko-KR": "마이크로에이전트 활성화됨",
"no": "MikroAgent aktivert",
"it": "Microagent attivato",
"pt": "Microagent ativado",
"es": "Microagent activado",
"ar": "تم تنشيط الوكيل المصغر",
"fr": "Microagent activé",
"tr": "MikroAjan Etkinleştirildi",
"de": "Microagent aktiviert",
"uk": "Мікроагент активований"
},
"EXPANDABLE_MESSAGE$SHOW_DETAILS": {
"en": "Show details",
@@ -6994,18 +6597,7 @@
"SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL": {
"en": "Enable Memory Condenser",
"zh-TW": "啟用記憶體壓縮器",
"uk": "Увімкнути конденсатор пам'яті",
"ja": "メモリコンデンサーを有効にする",
"zh-CN": "启用内存压缩器",
"ko-KR": "메모리 컨덴서 활성화",
"no": "Aktiver minnekondensator",
"ar": "تمكين مكثف الذاكرة",
"de": "Speicherkondensator aktivieren",
"fr": "Activer le condensateur de mémoire",
"it": "Abilita condensatore di memoria",
"pt": "Ativar condensador de memória",
"es": "Habilitar condensador de memoria",
"tr": "Bellek Yoğunlaştırıcıyı Etkinleştir"
"uk": "Увімкнути конденсатор пам'яті"
},
"BUTTON$MARK_HELPFUL": {
"en": "Mark this solution as helpful",
@@ -7516,8 +7108,7 @@
"es": "Ventana de contexto",
"ar": "نافذة السياق",
"fr": "Fenêtre de contexte",
"tr": "Bağlam Penceresi",
"uk": "Вікно контексту"
"tr": "Bağlam Penceresi"
},
"CONVERSATION$USED": {
"en": "used",
@@ -7532,8 +7123,7 @@
"es": "usado",
"ar": "مستخدم",
"fr": "utilisé",
"tr": "kullanıldı",
"uk": "використано"
"tr": "kullanıldı"
},
"SETTINGS$RUNTIME_SETTINGS": {
"en": "Runtime Settings (",
@@ -7884,8 +7474,7 @@
"ar": "مضيف GitLab (اختياري)",
"fr": "Hôte GitLab (optionnel)",
"tr": "GitLab Sunucusu (isteğe bağlı)",
"de": "GitLab-Host (optional)",
"uk": "Хост GitLab (необов'язково)"
"de": "GitLab-Host (optional)"
},
"GITLAB$GET_TOKEN": {
"en": "Generate a token on",
@@ -8000,20 +7589,20 @@
"uk": "Дію не виконано. Можливо, це сталося через натискання користувачем кнопки зупинки або через збій та перезапуск системи виконання через обмеження ресурсів. Можливо, було втрачено будь-який раніше встановлений стан системи, залежності або змінні середовища."
},
"DIFF_VIEWER$LOADING": {
"en": "Loading changes...",
"ja": "変更を読み込んでいます...",
"zh-CN": "正在加载更改...",
"zh-TW": "正在加載更改...",
"ko-KR": "변경 사항을 로드하는 중...",
"no": "Laster inn endringer...",
"it": "Caricamento modifiche...",
"pt": "Carregando alterações...",
"es": "Cargando cambios...",
"ar": "جارٍ تحميل التغييرات...",
"fr": "Chargement des modifications...",
"tr": "Değişiklikler yükleniyor...",
"de": "Änderungen werden geladen...",
"uk": "Завантаження змін..."
"en": "Loading...",
"ja": "読み込み中...",
"zh-CN": "加载中...",
"zh-TW": "加載中...",
"ko-KR": "로딩 중...",
"no": "Laster inn...",
"it": "Caricamento in corso...",
"pt": "Carregando...",
"es": "Cargando...",
"ar": "جار التحميل...",
"fr": "Chargement...",
"tr": "Yükleniyor...",
"de": "Wird geladen...",
"uk": "Завантаження..."
},
"DIFF_VIEWER$GETTING_LATEST_CHANGES": {
"en": "Getting latest changes...",
@@ -8218,10 +7807,7 @@
"de": "Nutzungsbedingungen akzeptieren",
"it": "Accetta i termini di servizio",
"pt": "Aceitar termos de serviço",
"uk": "Прийняти Умови надання послуг",
"no": "Godta vilkår for tjenesten",
"ar": "قبول شروط الخدمة",
"tr": "Hizmet Şartlarını Kabul Et"
"uk": "Прийняти Умови надання послуг"
},
"TOS$ACCEPT_TERMS_DESCRIPTION": {
"en": "Please review and accept our terms of service before continuing",
@@ -8234,10 +7820,7 @@
"de": "Bitte überprüfen und akzeptieren Sie unsere Nutzungsbedingungen, bevor Sie fortfahren",
"it": "Si prega di rivedere e accettare i nostri termini di servizio prima di continuare",
"pt": "Por favor, revise e aceite nossos termos de serviço antes de continuar",
"uk": "Будь ласка, ознайомтеся та прийміть наші умови надання послуг, перш ніж продовжити",
"no": "Vennligst gjennomgå og godta våre vilkår for tjenesten før du fortsetter",
"ar": "يرجى مراجعة وقبول شروط الخدمة الخاصة بنا قبل المتابعة",
"tr": "Devam etmeden önce lütfen hizmet şartlarımızı gözden geçirin ve kabul edin"
"uk": "Будь ласка, ознайомтеся та прийміть наші умови надання послуг, перш ніж продовжити"
},
"TOS$CONTINUE": {
"en": "Continue",
@@ -8250,10 +7833,7 @@
"de": "Fortfahren",
"it": "Continua",
"pt": "Continuar",
"uk": "Продовжити",
"no": "Fortsett",
"ar": "متابعة",
"tr": "Devam Et"
"uk": "Продовжити"
},
"TOS$ERROR_ACCEPTING": {
"en": "Error accepting Terms of Service",
@@ -8266,10 +7846,7 @@
"de": "Fehler beim Akzeptieren der Nutzungsbedingungen",
"it": "Errore nell'accettazione dei Termini di Servizio",
"pt": "Erro ao aceitar os Termos de Serviço",
"uk": "Помилка прийняття Умов обслуговування",
"no": "Feil ved godkjenning av vilkår for tjenesten",
"ar": "خطأ في قبول شروط الخدمة",
"tr": "Hizmet Şartlarını kabul ederken hata oluştu"
"uk": "Помилка прийняття Умов обслуговування"
},
"TIPS$CUSTOMIZE_MICROAGENT": {
"en": "You can customize OpenHands for your repo using a microagent. Ask OpenHands to put a description of the repo, including how to run the code, into .openhands/microagents/repo.md.",

View File

@@ -10,7 +10,6 @@ import { ApiSettings, PostApiSettings, Provider } from "#/types/settings";
import { FILE_SERVICE_HANDLERS } from "./file-service-handlers";
import { GitRepository, GitUser } from "#/types/git";
import { TASK_SUGGESTIONS_HANDLERS } from "./task-suggestions-handlers";
import { SECRETS_HANDLERS } from "./secrets-handlers";
export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
llm_model: DEFAULT_SETTINGS.LLM_MODEL,
@@ -93,7 +92,7 @@ const openHandsHandlers = [
"gpt-4o",
"gpt-4o-mini",
"anthropic/claude-3.5",
"anthropic/claude-3-7-sonnet-20250219",
"anthropic/claude-3-5-sonnet-20241022",
]),
),
@@ -119,7 +118,6 @@ export const handlers = [
...STRIPE_BILLING_HANDLERS,
...FILE_SERVICE_HANDLERS,
...TASK_SUGGESTIONS_HANDLERS,
...SECRETS_HANDLERS,
...openHandsHandlers,
http.get("/api/user/repositories", () => {
const data: GitRepository[] = [
@@ -166,7 +164,7 @@ export const handlers = [
POSTHOG_CLIENT_KEY: "fake-posthog-client-key",
STRIPE_PUBLISHABLE_KEY: "",
FEATURE_FLAGS: {
ENABLE_BILLING: false,
ENABLE_BILLING: mockSaas,
HIDE_LLM_SETTINGS: mockSaas,
},
};
@@ -212,6 +210,8 @@ export const handlers = [
HttpResponse.json({ message: "Authenticated" }),
),
http.get("/api/options/config", () => HttpResponse.json({ APP_MODE: "oss" })),
http.get("/api/conversations", async () => {
const values = Array.from(CONVERSATIONS.values());
const results: ResultSet<Conversation> = {

View File

@@ -1,72 +0,0 @@
import { http, HttpResponse } from "msw";
import { CustomSecret, GetSecretsResponse } from "#/api/secrets-service.types";
const DEFAULT_SECRETS: CustomSecret[] = [
{
name: "OpenAI_API_Key",
value: "test-123",
description: "OpenAI API Key",
},
{
name: "Google_Maps_API_Key",
value: "test-123",
description: "Google Maps API Key",
},
];
const secrets = new Map<string, CustomSecret>(
DEFAULT_SECRETS.map((secret) => [secret.name, secret]),
);
export const SECRETS_HANDLERS = [
http.get("/api/secrets", async () => {
const secretsArray = Array.from(secrets.values());
const secretsWithoutValue: Omit<CustomSecret, "value">[] = secretsArray.map(
({ value, ...rest }) => rest,
);
const data: GetSecretsResponse = {
custom_secrets: secretsWithoutValue,
};
return HttpResponse.json(data);
}),
http.post("/api/secrets", async ({ request }) => {
const body = (await request.json()) as CustomSecret;
if (typeof body === "object" && body && body.name) {
secrets.set(body.name, body);
return HttpResponse.json(true);
}
return HttpResponse.json(false, { status: 400 });
}),
http.put("/api/secrets/:id", async ({ params, request }) => {
const { id } = params;
const body = (await request.json()) as Omit<CustomSecret, "value">;
if (typeof id === "string" && typeof body === "object") {
const secret = secrets.get(id);
if (secret && body && body.name) {
const newSecret: CustomSecret = { ...secret, ...body };
secrets.delete(id);
secrets.set(body.name, newSecret);
return HttpResponse.json(true);
}
}
return HttpResponse.json(false, { status: 400 });
}),
http.delete("/api/secrets/:id", async ({ params }) => {
const { id } = params;
if (typeof id === "string") {
secrets.delete(id);
return HttpResponse.json(true);
}
return HttpResponse.json(false, { status: 400 });
}),
];

View File

@@ -15,7 +15,6 @@ export default [
route("git", "routes/git-settings.tsx"),
route("app", "routes/app-settings.tsx"),
route("billing", "routes/billing.tsx"),
route("secrets", "routes/secrets-settings.tsx"),
route("api-keys", "routes/api-keys.tsx"),
]),
route("conversations/:conversationId", "routes/conversation.tsx", [

View File

@@ -14,7 +14,7 @@ const GIT_REPO_ERROR_PATTERN = /not a git repository/i;
function StatusMessage({ children }: React.PropsWithChildren) {
return (
<div className="w-full h-full flex flex-col items-center text-center justify-center text-2xl text-tertiary-light">
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
{children}
</div>
);
@@ -22,17 +22,7 @@ function StatusMessage({ children }: React.PropsWithChildren) {
function GitChanges() {
const { t } = useTranslation();
const {
data: gitChanges,
isSuccess,
isError,
error,
isLoading: loadingGitChanges,
} = useGetGitChanges();
const [statusMessage, setStatusMessage] = React.useState<string[] | null>(
null,
);
const { data: gitChanges, isSuccess, isError, error } = useGetGitChanges();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const runtimeIsActive = !RUNTIME_INACTIVE_STATES.includes(curAgentState);
@@ -40,44 +30,29 @@ function GitChanges() {
const isNotGitRepoError =
error && GIT_REPO_ERROR_PATTERN.test(retrieveAxiosErrorMessage(error));
React.useEffect(() => {
if (!runtimeIsActive) {
setStatusMessage([I18nKey.DIFF_VIEWER$WAITING_FOR_RUNTIME]);
} else if (error) {
const errorMessage = retrieveAxiosErrorMessage(error);
if (GIT_REPO_ERROR_PATTERN.test(errorMessage)) {
setStatusMessage([
I18nKey.DIFF_VIEWER$NOT_A_GIT_REPO,
I18nKey.DIFF_VIEWER$ASK_OH,
]);
} else {
setStatusMessage([errorMessage]);
}
} else if (loadingGitChanges) {
setStatusMessage([I18nKey.DIFF_VIEWER$LOADING]);
let statusMessage: React.ReactNode = null;
if (!runtimeIsActive) {
statusMessage = <span>{t(I18nKey.DIFF_VIEWER$WAITING_FOR_RUNTIME)}</span>;
} else if (isNotGitRepoError) {
if (error) {
statusMessage = <span>{retrieveAxiosErrorMessage(error)}</span>;
} else {
setStatusMessage(null);
statusMessage = (
<span>
{t(I18nKey.DIFF_VIEWER$NOT_A_GIT_REPO)}
<br />
{t(I18nKey.DIFF_VIEWER$ASK_OH)}
</span>
);
}
}, [
runtimeIsActive,
isNotGitRepoError,
loadingGitChanges,
error,
setStatusMessage,
]);
}
return (
<main className="h-full overflow-y-scroll px-4 py-3 gap-3 flex flex-col items-center">
{!isSuccess || !gitChanges.length ? (
<div className="relative flex h-full w-full items-center">
<div className="absolute inset-x-0 top-1/2 -translate-y-1/2">
{statusMessage && (
<StatusMessage>
{statusMessage.map((msg) => (
<span key={msg}>{t(msg)}</span>
))}
</StatusMessage>
)}
{statusMessage && <StatusMessage>{statusMessage}</StatusMessage>}
</div>
<div className="absolute inset-x-0 bottom-0">

View File

@@ -304,9 +304,9 @@ function LlmSettingsScreen() {
name="llm-custom-model-input"
label={t(I18nKey.SETTINGS$CUSTOM_MODEL)}
defaultValue={
settings.LLM_MODEL || "anthropic/claude-3-7-sonnet-20250219"
settings.LLM_MODEL || "anthropic/claude-3-5-sonnet-20241022"
}
placeholder="anthropic/claude-3-7-sonnet-20250219"
placeholder="anthropic/claude-3-5-sonnet-20241022"
type="text"
className="w-[680px]"
onChange={handleCustomModelIsDirty}

View File

@@ -17,6 +17,7 @@ import { AuthModal } from "#/components/features/waitlist/auth-modal";
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
import { useSettings } from "#/hooks/query/use-settings";
import { useMigrateUserConsent } from "#/hooks/use-migrate-user-consent";
import { useBalance } from "#/hooks/query/use-balance";
import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal";
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
@@ -59,6 +60,7 @@ export default function MainApp() {
const { pathname } = useLocation();
const tosPageStatus = useIsOnTosPage();
const { data: settings } = useSettings();
const { error } = useBalance();
const { migrateUserConsent } = useMigrateUserConsent();
const { t } = useTranslation();
@@ -66,8 +68,7 @@ export default function MainApp() {
const {
data: isAuthed,
isFetching: isFetchingAuth,
isError: isAuthError,
error: authError,
isError: authError,
} = useIsAuthed();
// Always call the hook, but we'll only use the result when not on TOS page
@@ -119,13 +120,13 @@ export default function MainApp() {
React.useEffect(() => {
// Don't do any redirects when on TOS page
// Don't allow users to use the app if it 402s
if (!tosPageStatus && authError?.status === 402 && pathname !== "/") {
if (!tosPageStatus && error?.status === 402 && pathname !== "/") {
navigate("/");
}
}, [authError?.status, pathname, tosPageStatus]);
}, [error?.status, pathname, tosPageStatus]);
// When on TOS page, we don't make any API calls, so we need to handle this case
const userIsAuthed = tosPageStatus ? false : !!isAuthed && !isAuthError;
const userIsAuthed = tosPageStatus ? false : !!isAuthed && !authError;
// Only show the auth modal if:
// 1. User is not authenticated

View File

@@ -1,151 +0,0 @@
import { useQueryClient } from "@tanstack/react-query";
import React from "react";
import { Link } from "react-router";
import { useTranslation } from "react-i18next";
import { useGetSecrets } from "#/hooks/query/use-get-secrets";
import { useDeleteSecret } from "#/hooks/mutation/use-delete-secret";
import { SecretForm } from "#/components/features/settings/secrets-settings/secret-form";
import {
SecretListItem,
SecretListItemSkeleton,
} from "#/components/features/settings/secrets-settings/secret-list-item";
import { BrandButton } from "#/components/features/settings/brand-button";
import { ConfirmationModal } from "#/components/shared/modals/confirmation-modal";
import { GetSecretsResponse } from "#/api/secrets-service.types";
import { useUserProviders } from "#/hooks/use-user-providers";
import { useConfig } from "#/hooks/query/use-config";
function SecretsSettingsScreen() {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { data: config } = useConfig();
const { data: secrets, isLoading: isLoadingSecrets } = useGetSecrets();
const { mutate: deleteSecret } = useDeleteSecret();
const { providers } = useUserProviders();
const isSaas = config?.APP_MODE === "saas";
const hasProviderSet = providers.length > 0;
const [view, setView] = React.useState<
"list" | "add-secret-form" | "edit-secret-form"
>("list");
const [selectedSecret, setSelectedSecret] = React.useState<string | null>(
null,
);
const [confirmationModalIsVisible, setConfirmationModalIsVisible] =
React.useState(false);
const deleteSecretOptimistically = (secret: string) => {
queryClient.setQueryData<GetSecretsResponse["custom_secrets"]>(
["secrets"],
(oldSecrets) => {
if (!oldSecrets) return [];
return oldSecrets.filter((s) => s.name !== secret);
},
);
};
const revertOptimisticUpdate = () => {
queryClient.invalidateQueries({ queryKey: ["secrets"] });
};
const handleDeleteSecret = (secret: string) => {
deleteSecretOptimistically(secret);
deleteSecret(secret, {
onSettled: () => {
setConfirmationModalIsVisible(false);
},
onError: revertOptimisticUpdate,
});
};
const onConfirmDeleteSecret = () => {
if (selectedSecret) handleDeleteSecret(selectedSecret);
};
const onCancelDeleteSecret = () => {
setConfirmationModalIsVisible(false);
};
const shouldRenderConnectToGitButton = isSaas && !hasProviderSet;
return (
<div
data-testid="secrets-settings-screen"
className="px-11 py-9 flex flex-col gap-5"
>
{isLoadingSecrets && view === "list" && (
<ul>
<SecretListItemSkeleton />
<SecretListItemSkeleton />
<SecretListItemSkeleton />
</ul>
)}
{shouldRenderConnectToGitButton && (
<Link to="/settings/git" data-testid="connect-git-button" type="button">
<BrandButton type="button" variant="secondary">
Connect a Git provider to manage secrets
</BrandButton>
</Link>
)}
{secrets?.length === 0 && view === "list" && (
<p data-testid="no-secrets-message">{t("SECRETS$NO_SECRETS_FOUND")}</p>
)}
{view === "list" && (
<table className="w-full">
<tbody>
{secrets?.map((secret) => (
<SecretListItem
key={secret.name}
title={secret.name}
description={secret.description}
onEdit={() => {
setView("edit-secret-form");
setSelectedSecret(secret.name);
}}
onDelete={() => {
setConfirmationModalIsVisible(true);
setSelectedSecret(secret.name);
}}
/>
))}
</tbody>
</table>
)}
{!shouldRenderConnectToGitButton && view === "list" && (
<BrandButton
testId="add-secret-button"
type="button"
variant="primary"
onClick={() => setView("add-secret-form")}
isDisabled={isLoadingSecrets}
>
{t("SECRETS$ADD_NEW_SECRET")}
</BrandButton>
)}
{(view === "add-secret-form" || view === "edit-secret-form") && (
<SecretForm
mode={view === "add-secret-form" ? "add" : "edit"}
selectedSecret={selectedSecret}
onCancel={() => setView("list")}
/>
)}
{confirmationModalIsVisible && (
<ConfirmationModal
text={t("SECRETS$CONFIRM_DELETE_KEY")}
onConfirm={onConfirmDeleteSecret}
onCancel={onCancelDeleteSecret}
/>
)}
</div>
);
}
export default SecretsSettingsScreen;

View File

@@ -18,7 +18,6 @@ function SettingsScreen() {
{ to: "/settings/git", text: t("SETTINGS$NAV_GIT") },
{ to: "/settings/app", text: t("SETTINGS$NAV_APPLICATION") },
{ to: "/settings/billing", text: t("SETTINGS$NAV_CREDITS") },
{ to: "/settings/secrets", text: t("SETTINGS$NAV_SECRETS") },
{ to: "/settings/api-keys", text: t("SETTINGS$NAV_API_KEYS") },
];
@@ -27,7 +26,6 @@ function SettingsScreen() {
{ to: "/settings/mcp", text: t("SETTINGS$NAV_MCP") },
{ to: "/settings/git", text: t("SETTINGS$NAV_GIT") },
{ to: "/settings/app", text: t("SETTINGS$NAV_APPLICATION") },
{ to: "/settings/secrets", text: t("SETTINGS$NAV_SECRETS") },
];
React.useEffect(() => {

Some files were not shown because too many files have changed in this diff Show More