Compare commits

..

33 Commits

Author SHA1 Message Date
openhands c377f9e9ae Merge main into feature/runtime-manager 2025-01-03 20:37:08 +00:00
Robert Brennan 9a6084c6d5 only destroy runtime if no one is active 2024-12-24 16:13:45 -05:00
Robert Brennan 30c1d032e3 move close 2024-12-24 16:12:24 -05:00
Robert Brennan 615eabe5ed add log statement 2024-12-24 16:08:16 -05:00
Robert Brennan 3ecd214d69 Merge branch 'main' into feature/runtime-manager 2024-12-24 15:54:40 -05:00
Robert Brennan c9a6402103 add cleanup logic 2024-12-24 15:52:58 -05:00
Robert Brennan 33a1dd89e7 remove conversation logic 2024-12-24 15:48:11 -05:00
Robert Brennan d3f726df51 change connect logic 2024-12-24 15:33:04 -05:00
Robert Brennan 333f9a5bdf fix tests 2024-12-24 15:27:57 -05:00
Robert Brennan 0d454d46f2 Revert "refactor: move runtime creation logic to RuntimeManager.get_runtime()"
This reverts commit 42730014d5.
2024-12-24 15:16:32 -05:00
Robert Brennan e7685f185c fix cli 2024-12-24 15:15:08 -05:00
Robert Brennan 749da6367e update cli 2024-12-24 15:14:18 -05:00
Robert Brennan 4b497c8e64 fix runtime_manager plumbing 2024-12-24 15:12:15 -05:00
openhands 42730014d5 refactor: move runtime creation logic to RuntimeManager.get_runtime() 2024-12-24 19:58:14 +00:00
openhands 81110671b2 refactor: use singleton RuntimeManager from shared.py 2024-12-24 19:57:06 +00:00
openhands 25f3349e1a refactor: use RuntimeManager to get existing runtime in Conversation 2024-12-24 19:52:35 +00:00
openhands 30f6166bf6 fix: Fix async mock in test_process_issue 2024-12-24 17:55:26 +00:00
Robert Brennan 1f706fe2f2 fix test 2024-12-24 12:32:02 -05:00
Robert Brennan 4123c65317 Merge branch 'main' into feature/runtime-manager 2024-12-24 12:04:33 -05:00
Robert Brennan 6dfd54be9f fix plumbing 2024-12-24 11:53:17 -05:00
openhands 8eef9b2563 Use server's shared config for RuntimeManager 2024-12-24 16:39:27 +00:00
openhands 5d5978c6cb Move runtime class resolution to RuntimeManager and remove redundant error callback 2024-12-24 16:35:14 +00:00
openhands 1a17972b4e Move RuntimeManager to module level and simplify config handling 2024-12-24 16:30:14 +00:00
openhands 4de7a4f85d Simplify RuntimeManager config handling and fix type issues 2024-12-24 16:25:24 +00:00
openhands 8befeca41d Fix linting issues and add missing await for create_runtime 2024-12-24 16:16:16 +00:00
openhands 918139e886 Move AppConfig to RuntimeManager class level and update initialization flow 2024-12-24 16:13:09 +00:00
openhands 6374174095 Update main.py to use RuntimeManager for runtime creation 2024-12-24 16:02:47 +00:00
Robert Brennan 138f6932eb move import 2024-12-24 10:08:13 -05:00
Robert Brennan 7181efd26d move import 2024-12-24 10:07:24 -05:00
Robert Brennan 3a52360ab0 remove exit 2024-12-24 10:05:07 -05:00
openhands cd9eb1d85c Fix singleton import and add tests for RuntimeManager 2024-12-24 15:03:10 +00:00
openhands ada657b476 Fix linting issues in runtime_manager.py 2024-12-24 14:54:11 +00:00
openhands b630d65626 Add RuntimeManager for centralized runtime management 2024-12-24 14:52:19 +00:00
313 changed files with 8638 additions and 15425 deletions
-2
View File
@@ -36,8 +36,6 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Install tmux
run: sudo apt-get update && sudo apt-get install -y tmux
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
-2
View File
@@ -29,8 +29,6 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install tmux
run: sudo apt-get update && sudo apt-get install -y tmux
- name: Install poetry via pipx
run: pipx install poetry
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
uses: docker/setup-qemu-action@v3.0.0
with:
image: tonistiigi/binfmt:latest
- name: Login to GHCR
@@ -119,7 +119,7 @@ jobs:
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
uses: docker/setup-qemu-action@v3.0.0
with:
image: tonistiigi/binfmt:latest
- name: Login to GHCR
-2
View File
@@ -31,8 +31,6 @@ jobs:
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
-2
View File
@@ -30,8 +30,6 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Install tmux
run: sudo apt-get update && sudo apt-get install -y tmux
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
+11 -11
View File
@@ -18,24 +18,24 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people.
* Being respectful of differing opinions, viewpoints, and experiences.
* Giving and gracefully accepting constructive feedback.
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience.
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community.
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind.
* Trolling, insulting or derogatory comments, and personal or political attacks.
* Public or private harassment.
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission.
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting.
professional setting
## Enforcement Responsibilities
@@ -61,7 +61,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
contact@all-hands.dev.
contact@all-hands.dev
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
+6 -6
View File
@@ -11,11 +11,11 @@ To understand the codebase, please refer to the README in each module:
- [agenthub](./openhands/agenthub/README.md)
- [server](./openhands/server/README.md)
## Setting up Your Development Environment
## Setting up your development environment
We have a separate doc [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md) that tells you how to set up a development workflow.
## How Can I Contribute?
## How can I contribute?
There are many ways that you can contribute:
@@ -23,7 +23,7 @@ There are many ways that you can contribute:
2. **Send feedback** after each session by [clicking the thumbs-up thumbs-down buttons](https://docs.all-hands.dev/modules/usage/feedback), so we can see where things are working and failing, and also build an open dataset for training code agents.
3. **Improve the Codebase** by sending [PRs](#sending-pull-requests-to-openhands) (see details below). In particular, we have some [good first issues](https://github.com/All-Hands-AI/OpenHands/labels/good%20first%20issue) that may be ones to start on.
## What Can I Build?
## What can I build?
Here are a few ways you can help improve the codebase.
#### UI/UX
@@ -35,7 +35,7 @@ of the application, please open an issue first, or better, join the #frontend ch
to gather consensus from our design team first.
#### Improving the agent
Our main agent is the CodeAct agent. You can [see its prompts here](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/agenthub/codeact_agent).
Our main agent is the CodeAct agent. You can [see its prompts here](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/agenthub/codeact_agent)
Changes to these prompts, and to the underlying behavior in Python, can have a huge impact on user experience.
You can try modifying the prompts to see how they change the behavior of the agent as you use the app
@@ -63,7 +63,7 @@ At the moment, we have two kinds of tests: [`unit`](./tests/unit) and [`integrat
## Sending Pull Requests to OpenHands
You'll need to fork our repository to send us a Pull Request. You can learn more
about how to fork a GitHub repo and open a PR with your changes in [this article](https://medium.com/swlh/forks-and-pull-requests-how-to-contribute-to-github-repos-8843fac34ce8).
about how to fork a GitHub repo and open a PR with your changes in [this article](https://medium.com/swlh/forks-and-pull-requests-how-to-contribute-to-github-repos-8843fac34ce8)
### Pull Request title
As described [here](https://github.com/commitizen/conventional-commit-types/blob/master/index.json), a valid PR title should begin with one of the following prefixes:
@@ -103,7 +103,7 @@ Further, if you see an issue you like, please leave a "thumbs-up" or a comment,
### Making Pull Requests
We're generally happy to consider all pull requests with the evaluation process varying based on the type of change:
We're generally happy to consider all [PRs](https://github.com/All-Hands-AI/OpenHands/pulls), with the evaluation process varying based on the type of change:
#### For Small Improvements
+9 -9
View File
@@ -3,7 +3,7 @@ This guide is for people working on OpenHands and editing the source code.
If you wish to contribute your changes, check out the [CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md) on how to clone and setup the project initially before moving on.
Otherwise, you can clone the OpenHands project directly.
## Start the Server for Development
## Start the server for development
### 1. Requirements
* Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install) [Ubuntu <= 22.04]
* [Docker](https://docs.docker.com/engine/install/) (For those on MacOS, make sure to allow the default Docker socket to be used from advanced settings!)
@@ -58,7 +58,7 @@ See [our documentation](https://docs.all-hands.dev/modules/usage/llms) for recom
### 4. Running the application
#### Option A: Run the Full Application
Once the setup is complete, this command starts both the backend and frontend servers, allowing you to interact with OpenHands:
Once the setup is complete, launching OpenHands is as simple as running a single command. This command starts both the backend and frontend servers seamlessly, allowing you to interact with OpenHands:
```bash
make run
```
@@ -75,11 +75,11 @@ make run
```
### 6. LLM Debugging
If you encounter any issues with the Language Model (LM) or you're simply curious, export DEBUG=1 in the environment and restart the backend.
OpenHands will log the prompts and responses in the logs/llm/CURRENT_DATE directory, allowing you to identify the causes.
If you encounter any issues with the Language Model (LM) or you're simply curious, you can inspect the actual LLM prompts and responses. To do so, export DEBUG=1 in the environment and restart the backend.
OpenHands will then log the prompts and responses in the logs/llm/CURRENT_DATE directory, allowing you to identify the causes.
### 7. Help
Need help or info on available targets and commands? Use the help command for all the guidance you need with OpenHands.
Need assistance or information on available targets and commands? The help command provides all the necessary guidance to ensure a smooth experience with OpenHands.
```bash
make help
```
@@ -93,14 +93,14 @@ poetry run pytest ./tests/unit/test_*.py
```
### 9. Add or update dependency
1. Add your dependency in `pyproject.toml` or use `poetry add xxx`.
2. Update the poetry.lock file via `poetry lock --no-update`.
1. Add your dependency in `pyproject.toml` or use `poetry add xxx`
2. Update the poetry.lock file via `poetry lock --no-update`
### 9. Use existing Docker image
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image by
setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.20-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.18-nikolaik`
## Develop inside Docker container
@@ -110,7 +110,7 @@ TL;DR
make docker-dev
```
See more details [here](./containers/dev/README.md).
See more details [here](./containers/dev/README.md)
If you are just interested in running `OpenHands` without installing all the required tools on your host.
+4 -4
View File
@@ -2,8 +2,8 @@
These are the procedures and guidelines on how issues are triaged in this repo by the maintainers.
## General
* Most issues must be tagged with **enhancement** or **bug**.
* Issues may be tagged with what it relates to (**backend**, **frontend**, **agent quality**, etc.).
* Most issues must be tagged with **enhancement** or **bug**
* Issues may be tagged with what it relates to (**backend**, **frontend**, **agent quality**, etc.)
## Severity
* **Low**: Minor issues or affecting single user.
@@ -11,10 +11,10 @@ These are the procedures and guidelines on how issues are triaged in this repo b
* **Critical**: Affecting all users or potential security issues.
## Effort
* Issues may be estimated with effort required (**small effort**, **medium effort**, **large effort**).
* Issues may be estimated with effort required (**small effort**, **medium effort**, **large effort**)
## Difficulty
* Issues with low implementation difficulty may be tagged with **good first issue**.
* Issues with low implementation difficulty may be tagged with **good first issue**
## Not Enough Information
* User is asked to provide more information (logs, how to reproduce, etc.) when the issue is not clear.
+2 -2
View File
@@ -106,7 +106,7 @@ check-poetry:
@if command -v poetry > /dev/null; then \
POETRY_VERSION=$(shell poetry --version 2>&1 | sed -E 's/Poetry \(version ([0-9]+\.[0-9]+\.[0-9]+)\)/\1/'); \
IFS='.' read -r -a POETRY_VERSION_ARRAY <<< "$$POETRY_VERSION"; \
if [ $${POETRY_VERSION_ARRAY[0]} -gt 1 ] || ([ $${POETRY_VERSION_ARRAY[0]} -eq 1 ] && [ $${POETRY_VERSION_ARRAY[1]} -ge 8 ]); then \
if [ $${POETRY_VERSION_ARRAY[0]} -ge 1 ] && [ $${POETRY_VERSION_ARRAY[1]} -ge 8 ]; then \
echo "$(BLUE)$(shell poetry --version) is already installed.$(RESET)"; \
else \
echo "$(RED)Poetry 1.8 or later is required. You can install poetry by running the following command, then adding Poetry to your PATH:"; \
@@ -190,7 +190,7 @@ build-frontend:
# Start backend
start-backend:
@echo "$(YELLOW)Starting backend...$(RESET)"
@poetry run uvicorn openhands.server.listen:app --host $(BACKEND_HOST) --port $(BACKEND_PORT) --reload --reload-exclude "./workspace"
@poetry run uvicorn openhands.server.listen:app --host $(BACKEND_HOST) --port $(BACKEND_PORT) --reload --reload-exclude "$(shell pwd)/workspace"
# Start frontend
start-frontend:
+3 -3
View File
@@ -43,17 +43,17 @@ See the [Installation](https://docs.all-hands.dev/modules/usage/installation) gu
system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-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.20
docker.all-hands.dev/all-hands-ai/openhands:0.18
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
+3 -4
View File
@@ -1,4 +1,4 @@
#
services:
openhands:
build:
@@ -7,8 +7,8 @@ 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.20-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
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.18-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
- "3000:3000"
@@ -16,7 +16,6 @@ services:
- "host.docker.internal:host-gateway"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ~/.openhands-state:/.openhands-state
- ${WORKSPACE_BASE:-$PWD/workspace}:/opt/workspace_base
pull_policy: build
stdin_open: true
+1 -17
View File
@@ -34,7 +34,7 @@ workspace_base = "./workspace"
# Path to store trajectories, can be a folder or a file
# If it's a folder, the session id will be used as the file name
#save_trajectory_path="./trajectories"
#trajectories_path="./trajectories"
# File store path
#file_store_path = "/tmp/file_store"
@@ -198,16 +198,6 @@ model = "gpt-4o"
# agent.CodeActAgent
##############################################################################
[agent]
# whether the browsing tool is enabled
codeact_enable_browsing = true
# whether the LLM draft editor is enabled
codeact_enable_llm_editor = false
# whether the IPython tool is enabled
codeact_enable_jupyter = true
# Name of the micro agent to use for this agent
#micro_agent_name = ""
@@ -220,12 +210,6 @@ codeact_enable_jupyter = true
# LLM config group to use
#llm_config = 'your-llm-config-group'
# Whether to use microagents at all
#use_microagents = true
# List of microagents to disable
#disabled_microagents = []
[agent.RepoExplorerAgent]
# Example: use a cheaper model for RepoExplorerAgent to reduce cost, especially
# useful when an agent doesn't demand high quality but uses a lot of tokens
+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.20-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.18-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
@@ -94,7 +94,7 @@ Les options de configuration de base sont définies dans la section `[core]` du
- Description : Désactiver la couleur dans la sortie du terminal
**Trajectoires**
- `save_trajectory_path`
- `trajectories_path`
- Type : `str`
- Valeur par défaut : `"./trajectories"`
- Description : Chemin pour stocker les trajectoires (peut être un dossier ou un fichier). Si c'est un dossier, les trajectoires seront enregistrées dans un fichier nommé avec l'ID de session et l'extension .json, dans ce dossier.
@@ -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.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-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.20 \
docker.all-hands.dev/all-hands-ai/openhands:0.18 \
python -m openhands.core.cli
```
@@ -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.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-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.20 \
docker.all-hands.dev/all-hands-ai/openhands:0.18 \
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.20-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-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.20
docker.all-hands.dev/all-hands-ai/openhands:0.18
```
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.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
@@ -91,7 +91,7 @@
- 描述: 禁用终端输出中的颜色
**轨迹**
- `save_trajectory_path`
- `trajectories_path`
- 类型: `str`
- 默认值: `"./trajectories"`
- 描述: 存储轨迹的路径(可以是文件夹或文件)。如果是文件夹,轨迹将保存在该文件夹中以会话 ID 命名的 .json 文件中。
@@ -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.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-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.20 \
docker.all-hands.dev/all-hands-ai/openhands:0.18 \
python -m openhands.core.cli
```
@@ -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.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-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.20 \
docker.all-hands.dev/all-hands-ai/openhands:0.18 \
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.20-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-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.20
docker.all-hands.dev/all-hands-ai/openhands:0.18
```
你也可以在可脚本化的[无头模式](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.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
+72 -30
View File
@@ -7,11 +7,53 @@ If you are running in [GUI Mode](https://docs.all-hands.dev/modules/usage/how-to
take precedence.
:::
---
# Table of Contents
- [Core Configuration](#core-configuration)
- [API Keys](#api-keys)
- [Workspace](#workspace)
- [Debugging and Logging](#debugging-and-logging)
- [Session Management](#session-management)
- [Trajectories](#trajectories)
- [File Store](#file-store)
- [Task Management](#task-management)
- [Sandbox Configuration](#sandbox-configuration)
- [Miscellaneous](#miscellaneous)
- [LLM Configuration](#llm-configuration)
- [AWS Credentials](#aws-credentials)
- [API Configuration](#api-configuration)
- [Custom LLM Provider](#custom-llm-provider)
- [Embeddings](#embeddings)
- [Message Handling](#message-handling)
- [Model Selection](#model-selection)
- [Retrying](#retrying)
- [Advanced Options](#advanced-options)
- [Agent Configuration](#agent-configuration)
- [Microagent Configuration](#microagent-configuration)
- [Memory Configuration](#memory-configuration)
- [LLM Configuration](#llm-configuration-2)
- [ActionSpace Configuration](#actionspace-configuration)
- [Microagent Usage](#microagent-usage)
- [Sandbox Configuration](#sandbox-configuration)
- [Execution](#execution)
- [Container Image](#container-image)
- [Networking](#networking)
- [Linting and Plugins](#linting-and-plugins)
- [Dependencies and Environment](#dependencies-and-environment)
- [Evaluation](#evaluation)
- [Security Configuration](#security-configuration)
- [Confirmation Mode](#confirmation-mode)
- [Security Analyzer](#security-analyzer)
---
## Core Configuration
The core configuration options are defined in the `[core]` section of the `config.toml` file.
### API Keys
**API Keys**
- `e2b_api_key`
- Type: `str`
- Default: `""`
@@ -27,7 +69,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `""`
- Description: API token secret for Modal
### Workspace
**Workspace**
- `workspace_base`
- Type: `str`
- Default: `"./workspace"`
@@ -38,7 +80,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `"/tmp/cache"`
- Description: Cache directory path
### Debugging and Logging
**Debugging and Logging**
- `debug`
- Type: `bool`
- Default: `false`
@@ -49,13 +91,13 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `false`
- Description: Disable color in terminal output
### Trajectories
- `save_trajectory_path`
**Trajectories**
- `trajectories_path`
- Type: `str`
- Default: `"./trajectories"`
- Description: Path to store trajectories (can be a folder or a file). If it's a folder, the trajectories will be saved in a file named with the session id name and .json extension, in that folder.
### File Store
**File Store**
- `file_store_path`
- Type: `str`
- Default: `"/tmp/file_store"`
@@ -86,7 +128,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `[".*"]`
- Description: List of allowed file extensions for uploads
### Task Management
**Task Management**
- `max_budget_per_task`
- Type: `float`
- Default: `0.0`
@@ -97,7 +139,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `100`
- Description: Maximum number of iterations
### Sandbox Configuration
**Sandbox Configuration**
- `workspace_mount_path_in_sandbox`
- Type: `str`
- Default: `"/workspace"`
@@ -113,7 +155,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `""`
- Description: Path to rewrite the workspace mount path to. You can usually ignore this, it refers to special cases of running inside another container.
### Miscellaneous
**Miscellaneous**
- `run_as_openhands`
- Type: `bool`
- Default: `true`
@@ -140,7 +182,7 @@ The LLM (Large Language Model) configuration options are defined in the `[llm]`
To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LLM_NUM_RETRIES`.
### AWS Credentials
**AWS Credentials**
- `aws_access_key_id`
- Type: `str`
- Default: `""`
@@ -156,7 +198,7 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `""`
- Description: AWS secret access key
### API Configuration
**API Configuration**
- `api_key`
- Type: `str`
- Default: `None`
@@ -182,13 +224,13 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `0.0`
- Description: Cost per output token
### Custom LLM Provider
**Custom LLM Provider**
- `custom_llm_provider`
- Type: `str`
- Default: `""`
- Description: Custom LLM provider
### Embeddings
**Embeddings**
- `embedding_base_url`
- Type: `str`
- Default: `""`
@@ -204,7 +246,7 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `"local"`
- Description: Embedding model to use
### Message Handling
**Message Handling**
- `max_message_chars`
- Type: `int`
- Default: `30000`
@@ -220,13 +262,13 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `0`
- Description: Maximum number of output tokens
### Model Selection
**Model Selection**
- `model`
- Type: `str`
- Default: `"claude-3-5-sonnet-20241022"`
- Description: Model to use
### Retrying
**Retrying**
- `num_retries`
- Type: `int`
- Default: `8`
@@ -247,7 +289,7 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `2.0`
- Description: Multiplier for exponential backoff calculation
### Advanced Options
**Advanced Options**
- `drop_params`
- Type: `bool`
- Default: `false`
@@ -287,13 +329,13 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
The agent configuration options are defined in the `[agent]` and `[agent.<agent_name>]` sections of the `config.toml` file.
### Microagent Configuration
**Microagent Configuration**
- `micro_agent_name`
- Type: `str`
- Default: `""`
- Description: Name of the micro agent to use for this agent
### Memory Configuration
**Memory Configuration**
- `memory_enabled`
- Type: `bool`
- Default: `false`
@@ -304,13 +346,13 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
- Default: `3`
- Description: The maximum number of threads indexing at the same time for embeddings
### LLM Configuration
**LLM Configuration**
- `llm_config`
- Type: `str`
- Default: `'your-llm-config-group'`
- Description: The name of the LLM config to use
### ActionSpace Configuration
**ActionSpace Configuration**
- `function_calling`
- Type: `bool`
- Default: `true`
@@ -331,7 +373,7 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
- Default: `false`
- Description: Whether Jupyter is enabled in the action space
### Microagent Usage
**Microagent Usage**
- `use_microagents`
- Type: `bool`
- Default: `true`
@@ -348,7 +390,7 @@ The sandbox configuration options are defined in the `[sandbox]` section of the
To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-e SANDBOX_TIMEOUT`.
### Execution
**Execution**
- `timeout`
- Type: `int`
- Default: `120`
@@ -359,19 +401,19 @@ To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-
- Default: `1000`
- Description: Sandbox user ID
### Container Image
**Container Image**
- `base_container_image`
- Type: `str`
- Default: `"nikolaik/python-nodejs:python3.12-nodejs22"`
- Description: Container image to use for the sandbox
### Networking
**Networking**
- `use_host_network`
- Type: `bool`
- Default: `false`
- Description: Use host network
### Linting and Plugins
**Linting and Plugins**
- `enable_auto_lint`
- Type: `bool`
- Default: `false`
@@ -382,7 +424,7 @@ To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-
- Default: `true`
- Description: Whether to initialize plugins
### Dependencies and Environment
**Dependencies and Environment**
- `runtime_extra_deps`
- Type: `str`
- Default: `""`
@@ -393,7 +435,7 @@ To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-
- Default: `{}`
- Description: Environment variables to set at the launch of the runtime
### Evaluation
**Evaluation**
- `browsergym_eval_env`
- Type: `str`
- Default: `""`
@@ -405,13 +447,13 @@ The security configuration options are defined in the `[security]` section of th
To use these with the docker command, pass in `-e SECURITY_<option>`. Example: `-e SECURITY_CONFIRMATION_MODE`.
### Confirmation Mode
**Confirmation Mode**
- `confirmation_mode`
- Type: `bool`
- Default: `false`
- Description: Enable confirmation mode
### Security Analyzer
**Security Analyzer**
- `security_analyzer`
- Type: `str`
- Default: `""`
+5 -5
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.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-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.20 \
docker.all-hands.dev/all-hands-ai/openhands:0.18 \
python -m openhands.core.cli
```
@@ -58,7 +58,7 @@ Here are some examples of CLI commands and their expected outputs:
### Example 1: Simple Task
```bash
>> Write a Python script that prints "Hello, World!"
How can I help? >> Write a Python script that prints "Hello, World!"
```
Expected Output:
@@ -72,7 +72,7 @@ Expected Output:
### Example 2: Bash Command
```bash
>> Create a directory named "test_dir"
How can I help? >> Create a directory named "test_dir"
```
Expected Output:
@@ -86,7 +86,7 @@ Expected Output:
### Example 3: Error Handling
```bash
>> Delete a non-existent file
How can I help? >> Delete a non-existent file
```
Expected Output:
+3 -3
View File
@@ -76,18 +76,18 @@ When using OpenHands in online mode, the GitHub OAuth flow:
Common issues and solutions:
- **Token Not Recognized**:
1. **Token Not Recognized**:
- Ensure the token is properly saved in settings.
- Check that the token hasn't expired.
- Verify the token has the required scopes.
- Try regenerating the token.
- **Organization Access Denied**:
2. **Organization Access Denied**:
- Check if SSO is required but not enabled.
- Verify organization membership.
- Contact organization admin if token policies are blocking access.
- **Verifying Token Works**:
3. **Verifying Token Works**:
- The app will show a green checkmark if the token is valid.
- Try accessing a repository to confirm permissions.
- Check the browser console for any error messages.
+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.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-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.20 \
docker.all-hands.dev/all-hands-ai/openhands:0.18 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
@@ -0,0 +1,16 @@
# Persisting Session Data
Using the standard Development Workflow, the session data is stored in memory. Currently, if OpenHands' service is restarted,
previous sessions become invalid (a new secret is generated) and thus not recoverable.
## How to Persist Session Data
### Development Workflow
In the `config.toml` file, specify the following:
```
[core]
...
file_store="local"
file_store_path="/absolute/path/to/openhands/cache/directory"
jwt_secret="secretpass"
```
+3 -3
View File
@@ -11,17 +11,17 @@
The easiest way to run OpenHands is in Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-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.20
docker.all-hands.dev/all-hands-ai/openhands:0.18
```
You'll find OpenHands running at http://localhost:3000!
+14 -5
View File
@@ -5,14 +5,23 @@ OpenHands can connect to any LLM supported by LiteLLM. However, it requires a po
## Model Recommendations
Based on our evaluations of language models for coding tasks (using the SWE-bench dataset), we can provide some
recommendations for model selection. Our latest benchmarking results can be found in [this spreadsheet](https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0).
recommendations for model selection. Some analyses can be found in [this blog article comparing LLMs](https://www.all-hands.dev/blog/evaluation-of-llms-as-coding-agents-on-swe-bench-at-30x-speed) and
[this blog article with some more recent results](https://www.all-hands.dev/blog/openhands-codeact-21-an-open-state-of-the-art-software-development-agent).
When choosing a model, consider both the quality of outputs and the associated costs. Here's a summary of the findings:
- Claude 3.5 Sonnet is the best by a fair amount, achieving a 53% resolve rate on SWE-Bench Verified with the default agent in OpenHands.
- GPT-4o lags behind, and o1-mini actually performed somewhat worse than GPT-4o. We went in and analyzed the results a little, and briefly it seemed like o1 was sometimes "overthinking" things, performing extra environment configuration tasks when it could just go ahead and finish the task.
- Finally, the strongest open models were Llama 3.1 405 B and deepseek-v2.5, and they performed reasonably, even besting some of the closed models.
Please refer to the [full article](https://www.all-hands.dev/blog/evaluation-of-llms-as-coding-agents-on-swe-bench-at-30x-speed) for more details.
Based on these findings and community feedback, the following models have been verified to work reasonably well with OpenHands:
- anthropic/claude-3-5-sonnet-20241022 (recommended)
- anthropic/claude-3-5-haiku-20241022
- deepseek/deepseek-chat
- gpt-4o
- claude-3-5-sonnet (recommended)
- gpt-4 / gpt-4o
- llama-3.1-405b
- deepseek-v2.5
:::warning
OpenHands will issue many prompts to the LLM you configure. Most of these LLMs cost money, so be sure to set spending
@@ -0,0 +1,67 @@
# Customizing Agent Behavior
OpenHands can be customized to work more effectively with specific repositories by providing repository-specific context and guidelines. This section explains how to optimize OpenHands for your project.
## Repository Configuration
You can customize OpenHands' behavior for your repository by creating a `.openhands` directory in your repository's root. At minimum, it should contain the file
`.openhands/microagents/repo.md`, which includes instructions that will
be given to the agent every time it works with this repository.
We suggest including the following information:
- **Repository Overview**: A brief description of your project's purpose and architecture.
- **Directory Structure**: Key directories and their purposes.
- **Development Guidelines**: Project-specific coding standards and practices.
- **Testing Requirements**: How to run tests and what types of tests are required.
- **Setup Instructions**: Steps needed to build and run the project.
### Example Repository Configuration
Example `.openhands/microagents/repo.md` file:
```
Repository: MyProject
Description: A web application for task management
Directory Structure:
- src/: Main application code
- tests/: Test files
- docs/: Documentation
Setup:
- Run `npm install` to install dependencies
- Use `npm run dev` for development
- Run `npm test` for testing
Guidelines:
- Follow ESLint configuration
- Write tests for all new features
- Use TypeScript for new code
```
### Customizing Prompts
When working with a repository:
- **Reference Project Standards**: Mention specific coding standards or patterns used in your project.
- **Include Context**: Reference relevant documentation or existing implementations.
- **Specify Testing Requirements**: Include project-specific testing requirements in your prompts.
Example customized prompt:
```
Add a new task completion feature to src/components/TaskList.tsx following our existing component patterns.
Include unit tests in tests/components/ and update the documentation in docs/features/.
The component should use our shared styling from src/styles/components.
```
### Best Practices for Repository Customization
- **Keep Instructions Updated**: Regularly update your `.openhands` directory as your project evolves.
- **Be Specific**: Include specific paths, patterns, and requirements unique to your project.
- **Document Dependencies**: List all tools and dependencies required for development.
- **Include Examples**: Provide examples of good code patterns from your project.
- **Specify Conventions**: Document naming conventions, file organization, and code style preferences.
By customizing OpenHands for your repository, you'll get more accurate and consistent results that align with your project's standards and requirements.
## Other Microagents
You can create other instructions in the `.openhands/microagents/` directory
that will be sent to the agent if a particular keyword is found, like `test`, `frontend`, or `migration`. See [Micro-Agents](microagents.md) for more information.
@@ -1,36 +0,0 @@
# Microagents Overview
Microagents are specialized prompts that enhance OpenHands with domain-specific knowledge, repository-specific context
and task-specific workflows. They help by providing expert guidance, automating common tasks, and ensuring
consistent practices across projects.
## Microagent Types
Currently OpenHands supports the following types of microagents:
* [Repository Microagents](./microagents-repo): Repository-specific context and guidelines for OpenHands.
* [Public Microagents](./microagents-public): General guidelines triggered by keywords for all OpenHands users.
When OpenHands works with a repository, it:
1. Loads repository-specific instructions from `.openhands/microagents/` if present in the repository.
2. Loads general guidelines triggered by keywords in conversations.
See current [Public Microagents](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge).
## Microagent Format
All microagents use markdown files with YAML frontmatter that have special instructions to help OpenHands accomplish
tasks:
```
---
name: <Name of the microagent>
type: <MicroAgent type>
version: <MicroAgent version>
agent: <The agent type (Typically CodeActAgent)>
triggers:
- <Optional keywords triggering the microagent. If triggers are removed, it will always be included>
---
<Markdown with any special guidelines, instructions, and prompts that OpenHands should follow.
Check out the specific documentation for each microagent on best practices for more information.>
```
@@ -1,153 +0,0 @@
# Public Microagents
## Overview
Public microagents are specialized guidelines triggered by keywords for all OpenHands users.
They are defined in markdown files under the
[`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) directory.
Public microagents:
- Monitor incoming commands for their trigger words.
- Activate when relevant triggers are detected.
- Apply their specialized knowledge and capabilities.
- Follow their specific guidelines and restrictions.
## Current Public Microagents
For more information about specific microagents, refer to their individual documentation files in
the [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/) directory.
### GitHub Agent
**File**: `github.md`
**Triggers**: `github`, `git`
The GitHub agent specializes in GitHub API interactions and repository management. It:
- Has access to a `GITHUB_TOKEN` for API authentication.
- Follows strict guidelines for repository interactions.
- Handles branch management and pull requests.
- Uses the GitHub API instead of web browser interactions.
Key features:
- Branch protection (prevents direct pushes to main/master)
- Automated PR creation
- Git configuration management
- API-first approach for GitHub operations
Usage Example:
```bash
git checkout -b feature-branch
git commit -m "Add new feature"
git push origin feature-branch
```
### NPM Agent
**File**: `npm.md`
**Triggers**: `npm`
Specializes in handling npm package management with specific focus on:
- Non-interactive shell operations.
- Automated confirmation handling using Unix 'yes' command.
- Package installation automation.
Usage Example:
```bash
yes | npm install package-name
```
## Contributing a Public Microagent
You can create your own public microagents by adding new markdown files to the
[`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/) directory.
### Public Microagents Best Practices
- **Clear Scope**: Keep the microagent focused on a specific domain or task.
- **Explicit Instructions**: Provide clear, unambiguous guidelines.
- **Useful Examples**: Include practical examples of common use cases.
- **Safety First**: Include necessary warnings and constraints.
- **Integration Awareness**: Consider how the microagent interacts with other components.
### Steps to Contribute a Public Microagent
#### 1. Plan the Public Microagent
Before creating a public microagent, consider:
- What specific problem or use case will it address?
- What unique capabilities or knowledge should it have?
- What trigger words make sense for activating it?
- What constraints or guidelines should it follow?
#### 2. Create File
Create a new markdown file in [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/)
with a descriptive name (e.g., `docker.md` for a Docker-focused agent).
Update the file with the required frontmatter [according to the required format](./microagents-overview#microagent-format)
and the required specialized guidelines while following the [best practices above](#public-microagents-best-practices).
#### 3. Testing the Public Microagent
- Test the agent with various prompts.
- Verify trigger words activate the agent correctly.
- Ensure instructions are clear and comprehensive.
- Check for potential conflicts with existing agents.
#### 4. Submission Process
Submit a pull request with:
- The new microagent file.
- Updated documentation if needed.
- Description of the agent's purpose and capabilities.
### Example Public Microagent Implementation
Here's a template for a new microagent:
```markdown
---
name: docker
agent: CodeActAgent
triggers:
- docker
- container
---
You are responsible for Docker container management and Dockerfile creation.
Key responsibilities:
1. Create and modify Dockerfiles
2. Manage container lifecycle
3. Handle Docker Compose configurations
Guidelines:
- Always use official base images when possible
- Include necessary security considerations
- Follow Docker best practices for layer optimization
Examples:
1. Creating a Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]
2. Docker Compose usage:
version: '3'
services:
web:
build: .
ports:
- "3000:3000"
Remember to:
- Validate Dockerfile syntax
- Check for security vulnerabilities
- Optimize for build time and image size
```
See the [current public micro-agents](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) for
more examples.
@@ -1,68 +0,0 @@
# Repository Microagents
## Overview
OpenHands can be customized to work more effectively with specific repositories by providing repository-specific context
and guidelines. This section explains how to optimize OpenHands for your project.
## Creating a Repository Micro-Agent
You can customize OpenHands' behavior for your repository by creating a `.openhands/microagents/` directory in your repository's root.
At minimum it should contain the file
`.openhands/microagents/repo.md`, which includes instructions that will
be given to the agent every time it works with this repository.
### Repository Microagents Best Practices
- **Keep Instructions Updated**: Regularly update your `.openhands/microagents/` directory as your project evolves.
- **Be Specific**: Include specific paths, patterns, and requirements unique to your project.
- **Document Dependencies**: List all tools and dependencies required for development.
- **Include Examples**: Provide examples of good code patterns from your project.
- **Specify Conventions**: Document naming conventions, file organization, and code style preferences.
### Steps to Create a Repository Microagent
#### 1. Plan the Repository Microagent
When creating a repository-specific micro-agent, we suggest including the following information:
- **Repository Overview**: A brief description of your project's purpose and architecture.
- **Directory Structure**: Key directories and their purposes.
- **Development Guidelines**: Project-specific coding standards and practices.
- **Testing Requirements**: How to run tests and what types of tests are required.
- **Setup Instructions**: Steps needed to build and run the project.
#### 2. Create File
Create a file in your repository under `.openhands/microagents/` (Example: `.openhands/microagents/repo.md`)
Update the file with the required frontmatter [according to the required format](./microagents-overview#microagent-format)
and the required specialized guidelines for your repository.
### Example Repository Microagent
```
---
name: repo
type: repo
agent: CodeActAgent
---
Repository: MyProject
Description: A web application for task management
Directory Structure:
- src/: Main application code
- tests/: Test files
- docs/: Documentation
Setup:
- Run `npm install` to install dependencies
- Use `npm run dev` for development
- Run `npm test` for testing
Guidelines:
- Follow ESLint configuration
- Write tests for all new features
- Use TypeScript for new code
If adding a new component in src/components, always add appropriate unit tests in tests/components/.
```
+210
View File
@@ -0,0 +1,210 @@
# Micro-Agents
OpenHands uses specialized micro-agents to handle specific tasks and contexts efficiently. These micro-agents are small, focused components that provide specialized behavior and knowledge for particular scenarios.
## Overview
Micro-agents are defined in markdown files under the `openhands/agenthub/codeact_agent/micro/` directory. Each micro-agent is configured with:
- A unique name.
- The agent type (typically CodeActAgent).
- Trigger keywords that activate the agent.
- Specific instructions and capabilities.
## Available Micro-Agents
### GitHub Agent
**File**: `github.md`
**Triggers**: `github`, `git`
The GitHub agent specializes in GitHub API interactions and repository management. It:
- Has access to a `GITHUB_TOKEN` for API authentication.
- Follows strict guidelines for repository interactions.
- Handles branch management and pull requests.
- Uses the GitHub API instead of web browser interactions.
Key features:
- Branch protection (prevents direct pushes to main/master)
- Automated PR creation
- Git configuration management
- API-first approach for GitHub operations
### NPM Agent
**File**: `npm.md`
**Triggers**: `npm`
Specializes in handling npm package management with specific focus on:
- Non-interactive shell operations.
- Automated confirmation handling using Unix 'yes' command.
- Package installation automation.
### Custom Micro-Agents
You can create your own micro-agents by adding new markdown files to the micro-agents directory.
Each file should follow this structure:
```markdown
---
name: agent_name
agent: CodeActAgent
triggers:
- trigger_word1
- trigger_word2
---
Instructions and capabilities for the micro-agent...
```
## Best Practices
When working with micro-agents:
- **Use Appropriate Triggers**: Ensure your commands include the relevant trigger words to activate the correct micro-agent.
- **Follow Agent Guidelines**: Each agent has specific instructions and limitations. Respect these for optimal results.
- **API-First Approach**: When available, use API endpoints rather than web interfaces.
- **Automation Friendly**: Design commands that work well in non-interactive environments.
## Integration
Micro-agents are automatically integrated into OpenHands' workflow. They:
- Monitor incoming commands for their trigger words.
- Activate when relevant triggers are detected.
- Apply their specialized knowledge and capabilities.
- Follow their specific guidelines and restrictions.
## Example Usage
```bash
# GitHub agent example
git checkout -b feature-branch
git commit -m "Add new feature"
git push origin feature-branch
# NPM agent example
yes | npm install package-name
```
For more information about specific agents, refer to their individual documentation files in the micro-agents directory.
## Contributing a Micro-Agent
To contribute a new micro-agent to OpenHands, follow these guidelines:
### 1. Planning Your Micro-Agent
Before creating a micro-agent, consider:
- What specific problem or use case will it address?
- What unique capabilities or knowledge should it have?
- What trigger words make sense for activating it?
- What constraints or guidelines should it follow?
### 2. File Structure
Create a new markdown file in `openhands/agenthub/codeact_agent/micro/` with a descriptive name (e.g., `docker.md` for a Docker-focused agent).
### 3. Required Components
Your micro-agent file must include:
- **Front Matter**: YAML metadata at the start of the file:
```markdown
---
name: your_agent_name
agent: CodeActAgent
triggers:
- trigger_word1
- trigger_word2
---
```
- **Instructions**: Clear, specific guidelines for the agent's behavior:
```markdown
You are responsible for [specific task/domain].
Key responsibilities:
1. [Responsibility 1]
2. [Responsibility 2]
Guidelines:
- [Guideline 1]
- [Guideline 2]
Examples of usage:
[Example 1]
[Example 2]
```
### 4. Best Practices for Micro-Agent Development
- **Clear Scope**: Keep the agent focused on a specific domain or task.
- **Explicit Instructions**: Provide clear, unambiguous guidelines.
- **Useful Examples**: Include practical examples of common use cases.
- **Safety First**: Include necessary warnings and constraints.
- **Integration Awareness**: Consider how the agent interacts with other components.
### 5. Testing Your Micro-Agent
Before submitting:
- Test the agent with various prompts.
- Verify trigger words activate the agent correctly.
- Ensure instructions are clear and comprehensive.
- Check for potential conflicts with existing agents.
### 6. Example Implementation
Here's a template for a new micro-agent:
```markdown
---
name: docker
agent: CodeActAgent
triggers:
- docker
- container
---
You are responsible for Docker container management and Dockerfile creation.
Key responsibilities:
1. Create and modify Dockerfiles
2. Manage container lifecycle
3. Handle Docker Compose configurations
Guidelines:
- Always use official base images when possible
- Include necessary security considerations
- Follow Docker best practices for layer optimization
Examples:
1. Creating a Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]
2. Docker Compose usage:
version: '3'
services:
web:
build: .
ports:
- "3000:3000"
Remember to:
- Validate Dockerfile syntax
- Check for security vulnerabilities
- Optimize for build time and image size
```
### 7. Submission Process
1. Create your micro-agent file in the correct directory.
2. Test thoroughly.
3. Submit a pull request with:
- The new micro-agent file.
- Updated documentation if needed.
- Description of the agent's purpose and capabilities.
Remember that micro-agents are a powerful way to extend OpenHands' capabilities in specific domains. Well-designed
agents can significantly improve the system's ability to handle specialized tasks.
+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.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
+738 -580
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -15,10 +15,10 @@
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "^3.7.0",
"@docusaurus/plugin-content-pages": "^3.7.0",
"@docusaurus/preset-classic": "^3.7.0",
"@docusaurus/theme-mermaid": "^3.7.0",
"@docusaurus/core": "^3.6.3",
"@docusaurus/plugin-content-pages": "^3.6.3",
"@docusaurus/preset-classic": "^3.6.3",
"@docusaurus/theme-mermaid": "^3.6.3",
"@mdx-js/react": "^3.1.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.4.1",
@@ -29,7 +29,7 @@
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.5.1",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/tsconfig": "^3.6.3",
"@docusaurus/types": "^3.5.1",
"typescript": "~5.7.2"
},
+13 -19
View File
@@ -23,26 +23,15 @@ const sidebars: SidebarsConfig = {
id: 'usage/prompting/prompting-best-practices',
},
{
type: 'category',
type: 'doc',
label: 'Customization',
id: 'usage/prompting/customization',
},
{
type: 'doc',
label: 'Microagents',
items: [
{
type: 'doc',
label: 'Overview',
id: 'usage/prompting/microagents-overview',
},
{
type: 'doc',
label: 'Repository',
id: 'usage/prompting/microagents-repo',
},
{
type: 'doc',
label: 'Public',
id: 'usage/prompting/microagents-public',
},
],
}
id: 'usage/prompting/microagents',
},
],
},
{
@@ -137,6 +126,11 @@ const sidebars: SidebarsConfig = {
label: 'Custom Sandbox',
id: 'usage/how-to/custom-sandbox-guide',
},
{
type: 'doc',
label: 'Persist Session Data',
id: 'usage/how-to/persist-session-data',
},
],
},
{
+1
View File
@@ -123,6 +123,7 @@ class openhands.state.State {
updated_info: List[Tuple[Action, Observation]]
}
class openhands.observation.CmdOutputObservation {
command_id: int
command: str
exit_code: int
observation: str
@@ -137,6 +137,7 @@ def complete_runtime(
action = CmdRunAction(
command=f'chmod +x ./{script_name} && ./{script_name}',
keep_prompt=False,
)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
@@ -163,7 +164,8 @@ def complete_runtime(
logger.info(f'Running get ground truth cmd: {script_name}')
action = CmdRunAction(
command=f'chmod +x ./{script_name} && ./{script_name}'
command=f'chmod +x ./{script_name} && ./{script_name}',
keep_prompt=False,
)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
@@ -145,7 +145,10 @@ def complete_runtime(
)
logger.info(f'Running test file: {script_name}')
action = CmdRunAction(command=f'python3 -m unittest {script_name}')
action = CmdRunAction(
command=f'python3 -m unittest {script_name}',
keep_prompt=False,
)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+4 -2
View File
@@ -199,7 +199,7 @@ def complete_runtime(
if obs.exit_code == 0:
test_result['metadata']['1_copy_change_success'] = True
action = CmdRunAction(command=f'cat {generated_path}')
action = CmdRunAction(command=f'cat {generated_path}', keep_prompt=False)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
@@ -223,7 +223,9 @@ def complete_runtime(
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
action = CmdRunAction(command='cat /testing_files/results_biocoder.json')
action = CmdRunAction(
command='cat /testing_files/results_biocoder.json', keep_prompt=False
)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
if obs.exit_code == 0:
+1
View File
@@ -127,6 +127,7 @@ For each problem, OpenHands is given a set number of iterations to fix the faili
"observation": "run",
"content": "california_schools/california_schools.sqlite\r\n[(1.0,)]",
"extras": {
"command_id": -1,
"command": "python3 0.py",
"exit_code": 0
}
+8 -2
View File
@@ -268,7 +268,10 @@ def initialize_runtime(
runtime.copy_to(db_file, '/workspace')
# Check the database is copied
action = CmdRunAction(command='cd /workspace && ls -l')
action = CmdRunAction(
command='cd /workspace && ls -l',
keep_prompt=False,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
@@ -297,7 +300,10 @@ def complete_runtime(
instance_id = instance.instance_id.replace('/', '__')
path = os.path.join('/workspace', f'{instance_id}.py')
action = CmdRunAction(command=f'cat {path}')
action = CmdRunAction(
command=f'cat {path}',
keep_prompt=False,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -71,6 +71,7 @@ For each problem, OpenHands is given a set number of iterations to fix the faili
"observation": "run",
"content": "[File: /workspace/Python__2.py (14 lines total)]\r\n1:def truncate_number(number: float) -> float:\r\n2: return number % 1.0 + 1.0\r\n3:\r\n4:\r\n5:\r\n6:\r\n7:\r\n8:\r\n9:def check(truncate_number):\r\n10: assert truncate_number(3.5) == 0.5\r\n11: assert abs(truncate_number(1.33) - 0.33) < 1e-6\r\n12: assert abs(truncate_number(123.456) - 0.456) < 1e-6\r\n13:\r\n14:check(truncate_number)",
"extras": {
"command_id": -1,
"command": "open Python__2.py",
"exit_code": 0
}
@@ -97,6 +98,7 @@ For each problem, OpenHands is given a set number of iterations to fix the faili
"observation": "run",
"content": "> > [File: /workspace/Python__2.py (14 lines total)]\r\n1:def truncate_number(number: float) -> float:\r\n2: return number % 1.0\r\n3:\r\n4:\r\n5:\r\n6:\r\n7:\r\n8:\r\n9:def check(truncate_number):\r\n10: assert truncate_number(3.5) == 0.5\r\n11: assert abs(truncate_number(1.33) - 0.33) < 1e-6\r\n12: assert abs(truncate_number(123.456) - 0.456) < 1e-6\r\n13:\r\n14:check(truncate_number)\r\nFile updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.",
"extras": {
"command_id": -1,
"command": "edit 2:2 <<EOF\n return number % 1.0\nEOF",
"exit_code": 0
}
@@ -123,6 +125,7 @@ For each problem, OpenHands is given a set number of iterations to fix the faili
"observation": "run",
"content": "",
"extras": {
"command_id": -1,
"command": "python3 Python__2.py",
"exit_code": 0
}
@@ -171,7 +171,9 @@ def complete_runtime(
num_workers = LANGUAGE_TO_NUM_WORKERS[language]
python_imports = '\n'.join(IMPORT_HELPER[language])
action = CmdRunAction(command=f'cat /workspace/{_get_instance_id(instance)}.py')
action = CmdRunAction(
command=f'cat /workspace/{_get_instance_id(instance)}.py', keep_prompt=False
)
obs = runtime.run_action(action)
assert obs.exit_code == 0
+1 -1
View File
@@ -163,7 +163,7 @@ def complete_runtime(
eval_script = os.path.join(task_path, 'run.sh')
logger.info(f'Running evaluation script: {eval_script}')
action = CmdRunAction(command=f'cat {eval_script}')
action = CmdRunAction(command=f'cat {eval_script}', keep_prompt=False)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
if obs.exit_code == 0:
@@ -121,7 +121,10 @@ def initialize_runtime(
runtime.copy_to(dataset_dir, '/workspace/benchmark/datasets', recursive=True)
# Check the dataset exists
action = CmdRunAction(command='cd /workspace/benchmark/datasets && ls')
action = CmdRunAction(
command='cd /workspace/benchmark/datasets && ls',
keep_prompt=False,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
@@ -151,7 +154,10 @@ def complete_runtime(
assert obs.exit_code == 0
action = CmdRunAction(command=f'cat pred_programs/{instance.pred_program_name}')
action = CmdRunAction(
command=f'cat pred_programs/{instance.pred_program_name}',
keep_prompt=False,
)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
+1 -1
View File
@@ -204,7 +204,7 @@ Then, in a separate Python environment with `streamlit` library, you can run the
```bash
# Make sure you are inside the cloned `evaluation` repo
conda activate streamlit # if you follow the optional conda env setup above
streamlit run app.py --server.port 8501 --server.address 0.0.0.0
streamlit app.py --server.port 8501 --server.address 0.0.0.0
```
Then you can access the SWE-Bench trajectory visualizer at `localhost:8501`.
+6 -15
View File
@@ -98,7 +98,6 @@ def process_instance(
metadata: EvalMetadata,
reset_logger: bool = True,
log_dir: str | None = None,
runtime_failure_count: int = 0,
) -> EvalOutput:
"""
Evaluate agent performance on a SWE-bench problem instance.
@@ -147,16 +146,6 @@ def process_instance(
metadata=metadata,
)
# Increase resource_factor with increasing attempt_id
if runtime_failure_count > 0:
config.sandbox.remote_runtime_resource_factor = min(
config.sandbox.remote_runtime_resource_factor * (2**runtime_failure_count),
4, # hardcode maximum resource factor to 4
)
logger.warning(
f'This is the second attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
# Get patch and save it to /tmp/patch.diff
@@ -188,7 +177,7 @@ def process_instance(
"(patch --batch --fuzz=5 -p1 -i /tmp/patch.diff && echo 'APPLY_PATCH_PASS' || "
"echo 'APPLY_PATCH_FAIL')))"
)
action = CmdRunAction(command=exec_command)
action = CmdRunAction(command=exec_command, keep_prompt=False)
action.timeout = 600
obs = runtime.run_action(action)
assert isinstance(obs, CmdOutputObservation)
@@ -211,7 +200,9 @@ def process_instance(
# Run eval script in background and save output to log file
log_file = '/tmp/eval_output.log'
action = CmdRunAction(command=f'/tmp/eval.sh > {log_file} 2>&1 & echo $!')
action = CmdRunAction(
command=f'/tmp/eval.sh > {log_file} 2>&1 & echo $!', keep_prompt=False
)
action.timeout = 60 # Short timeout just to get the process ID
obs = runtime.run_action(action)
@@ -233,7 +224,7 @@ def process_instance(
instance['test_result']['report']['test_timeout'] = True
break
check_action = CmdRunAction(
command=f'ps -p {pid} > /dev/null; echo $?'
command=f'ps -p {pid} > /dev/null; echo $?', keep_prompt=False
)
check_action.timeout = 60
check_obs = runtime.run_action(check_action)
@@ -251,7 +242,7 @@ def process_instance(
time.sleep(30) # Wait for 30 seconds before checking again
# Read the log file
cat_action = CmdRunAction(command=f'cat {log_file}')
cat_action = CmdRunAction(command=f'cat {log_file}', keep_prompt=False)
cat_action.timeout = 300
cat_obs = runtime.run_action(cat_action)
+4 -16
View File
@@ -15,7 +15,6 @@ from evaluation.utils.shared import (
EvalOutput,
assert_and_raise,
codeact_user_response,
get_metrics,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
@@ -149,7 +148,6 @@ def get_config(
codeact_enable_jupyter=False,
codeact_enable_browsing=RUN_WITH_BROWSING,
codeact_enable_llm_editor=False,
condenser=metadata.condenser_config,
)
config.set_agent_config(agent_config)
return config
@@ -284,16 +282,6 @@ def initialize_runtime(
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to remove git remotes: {str(obs)}')
action = CmdRunAction(command='which python')
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0 and 'testbed' in obs.content,
f'Expected to find python interpreter from testbed, but got: {str(obs)}',
)
logger.info('-' * 30)
logger.info('END Runtime Initialization Fn')
logger.info('-' * 30)
@@ -349,7 +337,8 @@ def complete_runtime(
git_patch = None
while n_retries < 5:
action = CmdRunAction(
command=f'git diff --no-color --cached {instance["base_commit"]}'
command=f'git diff --no-color --cached {instance["base_commit"]}',
keep_prompt=False,
)
action.timeout = 600 + 100 * n_retries
logger.info(action, extra={'msg_type': 'ACTION'})
@@ -396,7 +385,7 @@ def process_instance(
if runtime_failure_count > 0:
config.sandbox.remote_runtime_resource_factor = min(
config.sandbox.remote_runtime_resource_factor * (2**runtime_failure_count),
8,
2, # hardcode maximum resource factor to 2
)
logger.warning(
f'This is the second attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
@@ -450,7 +439,7 @@ def process_instance(
# NOTE: this is NO LONGER the event stream, but an agent history that includes delegate agent's events
histories = [event_to_dict(event) for event in state.history]
metrics = get_metrics(state)
metrics = state.metrics.get() if state.metrics else None
# Save the output
output = EvalOutput(
@@ -546,5 +535,4 @@ if __name__ == '__main__':
args.eval_num_workers,
process_instance,
timeout_seconds=120 * 60, # 2 hour PER instance should be more than enough
max_retries=5,
)
@@ -104,9 +104,9 @@ for repo, diff in repo_diffs:
# Determine if this repo has a significant diff
is_significant = diff >= threshold
repo_color = 'red' if is_significant else 'yellow'
print(colored(f'Difference: {diff} instances!', repo_color, attrs=['bold']))
print(f"\n{colored(repo, repo_color, attrs=['bold'])}:")
print(colored(f'Difference: {diff} instances!', repo_color, attrs=['bold']))
print(colored(f'X resolved but Y failed: ({len(x_instances)} instances)', 'green'))
if x_instances:
print(' ' + str(x_instances))
@@ -20,13 +20,6 @@ output_md_folder = args.oh_output_file.replace('.jsonl', '.viz')
print(f'Converting {args.oh_output_file} to markdown files in {output_md_folder}')
oh_format = pd.read_json(args.oh_output_file, orient='records', lines=True)
swebench_eval_file = args.oh_output_file.replace('.jsonl', '.swebench_eval.jsonl')
if os.path.exists(swebench_eval_file):
eval_output_df = pd.read_json(swebench_eval_file, orient='records', lines=True)
else:
eval_output_df = None
# model name is the folder name of oh_output_file
model_name = os.path.basename(os.path.dirname(args.oh_output_file))
@@ -57,7 +50,7 @@ def convert_history_to_str(history):
return ret
def write_row_to_md_file(row, instance_id_to_test_result):
def write_row_to_md_file(row):
if 'git_patch' in row:
model_patch = row['git_patch']
elif 'test_result' in row and 'git_patch' in row['test_result']:
@@ -65,21 +58,8 @@ def write_row_to_md_file(row, instance_id_to_test_result):
else:
raise ValueError(f'Row {row} does not have a git_patch')
test_output = None
if row['instance_id'] in instance_id_to_test_result:
report = instance_id_to_test_result[row['instance_id']].get('report', {})
resolved = report.get('resolved', False)
test_output = instance_id_to_test_result[row['instance_id']].get(
'test_output', None
)
elif 'report' in row and row['report'] is not None:
if not isinstance(row['report'], dict):
resolved = None
print(
f'ERROR: Report is not a dict, but a {type(row["report"])}. Row: {row}'
)
else:
resolved = row['report'].get('resolved', False)
if 'report' in row:
resolved = row['report'].get('resolved', False)
else:
resolved = None
@@ -104,18 +84,5 @@ def write_row_to_md_file(row, instance_id_to_test_result):
f.write('## Model Patch\n')
f.write(f'{process_git_patch(model_patch)}\n')
f.write('## Test Output\n')
f.write(str(test_output))
instance_id_to_test_result = {}
if eval_output_df is not None:
instance_id_to_test_result = (
eval_output_df[['instance_id', 'test_result']]
.set_index('instance_id')['test_result']
.to_dict()
)
oh_format.progress_apply(
write_row_to_md_file, axis=1, instance_id_to_test_result=instance_id_to_test_result
)
oh_format.progress_apply(write_row_to_md_file, axis=1)
@@ -111,11 +111,6 @@ elif os.path.exists(openhands_remote_report_jsonl):
instance_id_to_status[row['instance_id']] = row['test_result']['report']
df['report'] = df.apply(apply_report, axis=1)
report_is_dict = df['report'].apply(lambda x: isinstance(x, dict))
if not report_is_dict.all():
print(df[~report_is_dict])
raise ValueError(f'Report is not a dict, but a {type(row["report"])}')
_n_instances = len(df)
_n_resolved = len(df[df['report'].apply(lambda x: x.get('resolved', False))])
_n_unresolved = _n_instances - _n_resolved
@@ -39,7 +39,7 @@ def get_config(
run_as_openhands=False,
max_budget_per_task=4,
max_iterations=100,
save_trajectory_path=os.path.join(
trajectories_path=os.path.join(
mount_path_on_host, f'traj_{task_short_name}.json'
),
sandbox=SandboxConfig(
@@ -24,7 +24,7 @@ class Test(BaseIntegrationTest):
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the file /workspace/bad.txt has been fixed
action = CmdRunAction(command='cat /workspace/bad.txt')
action = CmdRunAction(command='cat /workspace/bad.txt', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
@@ -10,14 +10,14 @@ class Test(BaseIntegrationTest):
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
action = CmdRunAction(command='mkdir -p /workspace')
action = CmdRunAction(command='mkdir -p /workspace', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the file /workspace/hello.sh exists
action = CmdRunAction(command='cat /workspace/hello.sh')
action = CmdRunAction(command='cat /workspace/hello.sh', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
@@ -26,7 +26,7 @@ class Test(BaseIntegrationTest):
)
# execute the script
action = CmdRunAction(command='bash /workspace/hello.sh')
action = CmdRunAction(command='bash /workspace/hello.sh', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
@@ -10,14 +10,14 @@ class Test(BaseIntegrationTest):
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
action = CmdRunAction(command='mkdir -p /workspace')
action = CmdRunAction(command='mkdir -p /workspace', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the file /workspace/hello.sh exists
action = CmdRunAction(command='cat /workspace/test.txt')
action = CmdRunAction(command='cat /workspace/test.txt', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
@@ -26,7 +26,7 @@ class Test(BaseIntegrationTest):
)
# execute the script
action = CmdRunAction(command='cat /workspace/test.txt')
action = CmdRunAction(command='cat /workspace/test.txt', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
@@ -10,29 +10,31 @@ class Test(BaseIntegrationTest):
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
action = CmdRunAction(command='mkdir -p /workspace')
action = CmdRunAction(command='mkdir -p /workspace', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# git init
action = CmdRunAction(command='git init')
action = CmdRunAction(command='git init', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# create README.md
action = CmdRunAction(command='echo \'print("hello world")\' > hello.py')
action = CmdRunAction(
command='echo \'print("hello world")\' > hello.py', keep_prompt=False
)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# git add README.md
action = CmdRunAction(command='git add hello.py')
action = CmdRunAction(command='git add hello.py', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the file /workspace/hello.py exists
action = CmdRunAction(command='cat /workspace/hello.py')
action = CmdRunAction(command='cat /workspace/hello.py', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
@@ -41,7 +43,7 @@ class Test(BaseIntegrationTest):
)
# check if the staging area is empty
action = CmdRunAction(command='git status')
action = CmdRunAction(command='git status', keep_prompt=False)
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
@@ -83,11 +83,11 @@ class Test(BaseIntegrationTest):
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
action = CmdRunAction(command='mkdir -p /workspace')
action = CmdRunAction(command='mkdir -p /workspace', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
action = CmdRunAction(command='mkdir -p /tmp/server')
action = CmdRunAction(command='mkdir -p /tmp/server', keep_prompt=False)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
@@ -101,7 +101,8 @@ class Test(BaseIntegrationTest):
# create README.md
action = CmdRunAction(
command='cd /tmp/server && nohup python3 -m http.server 8000 &'
command='cd /tmp/server && nohup python3 -m http.server 8000 &',
keep_prompt=False,
)
obs = runtime.run_action(action)
-27
View File
@@ -17,10 +17,6 @@ from tqdm import tqdm
from openhands.controller.state.state import State
from openhands.core.config import LLMConfig
from openhands.core.config.condenser_config import (
CondenserConfig,
NoOpCondenserConfig,
)
from openhands.core.exceptions import (
AgentRuntimeBuildError,
AgentRuntimeDisconnectedError,
@@ -37,7 +33,6 @@ from openhands.events.action.message import MessageAction
from openhands.events.event import Event
from openhands.events.serialization.event import event_to_dict
from openhands.events.utils import get_pairs_from_events
from openhands.memory.condenser import get_condensation_metadata
class EvalMetadata(BaseModel):
@@ -50,17 +45,11 @@ class EvalMetadata(BaseModel):
dataset: str | None = None
data_split: str | None = None
details: dict[str, Any] | None = None
condenser_config: CondenserConfig | None = None
def model_dump(self, *args, **kwargs):
dumped_dict = super().model_dump(*args, **kwargs)
# avoid leaking sensitive information
dumped_dict['llm_config'] = self.llm_config.to_safe_dict()
if hasattr(self.condenser_config, 'llm_config'):
dumped_dict['condenser_config']['llm_config'] = (
self.condenser_config.llm_config.to_safe_dict()
)
return dumped_dict
def model_dump_json(self, *args, **kwargs):
@@ -68,11 +57,6 @@ class EvalMetadata(BaseModel):
dumped_dict = json.loads(dumped)
# avoid leaking sensitive information
dumped_dict['llm_config'] = self.llm_config.to_safe_dict()
if hasattr(self.condenser_config, 'llm_config'):
dumped_dict['condenser_config']['llm_config'] = (
self.condenser_config.llm_config.to_safe_dict()
)
logger.debug(f'Dumped metadata: {dumped_dict}')
return json.dumps(dumped_dict)
@@ -208,7 +192,6 @@ def make_metadata(
eval_output_dir: str,
data_split: str | None = None,
details: dict[str, Any] | None = None,
condenser_config: CondenserConfig | None = None,
) -> EvalMetadata:
model_name = llm_config.model.split('/')[-1]
model_path = model_name.replace(':', '_').replace('@', '-')
@@ -239,9 +222,6 @@ def make_metadata(
dataset=dataset_name,
data_split=data_split,
details=details,
condenser_config=condenser_config
if condenser_config
else NoOpCondenserConfig(),
)
metadata_json = metadata.model_dump_json()
logger.info(f'Metadata: {metadata_json}')
@@ -571,10 +551,3 @@ def is_fatal_evaluation_error(error: str | None) -> bool:
return True
return False
def get_metrics(state: State) -> dict[str, Any]:
"""Extract metrics from the state."""
metrics = state.metrics.get() if state.metrics else {}
metrics['condenser'] = get_condensation_metadata(state)
return metrics
+47
View File
@@ -0,0 +1,47 @@
import { describe, expect, it, vi } from "vitest";
import { retrieveLatestGitHubCommit } from "../../src/api/github";
describe("retrieveLatestGitHubCommit", () => {
const { githubGetMock } = vi.hoisted(() => ({
githubGetMock: vi.fn(),
}));
vi.mock("../../src/api/github-axios-instance", () => ({
github: {
get: githubGetMock,
},
}));
it("should return the latest commit when repository has commits", async () => {
const mockCommit = {
sha: "123abc",
commit: {
message: "Initial commit",
},
};
githubGetMock.mockResolvedValueOnce({
data: [mockCommit],
});
const result = await retrieveLatestGitHubCommit("user/repo");
expect(result).toEqual(mockCommit);
});
it("should return null when repository is empty", async () => {
const error = new Error("Repository is empty");
(error as any).response = { status: 409 };
githubGetMock.mockRejectedValueOnce(error);
const result = await retrieveLatestGitHubCommit("user/empty-repo");
expect(result).toBeNull();
});
it("should throw error for other error cases", async () => {
const error = new Error("Network error");
(error as any).response = { status: 500 };
githubGetMock.mockRejectedValueOnce(error);
await expect(retrieveLatestGitHubCommit("user/repo")).rejects.toThrow();
});
});
@@ -45,7 +45,7 @@ describe("Browser", () => {
});
// i18n empty message key
expect(screen.getByText("BROWSER$NO_PAGE_LOADED")).toBeInTheDocument();
expect(screen.getByText("BROWSER$EMPTY_MESSAGE")).toBeInTheDocument();
});
it("renders the url and a screenshot", () => {
@@ -84,10 +84,12 @@ describe("ChatInput", () => {
expect(onSubmitMock).not.toHaveBeenCalled();
});
it("should render a placeholder with translation key", () => {
render(<ChatInput onSubmit={onSubmitMock} />);
it("should render a placeholder", () => {
render(
<ChatInput placeholder="Enter your message" onSubmit={onSubmitMock} />,
);
const textarea = screen.getByPlaceholderText("SUGGESTIONS$WHAT_TO_BUILD");
const textarea = screen.getByPlaceholderText("Enter your message");
expect(textarea).toBeInTheDocument();
});
@@ -216,30 +218,4 @@ describe("ChatInput", () => {
// Verify image paste was handled
expect(onImagePaste).toHaveBeenCalledWith([file]);
});
it("should not submit when Enter is pressed during IME composition", async () => {
const user = userEvent.setup();
render(<ChatInput onSubmit={onSubmitMock} />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "こんにちは");
// Simulate Enter during IME composition
fireEvent.keyDown(textarea, {
key: "Enter",
isComposing: true,
nativeEvent: { isComposing: true },
});
expect(onSubmitMock).not.toHaveBeenCalled();
// Simulate normal Enter after composition is done
fireEvent.keyDown(textarea, {
key: "Enter",
isComposing: false,
nativeEvent: { isComposing: false },
});
expect(onSubmitMock).toHaveBeenCalledWith("こんにちは");
});
});
@@ -3,13 +3,11 @@ 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";
import { clickOnEditButton } from "./utils";
describe("ConversationCard", () => {
const onClick = vi.fn();
const onDelete = vi.fn();
const onChangeTitle = vi.fn();
const onDownloadWorkspace = vi.fn();
afterEach(() => {
vi.clearAllMocks();
@@ -19,11 +17,11 @@ describe("ConversationCard", () => {
render(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
/>,
);
const expectedDate = `${formatTimeDelta(new Date("2021-10-01T12:00:00Z"))} ago`;
@@ -35,34 +33,53 @@ describe("ConversationCard", () => {
within(card).getByText(expectedDate);
});
it("should render the selectedRepository if available", () => {
it("should render the repo if available", () => {
const { rerender } = render(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
/>,
);
expect(
screen.queryByTestId("conversation-card-selected-repository"),
screen.queryByTestId("conversation-card-repo"),
).not.toBeInTheDocument();
rerender(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
isActive
title="Conversation 1"
selectedRepository="org/selectedRepository"
lastUpdatedAt="2021-10-01T12:00:00Z"
name="Conversation 1"
repo="org/repo"
lastUpdated="2021-10-01T12:00:00Z"
/>,
);
screen.getByTestId("conversation-card-selected-repository");
screen.getByTestId("conversation-card-repo");
});
it("should call onClick when the card is clicked", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
/>,
);
const card = screen.getByTestId("conversation-card");
await user.click(card);
expect(onClick).toHaveBeenCalled();
});
it("should toggle a context menu when clicking the ellipsis button", async () => {
@@ -70,11 +87,11 @@ describe("ConversationCard", () => {
render(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
/>,
);
@@ -94,12 +111,12 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
/>,
);
@@ -114,23 +131,21 @@ describe("ConversationCard", () => {
expect(onDelete).toHaveBeenCalled();
});
test("clicking the selectedRepository should not trigger the onClick handler", async () => {
test("clicking the repo should not trigger the onClick handler", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository="org/selectedRepository"
lastUpdatedAt="2021-10-01T12:00:00Z"
name="Conversation 1"
repo="org/repo"
lastUpdated="2021-10-01T12:00:00Z"
/>,
);
const selectedRepository = screen.getByTestId(
"conversation-card-selected-repository",
);
await user.click(selectedRepository);
const repo = screen.getByTestId("conversation-card-repo");
await user.click(repo);
expect(onClick).not.toHaveBeenCalled();
});
@@ -139,24 +154,16 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
onChangeTitle={onChangeTitle}
/>,
);
const title = screen.getByTestId("conversation-card-title");
expect(title).toBeDisabled();
await clickOnEditButton(user);
expect(title).toBeEnabled();
expect(screen.queryByTestId("context-menu")).not.toBeInTheDocument();
// expect to be focused
expect(document.activeElement).toBe(title);
await user.clear(title);
await user.type(title, "New Conversation Name ");
@@ -164,24 +171,21 @@ 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 () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
/>,
);
await clickOnEditButton(user);
const title = screen.getByTestId("conversation-card-title");
await user.clear(title);
@@ -195,12 +199,12 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
/>,
);
@@ -214,12 +218,12 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
/>,
);
@@ -234,151 +238,37 @@ describe("ConversationCard", () => {
expect(onClick).not.toHaveBeenCalled();
});
it("should call onDownloadWorkspace when the download button is clicked", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const menu = screen.getByTestId("context-menu");
const downloadButton = within(menu).getByTestId("download-button");
await user.click(downloadButton);
expect(onDownloadWorkspace).toHaveBeenCalled();
});
it("should not display the edit or delete options if the handler is not provided", async () => {
const user = userEvent.setup();
const { rerender } = render(
<ConversationCard
onClick={onClick}
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
expect(screen.queryByTestId("edit-button")).toBeInTheDocument();
expect(screen.queryByTestId("delete-button")).not.toBeInTheDocument();
// toggle to hide the context menu
await user.click(ellipsisButton);
rerender(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
await user.click(ellipsisButton);
expect(screen.queryByTestId("edit-button")).not.toBeInTheDocument();
expect(screen.queryByTestId("delete-button")).toBeInTheDocument();
});
it("should not render the ellipsis button if there are no actions", () => {
const { rerender } = render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(screen.getByTestId("ellipsis-button")).toBeInTheDocument();
rerender(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(screen.getByTestId("ellipsis-button")).toBeInTheDocument();
rerender(
<ConversationCard
onClick={onClick}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(screen.queryByTestId("ellipsis-button")).toBeInTheDocument();
rerender(
<ConversationCard
onClick={onClick}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(screen.queryByTestId("ellipsis-button")).not.toBeInTheDocument();
});
describe("state indicator", () => {
it("should render the 'STOPPED' indicator by default", () => {
it("should render the 'cold' indicator by default", () => {
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
/>,
);
screen.getByTestId("STOPPED-indicator");
screen.getByTestId("cold-indicator");
});
it("should render the other indicators when provided", () => {
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
status="RUNNING"
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
state="warm"
/>,
);
expect(screen.queryByTestId("STOPPED-indicator")).not.toBeInTheDocument();
screen.getByTestId("RUNNING-indicator");
expect(screen.queryByTestId("cold-indicator")).not.toBeInTheDocument();
screen.getByTestId("warm-indicator");
});
});
});
@@ -6,23 +6,15 @@ import {
QueryClientConfig,
} from "@tanstack/react-query";
import userEvent from "@testing-library/user-event";
import { createRoutesStub } from "react-router";
import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel";
import OpenHands from "#/api/open-hands";
import { AuthProvider } from "#/context/auth-context";
import { clickOnEditButton } from "./utils";
describe("ConversationPanel", () => {
const onCloseMock = vi.fn();
const RouterStub = createRoutesStub([
{
Component: () => <ConversationPanel onClose={onCloseMock} />,
path: "/",
},
]);
const renderConversationPanel = (config?: QueryClientConfig) =>
render(<RouterStub />, {
render(<ConversationPanel onClose={onCloseMock} />, {
wrapper: ({ children }) => (
<AuthProvider>
<QueryClientProvider client={new QueryClient(config)}>
@@ -60,8 +52,6 @@ describe("ConversationPanel", () => {
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// NOTE that we filter out conversations that don't have a created_at property
// (mock data has 4 conversations, but only 3 have a created_at property)
expect(cards).toHaveLength(3);
});
@@ -71,7 +61,7 @@ describe("ConversationPanel", () => {
renderConversationPanel();
const emptyState = await screen.findByText("CONVERSATION$NO_CONVERSATIONS");
const emptyState = await screen.findByText("No conversations found");
expect(emptyState).toBeInTheDocument();
});
@@ -179,15 +169,13 @@ describe("ConversationPanel", () => {
const cards = await screen.findAllByTestId("conversation-card");
const title = within(cards[0]).getByTestId("conversation-card-title");
await clickOnEditButton(user);
await user.clear(title);
await user.type(title, "Conversation 1 Renamed");
await user.tab();
// Ensure the conversation is renamed
expect(updateUserConversationSpy).toHaveBeenCalledWith("3", {
title: "Conversation 1 Renamed",
name: "Conversation 1 Renamed",
});
});
@@ -208,8 +196,6 @@ describe("ConversationPanel", () => {
// Ensure the conversation is not renamed
expect(updateUserConversationSpy).not.toHaveBeenCalled();
await clickOnEditButton(user);
await user.type(title, "Conversation 1");
await user.click(title);
await user.tab();
@@ -231,4 +217,51 @@ describe("ConversationPanel", () => {
expect(onCloseMock).toHaveBeenCalledOnce();
});
describe("New Conversation Button", () => {
it("should display a confirmation modal when clicking", async () => {
const user = userEvent.setup();
renderConversationPanel();
expect(
screen.queryByTestId("confirm-new-conversation-modal"),
).not.toBeInTheDocument();
const newProjectButton = screen.getByTestId("new-conversation-button");
await user.click(newProjectButton);
const modal = screen.getByTestId("confirm-new-conversation-modal");
expect(modal).toBeInTheDocument();
});
it("should call endSession and close panel after confirming", async () => {
const user = userEvent.setup();
renderConversationPanel();
const newProjectButton = screen.getByTestId("new-conversation-button");
await user.click(newProjectButton);
const confirmButton = screen.getByText("Confirm");
await user.click(confirmButton);
expect(endSessionMock).toHaveBeenCalledOnce();
expect(onCloseMock).toHaveBeenCalledOnce();
});
it("should close the modal when cancelling", async () => {
const user = userEvent.setup();
renderConversationPanel();
const newProjectButton = screen.getByTestId("new-conversation-button");
await user.click(newProjectButton);
const cancelButton = screen.getByText("Cancel");
await user.click(cancelButton);
expect(endSessionMock).not.toHaveBeenCalled();
expect(
screen.queryByTestId("confirm-new-conversation-modal"),
).not.toBeInTheDocument();
});
});
});
@@ -1,12 +0,0 @@
import { screen, within } from "@testing-library/react";
import { UserEvent } from "@testing-library/user-event";
export const clickOnEditButton = async (user: UserEvent) => {
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const menu = screen.getByTestId("context-menu");
const editButton = within(menu).getByTestId("edit-button");
await user.click(editButton);
};
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { GitHubRepositorySelector } from "#/components/features/github/github-repo-selector";
import OpenHands from "#/api/open-hands";
import * as GitHubAPI from "#/api/github";
describe("GitHubRepositorySelector", () => {
const onInputChangeMock = vi.fn();
@@ -13,13 +14,12 @@ describe("GitHubRepositorySelector", () => {
<GitHubRepositorySelector
onInputChange={onInputChangeMock}
onSelect={onSelectMock}
publicRepositories={[]}
userRepositories={[]}
repositories={[]}
/>,
);
expect(
screen.getByPlaceholderText("LANDING$SELECT_REPO"),
screen.getByPlaceholderText("Select a GitHub project"),
).toBeInTheDocument();
});
@@ -36,8 +36,7 @@ describe("GitHubRepositorySelector", () => {
<GitHubRepositorySelector
onInputChange={onInputChangeMock}
onSelect={onSelectMock}
publicRepositories={[]}
userRepositories={[]}
repositories={[]}
/>,
);
@@ -59,8 +58,8 @@ describe("GitHubRepositorySelector", () => {
];
const searchPublicRepositoriesSpy = vi.spyOn(
OpenHands,
"searchGitHubRepositories",
GitHubAPI,
"searchPublicRepositories",
);
searchPublicRepositoriesSpy.mockResolvedValue(mockSearchedRepos);
@@ -68,8 +67,7 @@ describe("GitHubRepositorySelector", () => {
<GitHubRepositorySelector
onInputChange={onInputChangeMock}
onSelect={onSelectMock}
publicRepositories={[]}
userRepositories={[]}
repositories={[]}
/>,
);
@@ -1,12 +1,10 @@
import { screen, within } from "@testing-library/react";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
import { renderWithProviders } from "test-utils";
import { createRoutesStub } from "react-router";
import { Sidebar } from "#/components/features/sidebar/sidebar";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
import OpenHands from "#/api/open-hands";
import { MOCK_USER_PREFERENCES } from "#/mocks/handlers";
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
const renderSidebar = () => {
const RouterStub = createRoutesStub([
@@ -20,7 +18,7 @@ const renderSidebar = () => {
};
describe("Sidebar", () => {
it.skipIf(!MULTI_CONVERSATION_UI)(
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
"should have the conversation panel open by default",
() => {
renderSidebar();
@@ -28,7 +26,7 @@ describe("Sidebar", () => {
},
);
it.skipIf(!MULTI_CONVERSATION_UI)(
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
"should toggle the conversation panel",
async () => {
const user = userEvent.setup();
@@ -45,130 +43,4 @@ describe("Sidebar", () => {
).not.toBeInTheDocument();
},
);
describe("Settings", () => {
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
afterEach(() => {
vi.clearAllMocks();
});
it("should fetch settings data on mount", () => {
renderSidebar();
expect(getSettingsSpy).toHaveBeenCalledOnce();
});
it("should send all settings data when saving AI configuration", async () => {
const user = userEvent.setup();
renderSidebar();
const settingsButton = screen.getByTestId("settings-button");
await user.click(settingsButton);
const settingsModal = screen.getByTestId("ai-config-modal");
const saveButton = within(settingsModal).getByTestId(
"save-settings-button",
);
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
// the actual values are falsey (null or "") but we're checking for undefined
llm_api_key: undefined,
llm_base_url: undefined,
security_analyzer: undefined,
});
});
it("should send all settings data when saving account settings", async () => {
const user = userEvent.setup();
renderSidebar();
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
const menu = screen.getByTestId("account-settings-context-menu");
const accountSettingsButton = within(menu).getByTestId(
"account-settings-button",
);
await user.click(accountSettingsButton);
const accountSettingsModal = screen.getByTestId("account-settings-form");
const saveButton =
within(accountSettingsModal).getByTestId("save-settings");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
llm_api_key: undefined, // null or undefined
});
});
it("should not reset AI configuration when saving account settings", async () => {
const user = userEvent.setup();
renderSidebar();
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
const menu = screen.getByTestId("account-settings-context-menu");
const accountSettingsButton = within(menu).getByTestId(
"account-settings-button",
);
await user.click(accountSettingsButton);
const accountSettingsModal = screen.getByTestId("account-settings-form");
const languageInput =
within(accountSettingsModal).getByLabelText(/language/i);
await user.click(languageInput);
const norskOption = screen.getByText(/norsk/i);
await user.click(norskOption);
const tokenInput =
within(accountSettingsModal).getByLabelText(/GITHUB\$TOKEN_OPTIONAL/i);
await user.type(tokenInput, "new-token");
const saveButton =
within(accountSettingsModal).getByTestId("save-settings");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
language: "no",
llm_api_key: undefined, // null or undefined
});
});
it("should not send the api key if its SET", async () => {
const user = userEvent.setup();
renderSidebar();
const settingsButton = screen.getByTestId("settings-button");
await user.click(settingsButton);
const settingsModal = screen.getByTestId("ai-config-modal");
// Click the advanced options switch to show the API key input
const advancedOptionsSwitch = within(settingsModal).getByTestId("advanced-option-switch");
await user.click(advancedOptionsSwitch);
const apiKeyInput = within(settingsModal).getByLabelText(/API\$KEY/i);
await user.type(apiKeyInput, "SET");
const saveButton = within(settingsModal).getByTestId(
"save-settings-button",
);
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
llm_api_key: undefined,
llm_base_url: "",
security_analyzer: undefined,
});
});
});
});
@@ -14,7 +14,6 @@ import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { FeedbackForm } from "#/components/features/feedback/feedback-form";
import { I18nKey } from "#/i18n/declaration";
describe("FeedbackForm", () => {
const user = userEvent.setup();
@@ -29,20 +28,20 @@ describe("FeedbackForm", () => {
<FeedbackForm polarity="positive" onClose={onCloseMock} />,
);
screen.getByLabelText(I18nKey.FEEDBACK$EMAIL_LABEL);
screen.getByLabelText(I18nKey.FEEDBACK$PRIVATE_LABEL);
screen.getByLabelText(I18nKey.FEEDBACK$PUBLIC_LABEL);
screen.getByLabelText("Email");
screen.getByLabelText("Private");
screen.getByLabelText("Public");
screen.getByRole("button", { name: I18nKey.FEEDBACK$CONTRIBUTE_LABEL });
screen.getByRole("button", { name: I18nKey.FEEDBACK$CANCEL_LABEL });
screen.getByRole("button", { name: "Submit" });
screen.getByRole("button", { name: "Cancel" });
});
it("should switch between private and public permissions", async () => {
renderWithProviders(
<FeedbackForm polarity="positive" onClose={onCloseMock} />,
);
const privateRadio = screen.getByLabelText(I18nKey.FEEDBACK$PRIVATE_LABEL);
const publicRadio = screen.getByLabelText(I18nKey.FEEDBACK$PUBLIC_LABEL);
const privateRadio = screen.getByLabelText("Private");
const publicRadio = screen.getByLabelText("Public");
expect(privateRadio).toBeChecked(); // private is the default value
expect(publicRadio).not.toBeChecked();
@@ -60,7 +59,7 @@ describe("FeedbackForm", () => {
renderWithProviders(
<FeedbackForm polarity="positive" onClose={onCloseMock} />,
);
await user.click(screen.getByRole("button", { name: I18nKey.FEEDBACK$CANCEL_LABEL }));
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(onCloseMock).toHaveBeenCalled();
});
@@ -157,7 +157,7 @@ describe("InteractiveChatBox", () => {
expect(onChange).not.toHaveBeenCalledWith("");
// Submit the message with image
const submitButton = screen.getByRole("button", { name: "BUTTON$SEND" });
const submitButton = screen.getByRole("button", { name: "Send" });
await user.click(submitButton);
// Verify onSubmit was called with the message and image
@@ -1,190 +0,0 @@
import { render, screen } from "@testing-library/react";
import { test, expect, describe, vi } from "vitest";
import { useTranslation } from "react-i18next";
import translations from "../../src/i18n/translation.json";
import { UserAvatar } from "../../src/components/features/sidebar/user-avatar";
vi.mock("@nextui-org/react", () => ({
Tooltip: ({ content, children }: { content: string; children: React.ReactNode }) => (
<div>
{children}
<div>{content}</div>
</div>
),
}));
const supportedLanguages = ['en', 'ja', 'zh-CN', 'zh-TW', 'ko-KR', 'de', 'no', 'it', 'pt', 'es', 'ar', 'fr', 'tr'];
// Helper function to check if a translation exists for all supported languages
function checkTranslationExists(key: string) {
const missingTranslations: string[] = [];
const translationEntry = (translations as Record<string, Record<string, string>>)[key];
if (!translationEntry) {
throw new Error(`Translation key "${key}" does not exist in translation.json`);
}
for (const lang of supportedLanguages) {
if (!translationEntry[lang]) {
missingTranslations.push(lang);
}
}
return missingTranslations;
}
// Helper function to find duplicate translation keys
function findDuplicateKeys(obj: Record<string, any>) {
const seen = new Set<string>();
const duplicates = new Set<string>();
// Only check top-level keys as these are our translation keys
for (const key in obj) {
if (seen.has(key)) {
duplicates.add(key);
} else {
seen.add(key);
}
}
return Array.from(duplicates);
}
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translationEntry = (translations as Record<string, Record<string, string>>)[key];
return translationEntry?.ja || key;
},
}),
}));
describe("Landing page translations", () => {
test("should render Japanese translations correctly", () => {
// Mock a simple component that uses the translations
const TestComponent = () => {
const { t } = useTranslation();
return (
<div>
<UserAvatar onClick={() => {}} />
<div data-testid="main-content">
<h1>{t("LANDING$TITLE")}</h1>
<button>{t("VSCODE$OPEN")}</button>
<button>{t("SUGGESTIONS$INCREASE_TEST_COVERAGE")}</button>
<button>{t("SUGGESTIONS$AUTO_MERGE_PRS")}</button>
<button>{t("SUGGESTIONS$FIX_README")}</button>
<button>{t("SUGGESTIONS$CLEAN_DEPENDENCIES")}</button>
</div>
<div data-testid="tabs">
<span>{t("WORKSPACE$TERMINAL_TAB_LABEL")}</span>
<span>{t("WORKSPACE$BROWSER_TAB_LABEL")}</span>
<span>{t("WORKSPACE$JUPYTER_TAB_LABEL")}</span>
<span>{t("WORKSPACE$CODE_EDITOR_TAB_LABEL")}</span>
</div>
<div data-testid="workspace-label">{t("WORKSPACE$TITLE")}</div>
<button data-testid="new-project">{t("PROJECT$NEW_PROJECT")}</button>
<div data-testid="status">
<span>{t("TERMINAL$WAITING_FOR_CLIENT")}</span>
<span>{t("STATUS$CONNECTED")}</span>
<span>{t("STATUS$CONNECTED_TO_SERVER")}</span>
</div>
<div data-testid="time">
<span>{`5 ${t("TIME$MINUTES_AGO")}`}</span>
<span>{`2 ${t("TIME$HOURS_AGO")}`}</span>
<span>{`3 ${t("TIME$DAYS_AGO")}`}</span>
</div>
</div>
);
};
render(<TestComponent />);
// Check main content translations
expect(screen.getByText("開発を始めましょう!")).toBeInTheDocument();
expect(screen.getByText("VS Codeで開く")).toBeInTheDocument();
expect(screen.getByText("テストカバレッジを向上させる")).toBeInTheDocument();
expect(screen.getByText("Dependabot PRを自動マージ")).toBeInTheDocument();
expect(screen.getByText("READMEを改善")).toBeInTheDocument();
expect(screen.getByText("依存関係を整理")).toBeInTheDocument();
// Check user avatar tooltip
const userAvatar = screen.getByTestId("user-avatar");
userAvatar.focus();
expect(screen.getByText("アカウント設定")).toBeInTheDocument();
// Check tab labels
const tabs = screen.getByTestId("tabs");
expect(tabs).toHaveTextContent("ターミナル");
expect(tabs).toHaveTextContent("ブラウザ");
expect(tabs).toHaveTextContent("Jupyter");
expect(tabs).toHaveTextContent("コードエディタ");
// Check workspace label and new project button
expect(screen.getByTestId("workspace-label")).toHaveTextContent("ワークスペース");
expect(screen.getByTestId("new-project")).toHaveTextContent("新規プロジェクト");
// Check status messages
const status = screen.getByTestId("status");
expect(status).toHaveTextContent("クライアントの準備を待機中");
expect(status).toHaveTextContent("接続済み");
expect(status).toHaveTextContent("サーバーに接続済み");
// Check account settings menu
expect(screen.getByText("アカウント設定")).toBeInTheDocument();
// Check time-related translations
const time = screen.getByTestId("time");
expect(time).toHaveTextContent("5 分前");
expect(time).toHaveTextContent("2 時間前");
expect(time).toHaveTextContent("3 日前");
});
test("all translation keys should have translations for all supported languages", () => {
// Test all translation keys used in the component
const translationKeys = [
"LANDING$TITLE",
"VSCODE$OPEN",
"SUGGESTIONS$INCREASE_TEST_COVERAGE",
"SUGGESTIONS$AUTO_MERGE_PRS",
"SUGGESTIONS$FIX_README",
"SUGGESTIONS$CLEAN_DEPENDENCIES",
"WORKSPACE$TERMINAL_TAB_LABEL",
"WORKSPACE$BROWSER_TAB_LABEL",
"WORKSPACE$JUPYTER_TAB_LABEL",
"WORKSPACE$CODE_EDITOR_TAB_LABEL",
"WORKSPACE$TITLE",
"PROJECT$NEW_PROJECT",
"TERMINAL$WAITING_FOR_CLIENT",
"STATUS$CONNECTED",
"STATUS$CONNECTED_TO_SERVER",
"TIME$MINUTES_AGO",
"TIME$HOURS_AGO",
"TIME$DAYS_AGO"
];
// Check all keys and collect missing translations
const missingTranslationsMap = new Map<string, string[]>();
translationKeys.forEach(key => {
const missing = checkTranslationExists(key);
if (missing.length > 0) {
missingTranslationsMap.set(key, missing);
}
});
// If any translations are missing, throw an error with all missing translations
if (missingTranslationsMap.size > 0) {
const errorMessage = Array.from(missingTranslationsMap.entries())
.map(([key, langs]) => `\n- "${key}" is missing translations for: ${langs.join(', ')}`)
.join('');
throw new Error(`Missing translations:${errorMessage}`);
}
});
test("translation file should not have duplicate keys", () => {
const duplicates = findDuplicateKeys(translations);
if (duplicates.length > 0) {
throw new Error(`Found duplicate translation keys: ${duplicates.join(', ')}`);
}
});
});
@@ -1,23 +1,7 @@
import { describe, it, expect, vi } from "vitest";
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ModelSelector } from "#/components/shared/modals/settings/model-selector";
import { I18nKey } from "#/i18n/declaration";
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: { [key: string]: string } = {
LLM$PROVIDER: "LLM Provider",
LLM$MODEL: "LLM Model",
LLM$SELECT_PROVIDER_PLACEHOLDER: "Select a provider",
LLM$SELECT_MODEL_PLACEHOLDER: "Select a model",
};
return translations[key] || key;
},
}),
}));
describe("ModelSelector", () => {
const models = {
@@ -1,35 +0,0 @@
import { screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { renderWithProviders } from "test-utils";
import { RuntimeSizeSelector } from "#/components/shared/modals/settings/runtime-size-selector";
const renderRuntimeSizeSelector = () =>
renderWithProviders(<RuntimeSizeSelector isDisabled={false} />);
describe("RuntimeSizeSelector", () => {
it("should show both runtime size options", () => {
renderRuntimeSizeSelector();
// The options are in the hidden select element
const select = screen.getByRole("combobox", { hidden: true });
expect(select).toHaveValue("1");
expect(select).toHaveDisplayValue("1x (2 core, 8G)");
expect(select.children).toHaveLength(3); // Empty option + 2 size options
});
it("should show the full description text for disabled options", async () => {
renderRuntimeSizeSelector();
// Click the button to open the dropdown
const button = screen.getByRole("button", {
name: "1x (2 core, 8G) SETTINGS_FORM$RUNTIME_SIZE_LABEL",
});
button.click();
// Wait for the dropdown to open and find the description text
const description = await screen.findByText(
"Runtime sizes over 1 are disabled by default, please contact contact@all-hands.dev to get access to larger runtimes.",
);
expect(description).toBeInTheDocument();
expect(description).toHaveClass("whitespace-normal", "break-words");
});
});
@@ -1,76 +0,0 @@
import { screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi, afterEach } from "vitest";
import { renderWithProviders } from "test-utils";
import { createRoutesStub } from "react-router";
import userEvent from "@testing-library/user-event";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { SettingsForm } from "#/components/shared/modals/settings/settings-form";
import OpenHands from "#/api/open-hands";
describe("SettingsForm", () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
const onCloseMock = vi.fn();
afterEach(() => {
vi.clearAllMocks();
});
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "123",
});
const RouterStub = createRoutesStub([
{
Component: () => (
<SettingsForm
settings={DEFAULT_SETTINGS}
models={["anthropic/claude-3-5-sonnet-20241022", "model2"]}
agents={["CodeActAgent", "agent2"]}
securityAnalyzers={["analyzer1", "analyzer2"]}
onClose={onCloseMock}
/>
),
path: "/",
},
]);
it("should not show runtime size selector by default", () => {
renderWithProviders(<RouterStub />);
expect(screen.queryByText("Runtime Size")).not.toBeInTheDocument();
});
it("should show runtime size selector when advanced options are enabled", async () => {
const user = userEvent.setup();
renderWithProviders(<RouterStub />);
const toggleAdvancedMode = screen.getByTestId("advanced-option-switch");
await user.click(toggleAdvancedMode);
await screen.findByTestId("runtime-size");
});
it("should not submit the form if required fields are empty", async () => {
const user = userEvent.setup();
renderWithProviders(<RouterStub />);
expect(screen.queryByTestId("custom-model-input")).not.toBeInTheDocument();
const toggleAdvancedMode = screen.getByTestId("advanced-option-switch");
await user.click(toggleAdvancedMode);
const customModelInput = screen.getByTestId("custom-model-input");
expect(customModelInput).toBeInTheDocument();
await user.clear(customModelInput);
const saveButton = screen.getByTestId("save-settings-button");
await user.click(saveButton);
expect(saveSettingsSpy).not.toHaveBeenCalled();
expect(onCloseMock).not.toHaveBeenCalled();
});
});
@@ -2,20 +2,6 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { SuggestionItem } from "#/components/features/suggestions/suggestion-item";
import { I18nKey } from "#/i18n/declaration";
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スクリプトを作成する",
};
return translations[key] || key;
},
}),
}));
describe("SuggestionItem", () => {
const suggestionItem = { label: "suggestion1", value: "a long text value" };
@@ -32,19 +18,6 @@ describe("SuggestionItem", () => {
expect(screen.getByText(/suggestion1/i)).toBeInTheDocument();
});
it("should render a translated suggestion when using I18nKey", async () => {
const translatedSuggestion = {
label: I18nKey.SUGGESTIONS$TODO_APP,
value: "todo app value",
};
const { container } = render(<SuggestionItem suggestion={translatedSuggestion} onClick={onClick} />);
console.log('Rendered HTML:', container.innerHTML);
expect(screen.getByText("ToDoリストアプリを開発する")).toBeInTheDocument();
});
it("should call onClick when clicking a suggestion", async () => {
const user = userEvent.setup();
render(<SuggestionItem suggestion={suggestionItem} onClick={onClick} />);
@@ -14,7 +14,7 @@ describe("UserAvatar", () => {
render(<UserAvatar onClick={onClickMock} />);
expect(screen.getByTestId("user-avatar")).toBeInTheDocument();
expect(
screen.getByLabelText("USER$AVATAR_PLACEHOLDER"),
screen.getByLabelText("user avatar placeholder"),
).toBeInTheDocument();
});
@@ -38,7 +38,7 @@ describe("UserAvatar", () => {
expect(screen.getByAltText("user avatar")).toBeInTheDocument();
expect(
screen.queryByLabelText("USER$AVATAR_PLACEHOLDER"),
screen.queryByLabelText("user avatar placeholder"),
).not.toBeInTheDocument();
});
@@ -46,13 +46,13 @@ describe("UserAvatar", () => {
const { rerender } = render(<UserAvatar onClick={onClickMock} />);
expect(screen.queryByTestId("loading-spinner")).not.toBeInTheDocument();
expect(
screen.getByLabelText("USER$AVATAR_PLACEHOLDER"),
screen.getByLabelText("user avatar placeholder"),
).toBeInTheDocument();
rerender(<UserAvatar onClick={onClickMock} isLoading />);
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
expect(
screen.queryByLabelText("USER$AVATAR_PLACEHOLDER"),
screen.queryByLabelText("user avatar placeholder"),
).not.toBeInTheDocument();
rerender(
@@ -1,43 +0,0 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import * as ChatSlice from "#/state/chat-slice";
import {
updateStatusWhenErrorMessagePresent,
} from "#/context/ws-client-provider";
describe("Propagate error message", () => {
it("should do nothing when no message was passed from server", () => {
const addErrorMessageSpy = vi.spyOn(ChatSlice, "addErrorMessage");
updateStatusWhenErrorMessagePresent(null)
updateStatusWhenErrorMessagePresent(undefined)
updateStatusWhenErrorMessagePresent({})
updateStatusWhenErrorMessagePresent({message: null})
expect(addErrorMessageSpy).not.toHaveBeenCalled();
});
it("should display error to user when present", () => {
const message = "We have a problem!"
const addErrorMessageSpy = vi.spyOn(ChatSlice, "addErrorMessage")
updateStatusWhenErrorMessagePresent({message})
expect(addErrorMessageSpy).toHaveBeenCalledWith({
message,
status_update: true,
type: 'error'
});
});
it("should display error including translation id when present", () => {
const message = "We have a problem!"
const addErrorMessageSpy = vi.spyOn(ChatSlice, "addErrorMessage")
updateStatusWhenErrorMessagePresent({message, data: {msg_id: '..id..'}})
expect(addErrorMessageSpy).toHaveBeenCalledWith({
message,
id: '..id..',
status_update: true,
type: 'error'
});
});
});
@@ -1,76 +0,0 @@
import { describe, expect, it } from 'vitest';
import fs from 'fs';
import path from 'path';
describe('translation.json', () => {
it('should not have duplicate translation keys', () => {
// Read the translation.json file
const translationPath = path.join(__dirname, '../../src/i18n/translation.json');
const translationContent = fs.readFileSync(translationPath, 'utf-8');
// First, let's check for exact string matches of key definitions
const keyRegex = /"([^"]+)": {/g;
const matches = translationContent.matchAll(keyRegex);
const keyOccurrences = new Map<string, number>();
const duplicateKeys: string[] = [];
for (const match of matches) {
const key = match[1];
const count = (keyOccurrences.get(key) || 0) + 1;
keyOccurrences.set(key, count);
if (count > 1) {
duplicateKeys.push(key);
}
}
// Remove duplicates from duplicateKeys array
const uniqueDuplicates = [...new Set(duplicateKeys)];
// If there are duplicates, create a helpful error message
if (uniqueDuplicates.length > 0) {
const errorMessage = `Found duplicate translation keys:\n${uniqueDuplicates
.map((key) => ` - "${key}" appears ${keyOccurrences.get(key)} times`)
.join('\n')}`;
throw new Error(errorMessage);
}
// Expect no duplicates (this will pass if we reach here)
expect(uniqueDuplicates).toHaveLength(0);
});
it('should have consistent translations for each key', () => {
// Read the translation.json file
const translationPath = path.join(__dirname, '../../src/i18n/translation.json');
const translationContent = fs.readFileSync(translationPath, 'utf-8');
const translations = JSON.parse(translationContent);
// Create a map to store English translations for each key
const englishTranslations = new Map<string, string>();
const inconsistentKeys: string[] = [];
// Check each key's English translation
Object.entries(translations).forEach(([key, value]: [string, any]) => {
if (typeof value === 'object' && value.en !== undefined) {
const currentEn = value.en.toLowerCase();
const existingEn = englishTranslations.get(key)?.toLowerCase();
if (existingEn !== undefined && existingEn !== currentEn) {
inconsistentKeys.push(key);
} else {
englishTranslations.set(key, value.en);
}
}
});
// If there are inconsistencies, create a helpful error message
if (inconsistentKeys.length > 0) {
const errorMessage = `Found inconsistent translations for keys:\n${inconsistentKeys
.map((key) => ` - "${key}" has multiple different English translations`)
.join('\n')}`;
throw new Error(errorMessage);
}
// Expect no inconsistencies
expect(inconsistentKeys).toHaveLength(0);
});
});
@@ -1,20 +0,0 @@
import { screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import i18n from '../../src/i18n';
import { AccountSettingsContextMenu } from '../../src/components/features/context-menu/account-settings-context-menu';
import { renderWithProviders } from '../../test-utils';
describe('Translations', () => {
it('should render translated text', () => {
i18n.changeLanguage('en');
renderWithProviders(
<AccountSettingsContextMenu
onClickAccountSettings={() => {}}
onLogout={() => {}}
onClose={() => {}}
isLoggedIn={true}
/>
);
expect(screen.getByTestId('account-settings-context-menu')).toBeInTheDocument();
});
});
+6 -7
View File
@@ -5,7 +5,7 @@ import { screen, waitFor } from "@testing-library/react";
import toast from "react-hot-toast";
import App from "#/routes/_oh.app/route";
import OpenHands from "#/api/open-hands";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
describe("App", () => {
const RouteStub = createRoutesStub([
@@ -35,7 +35,7 @@ describe("App", () => {
await screen.findByTestId("app-route");
});
it.skipIf(!MULTI_CONVERSATION_UI)(
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
"should call endSession if the user does not have permission to view conversation",
async () => {
const errorToastSpy = vi.spyOn(toast, "error");
@@ -59,11 +59,10 @@ describe("App", () => {
getConversationSpy.mockResolvedValue({
conversation_id: "9999",
last_updated_at: "",
created_at: "",
title: "",
selected_repository: "",
status: "STOPPED",
lastUpdated: "",
name: "",
repo: "",
state: "cold",
});
const { rerender } = renderWithProviders(
<RouteStub initialEntries={["/conversation/9999"]} />,
@@ -1,40 +0,0 @@
import { render, screen } from "@testing-library/react";
import { test, expect, describe, vi } from "vitest";
import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
import { ChatInput } from "#/components/features/chat/chat-input";
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
describe("Check for hardcoded English strings", () => {
test("InteractiveChatBox should not have hardcoded English strings", () => {
const { container } = render(
<InteractiveChatBox
onSubmit={() => {}}
onStop={() => {}}
/>
);
// Get all text content
const text = container.textContent;
// List of English strings that should be translated
const hardcodedStrings = [
"What do you want to build?",
];
// Check each string
hardcodedStrings.forEach(str => {
expect(text).not.toContain(str);
});
});
test("ChatInput should use translation key for placeholder", () => {
render(<ChatInput onSubmit={() => {}} />);
screen.getByPlaceholderText("SUGGESTIONS$WHAT_TO_BUILD");
});
});
@@ -1,29 +0,0 @@
import { ReactNode } from "react";
import { I18nextProvider } from "react-i18next";
const mockI18n = {
language: "ja",
t: (key: string) => {
const translations: Record<string, string> = {
"SUGGESTIONS$TODO_APP": "ToDoリストアプリを開発する",
"LANDING$BUILD_APP_BUTTON": "プルリクエストを表示するアプリを開発する",
"SUGGESTIONS$HACKER_NEWS": "Hacker Newsのトップ記事を表示するbashスクリプトを作成する",
"LANDING$TITLE": "一緒に開発を始めましょう!",
"OPEN_IN_VSCODE": "VS Codeで開く",
"INCREASE_TEST_COVERAGE": "テストカバレッジを向上",
"AUTO_MERGE_PRS": "PRを自動マージ",
"FIX_README": "READMEを修正",
"CLEAN_DEPENDENCIES": "依存関係を整理"
};
return translations[key] || key;
},
exists: () => true,
changeLanguage: () => new Promise(() => {}),
use: () => mockI18n,
};
export function I18nTestProvider({ children }: { children: ReactNode }) {
return (
<I18nextProvider i18n={mockI18n as any}>{children}</I18nextProvider>
);
}
+668 -590
View File
File diff suppressed because it is too large Load Diff
+15 -15
View File
@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.20.0",
"version": "0.18.0",
"private": true,
"type": "module",
"engines": {
@@ -8,36 +8,36 @@
},
"dependencies": {
"@monaco-editor/react": "^4.7.0-rc.0",
"@nextui-org/react": "^2.6.11",
"@nextui-org/react": "^2.6.10",
"@react-router/node": "^7.1.1",
"@react-router/serve": "^7.1.1",
"@react-types/shared": "^3.25.0",
"@reduxjs/toolkit": "^2.5.0",
"@tanstack/react-query": "^5.63.0",
"@tanstack/react-query": "^5.62.12",
"@vitejs/plugin-react": "^4.3.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"i18next": "^24.2.1",
"i18next": "^24.2.0",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-http-backend": "^3.0.1",
"isbot": "^5.1.21",
"isbot": "^5.1.19",
"jose": "^5.9.4",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.205.0",
"posthog-js": "^1.203.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.5.1",
"react-i18next": "^15.4.0",
"react-icons": "^5.4.0",
"react-markdown": "^9.0.3",
"react-markdown": "^9.0.1",
"react-redux": "^9.2.0",
"react-router": "^7.1.1",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.7",
"react-textarea-autosize": "^8.5.4",
"remark-gfm": "^4.0.0",
"sirv-cli": "^3.0.0",
"socket.io-client": "^4.8.1",
@@ -78,13 +78,13 @@
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.49.1",
"@react-router/dev": "^7.1.1",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.62.16",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/eslint-plugin-query": "^5.62.9",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^22.10.5",
"@types/react": "^19.0.4",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
@@ -97,20 +97,20 @@
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^10.0.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
"jsdom": "^26.0.0",
"jsdom": "^25.0.1",
"lint-staged": "^15.3.0",
"msw": "^2.6.6",
"postcss": "^8.4.47",
"prettier": "^3.4.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3",
"typescript": "^5.7.2",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^1.6.0"
+87
View File
@@ -1,6 +1,19 @@
import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link";
import { github } from "./github-axios-instance";
import { openHands } from "./open-hands-axios";
/**
* Given the user, retrieves app installations IDs for OpenHands Github App
* Uses user access token for Github App
*/
export const retrieveGitHubAppInstallations = async (): Promise<number[]> => {
const response = await github.get<GithubAppInstallation>(
"/user/installations",
);
return response.data.installations.map((installation) => installation.id);
};
/**
* Retrieves repositories where OpenHands Github App has been installed
* @param installationIndex Pagination cursor position for app installation IDs
@@ -69,3 +82,77 @@ export const retrieveGitHubUserRepositories = async (
return { data: response.data, nextPage };
};
/**
* Given a GitHub token, retrieves the authenticated user
* @returns The authenticated user or an error response
*/
export const retrieveGitHubUser = async () => {
const response = await github.get<GitHubUser>("/user");
const { data } = response;
const user: GitHubUser = {
id: data.id,
login: data.login,
avatar_url: data.avatar_url,
company: data.company,
name: data.name,
email: data.email,
};
return user;
};
export const searchPublicRepositories = async (
query: string,
per_page = 5,
sort: "" | "updated" | "stars" | "forks" = "stars",
order: "desc" | "asc" = "desc",
): Promise<GitHubRepository[]> => {
const sanitizedQuery = query.trim();
if (!sanitizedQuery) {
return [];
}
const response = await github.get<{ items: GitHubRepository[] }>(
"/search/repositories",
{
params: {
q: sanitizedQuery,
per_page,
sort,
order,
},
},
);
return response.data.items;
};
export const retrieveLatestGitHubCommit = async (
repository: string,
): Promise<GitHubCommit | null> => {
try {
const response = await github.get<GitHubCommit[]>(
`/repos/${repository}/commits`,
{
params: {
per_page: 1,
},
},
);
return response.data[0] || null;
} catch (error) {
if (!error || typeof error !== "object") {
throw new Error("Unknown error occurred");
}
const axiosError = error as { response?: { status: number } };
if (axiosError.response?.status === 409) {
// Repository is empty, no commits yet
return null;
}
throw new Error(
error instanceof Error ? error.message : "Unknown error occurred",
);
}
};
+4 -46
View File
@@ -9,7 +9,6 @@ import {
GetVSCodeUrlResponse,
AuthenticateResponse,
Conversation,
ResultSet,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
import { ApiSettings } from "#/services/settings";
@@ -223,10 +222,8 @@ class OpenHands {
}
static async getUserConversations(): Promise<Conversation[]> {
const { data } = await openHands.get<ResultSet<Conversation>>(
"/api/conversations?limit=9",
);
return data.results;
const { data } = await openHands.get<Conversation[]>("/api/conversations");
return data;
}
static async deleteUserConversation(conversationId: string): Promise<void> {
@@ -235,9 +232,9 @@ class OpenHands {
static async updateUserConversation(
conversationId: string,
conversation: Partial<Omit<Conversation, "conversation_id">>,
conversation: Partial<Omit<Conversation, "id">>,
): Promise<void> {
await openHands.patch(`/api/conversations/${conversationId}`, conversation);
await openHands.put(`/api/conversations/${conversationId}`, conversation);
}
static async createConversation(
@@ -315,45 +312,6 @@ class OpenHands {
const data = await openHands.post("/api/settings", settings);
return data.status === 200;
}
static async getGitHubUser(): Promise<GitHubUser> {
const response = await openHands.get<GitHubUser>("/api/github/user");
const { data } = response;
const user: GitHubUser = {
id: data.id,
login: data.login,
avatar_url: data.avatar_url,
company: data.company,
name: data.name,
email: data.email,
};
return user;
}
static async getGitHubUserInstallationIds(): Promise<number[]> {
const response = await openHands.get<number[]>("/api/github/installations");
return response.data;
}
static async searchGitHubRepositories(
query: string,
per_page = 5,
): Promise<GitHubRepository[]> {
const response = await openHands.get<{ items: GitHubRepository[] }>(
"/api/github/search/repositories",
{
params: {
query,
per_page,
},
},
);
return response.data.items;
}
}
export default OpenHands;
+5 -11
View File
@@ -1,4 +1,4 @@
import { ProjectStatus } from "#/components/features/conversation-panel/conversation-state-indicator";
import { ProjectState } from "#/components/features/conversation-panel/conversation-state-indicator";
export interface ErrorResponse {
error: string;
@@ -62,14 +62,8 @@ export interface AuthenticateResponse {
export interface Conversation {
conversation_id: string;
title: string;
selected_repository: string | null;
last_updated_at: string;
created_at: string;
status: ProjectStatus;
}
export interface ResultSet<T> {
results: T[];
next_page_id: string | null;
name: string;
repo: string | null;
lastUpdated: string;
state: ProjectState;
}
@@ -61,8 +61,4 @@ export const AGENT_STATUS_MAP: {
message: I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE,
indicator: IndicatorColor.RED,
},
[AgentState.RATE_LIMITED]: {
message: I18nKey.CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE,
indicator: IndicatorColor.YELLOW,
},
};
@@ -8,7 +8,7 @@ export function EmptyBrowserMessage() {
return (
<div className="flex flex-col items-center h-full justify-center">
<IoIosGlobe size={100} />
{t(I18nKey.BROWSER$NO_PAGE_LOADED)}
{t(I18nKey.BROWSER$EMPTY_MESSAGE)}
</div>
);
}
@@ -1,7 +1,5 @@
import React from "react";
import TextareaAutosize from "react-textarea-autosize";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
import { SubmitButton } from "#/components/shared/buttons/submit-button";
import { StopButton } from "#/components/shared/buttons/stop-button";
@@ -10,6 +8,7 @@ interface ChatInputProps {
name?: string;
button?: "submit" | "stop";
disabled?: boolean;
placeholder?: string;
showButton?: boolean;
value?: string;
maxRows?: number;
@@ -27,6 +26,7 @@ export function ChatInput({
name,
button = "submit",
disabled,
placeholder,
showButton = true,
value,
maxRows = 4,
@@ -39,7 +39,6 @@ export function ChatInput({
className,
buttonClassName,
}: ChatInputProps) {
const { t } = useTranslation();
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
const [isDraggingOver, setIsDraggingOver] = React.useState(false);
@@ -95,12 +94,7 @@ export function ChatInput({
};
const handleKeyPress = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (
event.key === "Enter" &&
!event.shiftKey &&
!disabled &&
!event.nativeEvent.isComposing
) {
if (event.key === "Enter" && !event.shiftKey && !disabled) {
event.preventDefault();
handleSubmitMessage();
}
@@ -118,7 +112,7 @@ export function ChatInput({
<TextareaAutosize
ref={textareaRef}
name={name}
placeholder={t(I18nKey.SUGGESTIONS$WHAT_TO_BUILD)}
placeholder={placeholder}
onKeyDown={handleKeyPress}
onChange={handleChange}
onFocus={onFocus}
@@ -154,8 +154,7 @@ export function ChatInterface() {
onStop={handleStop}
isDisabled={
curAgentState === AgentState.LOADING ||
curAgentState === AgentState.AWAITING_USER_CONFIRMATION ||
curAgentState === AgentState.RATE_LIMITED
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
}
mode={curAgentState === AgentState.RUNNING ? "stop" : "submit"}
value={messageToSend ?? undefined}
@@ -1,6 +1,4 @@
import { useTranslation } from "react-i18next";
import { Suggestions } from "#/components/features/suggestions/suggestions";
import { I18nKey } from "#/i18n/declaration";
import BuildIt from "#/icons/build-it.svg?react";
import { SUGGESTIONS } from "#/utils/suggestions";
@@ -9,14 +7,12 @@ interface ChatSuggestionsProps {
}
export function ChatSuggestions({ onSuggestionsClick }: ChatSuggestionsProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col gap-6 h-full px-4 items-center justify-center">
<div className="flex flex-col items-center p-4 bg-neutral-700 rounded-xl w-full">
<BuildIt width={45} height={54} />
<span className="font-semibold text-[20px] leading-6 -tracking-[0.01em] gap-1">
{t(I18nKey.LANDING$TITLE)}
Let&apos;s start building!
</span>
</div>
<Suggestions
@@ -68,6 +68,7 @@ export function InteractiveChatBox({
<ChatInput
disabled={isDisabled}
button={mode}
placeholder="What do you want to build?"
onChange={onChange}
onSubmit={handleSubmit}
onStop={onStop}
@@ -27,10 +27,7 @@ export function AccountSettingsContextMenu({
ref={ref}
className="absolute left-full -top-1 z-10"
>
<ContextMenuListItem
testId="account-settings-button"
onClick={onClickAccountSettings}
>
<ContextMenuListItem onClick={onClickAccountSettings}>
{t(I18nKey.ACCOUNT_SETTINGS$SETTINGS)}
</ContextMenuListItem>
<ContextMenuSeparator />
@@ -18,7 +18,7 @@ export function ContextMenu({
<ul
data-testid={testId}
ref={ref}
className={cn("bg-[#404040] rounded-md w-[140px]", className)}
className={cn("bg-[#404040] rounded-md w-[224px]", className)}
>
{children}
</ul>
@@ -1,6 +1,4 @@
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import PauseIcon from "#/assets/pause";
import PlayIcon from "#/assets/play";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
@@ -11,7 +9,6 @@ import { IGNORE_TASK_STATE_MAP } from "#/ignore-task-state-map.constant";
import { ActionButton } from "#/components/shared/buttons/action-button";
export function AgentControlBar() {
const { t } = useTranslation();
const { send } = useWsClient();
const { curAgentState } = useSelector((state: RootState) => state.agent);
@@ -30,8 +27,8 @@ export function AgentControlBar() {
}
content={
curAgentState === AgentState.PAUSED
? t(I18nKey.AGENT$RESUME_TASK)
: t(I18nKey.AGENT$PAUSE_TASK)
? "Resume the agent task"
: "Pause the current task"
}
action={
curAgentState === AgentState.PAUSED
@@ -1,30 +1,39 @@
import { useParams } from "react-router";
import React from "react";
import posthog from "posthog-js";
import { useSelector } from "react-redux";
import { AgentControlBar } from "./agent-control-bar";
import { AgentStatusBar } from "./agent-status-bar";
import { ProjectMenuCard } from "../project-menu/ProjectMenuCard";
import { useAuth } from "#/context/auth-context";
import { RootState } from "#/store";
import { SecurityLock } from "./security-lock";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
import { ConversationCard } from "../conversation-panel/conversation-card";
import { DownloadModal } from "#/components/shared/download-modal";
interface ControlsProps {
setSecurityOpen: (isOpen: boolean) => void;
showSecurityLock: boolean;
lastCommitData: GitHubCommit | null;
}
export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
const params = useParams();
const { data: conversation } = useUserConversation(
params.conversationId ?? null,
export function Controls({
setSecurityOpen,
showSecurityLock,
lastCommitData,
}: ControlsProps) {
const { gitHubToken } = useAuth();
const { selectedRepository } = useSelector(
(state: RootState) => state.initialQuery,
);
const [downloading, setDownloading] = React.useState(false);
const handleDownloadWorkspace = () => {
posthog.capture("download_workspace_button_clicked");
setDownloading(true);
};
const projectMenuCardData = React.useMemo(
() =>
selectedRepository && lastCommitData
? {
repoName: selectedRepository,
lastCommit: lastCommitData,
avatar: null, // TODO: fetch repo avatar
}
: null,
[selectedRepository, lastCommitData],
);
return (
<div className="flex items-center justify-between">
@@ -37,19 +46,9 @@ export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
)}
</div>
<ConversationCard
variant="compact"
onDownloadWorkspace={handleDownloadWorkspace}
title={conversation?.title ?? ""}
lastUpdatedAt={conversation?.created_at ?? ""}
selectedRepository={conversation?.selected_repository ?? null}
status={conversation?.status}
/>
<DownloadModal
initialPath=""
onClose={() => setDownloading(false)}
isOpen={downloading}
<ProjectMenuCard
isConnectedToGitHub={!!gitHubToken}
githubData={projectMenuCardData}
/>
</div>
);

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