Compare commits

..

2 Commits

Author SHA1 Message Date
openhands 030e08b68f fix: implement manual sorting for installation repositories 2025-01-29 19:22:12 +00:00
openhands 92a8bcd037 fix: ensure GitHub repos are sorted by last update time 2025-01-29 19:19:19 +00:00
176 changed files with 2292 additions and 4701 deletions
+14
View File
@@ -19,6 +19,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
+60 -4
View File
@@ -41,6 +41,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
with:
@@ -90,6 +104,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
with:
@@ -191,7 +219,7 @@ jobs:
exit 1
fi
# Run unit tests with the Docker runtime Docker images as root
# Run unit tests with the EventStream runtime Docker images as root
test_runtime_root:
name: RT Unit Tests (Root)
needs: [ghcr_build_runtime]
@@ -202,6 +230,20 @@ jobs:
base_image: ['nikolaik']
steps:
- uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
@@ -244,7 +286,7 @@ jobs:
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
TEST_RUNTIME=docker \
TEST_RUNTIME=eventstream \
SANDBOX_USER_ID=$(id -u) \
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
@@ -255,7 +297,7 @@ jobs:
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# Run unit tests with the Docker runtime Docker images as openhands user
# Run unit tests with the EventStream runtime Docker images as openhands user
test_runtime_oh:
name: RT Unit Tests (openhands)
runs-on: ubuntu-latest
@@ -265,6 +307,20 @@ jobs:
base_image: ['nikolaik']
steps:
- uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
@@ -307,7 +363,7 @@ jobs:
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
TEST_RUNTIME=docker \
TEST_RUNTIME=eventstream \
SANDBOX_USER_ID=$(id -u) \
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
-7
View File
@@ -20,10 +20,6 @@ on:
required: false
type: string
default: "anthropic/claude-3-5-sonnet-20241022"
LLM_API_VERSION:
required: false
type: string
default: ""
base_container_image:
required: false
type: string
@@ -120,7 +116,6 @@ jobs:
LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }}
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
PAT_USERNAME: ${{ secrets.PAT_USERNAME }}
GITHUB_TOKEN: ${{ github.token }}
@@ -235,7 +230,6 @@ jobs:
LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }}
PYTHONPATH: ""
run: |
cd /tmp && python -m openhands.resolver.resolve_issue \
@@ -271,7 +265,6 @@ jobs:
LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }}
PYTHONPATH: ""
run: |
if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then
+98
View File
@@ -0,0 +1,98 @@
# Workflow that runs python unit tests on mac
name: Run Python Unit Tests Mac
# This job is flaky so only run it nightly
on:
schedule:
- cron: '0 0 * * *'
jobs:
# Run python unit tests on macOS
test-on-macos:
name: Python Unit Tests on macOS
runs-on: macos-14
env:
INSTALL_DOCKER: '1' # Set to '0' to skip Docker installation
strategy:
matrix:
python-version: ['3.12']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Cache Poetry dependencies
uses: actions/cache@v4
with:
path: |
~/.cache/pypoetry
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
- name: Install tmux
run: brew install tmux
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: poetry install --without evaluation,llama-index
- name: Install & Start Docker
if: env.INSTALL_DOCKER == '1'
run: |
INSTANCE_NAME="colima-${GITHUB_RUN_ID}"
# Uninstall colima to upgrade to the latest version
if brew list colima &>/dev/null; then
brew uninstall colima
# unlinking colima dependency: go
brew uninstall go@1.21
fi
rm -rf ~/.colima ~/.lima
brew install --HEAD colima
brew install docker
start_colima() {
# Find a free port in the range 10000-20000
RANDOM_PORT=$((RANDOM % 10001 + 10000))
# Original line:
if ! colima start --network-address --arch x86_64 --cpu=1 --memory=1 --verbose --ssh-port $RANDOM_PORT; then
echo "Failed to start Colima."
return 1
fi
return 0
}
# Attempt to start Colima for 5 total attempts:
ATTEMPT_LIMIT=5
for ((i=1; i<=ATTEMPT_LIMIT; i++)); do
if start_colima; then
echo "Colima started successfully."
break
else
colima stop -f
sleep 10
colima delete -f
if [ $i -eq $ATTEMPT_LIMIT ]; then
exit 1
fi
sleep 10
fi
done
# For testcontainers to find the Colima socket
# https://github.com/abiosoft/colima/blob/main/docs/FAQ.md#cannot-connect-to-the-docker-daemon-at-unixvarrundockersock-is-the-docker-daemon-running
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
- name: Build Environment
run: make build
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Run Tests
run: poetry run pytest --forked --cov=openhands --cov-report=xml ./tests/unit --ignore=tests/unit/test_memory.py
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-1
View File
@@ -19,4 +19,3 @@ jobs:
close-issue-message: 'This issue was closed because it has been stalled for over 30 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for over 30 days with no activity.'
days-before-close: 7
operations-per-run: 150
+1 -1
View File
@@ -100,7 +100,7 @@ poetry run pytest ./tests/unit/test_*.py
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image by
setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.23-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.21-nikolaik`
## Develop inside Docker container
+3 -3
View File
@@ -43,17 +43,17 @@ See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installatio
system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-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.23
docker.all-hands.dev/all-hands-ai/openhands:0.21
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
+1 -1
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.23-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.21-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+1 -1
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.23-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.21-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:
@@ -1,8 +1,8 @@
# 📦 Runtime Docker
# 📦 Runtime EventStream
Le Runtime Docker d'OpenHands est le composant principal qui permet l'exécution sécurisée et flexible des actions des agents d'IA.
Le Runtime EventStream d'OpenHands est le composant principal qui permet l'exécution sécurisée et flexible des actions des agents d'IA.
Il crée un environnement en bac à sable (sandbox) en utilisant Docker, où du code arbitraire peut être exécuté en toute sécurité sans risquer le système hôte.
## Pourquoi avons-nous besoin d'un runtime en bac à sable ?
@@ -163,7 +163,7 @@ Les options de configuration de base sont définies dans la section `[core]` du
- `runtime`
- Type : `str`
- Valeur par défaut : `"docker"`
- Valeur par défaut : `"eventstream"`
- Description : Environnement d'exécution
- `default_agent`
@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -61,7 +61,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
docker.all-hands.dev/all-hands-ai/openhands:0.21 \
python -m openhands.core.cli
```
@@ -114,7 +114,7 @@ Pour créer un workflow d'évaluation pour votre benchmark, suivez ces étapes :
def get_config(instance: pd.Series, metadata: EvalMetadata) -> AppConfig:
config = AppConfig(
default_agent=metadata.agent_class,
runtime='docker',
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
base_container_image='your_container_image',
@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -56,6 +56,6 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
docker.all-hands.dev/all-hands-ai/openhands:0.21 \
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```
@@ -13,16 +13,16 @@
La façon la plus simple d'exécuter OpenHands est avec Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.23
docker.all-hands.dev/all-hands-ai/openhands:0.21
```
Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action).
@@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands.
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
@@ -1,8 +1,8 @@
以下是翻译后的内容:
# 📦 Docker 运行时
# 📦 EventStream 运行时
OpenHands Docker 运行时是实现 AI 代理操作安全灵活执行的核心组件。
OpenHands EventStream 运行时是实现 AI 代理操作安全灵活执行的核心组件。
它使用 Docker 创建一个沙盒环境,可以安全地运行任意代码而不会危及主机系统。
## 为什么我们需要沙盒运行时?
@@ -162,7 +162,7 @@
- `runtime`
- 类型: `str`
- 默认值: `"docker"`
- 默认值: `"eventstream"`
- 描述: 运行时环境
- `default_agent`
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -59,7 +59,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
docker.all-hands.dev/all-hands-ai/openhands:0.21 \
python -m openhands.core.cli
```
@@ -112,7 +112,7 @@ OpenHands 的主要入口点在 `openhands/core/main.py` 中。以下是它的
def get_config(instance: pd.Series, metadata: EvalMetadata) -> AppConfig:
config = AppConfig(
default_agent=metadata.agent_class,
runtime='docker',
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
base_container_image='your_container_image',
@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -57,6 +57,6 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
docker.all-hands.dev/all-hands-ai/openhands:0.21 \
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```
@@ -11,16 +11,16 @@
在 Docker 中运行 OpenHands 是最简单的方式。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.23
docker.all-hands.dev/all-hands-ai/openhands:0.21
```
你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands,作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。
@@ -11,7 +11,7 @@
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
+2 -2
View File
@@ -1,6 +1,6 @@
# 📦 Docker Runtime
# 📦 EventStream Runtime
The OpenHands Docker Runtime is the core component that enables secure and flexible execution of AI agent's action.
The OpenHands EventStream Runtime is the core component that enables secure and flexible execution of AI agent's action.
It creates a sandboxed environment using Docker, where arbitrary code can be run safely without risking the host system.
## Why do we need a sandboxed runtime?
+1 -1
View File
@@ -126,7 +126,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- `runtime`
- Type: `str`
- Default: `"docker"`
- Default: `"eventstream"`
- Description: Runtime environment
- `default_agent`
+2 -2
View File
@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -45,7 +45,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
docker.all-hands.dev/all-hands-ai/openhands:0.21 \
python -m openhands.core.cli
```
@@ -41,16 +41,8 @@ docker build -t custom-image .
This will produce a new image called `custom-image`, which will be available in Docker.
## Using the Docker Command
When running OpenHands using [the docker command](/modules/usage/installation#start-the-app), replace
`-e SANDBOX_RUNTIME_CONTAINER_IMAGE=...` with `-e SANDBOX_BASE_CONTAINER_IMAGE=<custom image name>`:
```commandline
docker run -it --rm --pull=always \
-e SANDBOX_BASE_CONTAINER_IMAGE=custom-image \
...
```
> Note that in the configuration described in this document, OpenHands will run as user "openhands" inside the
> sandbox and thus all packages installed via the docker file should be available to all users on the system, not just root.
## Using the Development Workflow
@@ -112,7 +112,7 @@ To create an evaluation workflow for your benchmark, follow these steps:
def get_config(instance: pd.Series, metadata: EvalMetadata) -> AppConfig:
config = AppConfig(
default_agent=metadata.agent_class,
runtime='docker',
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
base_container_image='your_container_image',
+2 -2
View File
@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -43,7 +43,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
docker.all-hands.dev/all-hands-ai/openhands:0.21 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+3 -7
View File
@@ -43,10 +43,6 @@
- General: `Use the WSL 2 based engine` is enabled.
- Resources > WSL Integration: `Enable integration with my default WSL distro` is enabled.
:::note
The docker command below to start the app must be run inside the WSL terminal.
:::
</details>
## Start the App
@@ -54,17 +50,17 @@
The easiest way to run OpenHands is in Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-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.23
docker.all-hands.dev/all-hands-ai/openhands:0.21
```
You'll find OpenHands running at http://localhost:3000!
+1 -1
View File
@@ -16,7 +16,7 @@ some flags being passed to `docker run` that make this possible:
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.21-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
-1
View File
@@ -69,7 +69,6 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=False,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -53,7 +53,6 @@ def get_config(
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -61,7 +61,6 @@ def get_config(
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_runtime_alive=False,
remote_runtime_init_timeout=1800,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -67,7 +67,6 @@ def get_config(
base_container_image=BIOCODER_BENCH_CONTAINER_IMAGE,
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
-1
View File
@@ -80,7 +80,6 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -45,7 +45,6 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=False,
use_host_network=False,
remote_runtime_enable_retries=True,
),
workspace_base=None,
workspace_mount_path=None,
@@ -135,7 +135,6 @@ def get_config(
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -71,7 +71,6 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
-1
View File
@@ -56,7 +56,6 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -49,7 +49,6 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
-1
View File
@@ -70,7 +70,6 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -91,7 +91,6 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -55,7 +55,6 @@ def get_config(
enable_auto_lint=True,
use_host_network=False,
runtime_extra_deps='$OH_INTERPRETER_PATH -m pip install scitools-pyke',
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -70,7 +70,6 @@ def get_config(
remote_runtime_init_timeout=1800,
keep_runtime_alive=False,
timeout=120,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
-1
View File
@@ -113,7 +113,6 @@ def get_config(
enable_auto_lint=True,
use_host_network=False,
runtime_extra_deps=f'$OH_INTERPRETER_PATH -m pip install {" ".join(MINT_DEPENDENCIES)}',
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -73,7 +73,6 @@ def get_config(
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_runtime_alive=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -412,17 +412,6 @@ if __name__ == '__main__':
with open(metadata_filepath, 'r') as metadata_file:
data = metadata_file.read()
metadata = EvalMetadata.model_validate_json(data)
else:
# Initialize with a dummy metadata when file doesn't exist
metadata = EvalMetadata(
agent_class="dummy_agent", # Placeholder agent class
llm_config=LLMConfig(model="dummy_model"), # Minimal LLM config
max_iterations=1, # Minimal iterations
eval_output_dir=os.path.dirname(args.input_file), # Use input file dir as output dir
start_time=time.strftime('%Y-%m-%d %H:%M:%S'), # Current time
git_commit=subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8').strip(), # Current commit
dataset=args.dataset # Dataset name from args
)
# The evaluation harness constrains the signature of `process_instance_func` but we need to
# pass extra information. Build a new function object to avoid issues with multiprocessing.
@@ -139,12 +139,10 @@ def get_config(
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
remote_runtime_api_timeout=120,
remote_runtime_resource_factor=get_instance_resource_factor(
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
),
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -17,14 +17,11 @@ When the `run_infer.sh` script is started, it will automatically pull all task i
```bash
./evaluation/benchmarks/the_agent_company/scripts/run_infer.sh \
--agent-llm-config <agent-llm-config, default to 'agent'> \
--env-llm-config <env-llm-config, default to 'env'> \
--outputs-path <outputs-path, default to outputs> \
--server-hostname <server-hostname, default to localhost> \
--version <version, default to 1.0.0> \
--start-percentile <integer from 0 to 99, default to 0> \
--end-percentile <integer from 1 to 100, default to 100>
--agent-llm-config <agent-llm-config> \
--env-llm-config <env-llm-config> \
--outputs-path <outputs-path> \
--server-hostname <server-hostname> \
--version <version>
# Example
./evaluation/benchmarks/the_agent_company/scripts/run_infer.sh \
@@ -32,9 +29,7 @@ When the `run_infer.sh` script is started, it will automatically pull all task i
--env-llm-config claude-3-5-sonnet-20240620 \
--outputs-path outputs \
--server-hostname localhost \
--version 1.0.0 \
--start-percentile 10 \
--end-percentile 20
--version 1.0.0
```
- `agent-llm-config`: the config name for the agent LLM. This should match the config name in config.toml. This is the LLM used by the agent (e.g. CodeActAgent).
@@ -42,11 +37,7 @@ When the `run_infer.sh` script is started, it will automatically pull all task i
- `outputs-path`: the path to save trajectories and evaluation results.
- `server-hostname`: the hostname of the server that hosts all the web services. It could be localhost if you are running the evaluation and services on the same machine. If the services are hosted on a remote machine, you must use the hostname of the remote machine rather than IP address.
- `version`: the version of the task images to use. Currently, the only supported version is 1.0.0.
- `start-percentile`: the start percentile of the task split, must be an integer between 0 to 99.
- `end-percentile`: the end percentile of the task split, must be an integer between 1 to 100 and larger than start-percentile.
The script is idempotent. If you run it again, it will resume from the last checkpoint. It would usually take 2 days to finish evaluation if you run the whole task set.
To speed up evaluation, you can use `start-percentile` and `end-percentile` to split the tasks for higher parallelism,
provided concurrent runs are **targeting different servers**.
The script is idempotent. If you run it again, it will resume from the last checkpoint. It would usually take a few days to finish evaluation.
Note: the script will automatically skip a task if it encounters an error. This usually happens when the OpenHands runtime dies due to some unexpected errors. This means even if the script finishes, it might not have evaluated all tasks. You can manually resume the evaluation by running the script again.
@@ -50,7 +50,6 @@ def get_config(
# large enough timeout, since some testcases take very long to run
timeout=300,
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_enable_retries=True,
),
# we mount trajectories path so that trajectories, generated by OpenHands
# controller, can be accessible to the evaluator file in the runtime container
+2 -47
View File
@@ -56,14 +56,6 @@ while [[ $# -gt 0 ]]; do
VERSION="$2"
shift 2
;;
--start-percentile)
START_PERCENTILE="$2"
shift 2
;;
--end-percentile)
END_PERCENTILE="$2"
shift 2
;;
*)
echo "Unknown argument: $1"
exit 1
@@ -77,53 +69,16 @@ if [[ ! "$OUTPUTS_PATH" = /* ]]; then
OUTPUTS_PATH="$(cd "$(dirname "$OUTPUTS_PATH")" 2>/dev/null && pwd)/$(basename "$OUTPUTS_PATH")"
fi
: "${START_PERCENTILE:=0}" # Default to 0 percentile (first line)
: "${END_PERCENTILE:=100}" # Default to 100 percentile (last line)
# Validate percentile ranges if provided
if ! [[ "$START_PERCENTILE" =~ ^[0-9]+$ ]] || ! [[ "$END_PERCENTILE" =~ ^[0-9]+$ ]]; then
echo "Error: Percentiles must be integers"
exit 1
fi
if [ "$START_PERCENTILE" -ge "$END_PERCENTILE" ]; then
echo "Error: Start percentile must be less than end percentile"
exit 1
fi
if [ "$START_PERCENTILE" -lt 0 ] || [ "$END_PERCENTILE" -gt 100 ]; then
echo "Error: Percentiles must be between 0 and 100"
exit 1
fi
echo "Using agent LLM config: $AGENT_LLM_CONFIG"
echo "Using environment LLM config: $ENV_LLM_CONFIG"
echo "Outputs path: $OUTPUTS_PATH"
echo "Server hostname: $SERVER_HOSTNAME"
echo "Version: $VERSION"
echo "Start Percentile: $START_PERCENTILE"
echo "End Percentile: $END_PERCENTILE"
echo "Downloading tasks.md..."
rm -f tasks.md
wget https://github.com/TheAgentCompany/TheAgentCompany/releases/download/${VERSION}/tasks.md
total_lines=$(cat tasks.md | grep "ghcr.io/theagentcompany" | wc -l)
if [ "$total_lines" -ne 175 ]; then
echo "Error: Expected 175 tasks in tasks.md but found $total_lines lines"
exit 1
fi
# Calculate line numbers based on percentiles
start_line=$(echo "scale=0; ($total_lines * $START_PERCENTILE / 100) + 1" | bc)
end_line=$(echo "scale=0; $total_lines * $END_PERCENTILE / 100" | bc)
echo "Using tasks No. $start_line to $end_line (inclusive) out of 1-175 tasks"
# Create a temporary file with just the desired range
temp_file="tasks_${START_PERCENTILE}_${END_PERCENTILE}.md"
sed -n "${start_line},${end_line}p" tasks.md > "$temp_file"
while IFS= read -r task_image; do
docker pull $task_image
@@ -153,8 +108,8 @@ while IFS= read -r task_image; do
docker images "ghcr.io/all-hands-ai/runtime" -q | xargs -r docker rmi -f
docker volume prune -f
docker system prune -f
done < "$temp_file"
done < tasks.md
rm tasks.md "$temp_file"
rm tasks.md
echo "All evaluation completed successfully!"
@@ -50,7 +50,6 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -79,7 +79,6 @@ def get_config(
'VWA_HOMEPAGE': f'{base_url}:4399',
},
timeout=300,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -71,7 +71,6 @@ def get_config(
'MAP': f'{base_url}:3000',
'HOMEPAGE': f'{base_url}:4399',
},
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -1,6 +1,6 @@
import { render, screen, waitFor } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect } from "vitest";
import { describe, it, expect, test } from "vitest";
import { ChatMessage } from "#/components/features/chat/chat-message";
describe("ChatMessage", () => {
@@ -45,9 +45,7 @@ describe("ChatMessage", () => {
await user.click(copyToClipboardButton);
await waitFor(() =>
expect(navigator.clipboard.readText()).resolves.toBe("Hello, World!"),
);
expect(navigator.clipboard.readText()).resolves.toBe("Hello, World!");
});
it("should display an error toast if copying content to clipboard fails", async () => {});
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { screen } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import { ExpandableMessage } from "#/components/features/chat/expandable-message";
import { vi } from "vitest"
import { vi } from "vitest";
vi.mock("react-i18next", async () => {
const actual = await vi.importActual("react-i18next");
@@ -1,45 +0,0 @@
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
import OpenHands from "#/api/open-hands";
import { SettingsProvider } from "#/context/settings-context";
import { AuthProvider } from "#/context/auth-context";
describe("AnalyticsConsentFormModal", () => {
it("should call saveUserSettings with default settings on confirm reset settings", async () => {
const user = userEvent.setup();
const onCloseMock = vi.fn();
const saveUserSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
render(<AnalyticsConsentFormModal onClose={onCloseMock} />, {
wrapper: ({ children }) => (
<AuthProvider>
<QueryClientProvider client={new QueryClient()}>
<SettingsProvider>{children}</SettingsProvider>
</QueryClientProvider>
</AuthProvider>
),
});
const confirmButton = screen.getByTestId("confirm-preferences");
await user.click(confirmButton);
expect(saveUserSettingsSpy).toHaveBeenCalledWith({
user_consents_to_analytics: true,
agent: "CodeActAgent",
confirmation_mode: false,
enable_default_condenser: false,
github_token: undefined,
language: "en",
llm_api_key: undefined,
llm_base_url: "",
llm_model: "anthropic/claude-3-5-sonnet-20241022",
remote_runtime_resource_factor: 1,
security_analyzer: "",
unset_github_token: undefined,
});
expect(onCloseMock).toHaveBeenCalled();
});
});
@@ -1,14 +1,5 @@
import { render, screen, within } from "@testing-library/react";
import {
afterAll,
afterEach,
beforeAll,
describe,
expect,
it,
test,
vi,
} from "vitest";
import { afterEach, describe, expect, it, test, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { formatTimeDelta } from "#/utils/format-time-delta";
import { ConversationCard } from "#/components/features/conversation-panel/conversation-card";
@@ -20,18 +11,10 @@ describe("ConversationCard", () => {
const onChangeTitle = vi.fn();
const onDownloadWorkspace = vi.fn();
beforeAll(() => {
vi.stubGlobal("window", { open: vi.fn() });
});
afterEach(() => {
vi.clearAllMocks();
});
afterAll(() => {
vi.unstubAllGlobals();
});
it("should render the conversation card", () => {
render(
<ConversationCard
@@ -46,8 +29,9 @@ describe("ConversationCard", () => {
const expectedDate = `${formatTimeDelta(new Date("2021-10-01T12:00:00Z"))} ago`;
const card = screen.getByTestId("conversation-card");
const title = within(card).getByTestId("conversation-card-title");
within(card).getByText("Conversation 1");
expect(title).toHaveValue("Conversation 1");
within(card).getByText(expectedDate);
});
@@ -164,8 +148,10 @@ describe("ConversationCard", () => {
/>,
);
await clickOnEditButton(user);
const title = screen.getByTestId("conversation-card-title");
expect(title).toBeDisabled();
await clickOnEditButton(user);
expect(title).toBeEnabled();
expect(screen.queryByTestId("context-menu")).not.toBeInTheDocument();
@@ -178,6 +164,7 @@ describe("ConversationCard", () => {
expect(onChangeTitle).toHaveBeenCalledWith("New Conversation Name");
expect(title).toHaveValue("New Conversation Name");
expect(title).toBeDisabled();
});
it("should reset title and not call onChangeTitle when the title is empty", async () => {
@@ -204,27 +191,7 @@ describe("ConversationCard", () => {
expect(title).toHaveValue("Conversation 1");
});
test("clicking the title should trigger the onClick handler", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const title = screen.getByTestId("conversation-card-title");
await user.click(title);
expect(onClick).toHaveBeenCalled();
});
test("clicking the title should not trigger the onClick handler if edit mode", async () => {
test("clicking the title should not trigger the onClick handler", async () => {
const user = userEvent.setup();
render(
<ConversationCard
@@ -237,8 +204,6 @@ describe("ConversationCard", () => {
/>,
);
await clickOnEditButton(user);
const title = screen.getByTestId("conversation-card-title");
await user.click(title);
@@ -179,10 +179,9 @@ describe("ConversationPanel", () => {
const user = userEvent.setup();
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
const title = within(cards[0]).getByTestId("conversation-card-title");
const card = cards[0];
await clickOnEditButton(user, card);
const title = within(card).getByTestId("conversation-card-title");
await clickOnEditButton(user);
await user.clear(title);
await user.type(title, "Conversation 1 Renamed");
@@ -203,10 +202,7 @@ describe("ConversationPanel", () => {
const user = userEvent.setup();
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
const card = cards[0];
await clickOnEditButton(user, card);
const title = within(card).getByTestId("conversation-card-title");
const title = within(cards[0]).getByTestId("conversation-card-title");
await user.click(title);
await user.tab();
@@ -231,7 +227,7 @@ describe("ConversationPanel", () => {
it("should call onClose after clicking a card", async () => {
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
const firstCard = cards[1];
const firstCard = cards[0];
await userEvent.click(firstCard);
@@ -1,16 +1,11 @@
import { screen, within } from "@testing-library/react";
import { UserEvent } from "@testing-library/user-event";
export const clickOnEditButton = async (
user: UserEvent,
container?: HTMLElement,
) => {
const wrapper = container ? within(container) : screen;
const ellipsisButton = wrapper.getByTestId("ellipsis-button");
export const clickOnEditButton = async (user: UserEvent) => {
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const menu = wrapper.getByTestId("context-menu");
const menu = screen.getByTestId("context-menu");
const editButton = within(menu).getByTestId("edit-button");
await user.click(editButton);
@@ -3,7 +3,6 @@ import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { createRoutesStub } from "react-router";
import { AxiosError } from "axios";
import { Sidebar } from "#/components/features/sidebar/sidebar";
import OpenHands from "#/api/open-hands";
@@ -83,10 +82,6 @@ describe("Sidebar", () => {
within(accountSettingsModal).getByLabelText(/GITHUB\$TOKEN_LABEL/i);
await user.type(tokenInput, "new-token");
const analyticsConsentInput =
within(accountSettingsModal).getByTestId("analytics-consent");
await user.click(analyticsConsentInput);
const saveButton =
within(accountSettingsModal).getByTestId("save-settings");
await user.click(saveButton);
@@ -101,7 +96,6 @@ describe("Sidebar", () => {
llm_model: "anthropic/claude-3-5-sonnet-20241022",
remote_runtime_resource_factor: 1,
security_analyzer: "",
user_consents_to_analytics: true,
});
});
@@ -154,24 +148,11 @@ describe("Sidebar", () => {
expect(settingsModal).toBeInTheDocument();
});
it("should open the settings modal if GET /settings fails with a 404", async () => {
const error = new AxiosError(
"Request failed with status code 404",
"ERR_BAD_REQUEST",
undefined,
undefined,
{
status: 404,
statusText: "Not Found",
data: { message: "Settings not found" },
headers: {},
// @ts-expect-error - we only need the response object for this test
config: {},
},
it("should open the settings modal if GET /settings fails", async () => {
vi.spyOn(OpenHands, "getSettings").mockRejectedValue(
new Error("Failed to fetch settings"),
);
vi.spyOn(OpenHands, "getSettings").mockRejectedValue(error);
renderSidebar();
const settingsModal = await screen.findByTestId("ai-config-modal");
@@ -32,7 +32,7 @@ describe("FeedbackForm", () => {
screen.getByLabelText(I18nKey.FEEDBACK$PRIVATE_LABEL);
screen.getByLabelText(I18nKey.FEEDBACK$PUBLIC_LABEL);
screen.getByRole("button", { name: I18nKey.FEEDBACK$SHARE_LABEL });
screen.getByRole("button", { name: I18nKey.FEEDBACK$CONTRIBUTE_LABEL });
screen.getByRole("button", { name: I18nKey.FEEDBACK$CANCEL_LABEL });
});
@@ -1,74 +1,16 @@
import { screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { AccountSettingsModal } from "#/components/shared/modals/account-settings/account-settings-modal";
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
import OpenHands from "#/api/open-hands";
import * as ConsentHandlers from "#/utils/handle-capture-consent";
describe("AccountSettingsModal", () => {
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
afterEach(() => {
vi.clearAllMocks();
});
it.skip("should set the appropriate user analytics consent default", async () => {
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
user_consents_to_analytics: true,
});
renderWithProviders(<AccountSettingsModal onClose={() => {}} />);
const analyticsConsentInput = screen.getByTestId("analytics-consent");
await waitFor(() => expect(analyticsConsentInput).toBeChecked());
});
it("should save the users consent to analytics when saving account settings", async () => {
const user = userEvent.setup();
renderWithProviders(<AccountSettingsModal onClose={() => {}} />);
const analyticsConsentInput = screen.getByTestId("analytics-consent");
await user.click(analyticsConsentInput);
const saveButton = screen.getByTestId("save-settings");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
agent: "CodeActAgent",
confirmation_mode: false,
enable_default_condenser: false,
language: "en",
llm_base_url: "",
llm_model: "anthropic/claude-3-5-sonnet-20241022",
remote_runtime_resource_factor: 1,
security_analyzer: "",
user_consents_to_analytics: true,
});
});
it("should call handleCaptureConsent with the analytics consent value if the save is successful", async () => {
const user = userEvent.setup();
const handleCaptureConsentSpy = vi.spyOn(
ConsentHandlers,
"handleCaptureConsent",
);
renderWithProviders(<AccountSettingsModal onClose={() => {}} />);
const analyticsConsentInput = screen.getByTestId("analytics-consent");
await user.click(analyticsConsentInput);
const saveButton = screen.getByTestId("save-settings");
await user.click(saveButton);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
await user.click(analyticsConsentInput);
await user.click(saveButton);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(false);
beforeEach(() => {
vi.resetAllMocks();
});
it("should send all settings data when saving account settings", async () => {
@@ -97,11 +39,11 @@ describe("AccountSettingsModal", () => {
llm_model: "anthropic/claude-3-5-sonnet-20241022",
remote_runtime_resource_factor: 1,
security_analyzer: "",
user_consents_to_analytics: false,
});
});
it("should render a checkmark and not the input if the github token is set", async () => {
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
github_token_is_set: true,
@@ -119,6 +61,7 @@ describe("AccountSettingsModal", () => {
it("should send an unset github token property when pressing disconnect", async () => {
const user = userEvent.setup();
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
github_token_is_set: true,
@@ -143,6 +86,7 @@ describe("AccountSettingsModal", () => {
it("should not unset the github token when changing the language", async () => {
const user = userEvent.setup();
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
github_token_is_set: true,
@@ -167,7 +111,6 @@ describe("AccountSettingsModal", () => {
llm_model: "anthropic/claude-3-5-sonnet-20241022",
remote_runtime_resource_factor: 1,
security_analyzer: "",
user_consents_to_analytics: false,
});
});
});
@@ -8,10 +8,9 @@ vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
SUGGESTIONS$TODO_APP: "ToDoリストアプリを開発する",
LANDING$BUILD_APP_BUTTON: "プルリクエストを表示するアプリを開発する",
SUGGESTIONS$HACKER_NEWS:
"Hacker Newsのトップ記事を表示するbashスクリプトを作成する",
"SUGGESTIONS$TODO_APP": "ToDoリストアプリを開発する",
"LANDING$BUILD_APP_BUTTON": "プルリクエストを表示するアプリを開発する",
"SUGGESTIONS$HACKER_NEWS": "Hacker Newsのトップ記事を表示するbashスクリプトを作成する",
};
return translations[key] || key;
},
@@ -39,9 +38,9 @@ describe("SuggestionItem", () => {
value: "todo app value",
};
render(
<SuggestionItem suggestion={translatedSuggestion} onClick={onClick} />,
);
const { container } = render(<SuggestionItem suggestion={translatedSuggestion} onClick={onClick} />);
console.log('Rendered HTML:', container.innerHTML);
expect(screen.getByText("ToDoリストアプリを開発する")).toBeInTheDocument();
});
+13 -6
View File
@@ -58,7 +58,6 @@ describe("frontend/routes/_oh", () => {
it("should render and capture the user's consent if oss mode", async () => {
const user = userEvent.setup();
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const handleCaptureConsentSpy = vi.spyOn(
CaptureConsent,
"handleCaptureConsent",
@@ -70,16 +69,12 @@ describe("frontend/routes/_oh", () => {
POSTHOG_CLIENT_KEY: "test-key",
});
// @ts-expect-error - We only care about the user_consents_to_analytics field
getSettingsSpy.mockResolvedValue({
user_consents_to_analytics: null,
});
renderWithProviders(<RouteStub />);
// The user has not consented to tracking
const consentForm = await screen.findByTestId("user-capture-consent-form");
expect(handleCaptureConsentSpy).not.toHaveBeenCalled();
expect(localStorage.getItem("analytics-consent")).toBeNull();
const submitButton = within(consentForm).getByRole("button", {
name: /confirm preferences/i,
@@ -88,6 +83,7 @@ describe("frontend/routes/_oh", () => {
// The user has now consented to tracking
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
expect(localStorage.getItem("analytics-consent")).toBe("true");
expect(
screen.queryByTestId("user-capture-consent-form"),
).not.toBeInTheDocument();
@@ -110,6 +106,17 @@ describe("frontend/routes/_oh", () => {
});
});
it("should not render the user consent form if the user has already made a decision", async () => {
localStorage.setItem("analytics-consent", "true");
renderWithProviders(<RouteStub />);
await waitFor(() => {
expect(
screen.queryByTestId("user-capture-consent-form"),
).not.toBeInTheDocument();
});
});
// TODO: Likely failing due to how tokens are now handled in context. Move to e2e tests
it.skip("should render a new project button if a token is set", async () => {
localStorage.setItem("token", "test-token");
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.23.0",
"version": "0.21.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.23.0",
"version": "0.21.0",
"dependencies": {
"@monaco-editor/react": "^4.7.0-rc.0",
"@nextui-org/react": "^2.6.11",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.23.0",
"version": "0.21.0",
"private": true,
"type": "module",
"engines": {
+4 -11
View File
@@ -14,7 +14,7 @@ export const retrieveGitHubAppRepositories = async (
per_page = 30,
) => {
const installationId = installations[installationIndex];
const response = await openHands.get<GitHubRepository[]>(
const response = await openHands.get<GitHubAppRepository>(
"/api/github/repositories",
{
params: {
@@ -26,11 +26,7 @@ export const retrieveGitHubAppRepositories = async (
},
);
const link =
response.data.length > 0 && response.data[0].link_header
? response.data[0].link_header
: "";
const link = response.headers.link ?? "";
const nextPage = extractNextPageFromLink(link);
let nextInstallation: number | null;
@@ -43,7 +39,7 @@ export const retrieveGitHubAppRepositories = async (
}
return {
data: response.data,
data: response.data.repositories,
nextPage,
installationIndex: nextInstallation,
};
@@ -68,10 +64,7 @@ export const retrieveGitHubUserRepositories = async (
},
);
const link =
response.data.length > 0 && response.data[0].link_header
? response.data[0].link_header
: "";
const link = response.headers.link ?? "";
const nextPage = extractNextPageFromLink(link);
return { data: response.data, nextPage };
+31 -2
View File
@@ -254,6 +254,35 @@ class OpenHands {
return data;
}
static async searchEvents(
conversationId: string,
params: {
query?: string;
startId?: number;
limit?: number;
eventType?: string;
source?: string;
startDate?: string;
endDate?: string;
},
): Promise<{ events: Record<string, unknown>[]; has_more: boolean }> {
const { data } = await openHands.get<{
events: Record<string, unknown>[];
has_more: boolean;
}>(`/api/conversations/${conversationId}/events/search`, {
params: {
query: params.query,
start_id: params.startId,
limit: params.limit,
event_type: params.eventType,
source: params.source,
start_date: params.startDate,
end_date: params.endDate,
},
});
return data;
}
/**
* Get the settings from the server or use the default settings if not found
*/
@@ -297,7 +326,7 @@ class OpenHands {
query: string,
per_page = 5,
): Promise<GitHubRepository[]> {
const response = await openHands.get<GitHubRepository[]>(
const response = await openHands.get<{ items: GitHubRepository[] }>(
"/api/github/search/repositories",
{
params: {
@@ -307,7 +336,7 @@ class OpenHands {
},
);
return response.data;
return response.data.items;
}
static async getTrajectory(
@@ -5,7 +5,6 @@ import {
} from "#/components/shared/modals/confirmation-modals/base-modal";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { useCurrentSettings } from "#/context/settings-context";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
interface AnalyticsConsentFormModalProps {
@@ -15,21 +14,13 @@ interface AnalyticsConsentFormModalProps {
export function AnalyticsConsentFormModal({
onClose,
}: AnalyticsConsentFormModalProps) {
const { saveUserSettings } = useCurrentSettings();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const analytics = formData.get("analytics") === "on";
await saveUserSettings(
{ user_consents_to_analytics: analytics },
{
onSuccess: () => {
handleCaptureConsent(analytics);
},
},
);
handleCaptureConsent(analytics);
localStorage.setItem("analytics-consent", analytics.toString());
onClose();
};
@@ -55,7 +46,6 @@ export function AnalyticsConsentFormModal({
</label>
<ModalButton
testId="confirm-preferences"
type="submit"
text="Confirm Preferences"
className="bg-primary text-white w-full hover:opacity-80"
@@ -180,7 +180,8 @@ export function ChatInterface() {
onStop={handleStop}
isDisabled={
curAgentState === AgentState.LOADING ||
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
curAgentState === AgentState.AWAITING_USER_CONFIRMATION ||
curAgentState === AgentState.RATE_LIMITED
}
mode={curAgentState === AgentState.RUNNING ? "stop" : "submit"}
value={messageToSend ?? undefined}
@@ -58,10 +58,8 @@ export function ConversationCard({
};
const handleInputClick = (event: React.MouseEvent<HTMLInputElement>) => {
if (titleMode === "edit") {
event.preventDefault();
event.stopPropagation();
}
event.preventDefault();
event.stopPropagation();
};
const handleDelete = (event: React.MouseEvent<HTMLButtonElement>) => {
@@ -103,26 +101,17 @@ export function ConversationCard({
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 w-full">
{isActive && <span className="w-2 h-2 bg-blue-500 rounded-full" />}
{titleMode === "edit" && (
<input
ref={inputRef}
data-testid="conversation-card-title"
onClick={handleInputClick}
onBlur={handleBlur}
onKeyUp={handleKeyUp}
type="text"
defaultValue={title}
className="text-sm leading-6 font-semibold bg-transparent w-full"
/>
)}
{titleMode === "view" && (
<p
data-testid="conversation-card-title"
className="text-sm leading-6 font-semibold bg-transparent w-full"
>
{title}
</p>
)}
<input
ref={inputRef}
disabled={titleMode === "view"}
data-testid="conversation-card-title"
onClick={handleInputClick}
onBlur={handleBlur}
onKeyUp={handleKeyUp}
type="text"
defaultValue={title}
className="text-sm leading-6 font-semibold bg-transparent w-full"
/>
</div>
<div className="flex items-center gap-2 relative">
@@ -154,7 +143,10 @@ export function ConversationCard({
)}
>
{selectedRepository && (
<ConversationRepoLink selectedRepository={selectedRepository} />
<ConversationRepoLink
selectedRepository={selectedRepository}
onClick={(e) => e.stopPropagation()}
/>
)}
<p className="text-xs text-neutral-400">
<time>{formatTimeDelta(new Date(lastUpdatedAt))} ago</time>
@@ -1,27 +1,21 @@
interface ConversationRepoLinkProps {
selectedRepository: string;
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
}
export function ConversationRepoLink({
selectedRepository,
onClick,
}: ConversationRepoLinkProps) {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
window.open(
`https://github.com/${selectedRepository}`,
"_blank",
"noopener,noreferrer",
);
};
return (
<button
type="button"
<a
data-testid="conversation-card-selected-repository"
onClick={handleClick}
href={`https://github.com/${selectedRepository}`}
target="_blank noopener noreferrer"
onClick={onClick}
className="text-xs text-neutral-400 hover:text-neutral-200"
>
{selectedRepository}
</button>
</a>
);
}
@@ -124,7 +124,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
<ModalButton
disabled={isPending}
type="submit"
text={t(I18nKey.FEEDBACK$SHARE_LABEL)}
text={t(I18nKey.FEEDBACK$CONTRIBUTE_LABEL)}
className="bg-[#4465DB] grow"
/>
<ModalButton
@@ -70,7 +70,7 @@ export function GitHubRepositorySelector({
}}
onSelectionChange={(id) => handleRepoSelection(id?.toString() ?? null)}
onInputChange={onInputChange}
clearButtonProps={{ onPress: handleClearSelection }}
clearButtonProps={{ onClick: handleClearSelection }}
listboxProps={{
emptyContent,
}}
@@ -2,7 +2,6 @@ import React from "react";
import { FaListUl } from "react-icons/fa";
import { useDispatch } from "react-redux";
import posthog from "posthog-js";
import toast from "react-hot-toast";
import { useGitHubUser } from "#/hooks/query/use-github-user";
import { UserActions } from "./user-actions";
import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-button";
@@ -29,12 +28,7 @@ export function Sidebar() {
const endSession = useEndSession();
const user = useGitHubUser();
const { data: config } = useConfig();
const {
data: settings,
error: settingsError,
isError: settingsIsError,
isFetching: isFetchingSettings,
} = useSettings();
const { data: settings, isError: settingsError } = useSettings();
const { mutateAsync: logout } = useLogout();
const { saveUserSettings } = useCurrentSettings();
@@ -52,20 +46,6 @@ export function Sidebar() {
}
}, [user.isError]);
React.useEffect(() => {
// We don't show toast errors for settings in the global error handler
// because we have a special case for 404 errors
if (
!isFetchingSettings &&
settingsIsError &&
settingsError?.status !== 404
) {
toast.error(
"Something went wrong while fetching settings. Please reload the page.",
);
}
}, [settingsError?.status, settingsError, isFetchingSettings]);
const handleEndSession = () => {
dispatch(setCurrentAgentState(AgentState.LOADING));
endSession();
@@ -125,7 +105,7 @@ export function Sidebar() {
{accountSettingsModalOpen && (
<AccountSettingsModal onClose={handleAccountSettingsModalClose} />
)}
{(settingsError?.status === 404 || settingsModalIsOpen) && (
{(settingsError || settingsModalIsOpen) && (
<SettingsModal
settings={settings}
onClose={() => setSettingsModalIsOpen(false)}
@@ -15,21 +15,25 @@ import { useConfig } from "#/hooks/query/use-config";
import { useCurrentSettings } from "#/context/settings-context";
import { GitHubTokenInput } from "./github-token-input";
import { PostSettings } from "#/types/settings";
import { useGitHubUser } from "#/hooks/query/use-github-user";
interface AccountSettingsFormProps {
onClose: () => void;
selectedLanguage: string;
gitHubError: boolean;
analyticsConsent: string | null;
}
export function AccountSettingsForm({ onClose }: AccountSettingsFormProps) {
const { isError: isGitHubError } = useGitHubUser();
export function AccountSettingsForm({
onClose,
selectedLanguage,
gitHubError,
analyticsConsent,
}: AccountSettingsFormProps) {
const { data: config } = useConfig();
const { saveUserSettings, settings } = useCurrentSettings();
const { t } = useTranslation();
const githubTokenIsSet = !!settings?.GITHUB_TOKEN_IS_SET;
const analyticsConsentValue = !!settings?.USER_CONSENTS_TO_ANALYTICS;
const selectedLanguage = settings?.LANGUAGE || "en";
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -40,7 +44,6 @@ export function AccountSettingsForm({ onClose }: AccountSettingsFormProps) {
const analytics = formData.get("analytics")?.toString() === "on";
const newSettings: Partial<PostSettings> = {};
newSettings.user_consents_to_analytics = analytics;
if (ghToken) newSettings.github_token = ghToken;
@@ -54,11 +57,11 @@ export function AccountSettingsForm({ onClose }: AccountSettingsFormProps) {
if (languageKey) newSettings.LANGUAGE = languageKey;
}
await saveUserSettings(newSettings, {
onSuccess: () => {
handleCaptureConsent(analytics);
},
});
await saveUserSettings(newSettings);
handleCaptureConsent(analytics);
const ANALYTICS = analytics.toString();
localStorage.setItem("analytics-consent", ANALYTICS);
onClose();
};
@@ -114,12 +117,12 @@ export function AccountSettingsForm({ onClose }: AccountSettingsFormProps) {
)}
</>
)}
{isGitHubError && (
{gitHubError && (
<p className="text-danger text-xs">
{t(I18nKey.GITHUB$TOKEN_INVALID)}
</p>
)}
{githubTokenIsSet && !isGitHubError && (
{githubTokenIsSet && !gitHubError && (
<ModalButton
testId="disconnect-github"
variant="text-like"
@@ -132,10 +135,9 @@ export function AccountSettingsForm({ onClose }: AccountSettingsFormProps) {
<label className="flex gap-2 items-center self-start">
<input
data-testid="analytics-consent"
name="analytics"
type="checkbox"
defaultChecked={analyticsConsentValue}
defaultChecked={analyticsConsent === "true"}
/>
{t(I18nKey.ANALYTICS$ENABLE)}
</label>
@@ -1,3 +1,5 @@
import { useGitHubUser } from "#/hooks/query/use-github-user";
import { useSettings } from "#/hooks/query/use-settings";
import { ModalBackdrop } from "../modal-backdrop";
import { AccountSettingsForm } from "./account-settings-form";
@@ -6,9 +8,20 @@ interface AccountSettingsModalProps {
}
export function AccountSettingsModal({ onClose }: AccountSettingsModalProps) {
const user = useGitHubUser();
const { data: settings } = useSettings();
// FIXME: Bad practice to use localStorage directly
const analyticsConsent = localStorage.getItem("analytics-consent");
return (
<ModalBackdrop onClose={onClose}>
<AccountSettingsForm onClose={onClose} />
<AccountSettingsForm
onClose={onClose}
selectedLanguage={settings?.LANGUAGE || "en"}
gitHubError={user.isError}
analyticsConsent={analyticsConsent}
/>
</ModalBackdrop>
);
}
+3 -14
View File
@@ -1,18 +1,10 @@
import React from "react";
import { MutateOptions } from "@tanstack/react-query";
import { useSettings } from "#/hooks/query/use-settings";
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
import { PostSettings, Settings } from "#/types/settings";
type SaveUserSettingsConfig = {
onSuccess: MutateOptions<void, Error, Partial<PostSettings>>["onSuccess"];
};
interface SettingsContextType {
saveUserSettings: (
newSettings: Partial<PostSettings>,
config?: SaveUserSettingsConfig,
) => Promise<void>;
saveUserSettings: (newSettings: Partial<PostSettings>) => Promise<void>;
settings: Settings | undefined;
}
@@ -28,10 +20,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
const { data: userSettings } = useSettings();
const { mutateAsync: saveSettings } = useSaveSettings();
const saveUserSettings = async (
newSettings: Partial<PostSettings>,
config?: SaveUserSettingsConfig,
) => {
const saveUserSettings = async (newSettings: Partial<PostSettings>) => {
const updatedSettings: Partial<PostSettings> = {
...userSettings,
...newSettings,
@@ -41,7 +30,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
delete updatedSettings.LLM_API_KEY;
}
await saveSettings(updatedSettings, { onSuccess: config?.onSuccess });
await saveSettings(updatedSettings);
};
const value = React.useMemo(
@@ -16,7 +16,6 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
github_token: settings.github_token,
unset_github_token: settings.unset_github_token,
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
user_consents_to_analytics: settings.user_consents_to_analytics,
};
await OpenHands.saveSettings(apiSettings);
@@ -23,9 +23,6 @@ export const useActiveHost = () => {
},
enabled: !RUNTIME_INACTIVE_STATES.includes(curAgentState),
initialData: { hosts: [] },
meta: {
disableToast: true,
},
});
const apps = useQueries({
@@ -40,9 +37,6 @@ export const useActiveHost = () => {
}
},
refetchInterval: 3000,
meta: {
disableToast: true,
},
})),
});
@@ -16,8 +16,5 @@ export const useIsAuthed = () => {
enabled: !!appMode,
staleTime: 1000 * 60 * 5, // 5 minutes
retry: false,
meta: {
disableToast: true,
},
});
};
@@ -0,0 +1,24 @@
import { useQuery } from "@tanstack/react-query";
import { useConversation } from "#/context/conversation-context";
import OpenHands from "#/api/open-hands";
export const useSearchEvents = (params: {
query?: string;
startId?: number;
limit?: number;
eventType?: string;
source?: string;
startDate?: string;
endDate?: string;
}) => {
const { conversationId } = useConversation();
return useQuery({
queryKey: ["search_events", conversationId, params],
queryFn: () => {
if (!conversationId) throw new Error("No conversation ID");
return OpenHands.searchEvents(conversationId, params);
},
enabled: !!conversationId,
});
};
-4
View File
@@ -19,7 +19,6 @@ const getSettingsQueryFn = async () => {
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
GITHUB_TOKEN_IS_SET: apiSettings.github_token_is_set,
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
};
};
@@ -32,9 +31,6 @@ export const useSettings = () => {
initialData: DEFAULT_SETTINGS,
staleTime: 0,
retry: false,
meta: {
disableToast: true,
},
});
React.useEffect(() => {
@@ -1,34 +0,0 @@
import React from "react";
import { useCurrentSettings } from "#/context/settings-context";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
export const useMigrateUserConsent = () => {
const { saveUserSettings } = useCurrentSettings();
/**
* Migrate user consent to the settings store on the server.
*/
const migrateUserConsent = React.useCallback(
async (args?: { handleAnalyticsWasPresentInLocalStorage: () => void }) => {
const userAnalyticsConsent = localStorage.getItem("analytics-consent");
if (userAnalyticsConsent) {
args?.handleAnalyticsWasPresentInLocalStorage();
await saveUserSettings(
{ user_consents_to_analytics: userAnalyticsConsent === "true" },
{
onSuccess: () => {
handleCaptureConsent(userAnalyticsConsent === "true");
},
},
);
localStorage.removeItem("analytics-consent");
}
},
[],
);
return { migrateUserConsent };
};
+114 -121
View File
@@ -3,7 +3,7 @@
"en": "App",
"ja": "アプリ",
"zh-CN": "应用",
"zh-TW": "應用程式",
"zh-TW": "應用",
"ko-KR": "앱",
"no": "App",
"it": "App",
@@ -33,7 +33,7 @@
"en": "If you tell OpenHands to start a web server, the app will appear here.",
"ja": "OpenHandsにWebサーバーの起動を指示すると、ここにアプリが表示されます。",
"zh-CN": "如果您告诉OpenHands启动Web服务器,应用将在此处显示。",
"zh-TW": "如果您要求 OpenHands 啟動網頁伺服器,應用程式將會顯示在此處。",
"zh-TW": "如果您告訴OpenHands啟動Web伺服器,應用在此處顯示。",
"ko-KR": "OpenHands에게 웹 서버를 시작하도록 지시하면 여기에 앱이 표시됩니다.",
"no": "Hvis du ber OpenHands om å starte en webserver, vil appen vises her.",
"it": "Se chiedi a OpenHands di avviare un server web, l'app apparirà qui.",
@@ -138,7 +138,7 @@
"en": "Waiting for client to become ready...",
"ja": "クライアントの準備を待機中...",
"zh-CN": "等待客户端准备就绪...",
"zh-TW": "正在等待戶端就緒...",
"zh-TW": "等待戶端準備就緒...",
"ko-KR": "클라이언트 준비를 기다리는 중...",
"de": "Warte auf Bereitschaft des Clients...",
"no": "Venter på at klienten skal bli klar...",
@@ -227,7 +227,7 @@
"CODE_EDITOR$EMPTY_MESSAGE": {
"en": "No file selected.",
"zh-CN": "未选择文件",
"zh-TW": "未選擇檔案",
"zh-TW": "未選擇檔案",
"de": "Keine Datei ausgewählt.",
"ko-KR": "선택된 파일이 없습니다",
"no": "Ingen fil valgt.",
@@ -245,7 +245,7 @@
"de": "Fehler beim Auswählen der Datei. Bitte versuchen Sie es erneut.",
"ko-KR": "파일 선택 중 오류가 발생했습니다. 다시 시도해 주세요.",
"no": "Feil ved valg av fil. Vennligst prøv igjen.",
"zh-TW": "選擇檔案時發生錯誤,請再試一次",
"zh-TW": "選擇檔案時出錯。請重試。",
"it": "Errore durante la selezione del file. Riprova.",
"pt": "Erro ao selecionar o arquivo. Por favor, tente novamente.",
"es": "Error al seleccionar el archivo. Por favor, inténtelo de nuevo.",
@@ -260,7 +260,7 @@
"de": "Fehler beim Hochladen der Dateien. Bitte versuchen Sie es erneut.",
"ko-KR": "파일 업로드 중 오류가 발생했습니다. 다시 시도해 주세요.",
"no": "Feil ved opplasting av filer. Vennligst prøv igjen.",
"zh-TW": "上傳檔案時發生錯誤,請再試一次",
"zh-TW": "上傳檔案時出錯。請重試。",
"it": "Errore durante il caricamento dei file. Riprova.",
"pt": "Erro ao fazer upload dos arquivos. Por favor, tente novamente.",
"es": "Error al subir los archivos. Por favor, inténtelo de nuevo.",
@@ -275,7 +275,7 @@
"de": "Fehler beim Auflisten der Dateien. Bitte versuchen Sie es erneut.",
"ko-KR": "파일 목록을 가져오는 중 오류가 발생했습니다. 다시 시도해 주세요.",
"no": "Feil ved listing av filer. Vennligst prøv igjen.",
"zh-TW": "列出檔案時發生錯誤,請再試一次",
"zh-TW": "列出檔案時出錯。請重試。",
"it": "Errore durante l'elenco dei file. Riprova.",
"pt": "Erro ao listar arquivos. Por favor, tente novamente.",
"es": "Error al listar los archivos. Por favor, inténtelo de nuevo.",
@@ -290,7 +290,7 @@
"de": "Fehler beim Speichern der Datei. Bitte versuchen Sie es erneut.",
"ko-KR": "파일 저장 중 오류가 발생했습니다. 다시 시도해 주세요.",
"no": "Feil ved lagring av fil. Vennligst prøv igjen.",
"zh-TW": "儲存檔案時發生錯誤,請再試一次",
"zh-TW": "儲存檔案時出錯。請重試。",
"it": "Errore durante il salvataggio del file. Riprova.",
"pt": "Erro ao salvar o arquivo. Por favor, tente novamente.",
"es": "Error al guardar el archivo. Por favor, inténtelo de nuevo.",
@@ -318,7 +318,7 @@
"en": "Auto-merge Dependabot PRs",
"ja": "Dependabot PRを自動マージ",
"zh-CN": "自动合并Dependabot PR",
"zh-TW": "自動合併 Dependabot PR",
"zh-TW": "自動合併Dependabot PR",
"ko-KR": "Dependabot PR 자동 병합",
"de": "Dependabot PRs automatisch zusammenführen",
"no": "Auto-flett Dependabot PRs",
@@ -333,7 +333,7 @@
"en": "Improve README",
"ja": "READMEを改善",
"zh-CN": "改进README",
"zh-TW": "改README",
"zh-TW": "改README",
"ko-KR": "README 개선",
"de": "README verbessern",
"no": "Forbedre README",
@@ -348,7 +348,7 @@
"en": "Clean up dependencies",
"ja": "依存関係を整理",
"zh-CN": "清理依赖项",
"zh-TW": "清理相依性",
"zh-TW": "清理依賴項",
"ko-KR": "의존성 정리",
"de": "Abhängigkeiten bereinigen",
"no": "Rydd opp i avhengigheter",
@@ -365,7 +365,7 @@
"de": "Ungültiger Dateipfad. Bitte überprüfen Sie den Dateinamen und versuchen Sie es erneut.",
"ko-KR": "잘못된 파일 경로입니다. 파일 이름을 확인하고 다시 시도해 주세요.",
"no": "Ugyldig filbane. Vennligst sjekk filnavnet og prøv igjen.",
"zh-TW": "無效的檔案路徑請檢查檔案名稱後再試一次",
"zh-TW": "檔案路徑無效。請檢查檔案名稱並重試。",
"it": "Percorso del file non valido. Controlla il nome del file e riprova.",
"pt": "Caminho de arquivo inválido. Por favor, verifique o nome do arquivo e tente novamente.",
"es": "Ruta de archivo inválida. Por favor, verifique el nombre del archivo e inténtelo de nuevo.",
@@ -380,7 +380,7 @@
"de": "Planer",
"ko-KR": "플래너",
"no": "Planlegger",
"zh-TW": "規劃工具",
"zh-TW": "規劃",
"ar": "المخطط",
"fr": "Planificateur",
"it": "Pianificatore",
@@ -438,7 +438,7 @@
"en": "Open in VS Code",
"ja": "VS Codeで開く",
"zh-CN": "在VS Code中打开",
"zh-TW": "在 VS Code 中開啟",
"zh-TW": "在VS Code中開啟",
"ko-KR": "VS Code에서 열기",
"de": "In VS Code öffnen",
"no": "Åpne i VS Code",
@@ -468,7 +468,7 @@
"en": "Auto-merge PRs",
"ja": "PRを自動マージ",
"zh-CN": "自动合并PR",
"zh-TW": "自動合併 PR",
"zh-TW": "自動合併PR",
"ko-KR": "PR 자동 병합",
"de": "PRs automatisch zusammenführen",
"no": "Auto-flett PRer",
@@ -483,7 +483,7 @@
"en": "Fix README",
"ja": "READMEを修正",
"zh-CN": "修复README",
"zh-TW": "修README",
"zh-TW": "修README",
"ko-KR": "README 수정",
"de": "README korrigieren",
"no": "Fiks README",
@@ -515,7 +515,7 @@
"de": "OpenHands Arbeitsverzeichnis",
"ko-KR": "OpenHands 워크스페이스 디렉토리",
"no": "OpenHands arbeidsmappe",
"zh-TW": "OpenHands 工作區目錄",
"zh-TW": "OpenHands工作區目錄",
"it": "Directory dell'area di lavoro OpenHands",
"pt": "Diretório do espaço de trabalho OpenHands",
"es": "Directorio del espacio de trabajo de OpenHands",
@@ -528,7 +528,7 @@
"en": "LLM Provider",
"ja": "LLMプロバイダー",
"zh-CN": "LLM提供商",
"zh-TW": "LLM 供應商",
"zh-TW": "LLM提供者",
"ko-KR": "LLM 제공자",
"no": "LLM-leverandør",
"it": "Provider LLM",
@@ -543,7 +543,7 @@
"en": "Select a provider",
"ja": "プロバイダーを選択",
"zh-CN": "选择提供商",
"zh-TW": "選擇供應商",
"zh-TW": "選擇提供者",
"ko-KR": "제공자 선택",
"no": "Velg en leverandør",
"it": "Seleziona un provider",
@@ -558,7 +558,7 @@
"en": "API Key",
"ja": "APIキー",
"zh-CN": "API密钥",
"zh-TW": "API 金鑰",
"zh-TW": "API金鑰",
"ko-KR": "API 키",
"no": "API-nøkkel",
"it": "Chiave API",
@@ -573,7 +573,7 @@
"en": "Don't know your API key?",
"ja": "APIキーがわかりませんか?",
"zh-CN": "不知道您的API密钥?",
"zh-TW": "不知道您的 API 金鑰?",
"zh-TW": "不知道您的API金鑰?",
"ko-KR": "API 키를 모르시나요?",
"no": "Kjenner du ikke API-nøkkelen din?",
"it": "Non conosci la tua chiave API?",
@@ -618,7 +618,7 @@
"en": "Reset to defaults",
"ja": "デフォルトにリセット",
"zh-CN": "重置为默认值",
"zh-TW": "還原為預設值",
"zh-TW": "重置為預設值",
"ko-KR": "기본값으로 재설정",
"no": "Tilbakestill til standard",
"it": "Ripristina valori predefiniti",
@@ -648,7 +648,7 @@
"en": "All information will be deleted",
"ja": "すべての情報が削除されます",
"zh-CN": "确定要重置所有设置吗?",
"zh-TW": "所有資訊將會被刪除",
"zh-TW": "確定要重置所有設定嗎?",
"ko-KR": "모든 설정을 재설정하시겠습니까?",
"no": "All informasjon vil bli slettet",
"it": "Tutte le informazioni verranno eliminate",
@@ -678,7 +678,7 @@
"en": "Changing workspace settings will end the current session",
"ja": "ワークスペース設定を変更すると、現在のセッションが終了します",
"zh-CN": "确定要结束当前会话吗?",
"zh-TW": "變更工作區設定將會結束目前工作階段",
"zh-TW": "確定要結束目前工作階段嗎?",
"ko-KR": "현재 세션을 종료하시겠습니까?",
"no": "Endring av arbeidsområdeinnstillinger vil avslutte gjeldende økt",
"it": "La modifica delle impostazioni dell'area di lavoro terminerà la sessione corrente",
@@ -738,7 +738,7 @@
"en": "GitHub Token",
"ja": "GitHubトークン",
"zh-CN": "GitHub令牌",
"zh-TW": "GitHub 權杖",
"zh-TW": "GitHub權杖",
"ko-KR": "GitHub 토큰",
"no": "GitHub-token",
"it": "Token GitHub",
@@ -753,7 +753,7 @@
"en": "GitHub Token (Optional)",
"ja": "GitHubトークン(任意)",
"zh-CN": "(可选)",
"zh-TW": "GitHub 權杖(選填)",
"zh-TW": "(選填)",
"ko-KR": "(선택사항)",
"no": "GitHub-token (valgfritt)",
"it": "Token GitHub (opzionale)",
@@ -798,7 +798,7 @@
"en": "Enable analytics",
"ja": "アナリティクスを有効にする",
"zh-CN": "启用分析",
"zh-TW": "啟用分析功能",
"zh-TW": "啟用分析",
"ko-KR": "분석 활성화",
"no": "Aktiver analyse",
"it": "Abilita analisi",
@@ -813,7 +813,7 @@
"en": "Invalid GitHub token",
"ja": "GitHubトークンが無効です",
"zh-CN": "GitHub令牌无效",
"zh-TW": "GitHub 權杖無效",
"zh-TW": "GitHub權杖無效",
"ko-KR": "GitHub 토큰이 유효하지 않습니다",
"no": "Ugyldig GitHub-token",
"it": "Token GitHub non valido",
@@ -843,7 +843,7 @@
"en": "Configure Github Repositories",
"ja": "GitHubリポジトリを設定",
"zh-CN": "配置GitHub仓库",
"zh-TW": "設定 GitHub 儲存庫",
"zh-TW": "設定GitHub儲存庫",
"ko-KR": "GitHub 저장소 설정",
"no": "Konfigurer GitHub-repositorier",
"it": "Configura repository GitHub",
@@ -858,7 +858,7 @@
"en": "Click here for instructions",
"ja": "手順はこちらをクリック",
"zh-CN": "点击查看说明",
"zh-TW": "點此檢視說明",
"zh-TW": "點擊查看說明",
"ko-KR": "설명을 보려면 클릭하세요",
"no": "Klikk her for instruksjoner",
"it": "Clicca qui per le istruzioni",
@@ -888,7 +888,7 @@
"en": "LLM Model",
"ja": "LLMモデル",
"zh-CN": "LLM模型",
"zh-TW": "LLM 模型",
"zh-TW": "LLM模型",
"ko-KR": "LLM 모델",
"no": "LLM-modell",
"it": "Modello LLM",
@@ -905,7 +905,7 @@
"de": "Standard: ./workspace",
"ko-KR": "워크스페이스 디렉토리 경로 입력",
"no": "Standard: ./workspace",
"zh-TW": "預設:./workspace",
"zh-TW": "輸入工作區目錄路徑",
"ar": "الافتراضي: ./workspace",
"fr": "Par défaut: ./workspace",
"it": "Predefinito: ./workspace",
@@ -950,7 +950,7 @@
"de": "Wähle ein Modell",
"ko-KR": "모델 선택",
"no": "Velg en modell",
"zh-TW": "選擇模型",
"zh-TW": "選擇模型",
"it": "Seleziona un modello",
"pt": "Selecione um modelo",
"es": "Seleccionar un modelo",
@@ -965,7 +965,7 @@
"de": "Agent",
"ko-KR": "에이전트",
"no": "Agent",
"zh-TW": "智慧代理",
"zh-TW": "代理",
"it": "Agente",
"pt": "Agente",
"es": "Agente",
@@ -980,7 +980,7 @@
"de": "Wähle einen Agenten",
"ko-KR": "에이전트 선택",
"no": "Velg en agent",
"zh-TW": "選擇智慧代理",
"zh-TW": "選擇代理",
"it": "Seleziona un agente",
"pt": "Selecione um agente",
"es": "Seleccionar un agente",
@@ -1010,7 +1010,7 @@
"de": "Wähle eine Sprache",
"ko-KR": "언어 선택",
"no": "Velg et språk",
"zh-TW": "選擇語言",
"zh-TW": "選擇語言",
"it": "Seleziona una lingua",
"pt": "Selecione um idioma",
"es": "Seleccionar un idioma",
@@ -1025,7 +1025,7 @@
"zh-CN": "安全性",
"ko-KR": "보안",
"no": "Sikkerhetsanalysator",
"zh-TW": "安全性分析工具",
"zh-TW": "安全性",
"it": "Analizzatore di sicurezza",
"pt": "Analisador de segurança",
"es": "Analizador de seguridad",
@@ -1040,7 +1040,7 @@
"zh-CN": "选择安全级别",
"ko-KR": "보안 수준 선택",
"no": "Velg en sikkerhetsanalysator (valgfritt)",
"zh-TW": "選擇安全性分析工具(選填)",
"zh-TW": "選擇安全等級",
"it": "Seleziona un analizzatore di sicurezza (opzionale)",
"pt": "Selecione um analisador de segurança (opcional)",
"es": "Seleccione un analizador de seguridad (opcional)",
@@ -1085,7 +1085,7 @@
"de": "Auf Standardwerte zurücksetzen",
"ko-KR": "재설정",
"no": "Tilbakestill til standardverdier",
"zh-TW": "還原為預設值",
"zh-TW": "重置",
"it": "Reimposta ai valori predefiniti",
"pt": "Redefinir para os padrões",
"es": "Restablecer valores predeterminados",
@@ -1205,7 +1205,7 @@
"zh-CN": "设置需要更新",
"ko-KR": "설정 업데이트 필요",
"no": "Vi har endret noen innstillinger i den siste oppdateringen. Ta deg tid til å se gjennom dem.",
"zh-TW": "我們在最新的更新中改變了一些設定,請花一些時間檢視這些更新",
"zh-TW": "設定需要更新",
"it": "Abbiamo modificato alcune impostazioni nell'ultimo aggiornamento. Prenditi un momento per rivederle.",
"pt": "Alteramos algumas configurações na última atualização. Reserve um momento para revisar.",
"es": "Hemos cambiado algunas configuraciones en la última actualización. Tómate un momento para revisarlas.",
@@ -1220,7 +1220,7 @@
"zh-CN": "代理加载中",
"ko-KR": "에이전트 로딩 중",
"no": "Vennligst vent mens agenten laster. Dette kan ta noen minutter...",
"zh-TW": "請稍待智慧代理載入,這可能需要幾分鐘的時間...",
"zh-TW": "代理載入中",
"it": "Attendere mentre l'agente si carica. Potrebbe richiedere alcuni minuti...",
"pt": "Por favor, aguarde enquanto o agente carrega. Isso pode levar alguns minutos...",
"es": "Por favor, espere mientras el agente se carga. Esto puede tardar unos minutos...",
@@ -1235,7 +1235,7 @@
"zh-CN": "代理运行中",
"ko-KR": "에이전트 실행 중",
"no": "Vennligst stopp agenten før du redigerer disse innstillingene.",
"zh-TW": "請先停止智慧代理再編輯這些設定。",
"zh-TW": "代理執行中",
"it": "Si prega di fermare l'agente prima di modificare queste impostazioni.",
"pt": "Por favor, pare o agente antes de editar estas configurações.",
"es": "Por favor, detenga el agente antes de editar estas configuraciones.",
@@ -1248,7 +1248,7 @@
"en": "Failed to fetch models and agents",
"zh-CN": "获取模型时出错",
"de": "Fehler beim Abrufen der Modelle und Agenten",
"zh-TW": "取得模型與智慧代理失敗",
"zh-TW": "取得模型時出錯",
"es": "Error al obtener modelos y agentes",
"fr": "Échec de la récupération des modèles et des agents",
"it": "Impossibile recuperare modelli e agenti",
@@ -1261,13 +1261,11 @@
},
"CONFIGURATION$SETTINGS_NOT_FOUND": {
"en": "Settings not found. Please check your API key",
"es": "Configuraciones no encontradas. Por favor revisa tu API key",
"zh-TW": "找不到設定。請檢查您的 API 金鑰"
"es": "Configuraciones no encontradas. Por favor revisa tu API key"
},
"CONNECT_TO_GITHUB_BY_TOKEN_MODAL$TERMS_OF_SERVICE": {
"en": "terms of service",
"es": "términos de servicio",
"zh-TW": "服務條款"
"es": "términos de servicio"
},
"SESSION$SERVER_CONNECTED_MESSAGE": {
"en": "Connected to server",
@@ -1287,7 +1285,7 @@
"en": "Error handling message",
"zh-CN": "处理会话时出错",
"de": "Fehler beim Verarbeiten der Nachricht",
"zh-TW": "處理工作階段時發生錯誤",
"zh-TW": "處理工作階段時出錯",
"es": "Error al procesar el mensaje",
"fr": "Erreur lors du traitement du message",
"it": "Errore durante l'elaborazione del messaggio",
@@ -1302,7 +1300,7 @@
"en": "Error connecting to session",
"zh-CN": "连接会话时出错",
"de": "Verbindung zur Sitzung fehlgeschlagen",
"zh-TW": "連線工作階段時發生錯誤",
"zh-TW": "連線工作階段時出錯",
"es": "Error al conectar con la sesión",
"fr": "Erreur de connexion à la session",
"it": "Errore durante la connessione alla sessione",
@@ -1332,7 +1330,7 @@
"en": "Error uploading file",
"zh-CN": "上传时出错",
"de": "Fehler beim Hochladen der Datei",
"zh-TW": "上傳時發生錯誤",
"zh-TW": "上傳時出錯",
"es": "Error al subir el archivo",
"fr": "Erreur lors du téléchargement du fichier",
"it": "Errore durante il caricamento del file",
@@ -1407,7 +1405,7 @@
"en": "Error refreshing workspace",
"zh-CN": "刷新时出错",
"de": "Fehler beim Aktualisieren des Arbeitsbereichs",
"zh-TW": "重新整理時發生錯誤",
"zh-TW": "重新整理時出錯",
"es": "Error al actualizar el espacio de trabajo",
"fr": "Erreur lors de l'actualisation de l'espace de travail",
"it": "Errore durante l'aggiornamento dell'area di lavoro",
@@ -1481,7 +1479,7 @@
"EXPLORER$VSCODE_SWITCHING_MESSAGE": {
"en": "Switching to VS Code in 3 seconds...\nImportant: Please inform the agent of any changes you make in VS Code. To avoid conflicts, wait for the assistant to complete its work before making your own changes.",
"zh-CN": "切换到VS Code中...",
"zh-TW": "切換到 VS Code 中...",
"zh-TW": "切換到VS Code中...",
"ja": "3秒後にVS Codeに切り替わります...\n重要:VS Codeで行った変更はエージェントに通知してください。競合を避けるため、アシスタントの作業が完了するまで自身の変更を待ってください。",
"ko-KR": "VS Code로 전환 중...",
"no": "Bytter til VS Code om 3 sekunder...\nViktig: Vennligst informer agenten om eventuelle endringer du gjør i VS Code. For å unngå konflikter, vent til assistenten er ferdig med sitt arbeid før du gjør dine egne endringer.",
@@ -1496,7 +1494,7 @@
"EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE": {
"en": "Error switching to VS Code: {{error}}",
"zh-CN": "切换到VS Code时出错",
"zh-TW": "切換到 VS Code 時發生錯誤",
"zh-TW": "切換到VS Code時出錯",
"ja": "VS Codeへの切り替え中にエラーが発生しました: {{error}}",
"ko-KR": "VS Code로 전환 중 오류 발생",
"no": "Feil ved bytte til VS Code: {{error}}",
@@ -1512,7 +1510,7 @@
"en": "Return to existing session?",
"de": "Zurück zu vorhandener Sitzung?",
"zh-CN": "是否继续未完成的会话?",
"zh-TW": "是否繼續未完成的會話",
"zh-TW": "是否繼續未完成的會話?",
"es": "¿Volver a la sesión existente?",
"fr": "Revenir à la session existante ?",
"it": "Tornare alla sessione esistente?",
@@ -1572,7 +1570,7 @@
"en": "Share feedback",
"de": "Feedback teilen",
"zh-CN": "分享反馈",
"zh-TW": "分享饋",
"zh-TW": "分享饋",
"es": "Compartir comentarios",
"fr": "Partager des commentaires",
"it": "Condividi feedback",
@@ -1587,7 +1585,7 @@
"en": "To help us improve, we collect feedback from your interactions to improve our prompts. By submitting this form, you consent to us collecting this data.",
"de": "Um uns zu verbessern, sammeln wir Feedback aus Ihren Interaktionen, um unsere Prompts zu verbessern. Durch das Absenden dieses Formulars stimmen Sie der Erfassung dieser Daten zu.",
"zh-CN": "为了帮助我们改进,我们会收集您的互动反馈以改进我们的提示。提交此表单即表示您同意我们收集这些数据。",
"zh-TW": "為了幫助我們改進,我們會收集您的互動饋以改進我們的提示。提交此表單即表示您同意我們收集這些資料。",
"zh-TW": "為了幫助我們改進,我們會收集您的互動饋以改進我們的提示。提交此表單即表示您同意我們收集這些數據。",
"es": "Para ayudarnos a mejorar, recopilamos comentarios de sus interacciones para mejorar nuestras indicaciones. Al enviar este formulario, usted consiente que recopilemos estos datos.",
"fr": "Pour nous aider à nous améliorer, nous recueillons des commentaires de vos interactions pour améliorer nos invites. En soumettant ce formulaire, vous consentez à ce que nous collections ces données.",
"it": "Per aiutarci a migliorare, raccogliamo feedback dalle tue interazioni per migliorare i nostri prompt. Inviando questo modulo, acconsenti alla raccolta di questi dati.",
@@ -1617,7 +1615,7 @@
"en": "Contribute to public dataset",
"de": "Zum öffentlichen Datensatz beitragen",
"zh-CN": "贡献到公共数据集",
"zh-TW": "貢獻到公共資料集",
"zh-TW": "貢獻到公共數據集",
"es": "Contribuir al conjunto de datos público",
"fr": "Contribuer à l'ensemble de données public",
"it": "Contribuisci al dataset pubblico",
@@ -1677,7 +1675,7 @@
"en": "Password copied to clipboard.",
"es": "Contraseña copiada al portapapeles.",
"zh-CN": "密码已复制到剪贴板。",
"zh-TW": "密碼已複製到剪貼簿。",
"zh-TW": "密碼已複製到剪貼。",
"ko-KR": "비밀번호가 클립보드에 복사되었습니다.",
"no": "Passord kopiert til utklippstavlen.",
"ar": "تم نسخ كلمة المرور إلى الحافظة.",
@@ -1692,7 +1690,7 @@
"en": "Go to shared feedback",
"es": "Ir a feedback compartido",
"zh-CN": "转到共享反馈",
"zh-TW": "前往分享回饋",
"zh-TW": "前往共享反饋",
"ko-KR": "공유된 피드백으로 이동",
"no": "Gå til delt tilbakemelding",
"ar": "الذهاب إلى التعليقات المشتركة",
@@ -1737,7 +1735,7 @@
"en": "Failed to share, please contact the developers:",
"es": "Error al compartir, por favor contacta con los desarrolladores:",
"zh-CN": "分享失败,请联系开发人员:",
"zh-TW": "分享失敗,請聯開發人員:",
"zh-TW": "分享失敗,請聯開發人員:",
"ko-KR": "공유 실패, 개발자에게 문의하세요:",
"no": "Deling mislyktes, vennligst kontakt utviklerne:",
"ar": "فشل المشاركة، يرجى الاتصال بالمطورين:",
@@ -1842,7 +1840,7 @@
"en": "Ask for user confirmation on risk severity:",
"es": "Preguntar por confirmación del usuario sobre severidad del riesgo:",
"zh-CN": "询问用户确认风险等级:",
"zh-TW": "詢問使用者確認風險等級:",
"zh-TW": "詢問用戶確認風險等級:",
"ko-KR": "위험 심각도에 대한 사용자 확인 요청:",
"no": "Be om brukerbekreftelse på risikoalvorlighet:",
"ar": "اطلب تأكيد المستخدم على مستوى الخطورة:",
@@ -1902,7 +1900,7 @@
"en": "Click to learn more",
"es": "Clic para aprender más",
"zh-CN": "点击了解更多",
"zh-TW": "點了解更多",
"zh-TW": "點了解更多",
"ko-KR": "자세히 알아보기",
"no": "Klikk for å lære mer",
"ar": "انقر لمعرفة المزيد",
@@ -2022,7 +2020,7 @@
"en": "Agent is initialized, waiting for task...",
"de": "Agent ist initialisiert und wartet auf Aufgabe...",
"zh-CN": "智能体已初始化,等待任务中...",
"zh-TW": "智慧代理已初始化,等待任務中...",
"zh-TW": "智能體已初始化,等待任務中...",
"ko-KR": "에이전트가 초기화되었습니다. 작업을 기다리는 중...",
"no": "Agenten er initialisert, venter på oppgave...",
"it": "L'agente è inizializzato, in attesa di compiti...",
@@ -2037,7 +2035,7 @@
"en": "Agent is running task",
"de": "Agent führt Aufgabe aus",
"zh-CN": "智能体正在执行任务...",
"zh-TW": "智慧代理正在執行任務...",
"zh-TW": "智能體正在執行任務...",
"ko-KR": "에이전트가 작업을 실행 중입니다",
"no": "Agenten utfører oppgave",
"it": "L'agente sta eseguendo il compito",
@@ -2052,7 +2050,7 @@
"en": "Agent is awaiting user input...",
"de": "Agent wartet auf Benutzereingabe...",
"zh-CN": "智能体正在等待用户输入...",
"zh-TW": "智慧代理正在等待使用者輸入...",
"zh-TW": "智能體正在等待用戶輸入...",
"ko-KR": "에이전트가 사용자 입력을 기다리고 있습니다...",
"no": "Agenten venter på brukerinndata...",
"it": "L'agente è in attesa dell'input dell'utente...",
@@ -2066,7 +2064,7 @@
"CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE": {
"en": "Agent is Rate Limited",
"zh-CN": "智能体已达到速率限制",
"zh-TW": "智慧代理已達到速率限制",
"zh-TW": "智能體已達到速率限制",
"de": "Agent ist ratenbegrenzt",
"ko-KR": "에이전트가 속도 제한되었습니다",
"no": "Agenten er hastighetsbegrenset",
@@ -2082,7 +2080,7 @@
"en": "Agent has paused.",
"de": "Agent pausiert.",
"zh-CN": "智能体已暂停",
"zh-TW": "智慧代理已暫停",
"zh-TW": "智能體已暫停",
"ko-KR": "에이전트가 일시 중지되었습니다.",
"no": "Agenten har pauset.",
"it": "L'agente ha messo in pausa.",
@@ -2097,7 +2095,7 @@
"en": "Let's start building!",
"ja": "開発を始めましょう!",
"zh-CN": "让我们开始构建!",
"zh-TW": "讓我們開始構!",
"zh-TW": "讓我們開始構",
"ko-KR": "시작해봅시다!",
"fr": "Commençons à construire !",
"es": "¡Empecemos a construir!",
@@ -2112,7 +2110,7 @@
"en": "OpenHands makes it easy to build and maintain software using a simple prompt.",
"ja": "OpenHandsは、シンプルなプロンプトを使用してソフトウェアの開発と保守を簡単にします。",
"zh-CN": "OpenHands 使用简单的提示来轻松构建和维护软件。",
"zh-TW": "OpenHands 使用簡單的提示來輕鬆構和維護軟。",
"zh-TW": "OpenHands 使用簡單的提示來輕鬆構和維護軟。",
"ko-KR": "OpenHands는 간단한 프롬프트를 사용하여 소프트웨어를 쉽게 구축하고 유지관리할 수 있게 해줍니다.",
"fr": "OpenHands facilite la construction et la maintenance de logiciels à l'aide d'une invite simple.",
"es": "OpenHands facilita la construcción y el mantenimiento de software usando un prompt simple.",
@@ -2157,7 +2155,7 @@
"en": "Create a Hello World app",
"ja": "Hello World アプリを作成",
"zh-CN": "创建一个 Hello World 应用",
"zh-TW": "建一個 Hello World 應用程式",
"zh-TW": "建一個 Hello World 應用",
"ko-KR": "Hello World 앱 만들기",
"fr": "Créer une application Hello World",
"es": "Crear una aplicación Hello World",
@@ -2172,7 +2170,7 @@
"en": "Build a todo list application",
"ja": "ToDoリストアプリを開発する",
"zh-CN": "构建一个待办事项列表应用",
"zh-TW": "構一個待辦事項列表應用程式",
"zh-TW": "構一個待辦事項列表應用",
"ko-KR": "할 일 목록 앱 만들기",
"fr": "Construire une application de liste de tâches",
"es": "Construir una aplicación de lista de tareas",
@@ -2187,7 +2185,7 @@
"en": "Write a bash script that shows the top story on Hacker News",
"ja": "Hacker Newsのトップ記事を表示するbashスクリプトを作成する",
"zh-CN": "编写一个显示Hacker News头条新闻的bash脚本",
"zh-TW": "編寫一個顯示 Hacker News 頭條新聞的 bash 腳本",
"zh-TW": "編寫一個顯示Hacker News頭條新聞的bash腳本",
"ko-KR": "Hacker News의 상위 기사를 보여주는 bash 스크립트 작성하기",
"fr": "Écrire un script bash qui affiche l'article principal de Hacker News",
"es": "Escribir un script bash que muestre la noticia principal de Hacker News",
@@ -2217,7 +2215,7 @@
"en": "Connect to GitHub",
"ja": "GitHubに接続",
"zh-CN": "连接到GitHub",
"zh-TW": "連線到 GitHub",
"zh-TW": "連接到GitHub",
"ko-KR": "GitHub에 연결",
"fr": "Se connecter à GitHub",
"es": "Conectar con GitHub",
@@ -2247,7 +2245,7 @@
"en": "Add more repositories...",
"ja": "リポジトリを追加...",
"zh-CN": "添加更多仓库...",
"zh-TW": "新增更多儲存庫...",
"zh-TW": "添加更多儲存庫...",
"ko-KR": "더 많은 저장소 추가...",
"fr": "Ajouter plus de dépôts...",
"es": "Agregar más repositorios...",
@@ -2427,7 +2425,7 @@
"en": "Resume the agent task",
"ja": "エージェントのタスクを再開",
"zh-CN": "恢复代理任务",
"zh-TW": "恢復智慧代理任務",
"zh-TW": "恢復代理任務",
"ko-KR": "에이전트 작업 재개",
"fr": "Reprendre la tâche de l'agent",
"es": "Reanudar la tarea del agente",
@@ -2442,7 +2440,7 @@
"en": "Pause the current task",
"ja": "現在のタスクを一時停止",
"zh-CN": "暂停当前任务",
"zh-TW": "暫停前任務",
"zh-TW": "暫停前任務",
"ko-KR": "현재 작업 일시 중지",
"fr": "Mettre en pause la tâche actuelle",
"es": "Pausar la tarea actual",
@@ -2577,7 +2575,7 @@
"en": "Upload a .zip",
"ja": ".zipファイルをアップロード",
"zh-CN": "上传.zip文件",
"zh-TW": "上傳 .zip 檔案",
"zh-TW": "上傳.zip檔案",
"ko-KR": ".zip 파일 업로드",
"fr": "Télécharger un .zip",
"es": "Subir un .zip",
@@ -2637,7 +2635,7 @@
"en": "Auto-merge Dependabot PRs",
"ja": "DependabotのPRを自動マージ",
"zh-CN": "自动合并Dependabot PR",
"zh-TW": "自動合併 Dependabot PR",
"zh-TW": "自動合併Dependabot PR",
"ko-KR": "Dependabot PR 자동 병합",
"fr": "Fusion automatique des PR Dependabot",
"es": "Auto-fusionar PRs de Dependabot",
@@ -2652,7 +2650,7 @@
"en": "Agent has stopped.",
"de": "Agent hat angehalten.",
"zh-CN": "智能体已停止",
"zh-TW": "智慧代理已停止",
"zh-TW": "智能體已停止",
"ko-KR": "에이전트가 중지되었습니다.",
"no": "Agenten har stoppet.",
"it": "L'agente si è fermato.",
@@ -2667,7 +2665,7 @@
"en": "Agent has finished the task.",
"de": "Agent hat die Aufgabe erledigt.",
"zh-CN": "智能体已完成任务",
"zh-TW": "智慧代理已完成任務",
"zh-TW": "智能體已完成任務",
"ko-KR": "에이전트가 작업을 완료했습니다.",
"no": "Agenten har fullført oppgaven.",
"it": "L'agente ha completato il compito.",
@@ -2682,7 +2680,7 @@
"en": "Agent has rejected the task.",
"de": "Agent hat die Aufgabe abgelehnt.",
"zh-CN": "智能体拒绝任务",
"zh-TW": "智慧代理拒絕任務",
"zh-TW": "智能體拒絕任務",
"ko-KR": "에이전트가 작업을 거부했습니다.",
"no": "Agenten har avvist oppgaven.",
"it": "L'agente ha rifiutato il compito.",
@@ -2697,7 +2695,7 @@
"en": "Agent encountered an error.",
"de": "Agent ist auf einen Fehler gelaufen.",
"zh-CN": "智能体遇到错误",
"zh-TW": "智慧代理遇到錯誤",
"zh-TW": "智能體遇到錯誤",
"ko-KR": "에이전트에 오류가 발생했습니다.",
"no": "Agenten støtte på en feil.",
"it": "L'agente ha riscontrato un errore.",
@@ -2712,7 +2710,7 @@
"en": "Agent is awaiting user confirmation for the pending action.",
"de": "Agent wartet auf die Bestätigung des Benutzers für die ausstehende Aktion.",
"zh-CN": "代理正在等待用户确认待处理的操作。",
"zh-TW": "智慧代理正在等待用戶確認待處理的操作。",
"zh-TW": "代理正在等待用戶確認待處理的操作。",
"ko-KR": "에이전트가 대기 중인 작업에 대한 사용자 확인을 기다리고 있습니다.",
"no": "Agenten venter på brukerbekreftelse for den ventende handlingen.",
"it": "L'agente è in attesa della conferma dell'utente per l'azione in sospeso.",
@@ -2727,7 +2725,7 @@
"en": "Agent action has been confirmed!",
"de": "Die Aktion des Agenten wurde bestätigt!",
"zh-CN": "代理操作已确认!",
"zh-TW": "智慧代理操作已確認!",
"zh-TW": "代理操作已確認!",
"ko-KR": "에이전트 작업이 확인되었습니다!",
"no": "Agenthandlingen har blitt bekreftet!",
"it": "L'azione dell'agente è stata confermata!",
@@ -2742,7 +2740,7 @@
"en": "Agent action has been rejected!",
"de": "Die Aktion des Agenten wurde abgelehnt!",
"zh-CN": "代理操作已被拒绝!",
"zh-TW": "智慧代理操作已被拒絕!",
"zh-TW": "代理操作已被拒絕!",
"ko-KR": "에이전트 작업이 거부되었습니다!",
"no": "Agenthandlingen har blitt avvist!",
"it": "L'azione dell'agente è stata rifiutata!",
@@ -2759,7 +2757,7 @@
"de": "Sende eine Nachricht an den Assistenten...",
"ko-KR": "어시스턴트에게 메시지 보내기",
"no": "Send melding til assistenten...",
"zh-TW": "助理留言",
"zh-TW": "助理留言",
"it": "Invia un messaggio all'assistente...",
"pt": "Envie uma mensagem para o assistente...",
"es": "Mensaje al asistente...",
@@ -2834,7 +2832,7 @@
"de": "Senden",
"ko-KR": "전송",
"no": "Send",
"zh-TW": "送",
"zh-TW": "送",
"it": "Invia",
"pt": "Enviar",
"es": "Enviar",
@@ -2894,7 +2892,7 @@
"de": "Nachricht senden",
"ko-KR": "메시지 보내기",
"no": "Send melding",
"zh-TW": "送訊息",
"zh-TW": "送訊息",
"it": "Invia messaggio",
"pt": "Enviar mensagem",
"es": "Enviar mensaje",
@@ -2954,7 +2952,7 @@
"zh-CN": "回到底部",
"ko-KR": "맨 아래로",
"no": "Til bunnen",
"zh-TW": "回到底",
"zh-TW": "回到底",
"it": "In fondo",
"pt": "Para o fundo",
"es": "Ir al final",
@@ -3086,7 +3084,7 @@
"SETTINGS$AGENT_TOOLTIP": {
"en": "Select the agent to use.",
"zh-CN": "选择要使用的智能体",
"zh-TW": "選擇要使用的智慧代理。",
"zh-TW": "選擇要使用的智能體。",
"de": "Wähle den zu verwendenden Agenten.",
"ko-KR": "사용할 에이전트를 선택하세요.",
"no": "Velg agenten som skal brukes.",
@@ -3116,7 +3114,7 @@
"SETTINGS$DISABLED_RUNNING": {
"en": "Cannot be changed while the agent is running.",
"zh-CN": "在智能体运行时无法更改",
"zh-TW": "智慧代理正在執行時無法更改。",
"zh-TW": "智能體正在執行時無法更改。",
"de": "Kann bei laufender Aufgabe nicht geändert werden.",
"ko-KR": "에이전트가 실행 중일 때는 변경할 수 없습니다.",
"no": "Kan ikke endres mens agenten kjører.",
@@ -3176,7 +3174,7 @@
"SETTINGS$AGENT_SELECT_ENABLED": {
"en": "Enable Agent Selection - Advanced Users",
"zh-CN": "启用智能体选择 - 高级用户",
"zh-TW": "啟用智慧代理選擇 - 進階使用者",
"zh-TW": "啟用智能體選擇 - 進階使用者",
"de": "Agentenauswahl aktivieren - Fortgeschrittene Benutzer",
"ko-KR": "에이전트 선택 활성화 - 고급 사용자",
"no": "Aktiver agentvalg - Avanserte brukere",
@@ -3206,7 +3204,7 @@
"PLANNER$EMPTY_MESSAGE": {
"en": "No plan created.",
"zh-CN": "计划未创建",
"zh-TW": "未建任何計劃。",
"zh-TW": "未建任何計劃。",
"de": "Kein Plan erstellt.",
"ko-KR": "생성된 계획이 없습니다.",
"no": "Ingen plan opprettet.",
@@ -3251,7 +3249,7 @@
"STATUS$STARTING_RUNTIME": {
"en": "Starting Runtime...",
"zh-CN": "启动运行时...",
"zh-TW": "啟動行時...",
"zh-TW": "啟動行時...",
"de": "Laufzeitumgebung wird gestartet...",
"ko-KR": "런타임 시작 중...",
"no": "Starter kjøretidsmiljø...",
@@ -3357,7 +3355,7 @@
"en": "GitHub token is invalid. Please try again.",
"es": "El token de GitHub no es válido. Por favor, inténtelo de nuevo.",
"zh-CN": "GitHub令牌无效。请重试。",
"zh-TW": "GitHub 權杖無效",
"zh-TW": "GitHub權杖無效",
"ko-KR": "GitHub 토큰이 유효하지 않습니다",
"ja": "GitHubトークンが無効です",
"no": "GitHub-token er ugyldig. Vennligst prøv igjen.",
@@ -3507,7 +3505,7 @@
"en": "Base URL",
"es": "URL base",
"zh-CN": "基础URL",
"zh-TW": "基礎 URL",
"zh-TW": "基礎URL",
"ko-KR": "기본 URL",
"ja": "ベースURL",
"no": "Base URL",
@@ -3522,7 +3520,7 @@
"en": "API Key",
"es": "API Key",
"zh-CN": "API密钥",
"zh-TW": "API 金鑰",
"zh-TW": "API金鑰",
"ko-KR": "API 키",
"ja": "APIキー",
"no": "API-nøkkel",
@@ -3537,7 +3535,7 @@
"en": "Don't know your API key?",
"es": "¿No sabes tu API key?",
"zh-CN": "不知道您的API密钥?",
"zh-TW": "不知道您的 API 金鑰?",
"zh-TW": "不知道您的API金鑰?",
"ko-KR": "API 키를 모르시나요?",
"ja": "APIキーがわかりませんか?",
"no": "Kjenner du ikke API-nøkkelen din?",
@@ -3552,7 +3550,7 @@
"en": "Click here for instructions",
"es": "Clic aquí para instrucciones",
"zh-CN": "点击这里查看说明",
"zh-TW": "點此檢視說明",
"zh-TW": "點擊這裡查看說明",
"ko-KR": "설명을 보려면 여기를 클릭하세요",
"ja": "説明を見るにはここをクリック",
"no": "Klikk her for instruksjoner",
@@ -3567,7 +3565,7 @@
"en": "Agent",
"es": "Agente",
"zh-CN": "代理",
"zh-TW": "智慧代理",
"zh-TW": "代理",
"ko-KR": "에이전트",
"ja": "エージェント",
"no": "Agent",
@@ -3642,7 +3640,7 @@
"en": "Reset to defaults",
"es": "Reiniciar valores por defect",
"zh-CN": "重置为默认值",
"zh-TW": "還原為預設值",
"zh-TW": "重置為預設值",
"ko-KR": "기본값으로 재설정",
"ja": "デフォルトに戻す",
"no": "Tilbakestill til standardverdier",
@@ -3746,7 +3744,7 @@
"PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECT_TO_GITHUB": {
"en": "Connect to GitHub",
"zh-CN": "连接到GitHub",
"zh-TW": "連線到 GitHub",
"zh-TW": "連線到GitHub",
"ko-KR": "GitHub에 연결",
"ja": "GitHubに接続",
"no": "Koble til GitHub",
@@ -3792,7 +3790,7 @@
"en": "Error authenticating with the LLM provider. Please check your API key",
"es": "Error autenticando con el proveedor de LLM. Por favor revisa tu API key",
"zh-CN": "LLM认证错误",
"zh-TW": "LLM 認證錯誤",
"zh-TW": "LLM認證錯誤",
"ko-KR": "LLM 인증 오류",
"ja": "LLM認証エラー",
"no": "Feil ved autentisering med LLM-leverandøren. Vennligst sjekk API-nøkkelen din",
@@ -3818,10 +3816,6 @@
"es": "Hubo un error al conectar con el entorno de ejecución. Por favor, actualice la página.",
"tr": "Çalışma zamanına bağlanırken bir hata oluştu. Lütfen sayfayı yenileyin."
},
"STATUS$LLM_RETRY": {
"en": "Retrying LLM request",
"zh-TW": "重新嘗試 LLM 請求中"
},
"AGENT_ERROR$BAD_ACTION": {
"en": "Agent tried to execute a malformed action.",
"zh-CN": "错误的操作",
@@ -3856,7 +3850,7 @@
"en": "Connect to GitHub",
"es": "Conectar a GitHub",
"zh-CN": "连接到GitHub",
"zh-TW": "連線到 GitHub",
"zh-TW": "連線到GitHub",
"ko-KR": "GitHub에 연결",
"ja": "GitHubに接続",
"no": "Koble til GitHub",
@@ -3871,7 +3865,7 @@
"en": "Push to GitHub",
"es": "Subir a GitHub",
"zh-CN": "推送到GitHub",
"zh-TW": "推送到 GitHub",
"zh-TW": "推送到GitHub",
"ko-KR": "GitHub에 푸시",
"ja": "GitHubにプッシュ",
"no": "Push til GitHub",
@@ -4050,7 +4044,7 @@
"ACTION_MESSAGE$RUN_IPYTHON": {
"en": "Running a Python command",
"zh-CN": "运行IPython",
"zh-TW": "執行 IPython",
"zh-TW": "執行IPython",
"ko-KR": "IPython 실행",
"ja": "IPythonを実行",
"no": "Kjører en Python-kommando",
@@ -4140,7 +4134,7 @@
"OBSERVATION_MESSAGE$RUN_IPYTHON": {
"en": "Ran a Python command",
"zh-CN": "运行IPython",
"zh-TW": "執行 IPython",
"zh-TW": "執行IPython",
"ko-KR": "IPython 실행",
"ja": "IPythonを実行",
"no": "Kjørte en Python-kommando",
@@ -4215,7 +4209,7 @@
"EXPANDABLE_MESSAGE$SHOW_DETAILS": {
"en": "Show details",
"zh-CN": "显示详情",
"zh-TW": "顯示詳細資訊",
"zh-TW": "顯示詳",
"ko-KR": "상세 정보 표시",
"ja": "詳細を表示",
"no": "Vis detaljer",
@@ -4230,7 +4224,7 @@
"EXPANDABLE_MESSAGE$HIDE_DETAILS": {
"en": "Hide details",
"zh-CN": "隐藏详情",
"zh-TW": "隱藏詳細資訊",
"zh-TW": "隱藏詳",
"ko-KR": "상세 정보 숨기기",
"ja": "詳細を非表示",
"no": "Skjul detaljer",
@@ -4246,7 +4240,7 @@
"en": "AI Provider Configuration",
"ja": "AI プロバイダー設定",
"zh-CN": "AI 提供商配置",
"zh-TW": "AI 供應商設定",
"zh-TW": "AI 提供商配置",
"ko-KR": "AI 제공자 設정",
"no": "AI-leverandørkonfigurasjon",
"it": "Configurazione del provider AI",
@@ -4321,7 +4315,7 @@
"en": "Add best practices docs for contributors",
"ja": "コントリビューター向けのベストプラクティスドキュメントを追加",
"zh-CN": "为贡献者添加最佳实践文档",
"zh-TW": "為貢獻者新增最佳實踐文",
"zh-TW": "為貢獻者添加最佳實踐文",
"ko-KR": "기여자를 위한 모범 사례 문서 추가",
"no": "Legg til beste praksis dokumentasjon for bidragsytere",
"it": "Aggiungere documenti sulle migliori pratiche per i contributori",
@@ -4336,7 +4330,7 @@
"en": "Add/improve a Dockerfile",
"ja": "Dockerfileを追加/改善",
"zh-CN": "添加/改进 Dockerfile",
"zh-TW": "新增/改進 Dockerfile",
"zh-TW": "添加/改進 Dockerfile",
"ko-KR": "Dockerfile 추가/개선",
"no": "Legg til/forbedre en Dockerfile",
"it": "Aggiungere/migliorare un Dockerfile",
@@ -4366,7 +4360,7 @@
"en": "No page loaded.",
"ja": "ページが読み込まれていません。",
"zh-CN": "页面未加载",
"zh-TW": "未載任何頁面。",
"zh-TW": "未載任何頁面。",
"de": "Keine Seite geladen.",
"ko-KR": "페이지가 로드되지 않았습니다.",
"no": "Ingen side lastet.",
@@ -4457,7 +4451,7 @@
"en": "Select a GitHub project",
"ja": "GitHubプロジェクトを選択",
"zh-CN": "选择GitHub项目",
"zh-TW": "選擇 GitHub 專案",
"zh-TW": "選擇GitHub專案",
"ko-KR": "GitHub 프로젝트 선택",
"fr": "Sélectionner un projet GitHub",
"es": "Seleccionar un proyecto de GitHub",
@@ -4472,7 +4466,7 @@
"en": "Send",
"ja": "送信",
"zh-CN": "发送",
"zh-TW": "送",
"zh-TW": "送",
"ko-KR": "보내기",
"fr": "Envoyer",
"es": "Enviar",
@@ -4514,7 +4508,6 @@
"de": "Was möchten Sie erstellen?"
},
"SETTINGS_FORM$ENABLE_DEFAULT_CONDENSER_SWITCH_LABEL": {
"en": "Enable Memory Condenser",
"zh-TW": "啟用記憶體壓縮器"
"en": "Enable Memory Condenser"
}
}
-1
View File
@@ -19,7 +19,6 @@ export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
github_token_is_set: DEFAULT_SETTINGS.GITHUB_TOKEN_IS_SET,
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
user_consents_to_analytics: DEFAULT_SETTINGS.USER_CONSENTS_TO_ANALYTICS,
};
const MOCK_USER_PREFERENCES: {
+6 -16
View File
@@ -1,22 +1,13 @@
import { QueryClientConfig, QueryCache } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { retrieveAxiosErrorMessage } from "./utils/retrieve-axios-error-message";
import { renderToastIfError } from "./utils/render-toast-if-error";
const QUERY_KEYS_TO_IGNORE = ["authenticated", "hosts", "settings"];
const shownErrors = new Set<string>();
export const queryClientConfig: QueryClientConfig = {
queryCache: new QueryCache({
onError: (error, query) => {
if (!query.meta?.disableToast) {
const errorMessage = retrieveAxiosErrorMessage(error);
if (!shownErrors.has(errorMessage)) {
toast.error(errorMessage || "An error occurred");
shownErrors.add(errorMessage);
setTimeout(() => {
shownErrors.delete(errorMessage);
}, 3000);
}
if (!QUERY_KEYS_TO_IGNORE.some((key) => query.queryKey.includes(key))) {
renderToastIfError(error);
}
},
}),
@@ -27,8 +18,7 @@ export const queryClientConfig: QueryClientConfig = {
},
mutations: {
onError: (error) => {
const message = retrieveAxiosErrorMessage(error);
toast.error(message);
renderToastIfError(error);
},
},
},
+4 -22
View File
@@ -9,7 +9,6 @@ import { WaitlistModal } from "#/components/features/waitlist/waitlist-modal";
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
import { useSettings } from "#/hooks/query/use-settings";
import { useAuth } from "#/context/auth-context";
import { useMigrateUserConsent } from "#/hooks/use-migrate-user-consent";
export function ErrorBoundary() {
const error = useRouteError();
@@ -46,9 +45,10 @@ export function ErrorBoundary() {
export default function MainApp() {
const { githubTokenIsSet } = useAuth();
const { data: settings } = useSettings();
const { migrateUserConsent } = useMigrateUserConsent();
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(
!localStorage.getItem("analytics-consent"),
);
const config = useConfig();
const {
@@ -68,22 +68,6 @@ export default function MainApp() {
}
}, [settings?.LANGUAGE]);
React.useEffect(() => {
const consentFormModalIsOpen =
settings?.USER_CONSENTS_TO_ANALYTICS === null;
setConsentFormIsOpen(consentFormModalIsOpen);
}, [settings]);
React.useEffect(() => {
// Migrate user consent to the server if it was previously stored in localStorage
migrateUserConsent({
handleAnalyticsWasPresentInLocalStorage: () => {
setConsentFormIsOpen(false);
},
});
}, []);
const userIsAuthed = !!isAuthed && !authError;
const renderWaitlistModal =
!isFetchingAuth && !userIsAuthed && config.data?.APP_MODE === "saas";
@@ -111,9 +95,7 @@ export default function MainApp() {
{config.data?.APP_MODE === "oss" && consentFormIsOpen && (
<AnalyticsConsentFormModal
onClose={() => {
setConsentFormIsOpen(false);
}}
onClose={() => setConsentFormIsOpen(false)}
/>
)}
</div>
-1
View File
@@ -13,7 +13,6 @@ export const DEFAULT_SETTINGS: Settings = {
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
GITHUB_TOKEN_IS_SET: false,
ENABLE_DEFAULT_CONDENSER: false,
USER_CONSENTS_TO_ANALYTICS: false,
};
/**
+4 -1
View File
@@ -17,7 +17,10 @@ interface GitHubRepository {
id: number;
full_name: string;
stargazers_count?: number;
link_header?: string;
}
interface GitHubAppRepository {
repositories: GitHubRepository[];
}
interface GitHubCommit {
-4
View File
@@ -9,7 +9,6 @@ export type Settings = {
REMOTE_RUNTIME_RESOURCE_FACTOR: number;
GITHUB_TOKEN_IS_SET: boolean;
ENABLE_DEFAULT_CONDENSER: boolean;
USER_CONSENTS_TO_ANALYTICS: boolean | null;
};
export type ApiSettings = {
@@ -23,17 +22,14 @@ export type ApiSettings = {
remote_runtime_resource_factor: number;
github_token_is_set: boolean;
enable_default_condenser: boolean;
user_consents_to_analytics: boolean | null;
};
export type PostSettings = Settings & {
github_token: string;
unset_github_token: boolean;
user_consents_to_analytics: boolean | null;
};
export type PostApiSettings = ApiSettings & {
github_token: string;
unset_github_token: boolean;
user_consents_to_analytics: boolean | null;
};
@@ -1,11 +1,12 @@
import { AxiosError } from "axios";
import toast from "react-hot-toast";
import { isAxiosErrorWithResponse } from "./type-guards";
/**
* Retrieve the error message from an Axios error
* Renders a toast with the error message from an Axios error
* @param error The error to render a toast for
*/
export const retrieveAxiosErrorMessage = (error: AxiosError) => {
export const renderToastIfError = (error: AxiosError) => {
let errorMessage: string | null = null;
if (isAxiosErrorWithResponse(error) && error.response?.data.error) {
@@ -14,5 +15,5 @@ export const retrieveAxiosErrorMessage = (error: AxiosError) => {
errorMessage = error.message;
}
return errorMessage || "An error occurred";
toast.error(errorMessage || "An error occurred");
};
+1 -3
View File
@@ -1,7 +1,7 @@
// Here are the list of verified models and providers that we know work well with OpenHands.
export const VERIFIED_PROVIDERS = ["openai", "azure", "anthropic", "deepseek"];
export const VERIFIED_MODELS = [
"o3-mini-2025-01-31",
"gpt-4o",
"claude-3-5-sonnet-20241022",
"deepseek-chat",
];
@@ -16,8 +16,6 @@ export const VERIFIED_OPENAI_MODELS = [
"gpt-4-32k",
"o1-mini",
"o1-preview",
"o3-mini",
"o3-mini-2025-01-31",
];
// LiteLLM does not return the compatible Anthropic models with the provider, so we list them here to set them ourselves
@@ -33,6 +33,7 @@ test.beforeEach(async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
localStorage.setItem("FEATURE_MULTI_CONVERSATION_UI", "true");
localStorage.setItem("analytics-consent", "true");
});
});
+3
View File
@@ -7,6 +7,9 @@ const dirname = path.dirname(filename);
test.beforeEach(async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
localStorage.setItem("analytics-consent", "true");
});
});
test("should redirect to /conversations after uploading a project zip", async ({
+3
View File
@@ -2,6 +2,9 @@ import test, { expect, Page } from "@playwright/test";
test.beforeEach(async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
localStorage.setItem("analytics-consent", "true");
});
});
const selectGpt4o = async (page: Page) => {
+1
View File
@@ -59,6 +59,7 @@ export default defineConfig(({ mode }) => {
test: {
environment: "jsdom",
setupFiles: ["vitest.setup.ts"],
reporters: "basic",
exclude: [...configDefaults.exclude, "tests"],
coverage: {
reporter: ["text", "json", "html", "lcov", "text-summary"],
@@ -11,7 +11,6 @@ from openhands.controller.state.state import State
from openhands.core.config import AgentConfig
from openhands.core.logger import openhands_logger as logger
from openhands.core.message import ImageContent, Message, TextContent
from openhands.core.schema import ActionType
from openhands.events.action import (
Action,
AgentDelegateAction,
@@ -90,7 +89,15 @@ class CodeActAgent(Agent):
self.pending_actions: deque[Action] = deque()
self.reset()
# Retrieve the enabled tools
self.mock_function_calling = False
if not self.llm.is_function_calling_active():
logger.info(
f'Function calling not enabled for model {self.llm.config.model}. '
'Mocking function calling via prompting.'
)
self.mock_function_calling = True
# Function calling mode
self.tools = codeact_function_calling.get_tools(
codeact_enable_browsing=self.config.codeact_enable_browsing,
codeact_enable_jupyter=self.config.codeact_enable_jupyter,
@@ -297,27 +304,10 @@ class CodeActAgent(Agent):
) # Content is already truncated by openhands-aci
elif isinstance(obs, BrowserOutputObservation):
text = obs.get_agent_obs_text()
if (
obs.trigger_by_action == ActionType.BROWSE_INTERACTIVE
and obs.set_of_marks is not None
and len(obs.set_of_marks) > 0
and self.config.enable_som_visual_browsing
and self.llm.vision_is_active()
and self.llm.is_visual_browser_tool_supported()
):
text += 'Image: Current webpage screenshot (Note that only visible portion of webpage is present in the screenshot. You may need to scroll to view the remaining portion of the web-page.)\n'
message = Message(
role='user',
content=[
TextContent(text=text),
ImageContent(image_urls=[obs.set_of_marks]),
],
)
else:
message = Message(
role='user',
content=[TextContent(text=text)],
)
message = Message(
role='user',
content=[TextContent(text=text)],
)
elif isinstance(obs, AgentDelegateObservation):
text = truncate_content(
obs.outputs['content'] if 'content' in obs.outputs else '',
@@ -389,6 +379,8 @@ class CodeActAgent(Agent):
'messages': self.llm.format_messages_for_llm(messages),
}
params['tools'] = self.tools
if self.mock_function_calling:
params['mock_function_calling'] = True
response = self.llm.completion(**params)
actions = codeact_function_calling.response_to_actions(response)
for action in actions:

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