Compare commits

...

63 Commits

Author SHA1 Message Date
openhands b19476a6c5 fix: Pass base commit to send_pull_request to ensure consistent repository state 2025-01-09 05:37:11 +00:00
Graham Neubig 5458ebbd7d Fix issue #6048: Update documentation of recommended models and add deepseek (#6050)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-09 02:39:53 +00:00
Robert Brennan c411a29db4 Move GitHub Token export to backend (#6153)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-08 21:12:46 +00:00
ross 386e04a2ba Fix field deprecation in runloop runtime client (#6152) 2025-01-08 15:18:24 -05:00
sp.wack 62c4bab6ba hotfix(frontend): Prevent a redirect when clicking edit (#6151) 2025-01-08 19:53:24 +00:00
sp.wack e308b6fb6f chore(backend): Update default conversation title logic (#6138) 2025-01-08 22:30:29 +04:00
Engel Nyst 27a660fb6b Make runtime logs optional (#6141) 2025-01-08 19:20:46 +01:00
sp.wack 27d761a1fe chore(frontend): Improve conversation card (#6121) 2025-01-08 21:57:57 +04:00
Robert Brennan 8028e2c2dd fix: handle binary data in GoogleCloudFileStore.write (#6145)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-08 17:36:34 +00:00
dependabot[bot] ff9058e28a chore(deps): bump the version-all group with 6 updates (#6146)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-08 16:03:04 +00:00
dependabot[bot] c45caaef1f chore(deps): bump the version-all group in /frontend with 8 updates (#6144)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-08 15:40:43 +00:00
dependabot[bot] a3c107daa4 chore(deps): bump the docusaurus group in /docs with 7 updates (#6142)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-08 19:27:37 +04:00
Boxuan Li 040839bdd1 Support custom base container image in openhands-app container (#6039) 2025-01-07 21:28:37 -08:00
Engel Nyst aabbbb6c6a Fix duplicate state initialization (#6089) 2025-01-07 23:22:43 +01:00
mamoodi 9747c9e9f8 Some changes to microagents docs and new micro-agents section (#6020) 2025-01-07 16:21:12 -05:00
mamoodi bb85542aca Release 0.19.0 (#6129) 2025-01-07 16:14:22 -05:00
Calvin Smith 6e4ff56934 feature: Condenser Interface and Defaults (#5306)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-08 04:36:30 +08:00
Ray Myers 561f308401 Display connection rejection errors passed to client (#6101)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-01-08 00:26:05 +04:00
Robert Brennan 3733c646af Fix file uploads (#6126) 2025-01-07 19:19:24 +00:00
sp.wack cf0f6e5e38 Improve conversation panel (#6087) 2025-01-07 17:51:03 +00:00
Xingyao Wang 77aa843d53 feat: support running docker runtime stresstest in CI (#6100)
Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk>
2025-01-07 16:55:21 +00:00
Robert Brennan affbc49b08 fix for clone repo (#6116) 2025-01-07 16:42:41 +00:00
sp.wack 9016b9c434 chore(frontend): Fix "confirm delete conversation" modal button colors (#6118) 2025-01-07 16:42:06 +00:00
Robert Brennan e3a96097ba Remove leaked exception (#6086)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-07 16:15:47 +00:00
dependabot[bot] 6a41a3cb4f chore(deps-dev): bump @tanstack/eslint-plugin-query from 5.62.15 to 5.62.16 in /frontend in the eslint group (#6112)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-07 20:06:22 +04:00
sp.wack d1555e093c chore(frontend): Close conversation card context menu when clicking elsewhere (#6111) 2025-01-07 19:46:03 +04:00
dependabot[bot] 5469d5311d chore(deps): bump the version-all group across 1 directory with 11 updates (#6110)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-07 16:28:08 +01:00
tofarr eaf4c610b2 Fix for delete conversation (#6097) 2025-01-07 08:25:45 -07:00
Mark Watson aad7a612c1 fix(frontend): prevent repository name overflow in project menu card (#6091)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-01-07 14:48:06 +00:00
OpenHands 23425c85aa Fix issue #6063: [Bug]: Build error on opencv-python (#6064) 2025-01-07 14:49:59 +09:00
Boxuan Li fb53ae43c0 Add a stress test for eventstream runtime (#6038)
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
2025-01-06 22:36:59 +00:00
Graham Neubig 1f8a0180d3 Add runtime size configuration feature (#5805)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-01-07 06:22:58 +08:00
Robert Brennan 8cfcdd7ba3 Add close method to EventStream (#6093)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: tofarr <tofarr@gmail.com>
2025-01-06 21:59:42 +00:00
tofarr 9515ac5e62 Feat - browser client can now close sessions. (#6088) 2025-01-06 14:26:48 -07:00
Xingyao Wang cebd391b7a fix: better handle bashlex error (#6090) 2025-01-06 20:45:59 +00:00
Robert Brennan 343b86429e Retrieve GitHub IDs more efficiently (#6074)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-06 19:22:52 +00:00
sp.wack 09734467c0 fix(frontend): Only render loading indicator if events are messages (#6082) 2025-01-06 13:03:44 -05:00
Dmitry Kozlov 17d722f3b3 Update README.md (#6076)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-01-06 17:31:19 +00:00
tofarr e310f6b776 Feature - sort conversations by created at (#6079) 2025-01-06 09:07:53 -07:00
dependabot[bot] 5626a22e42 chore(deps-dev): bump @tanstack/eslint-plugin-query from 5.62.9 to 5.62.15 in /frontend in the eslint group (#6077)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-06 14:49:43 +00:00
tofarr cde8aad47f Feat multi conversations wiring (#6011)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-06 07:43:11 -07:00
stefand678 efd0267919 docs: Fix inconsistent comments (#6051) 2025-01-06 13:43:53 +09:00
Xingyao Wang f8735efadf chore: improve error logging for RuntimeError (#6055) 2025-01-05 23:02:42 +00:00
Boxuan Li 00d7395e09 Makefile: Fix poetry version detector (#6058) 2025-01-05 22:43:05 +00:00
மனோஜ்குமார் பழனிச்சாமி 150463e629 feat: Add GPU support (#6042) 2025-01-05 15:28:05 +09:00
Xingyao Wang b7bbf0f5eb fix(agent controller): missing await (#6040) 2025-01-05 04:57:07 +00:00
f-diao d2790c8b21 docs: Update the referenced py filename. (#6043) 2025-01-05 04:10:51 +00:00
Engel Nyst 3d2138d9ce Command line args fixes (#5990) 2025-01-05 02:58:26 +00:00
OpenHands e4cf2eee2d Fix issue #4864: [Bug]: make start-backend results in NotImplementedError: Non-relative patterns are unsupported (#5332)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-05 11:44:23 +09:00
dependabot[bot] 79551e67f6 chore(deps): bump docker/setup-qemu-action from 3.0.0 to 3.2.0 (#5798)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-05 03:43:26 +01:00
Xingyao Wang f5f988e552 fix(agent controller): state.metrics is missing on exception (#6036) 2025-01-05 01:08:47 +00:00
Talut Salako 0c58f469b4 fix: improve how llm models option (#6026) 2025-01-05 00:25:45 +00:00
Xingyao Wang 56d7dccec9 fix(runtime): replace send_request with _send_action_server_request (#6035) 2025-01-04 23:38:34 +00:00
Graham Neubig 411b63159f fix: Use _send_action_server_request in send_action_for_execution (#5951)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-05 08:13:18 +09:00
OpenHands 5ca0beadfb Fix issue #5995: [Resolver] Resolver's summary suggests UNRESOLVED due to "no human reviewer" (#5996)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-01-05 05:49:38 +09:00
Xingyao Wang aaff3dd075 fix(llm): cost metrics calculation for unsupport litellm prefix (#6022) 2025-01-04 18:09:13 +00:00
sai krishna rohith k ef2053011d feat: Added RateLimitError status on UI and Agent state (#5910) 2025-01-04 12:07:07 -05:00
siu e6499a68f6 fix(frontend): Prevent message submission during IME composition (#6025) 2025-01-04 10:41:48 +00:00
Ryan H. Tran 33cb1d5f3c chore: upgrade openhands-aci to 0.1.6 (#6023) 2025-01-04 07:53:18 +00:00
Graham Neubig 5bdebac741 Add git patch info to guess_success prompt (#5950)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-04 10:56:50 +09:00
Robert Brennan 510c1644dd Add bytes support to FileStore write operations (#6019)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-03 16:30:25 -07:00
Xingyao Wang ec70af9412 refactor: Replace pexpect with libtmux in BashSession (#4881)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-01-04 05:22:13 +08:00
Robert Brennan 761a574b09 Small style changes to repo picker (#6013)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-01-03 20:44:30 +00:00
217 changed files with 8412 additions and 4922 deletions
+2
View File
@@ -36,6 +36,8 @@ 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,6 +29,8 @@ 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.0.0
uses: docker/setup-qemu-action@v3.2.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.0.0
uses: docker/setup-qemu-action@v3.2.0
with:
image: tonistiigi/binfmt:latest
- name: Login to GHCR
+6 -3
View File
@@ -259,16 +259,19 @@ jobs:
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
PYTHONPATH: ""
run: |
cd /tmp && BASE_COMMIT=$(cd repo && git rev-parse HEAD) && \
if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then
cd /tmp && python -m openhands.resolver.send_pull_request \
python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \
--pr-type draft \
--reviewer ${{ github.actor }} | tee pr_result.txt && \
--reviewer ${{ github.actor }} \
--base-commit "$BASE_COMMIT" | tee pr_result.txt && \
grep "draft created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
else
cd /tmp && python -m openhands.resolver.send_pull_request \
python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \
--pr-type branch \
--base-commit "$BASE_COMMIT" \
--send-on-failure | tee branch_result.txt && \
grep "branch created" branch_result.txt | sed 's/.*\///g; s/.expand=1//g' > branch_name.txt
fi
+2
View File
@@ -31,6 +31,8 @@ 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,6 +30,8 @@ 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 [PRs](https://github.com/All-Hands-AI/OpenHands/pulls), with the evaluation process varying based on the type of change:
We're generally happy to consider all pull requests 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, 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:
Once the setup is complete, this command starts both the backend and frontend servers, 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, 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.
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.
### 7. Help
Need assistance or information on available targets and commands? The help command provides all the necessary guidance to ensure a smooth experience with OpenHands.
Need help or info on available targets and commands? Use the help command for all the guidance you need 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.18-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.19-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]} -ge 1 ] && [ $${POETRY_VERSION_ARRAY[1]} -ge 8 ]; then \
if [ $${POETRY_VERSION_ARRAY[0]} -gt 1 ] || ([ $${POETRY_VERSION_ARRAY[0]} -eq 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 "$(shell pwd)/workspace"
@poetry run uvicorn openhands.server.listen:app --host $(BACKEND_HOST) --port $(BACKEND_PORT) --reload --reload-exclude "./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.18-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-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.18
docker.all-hands.dev/all-hands-ai/openhands:0.19
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
+1 -1
View File
@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.18-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.19-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+16
View File
@@ -198,6 +198,16 @@ 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 = ""
@@ -210,6 +220,12 @@ model = "gpt-4o"
# 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.18-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.19-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
@@ -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.18-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-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.18 \
docker.all-hands.dev/all-hands-ai/openhands:0.19 \
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.18-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-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.18 \
docker.all-hands.dev/all-hands-ai/openhands:0.19 \
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.18-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-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.18
docker.all-hands.dev/all-hands-ai/openhands:0.19
```
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.18-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
@@ -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.18-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-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.18 \
docker.all-hands.dev/all-hands-ai/openhands:0.19 \
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.18-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-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.18 \
docker.all-hands.dev/all-hands-ai/openhands:0.19 \
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.18-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-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.18
docker.all-hands.dev/all-hands-ai/openhands:0.19
```
你也可以在可脚本化的[无头模式](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.18-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
+2 -2
View File
@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-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.18 \
docker.all-hands.dev/all-hands-ai/openhands:0.19 \
python -m openhands.core.cli
```
+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.18-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-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.18 \
docker.all-hands.dev/all-hands-ai/openhands:0.19 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+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.18-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-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.18
docker.all-hands.dev/all-hands-ai/openhands:0.19
```
You'll find OpenHands running at http://localhost:3000!
+5 -14
View File
@@ -5,23 +5,14 @@ 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. 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.
recommendations for model selection. Our latest benchmarking results can be found in [this spreadsheet](https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0).
Based on these findings and community feedback, the following models have been verified to work reasonably well with OpenHands:
- claude-3-5-sonnet (recommended)
- gpt-4 / gpt-4o
- llama-3.1-405b
- deepseek-v2.5
- anthropic/claude-3-5-sonnet-20241022 (recommended)
- anthropic/claude-3-5-haiku-20241022
- deepseek/deepseek-chat
- gpt-4o
:::warning
OpenHands will issue many prompts to the LLM you configure. Most of these LLMs cost money, so be sure to set spending
@@ -1,17 +1,31 @@
# Micro-Agents
# Public 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.
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:
Public micro-agents are defined in markdown files under the
[`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) 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
### Integration
Public 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.
## Available Public Micro-Agents
For more information about specific micro-agents, refer to their individual documentation files in
the [`micro-agents`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents) directory.
### GitHub Agent
**File**: `github.md`
@@ -29,6 +43,14 @@ Key features:
- 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`
@@ -38,9 +60,15 @@ Specializes in handling npm package management with specific focus on:
- Automated confirmation handling using Unix 'yes' command.
- Package installation automation.
### Custom Micro-Agents
Usage Example:
You can create your own micro-agents by adding new markdown files to the micro-agents directory.
```bash
yes | npm install package-name
```
### Custom Public Micro-Agents
You can create your own public micro-agents by adding new markdown files to the `microagents/knowledge/` directory.
Each file should follow this structure:
```markdown
@@ -55,43 +83,29 @@ triggers:
Instructions and capabilities for the micro-agent...
```
## Best Practices
## Working With Public Micro-Agents
When working with micro-agents:
When working with public 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
## Contributing a Public Micro-Agent
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.
Best practices for creating public micro-agents:
## Example Usage
- **Clear Scope**: Keep the micro-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 micro-agent interacts with other components.
```bash
# GitHub agent example
git checkout -b feature-branch
git commit -m "Add new feature"
git push origin feature-branch
To contribute a new micro-agent to OpenHands:
# NPM agent example
yes | npm install package-name
```
### 1. Plan the Public Micro-Agent
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:
Before creating a public 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?
@@ -99,11 +113,11 @@ Before creating a micro-agent, consider:
### 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).
Create a new markdown file in `microagents/knowledge/` with a descriptive name (e.g., `docker.md` for a Docker-focused agent).
### 3. Required Components
Your micro-agent file must include:
The micro-agent file must include:
- **Front Matter**: YAML metadata at the start of the file:
```markdown
@@ -133,15 +147,7 @@ Examples of usage:
[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
### 4. Testing the Public Micro-Agent
Before submitting:
- Test the agent with various prompts.
@@ -149,7 +155,14 @@ Before submitting:
- Ensure instructions are clear and comprehensive.
- Check for potential conflicts with existing agents.
### 6. Example Implementation
### 5. Submission Process
Submit a pull request with:
- The new micro-agent file.
- Updated documentation if needed.
- Description of the agent's purpose and capabilities.
### Example Public Micro-Agent Implementation
Here's a template for a new micro-agent:
@@ -197,14 +210,5 @@ Remember to:
- 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,10 +1,12 @@
# Customizing Agent Behavior
# Repository Micro-Agents
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.
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
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.
@@ -39,7 +41,8 @@ Guidelines:
### Customizing Prompts
When working with a repository:
You may also add customized prompts to the `.openhands/microagents/repo.md` file when working with a repository.
These could:
- **Reference Project Standards**: Mention specific coding standards or patterns used in your project.
- **Include Context**: Reference relevant documentation or existing implementations.
@@ -54,14 +57,10 @@ 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.
- **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.
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 -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.18-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
+582 -740
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.6.3",
"@docusaurus/plugin-content-pages": "^3.6.3",
"@docusaurus/preset-classic": "^3.6.3",
"@docusaurus/theme-mermaid": "^3.6.3",
"@docusaurus/core": "^3.7.0",
"@docusaurus/plugin-content-pages": "^3.7.0",
"@docusaurus/preset-classic": "^3.7.0",
"@docusaurus/theme-mermaid": "^3.7.0",
"@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.6.3",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.5.1",
"typescript": "~5.7.2"
},
+15 -9
View File
@@ -23,15 +23,21 @@ const sidebars: SidebarsConfig = {
id: 'usage/prompting/prompting-best-practices',
},
{
type: 'doc',
label: 'Customization',
id: 'usage/prompting/customization',
},
{
type: 'doc',
label: 'Microagents',
id: 'usage/prompting/microagents',
},
type: 'category',
label: 'Micro-Agents',
items: [
{
type: 'doc',
label: 'Public',
id: 'usage/prompting/microagents-public',
},
{
type: 'doc',
label: 'Repository',
id: 'usage/prompting/microagents-repo',
},
],
}
],
},
{
-1
View File
@@ -123,7 +123,6 @@ 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,7 +137,6 @@ 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)
@@ -164,8 +163,7 @@ def complete_runtime(
logger.info(f'Running get ground truth cmd: {script_name}')
action = CmdRunAction(
command=f'chmod +x ./{script_name} && ./{script_name}',
keep_prompt=False,
command=f'chmod +x ./{script_name} && ./{script_name}'
)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
@@ -145,10 +145,7 @@ def complete_runtime(
)
logger.info(f'Running test file: {script_name}')
action = CmdRunAction(
command=f'python3 -m unittest {script_name}',
keep_prompt=False,
)
action = CmdRunAction(command=f'python3 -m unittest {script_name}')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
+2 -4
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}', keep_prompt=False)
action = CmdRunAction(command=f'cat {generated_path}')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
@@ -223,9 +223,7 @@ def complete_runtime(
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
action = CmdRunAction(
command='cat /testing_files/results_biocoder.json', keep_prompt=False
)
action = CmdRunAction(command='cat /testing_files/results_biocoder.json')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
if obs.exit_code == 0:
-1
View File
@@ -127,7 +127,6 @@ 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
}
+2 -8
View File
@@ -268,10 +268,7 @@ def initialize_runtime(
runtime.copy_to(db_file, '/workspace')
# Check the database is copied
action = CmdRunAction(
command='cd /workspace && ls -l',
keep_prompt=False,
)
action = CmdRunAction(command='cd /workspace && ls -l')
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
@@ -300,10 +297,7 @@ def complete_runtime(
instance_id = instance.instance_id.replace('/', '__')
path = os.path.join('/workspace', f'{instance_id}.py')
action = CmdRunAction(
command=f'cat {path}',
keep_prompt=False,
)
action = CmdRunAction(command=f'cat {path}')
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -71,7 +71,6 @@ 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
}
@@ -98,7 +97,6 @@ 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
}
@@ -125,7 +123,6 @@ 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,9 +171,7 @@ 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', keep_prompt=False
)
action = CmdRunAction(command=f'cat /workspace/{_get_instance_id(instance)}.py')
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}', keep_prompt=False)
action = CmdRunAction(command=f'cat {eval_script}')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
if obs.exit_code == 0:
@@ -121,10 +121,7 @@ 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',
keep_prompt=False,
)
action = CmdRunAction(command='cd /workspace/benchmark/datasets && ls')
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
@@ -154,10 +151,7 @@ def complete_runtime(
assert obs.exit_code == 0
action = CmdRunAction(
command=f'cat pred_programs/{instance.pred_program_name}',
keep_prompt=False,
)
action = CmdRunAction(command=f'cat pred_programs/{instance.pred_program_name}')
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 app.py --server.port 8501 --server.address 0.0.0.0
streamlit run app.py --server.port 8501 --server.address 0.0.0.0
```
Then you can access the SWE-Bench trajectory visualizer at `localhost:8501`.
+15 -6
View File
@@ -98,6 +98,7 @@ 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.
@@ -146,6 +147,16 @@ 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
@@ -177,7 +188,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, keep_prompt=False)
action = CmdRunAction(command=exec_command)
action.timeout = 600
obs = runtime.run_action(action)
assert isinstance(obs, CmdOutputObservation)
@@ -200,9 +211,7 @@ 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 $!', keep_prompt=False
)
action = CmdRunAction(command=f'/tmp/eval.sh > {log_file} 2>&1 & echo $!')
action.timeout = 60 # Short timeout just to get the process ID
obs = runtime.run_action(action)
@@ -224,7 +233,7 @@ def process_instance(
instance['test_result']['report']['test_timeout'] = True
break
check_action = CmdRunAction(
command=f'ps -p {pid} > /dev/null; echo $?', keep_prompt=False
command=f'ps -p {pid} > /dev/null; echo $?'
)
check_action.timeout = 60
check_obs = runtime.run_action(check_action)
@@ -242,7 +251,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}', keep_prompt=False)
cat_action = CmdRunAction(command=f'cat {log_file}')
cat_action.timeout = 300
cat_obs = runtime.run_action(cat_action)
+16 -4
View File
@@ -15,6 +15,7 @@ from evaluation.utils.shared import (
EvalOutput,
assert_and_raise,
codeact_user_response,
get_metrics,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
@@ -148,6 +149,7 @@ 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
@@ -282,6 +284,16 @@ 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)
@@ -337,8 +349,7 @@ def complete_runtime(
git_patch = None
while n_retries < 5:
action = CmdRunAction(
command=f'git diff --no-color --cached {instance["base_commit"]}',
keep_prompt=False,
command=f'git diff --no-color --cached {instance["base_commit"]}'
)
action.timeout = 600 + 100 * n_retries
logger.info(action, extra={'msg_type': 'ACTION'})
@@ -385,7 +396,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),
2, # hardcode maximum resource factor to 2
8,
)
logger.warning(
f'This is the second attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
@@ -439,7 +450,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 = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Save the output
output = EvalOutput(
@@ -535,4 +546,5 @@ 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,6 +20,13 @@ 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))
@@ -50,7 +57,7 @@ def convert_history_to_str(history):
return ret
def write_row_to_md_file(row):
def write_row_to_md_file(row, instance_id_to_test_result):
if 'git_patch' in row:
model_patch = row['git_patch']
elif 'test_result' in row and 'git_patch' in row['test_result']:
@@ -58,8 +65,21 @@ def write_row_to_md_file(row):
else:
raise ValueError(f'Row {row} does not have a git_patch')
if 'report' in row:
resolved = row['report'].get('resolved', False)
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)
else:
resolved = None
@@ -84,5 +104,18 @@ def write_row_to_md_file(row):
f.write('## Model Patch\n')
f.write(f'{process_git_patch(model_patch)}\n')
f.write('## Test Output\n')
f.write(str(test_output))
oh_format.progress_apply(write_row_to_md_file, axis=1)
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
)
@@ -111,6 +111,11 @@ 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
@@ -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', keep_prompt=False)
action = CmdRunAction(command='cat /workspace/bad.txt')
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', keep_prompt=False)
action = CmdRunAction(command='mkdir -p /workspace')
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', keep_prompt=False)
action = CmdRunAction(command='cat /workspace/hello.sh')
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', keep_prompt=False)
action = CmdRunAction(command='bash /workspace/hello.sh')
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', keep_prompt=False)
action = CmdRunAction(command='mkdir -p /workspace')
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', keep_prompt=False)
action = CmdRunAction(command='cat /workspace/test.txt')
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', keep_prompt=False)
action = CmdRunAction(command='cat /workspace/test.txt')
obs = runtime.run_action(action)
if obs.exit_code != 0:
@@ -10,31 +10,29 @@ class Test(BaseIntegrationTest):
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
action = CmdRunAction(command='mkdir -p /workspace', keep_prompt=False)
action = CmdRunAction(command='mkdir -p /workspace')
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', keep_prompt=False)
action = CmdRunAction(command='git init')
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', keep_prompt=False
)
action = CmdRunAction(command='echo \'print("hello world")\' > hello.py')
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', keep_prompt=False)
action = CmdRunAction(command='git add hello.py')
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', keep_prompt=False)
action = CmdRunAction(command='cat /workspace/hello.py')
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
@@ -43,7 +41,7 @@ class Test(BaseIntegrationTest):
)
# check if the staging area is empty
action = CmdRunAction(command='git status', keep_prompt=False)
action = CmdRunAction(command='git status')
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', keep_prompt=False)
action = CmdRunAction(command='mkdir -p /workspace')
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', keep_prompt=False)
action = CmdRunAction(command='mkdir -p /tmp/server')
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
@@ -101,8 +101,7 @@ class Test(BaseIntegrationTest):
# create README.md
action = CmdRunAction(
command='cd /tmp/server && nohup python3 -m http.server 8000 &',
keep_prompt=False,
command='cd /tmp/server && nohup python3 -m http.server 8000 &'
)
obs = runtime.run_action(action)
+27
View File
@@ -17,6 +17,10 @@ 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,
@@ -33,6 +37,7 @@ 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):
@@ -45,11 +50,17 @@ 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):
@@ -57,6 +68,11 @@ 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)
@@ -192,6 +208,7 @@ 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('@', '-')
@@ -222,6 +239,9 @@ 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}')
@@ -551,3 +571,10 @@ 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
@@ -218,4 +218,30 @@ 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,6 +3,7 @@ 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();
@@ -17,11 +18,11 @@ describe("ConversationCard", () => {
render(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const expectedDate = `${formatTimeDelta(new Date("2021-10-01T12:00:00Z"))} ago`;
@@ -33,53 +34,34 @@ describe("ConversationCard", () => {
within(card).getByText(expectedDate);
});
it("should render the repo if available", () => {
it("should render the selectedRepository if available", () => {
const { rerender } = render(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(
screen.queryByTestId("conversation-card-repo"),
screen.queryByTestId("conversation-card-selected-repository"),
).not.toBeInTheDocument();
rerender(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo="org/repo"
lastUpdated="2021-10-01T12:00:00Z"
isActive
title="Conversation 1"
selectedRepository="org/selectedRepository"
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
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();
screen.getByTestId("conversation-card-selected-repository");
});
it("should toggle a context menu when clicking the ellipsis button", async () => {
@@ -87,11 +69,11 @@ describe("ConversationCard", () => {
render(
<ConversationCard
onDelete={onDelete}
onClick={onClick}
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
@@ -111,12 +93,12 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
@@ -131,21 +113,23 @@ describe("ConversationCard", () => {
expect(onDelete).toHaveBeenCalled();
});
test("clicking the repo should not trigger the onClick handler", async () => {
test("clicking the selectedRepository should not trigger the onClick handler", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo="org/repo"
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository="org/selectedRepository"
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const repo = screen.getByTestId("conversation-card-repo");
await user.click(repo);
const selectedRepository = screen.getByTestId(
"conversation-card-selected-repository",
);
await user.click(selectedRepository);
expect(onClick).not.toHaveBeenCalled();
});
@@ -154,16 +138,24 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="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 ");
@@ -171,21 +163,24 @@ 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}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
await clickOnEditButton(user);
const title = screen.getByTestId("conversation-card-title");
await user.clear(title);
@@ -199,12 +194,12 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
@@ -218,12 +213,12 @@ describe("ConversationCard", () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
@@ -239,36 +234,36 @@ describe("ConversationCard", () => {
});
describe("state indicator", () => {
it("should render the 'cold' indicator by default", () => {
it("should render the 'STOPPED' indicator by default", () => {
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
screen.getByTestId("cold-indicator");
screen.getByTestId("STOPPED-indicator");
});
it("should render the other indicators when provided", () => {
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
name="Conversation 1"
repo={null}
lastUpdated="2021-10-01T12:00:00Z"
state="warm"
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
status="RUNNING"
/>,
);
expect(screen.queryByTestId("cold-indicator")).not.toBeInTheDocument();
screen.getByTestId("warm-indicator");
expect(screen.queryByTestId("STOPPED-indicator")).not.toBeInTheDocument();
screen.getByTestId("RUNNING-indicator");
});
});
});
@@ -6,15 +6,23 @@ 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(<ConversationPanel onClose={onCloseMock} />, {
render(<RouterStub />, {
wrapper: ({ children }) => (
<AuthProvider>
<QueryClientProvider client={new QueryClient(config)}>
@@ -52,6 +60,8 @@ 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);
});
@@ -169,13 +179,15 @@ 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", {
name: "Conversation 1 Renamed",
title: "Conversation 1 Renamed",
});
});
@@ -196,6 +208,8 @@ 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();
@@ -217,51 +231,4 @@ 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();
});
});
});
@@ -0,0 +1,12 @@
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);
};
@@ -14,7 +14,8 @@ describe("GitHubRepositorySelector", () => {
<GitHubRepositorySelector
onInputChange={onInputChangeMock}
onSelect={onSelectMock}
repositories={[]}
publicRepositories={[]}
userRepositories={[]}
/>,
);
@@ -36,7 +37,8 @@ describe("GitHubRepositorySelector", () => {
<GitHubRepositorySelector
onInputChange={onInputChangeMock}
onSelect={onSelectMock}
repositories={[]}
publicRepositories={[]}
userRepositories={[]}
/>,
);
@@ -67,7 +69,8 @@ describe("GitHubRepositorySelector", () => {
<GitHubRepositorySelector
onInputChange={onInputChangeMock}
onSelect={onSelectMock}
repositories={[]}
publicRepositories={[]}
userRepositories={[]}
/>,
);
@@ -4,7 +4,7 @@ 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_CONVO_UI_IS_ENABLED } from "#/utils/constants";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
const renderSidebar = () => {
const RouterStub = createRoutesStub([
@@ -18,7 +18,7 @@ const renderSidebar = () => {
};
describe("Sidebar", () => {
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
it.skipIf(!MULTI_CONVERSATION_UI)(
"should have the conversation panel open by default",
() => {
renderSidebar();
@@ -26,7 +26,7 @@ describe("Sidebar", () => {
},
);
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
it.skipIf(!MULTI_CONVERSATION_UI)(
"should toggle the conversation panel",
async () => {
const user = userEvent.setup();
@@ -0,0 +1,35 @@
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");
});
});
@@ -0,0 +1,45 @@
import { screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { createRoutesStub } from "react-router";
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");
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "123",
});
const RouterStub = createRoutesStub([
{
Component: () => (
<SettingsForm
settings={DEFAULT_SETTINGS}
models={[]}
agents={[]}
securityAnalyzers={[]}
onClose={() => {}}
/>
),
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 () => {
renderWithProviders(<RouterStub />);
const advancedSwitch = screen.getByRole("switch", {
name: "SETTINGS_FORM$ADVANCED_OPTIONS_LABEL",
});
fireEvent.click(advancedSwitch);
await screen.findByText("SETTINGS_FORM$RUNTIME_SIZE_LABEL");
});
});
@@ -0,0 +1,30 @@
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'
});
});
});
+7 -6
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_CONVO_UI_IS_ENABLED } from "#/utils/constants";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
describe("App", () => {
const RouteStub = createRoutesStub([
@@ -35,7 +35,7 @@ describe("App", () => {
await screen.findByTestId("app-route");
});
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
it.skipIf(!MULTI_CONVERSATION_UI)(
"should call endSession if the user does not have permission to view conversation",
async () => {
const errorToastSpy = vi.spyOn(toast, "error");
@@ -59,10 +59,11 @@ describe("App", () => {
getConversationSpy.mockResolvedValue({
conversation_id: "9999",
lastUpdated: "",
name: "",
repo: "",
state: "cold",
last_updated_at: "",
created_at: "",
title: "",
selected_repository: "",
status: "STOPPED",
});
const { rerender } = renderWithProviders(
<RouteStub initialEntries={["/conversation/9999"]} />,
+419 -623
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.18.0",
"version": "0.19.0",
"private": true,
"type": "module",
"engines": {
@@ -8,32 +8,32 @@
},
"dependencies": {
"@monaco-editor/react": "^4.7.0-rc.0",
"@nextui-org/react": "^2.6.10",
"@nextui-org/react": "^2.6.11",
"@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.62.12",
"@tanstack/react-query": "^5.63.0",
"@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.0",
"i18next": "^24.2.1",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-http-backend": "^3.0.1",
"isbot": "^5.1.19",
"isbot": "^5.1.20",
"jose": "^5.9.4",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.203.3",
"posthog-js": "^1.205.0",
"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.1",
"react-markdown": "^9.0.3",
"react-redux": "^9.2.0",
"react-router": "^7.1.1",
"react-syntax-highlighter": "^15.6.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.15",
"@tanstack/eslint-plugin-query": "^5.62.9",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.62.16",
"@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.2",
"@types/react": "^19.0.3",
"@types/react-dom": "^19.0.2",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
+1 -6
View File
@@ -110,16 +110,11 @@ export const searchPublicRepositories = async (
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,
q: query,
per_page,
sort,
order,
+7 -4
View File
@@ -9,6 +9,7 @@ import {
GetVSCodeUrlResponse,
AuthenticateResponse,
Conversation,
ResultSet,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
import { ApiSettings } from "#/services/settings";
@@ -222,8 +223,10 @@ class OpenHands {
}
static async getUserConversations(): Promise<Conversation[]> {
const { data } = await openHands.get<Conversation[]>("/api/conversations");
return data;
const { data } = await openHands.get<ResultSet<Conversation>>(
"/api/conversations?limit=9",
);
return data.results;
}
static async deleteUserConversation(conversationId: string): Promise<void> {
@@ -232,9 +235,9 @@ class OpenHands {
static async updateUserConversation(
conversationId: string,
conversation: Partial<Omit<Conversation, "id">>,
conversation: Partial<Omit<Conversation, "conversation_id">>,
): Promise<void> {
await openHands.put(`/api/conversations/${conversationId}`, conversation);
await openHands.patch(`/api/conversations/${conversationId}`, conversation);
}
static async createConversation(
+11 -5
View File
@@ -1,4 +1,4 @@
import { ProjectState } from "#/components/features/conversation-panel/conversation-state-indicator";
import { ProjectStatus } from "#/components/features/conversation-panel/conversation-state-indicator";
export interface ErrorResponse {
error: string;
@@ -62,8 +62,14 @@ export interface AuthenticateResponse {
export interface Conversation {
conversation_id: string;
name: string;
repo: string | null;
lastUpdated: string;
state: ProjectState;
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;
}
@@ -61,4 +61,8 @@ 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,
},
};
@@ -94,7 +94,12 @@ export function ChatInput({
};
const handleKeyPress = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === "Enter" && !event.shiftKey && !disabled) {
if (
event.key === "Enter" &&
!event.shiftKey &&
!disabled &&
!event.nativeEvent.isComposing
) {
event.preventDefault();
handleSubmitMessage();
}
@@ -154,7 +154,8 @@ export function ChatInterface() {
onStop={handleStop}
isDisabled={
curAgentState === AgentState.LOADING ||
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
curAgentState === AgentState.AWAITING_USER_CONFIRMATION ||
curAgentState === AgentState.RATE_LIMITED
}
mode={curAgentState === AgentState.RUNNING ? "stop" : "submit"}
value={messageToSend ?? undefined}
@@ -18,7 +18,7 @@ export function ContextMenu({
<ul
data-testid={testId}
ref={ref}
className={cn("bg-[#404040] rounded-md w-[224px]", className)}
className={cn("bg-[#404040] rounded-md w-[140px]", className)}
>
{children}
</ul>
@@ -25,10 +25,14 @@ export function ConfirmDeleteModal({
<div className="flex flex-col gap-2 w-full">
<ModalButton
onClick={onConfirm}
className="bg-[#4465DB]"
className="bg-danger font-bold"
text="Confirm"
/>
<ModalButton onClick={onCancel} className="bg-danger" text="Cancel" />
<ModalButton
onClick={onCancel}
className="bg-neutral-500 font-bold"
text="Cancel"
/>
</div>
</ModalBody>
</ModalBackdrop>
@@ -0,0 +1,32 @@
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
import { ContextMenu } from "../context-menu/context-menu";
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
interface ConversationCardContextMenuProps {
onClose: () => void;
onDelete: (event: React.MouseEvent<HTMLButtonElement>) => void;
onEdit: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
export function ConversationCardContextMenu({
onClose,
onDelete,
onEdit,
}: ConversationCardContextMenuProps) {
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
return (
<ContextMenu
ref={ref}
testId="context-menu"
className="left-full float-right"
>
<ContextMenuListItem testId="delete-button" onClick={onDelete}>
Delete
</ContextMenuListItem>
<ContextMenuListItem testId="edit-button" onClick={onEdit}>
Edit Title
</ContextMenuListItem>
</ContextMenu>
);
}
@@ -2,33 +2,33 @@ import React from "react";
import { formatTimeDelta } from "#/utils/format-time-delta";
import { ConversationRepoLink } from "./conversation-repo-link";
import {
ProjectState,
ProjectStatus,
ConversationStateIndicator,
} from "./conversation-state-indicator";
import { ContextMenu } from "../context-menu/context-menu";
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
import { EllipsisButton } from "./ellipsis-button";
import { ConversationCardContextMenu } from "./conversation-card-context-menu";
interface ProjectCardProps {
onClick: () => void;
interface ConversationCardProps {
onDelete: () => void;
onChangeTitle: (title: string) => void;
name: string;
repo: string | null;
lastUpdated: string; // ISO 8601
state?: ProjectState;
isActive: boolean;
title: string;
selectedRepository: string | null;
lastUpdatedAt: string; // ISO 8601
status?: ProjectStatus;
}
export function ConversationCard({
onClick,
onDelete,
onChangeTitle,
name,
repo,
lastUpdated,
state = "cold",
}: ProjectCardProps) {
isActive,
title,
selectedRepository,
lastUpdatedAt,
status = "STOPPED",
}: ConversationCardProps) {
const [contextMenuVisible, setContextMenuVisible] = React.useState(false);
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
const inputRef = React.useRef<HTMLInputElement>(null);
const handleBlur = () => {
@@ -38,64 +38,89 @@ export function ConversationCard({
inputRef.current!.value = trimmed;
} else {
// reset the value if it's empty
inputRef.current!.value = name;
inputRef.current!.value = title;
}
setTitleMode("view");
};
const handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
event.currentTarget.blur();
}
};
const handleInputClick = (event: React.MouseEvent<HTMLInputElement>) => {
event.preventDefault();
event.stopPropagation();
};
const handleDelete = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
onDelete();
};
const handleEdit = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
setTitleMode("edit");
setContextMenuVisible(false);
};
React.useEffect(() => {
if (titleMode === "edit") {
inputRef.current?.focus();
}
}, [titleMode]);
return (
<div
data-testid="conversation-card"
onClick={onClick}
className="h-[100px] w-full px-[18px] py-4 border-b border-neutral-600"
className="h-[100px] w-full px-[18px] py-4 border-b border-neutral-600 cursor-pointer"
>
<div className="flex items-center justify-between">
<input
ref={inputRef}
data-testid="conversation-card-title"
onClick={handleInputClick}
onBlur={handleBlur}
type="text"
defaultValue={name}
className="text-sm leading-6 font-semibold bg-transparent"
/>
<div className="flex items-center gap-2 w-full">
{isActive && <span className="w-2 h-2 bg-blue-500 rounded-full" />}
<input
ref={inputRef}
disabled={titleMode === "view"}
data-testid="conversation-card-title"
onClick={handleInputClick}
onBlur={handleBlur}
onKeyUp={handleKeyUp}
type="text"
defaultValue={title}
className="text-sm leading-6 font-semibold bg-transparent w-full"
/>
</div>
<div className="flex items-center gap-2 relative">
<ConversationStateIndicator state={state} />
<ConversationStateIndicator status={status} />
<EllipsisButton
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
setContextMenuVisible((prev) => !prev);
}}
/>
{contextMenuVisible && (
<ContextMenu testId="context-menu" className="absolute left-full">
<ContextMenuListItem
testId="delete-button"
onClick={handleDelete}
>
Delete
</ContextMenuListItem>
</ContextMenu>
)}
</div>
</div>
{repo && (
{contextMenuVisible && (
<ConversationCardContextMenu
onClose={() => setContextMenuVisible(false)}
onDelete={handleDelete}
onEdit={handleEdit}
/>
)}
{selectedRepository && (
<ConversationRepoLink
repo={repo}
selectedRepository={selectedRepository}
onClick={(e) => e.stopPropagation()}
/>
)}
<p className="text-xs text-neutral-400">
<time>{formatTimeDelta(new Date(lastUpdated))} ago</time>
<time>{formatTimeDelta(new Date(lastUpdatedAt))} ago</time>
</p>
</div>
);
@@ -0,0 +1,22 @@
import ReactDOM from "react-dom";
interface ConversationPanelWrapperProps {
isOpen: boolean;
}
export function ConversationPanelWrapper({
isOpen,
children,
}: React.PropsWithChildren<ConversationPanelWrapperProps>) {
if (!isOpen) return null;
const portalTarget = document.getElementById("root-outlet");
if (!portalTarget) return null;
return ReactDOM.createPortal(
<div className="absolute h-full w-full left-0 top-0 z-20 bg-black/80 rounded-xl">
{children}
</div>,
portalTarget,
);
}
@@ -1,14 +1,14 @@
import React from "react";
import { useLocation, useNavigate, useParams } from "react-router";
import { NavLink, useParams } from "react-router";
import { ConversationCard } from "./conversation-card";
import { useUserConversations } from "#/hooks/query/use-user-conversations";
import { useDeleteConversation } from "#/hooks/mutation/use-delete-conversation";
import { ConfirmDeleteModal } from "./confirm-delete-modal";
import { NewConversationButton } from "./new-conversation-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { useUpdateConversation } from "#/hooks/mutation/use-update-conversation";
import { useEndSession } from "#/hooks/use-end-session";
import { ExitConversationModal } from "./exit-conversation-modal";
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
interface ConversationPanelProps {
onClose: () => void;
@@ -16,10 +16,8 @@ interface ConversationPanelProps {
export function ConversationPanel({ onClose }: ConversationPanelProps) {
const { conversationId: cid } = useParams();
const navigate = useNavigate();
const location = useLocation();
const endSession = useEndSession();
const ref = useClickOutsideElement<HTMLDivElement>(onClose);
const [confirmDeleteModalVisible, setConfirmDeleteModalVisible] =
React.useState(false);
@@ -60,26 +58,17 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
if (oldTitle !== newTitle)
updateConversation({
id: conversationId,
conversation: { name: newTitle },
conversation: { title: newTitle },
});
};
const handleClickCard = (conversationId: string) => {
navigate(`/conversations/${conversationId}`);
onClose();
};
return (
<div
ref={ref}
data-testid="conversation-panel"
className="w-[350px] h-full border border-neutral-700 bg-neutral-800 rounded-xl"
className="w-[350px] h-full border border-neutral-700 bg-neutral-800 rounded-xl overflow-y-auto"
>
<div className="pt-4 px-4 flex items-center justify-between">
{location.pathname.startsWith("/conversation") && (
<NewConversationButton
onClick={() => setConfirmExitConversationModalVisible(true)}
/>
)}
{isFetching && <LoadingSpinner size="small" />}
</div>
{error && (
@@ -93,18 +82,25 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
</div>
)}
{conversations?.map((project) => (
<ConversationCard
<NavLink
key={project.conversation_id}
onClick={() => handleClickCard(project.conversation_id)}
onDelete={() => handleDeleteProject(project.conversation_id)}
onChangeTitle={(title) =>
handleChangeTitle(project.conversation_id, project.name, title)
}
name={project.name}
repo={project.repo}
lastUpdated={project.lastUpdated}
state={project.state}
/>
to={`/conversations/${project.conversation_id}`}
onClick={onClose}
>
{({ isActive }) => (
<ConversationCard
isActive={isActive}
onDelete={() => handleDeleteProject(project.conversation_id)}
onChangeTitle={(title) =>
handleChangeTitle(project.conversation_id, project.title, title)
}
title={project.title}
selectedRepository={project.selected_repository}
lastUpdatedAt={project.last_updated_at}
status={project.status}
/>
)}
</NavLink>
))}
{confirmDeleteModalVisible && (
@@ -1,21 +1,21 @@
interface ConversationRepoLinkProps {
repo: string;
selectedRepository: string;
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
}
export function ConversationRepoLink({
repo,
selectedRepository,
onClick,
}: ConversationRepoLinkProps) {
return (
<a
data-testid="conversation-card-repo"
href={`https://github.com/${repo}`}
data-testid="conversation-card-selected-repository"
href={`https://github.com/${selectedRepository}`}
target="_blank noopener noreferrer"
onClick={onClick}
className="text-xs text-neutral-400 hover:text-neutral-200"
>
{repo}
{selectedRepository}
</a>
);
}
@@ -1,39 +1,25 @@
import ColdIcon from "./state-indicators/cold.svg?react";
import CoolingIcon from "./state-indicators/cooling.svg?react";
import FinishedIcon from "./state-indicators/finished.svg?react";
import RunningIcon from "./state-indicators/running.svg?react";
import WaitingIcon from "./state-indicators/waiting.svg?react";
import WarmIcon from "./state-indicators/warm.svg?react";
type SVGIcon = React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
export type ProjectState =
| "cold"
| "cooling"
| "finished"
| "running"
| "waiting"
| "warm";
export type ProjectStatus = "RUNNING" | "STOPPED";
const INDICATORS: Record<ProjectState, SVGIcon> = {
cold: ColdIcon,
cooling: CoolingIcon,
finished: FinishedIcon,
running: RunningIcon,
waiting: WaitingIcon,
warm: WarmIcon,
const INDICATORS: Record<ProjectStatus, SVGIcon> = {
STOPPED: ColdIcon,
RUNNING: RunningIcon,
};
interface ConversationStateIndicatorProps {
state: ProjectState;
status: ProjectStatus;
}
export function ConversationStateIndicator({
state,
status,
}: ConversationStateIndicatorProps) {
const StateIcon = INDICATORS[state];
const StateIcon = INDICATORS[status];
return (
<div data-testid={`${state}-indicator`}>
<div data-testid={`${status}-indicator`}>
<StateIcon />
</div>
);
@@ -1,46 +1,48 @@
import React from "react";
import { Autocomplete, AutocompleteItem } from "@nextui-org/react";
import {
Autocomplete,
AutocompleteItem,
AutocompleteSection,
} from "@nextui-org/react";
import { useDispatch } from "react-redux";
import posthog from "posthog-js";
import { setSelectedRepository } from "#/state/initial-query-slice";
import { useConfig } from "#/hooks/query/use-config";
interface GitHubRepositoryWithPublic extends GitHubRepository {
is_public?: boolean;
}
import { sanitizeQuery } from "#/utils/sanitize-query";
interface GitHubRepositorySelectorProps {
onInputChange: (value: string) => void;
onSelect: () => void;
repositories: GitHubRepositoryWithPublic[];
userRepositories: GitHubRepository[];
publicRepositories: GitHubRepository[];
}
export function GitHubRepositorySelector({
onInputChange,
onSelect,
repositories,
userRepositories,
publicRepositories,
}: GitHubRepositorySelectorProps) {
const { data: config } = useConfig();
const [selectedKey, setSelectedKey] = React.useState<string | null>(null);
const allRepositories: GitHubRepository[] = [
...publicRepositories.filter(
(repo) => !publicRepositories.find((r) => r.id === repo.id),
),
...userRepositories,
];
const dispatch = useDispatch();
const handleRepoSelection = (id: string | null) => {
const repo = repositories.find((r) => r.id.toString() === id);
if (!repo) return;
if (repo.id === -1000) {
window.open(
`https://github.com/apps/${config?.APP_SLUG}/installations/new`,
"_blank",
);
return;
const repo = allRepositories.find((r) => r.id.toString() === id);
if (repo) {
dispatch(setSelectedRepository(repo.full_name));
posthog.capture("repository_selected");
onSelect();
setSelectedKey(id);
}
dispatch(setSelectedRepository(repo.full_name));
posthog.capture("repository_selected");
onSelect();
setSelectedKey(id);
};
const handleClearSelection = () => {
@@ -55,8 +57,8 @@ export function GitHubRepositorySelector({
name="repo"
aria-label="GitHub Repository"
placeholder="Select a GitHub project"
isVirtualized={false}
selectedKey={selectedKey}
items={repositories}
inputProps={{
classNames: {
inputWrapper:
@@ -65,27 +67,61 @@ export function GitHubRepositorySelector({
}}
onSelectionChange={(id) => handleRepoSelection(id?.toString() ?? null)}
onInputChange={onInputChange}
clearButtonProps={{ onPress: handleClearSelection }}
clearButtonProps={{ onClick: handleClearSelection }}
listboxProps={{
emptyContent,
}}
defaultFilter={(textValue, inputValue) =>
!inputValue ||
sanitizeQuery(textValue).includes(sanitizeQuery(inputValue))
}
>
{(item) => (
<AutocompleteItem
data-testid="github-repo-item"
key={item.id}
value={item.id}
textValue={item.full_name}
>
<div className="flex items-center justify-between">
{item.full_name}
{item.is_public && !!item.stargazers_count && (
<span className="text-xs text-gray-400">
({item.stargazers_count})
{config?.APP_MODE === "saas" &&
config?.APP_SLUG &&
((
<AutocompleteItem key="install">
<a
href={`https://github.com/apps/${config.APP_SLUG}/installations/new`}
target="_blank"
rel="noreferrer noopener"
onClick={(e) => e.stopPropagation()}
>
Add more repositories...
</a>
</AutocompleteItem> // eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any)}
{userRepositories.length > 0 && (
<AutocompleteSection showDivider title="Your Repos">
{userRepositories.map((repo) => (
<AutocompleteItem
data-testid="github-repo-item"
key={repo.id}
value={repo.id}
className="data-[selected=true]:bg-default-100"
textValue={repo.full_name}
>
{repo.full_name}
</AutocompleteItem>
))}
</AutocompleteSection>
)}
{publicRepositories.length > 0 && (
<AutocompleteSection showDivider title="Public Repos">
{publicRepositories.map((repo) => (
<AutocompleteItem
data-testid="github-repo-item"
key={repo.id}
value={repo.id}
className="data-[selected=true]:bg-default-100"
textValue={repo.full_name}
>
{repo.full_name}
<span className="ml-1 text-gray-400">
({repo.stargazers_count || 0})
</span>
)}
</div>
</AutocompleteItem>
</AutocompleteItem>
))}
</AutocompleteSection>
)}
</Autocomplete>
);
@@ -11,7 +11,6 @@ import { useSearchRepositories } from "#/hooks/query/use-search-repositories";
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
import { sanitizeQuery } from "#/utils/sanitize-query";
import { useDebounce } from "#/hooks/use-debounce";
import { useConfig } from "#/hooks/query/use-config";
interface GitHubRepositoriesSuggestionBoxProps {
handleSubmit: () => void;
@@ -29,7 +28,6 @@ export function GitHubRepositoriesSuggestionBox({
const [searchQuery, setSearchQuery] = React.useState<string>("");
const debouncedSearchQuery = useDebounce(searchQuery, 300);
const { data: config } = useConfig();
// TODO: Use `useQueries` to fetch all repositories in parallel
const { data: appRepositories } = useAppRepositories();
const { data: userRepositories } = useUserRepositories();
@@ -37,19 +35,6 @@ export function GitHubRepositoriesSuggestionBox({
sanitizeQuery(debouncedSearchQuery),
);
const saasPlaceholderRepository = React.useMemo(() => {
if (config?.APP_MODE === "saas" && config?.APP_SLUG) {
return [
{
id: -1000,
full_name: "Add more repositories...",
},
];
}
return [];
}, [config]);
const repositories =
userRepositories?.pages.flatMap((page) => page.data) ||
appRepositories?.pages.flatMap((page) => page.data) ||
@@ -74,11 +59,8 @@ export function GitHubRepositoriesSuggestionBox({
<GitHubRepositorySelector
onInputChange={setSearchQuery}
onSelect={handleSubmit}
repositories={[
...saasPlaceholderRepository,
...searchedRepos,
...repositories,
]}
publicRepositories={searchedRepos}
userRepositories={repositories}
/>
) : (
<ModalButton
@@ -16,16 +16,24 @@ export function ProjectMenuDetails({
}: ProjectMenuDetailsProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col">
<div className="flex flex-col min-w-0">
<a
href={`https://github.com/${repoName}`}
target="_blank"
rel="noreferrer noopener"
className="flex items-center gap-2"
className="flex items-center gap-2 min-w-0"
>
{avatar && <img src={avatar} alt="" className="w-4 h-4 rounded-full" />}
<span className="text-sm leading-6 font-semibold">{repoName}</span>
<ExternalLinkIcon width={16} height={16} />
{avatar && (
<img
src={avatar}
alt=""
className="w-4 h-4 rounded-full flex-shrink-0"
/>
)}
<span className="text-sm leading-6 font-semibold truncate flex-1">
{repoName}
</span>
<ExternalLinkIcon width={16} height={16} className="flex-shrink-0" />
</a>
<a
href={lastCommit.html_url}
@@ -1,6 +1,6 @@
import React from "react";
import { useLocation } from "react-router";
import FolderIcon from "#/icons/docs.svg?react";
import { FaListUl } from "react-icons/fa";
import { useDispatch } from "react-redux";
import { useAuth } from "#/context/auth-context";
import { useGitHubUser } from "#/hooks/query/use-github-user";
import { useIsAuthed } from "#/hooks/query/use-is-authed";
@@ -11,16 +11,20 @@ import { ExitProjectButton } from "#/components/shared/buttons/exit-project-butt
import { SettingsButton } from "#/components/shared/buttons/settings-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { AccountSettingsModal } from "#/components/shared/modals/account-settings/account-settings-modal";
import { ExitProjectConfirmationModal } from "#/components/shared/modals/exit-project-confirmation-modal";
import { SettingsModal } from "#/components/shared/modals/settings/settings-modal";
import { useSettingsUpToDate } from "#/context/settings-up-to-date-context";
import { useSettings } from "#/hooks/query/use-settings";
import { ConversationPanel } from "../conversation-panel/conversation-panel";
import { cn } from "#/utils/utils";
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
import { useEndSession } from "#/hooks/use-end-session";
import { setCurrentAgentState } from "#/state/agent-slice";
import { AgentState } from "#/types/agent-state";
import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
import { ConversationPanelWrapper } from "../conversation-panel/conversation-panel-wrapper";
export function Sidebar() {
const location = useLocation();
const dispatch = useDispatch();
const endSession = useEndSession();
const user = useGitHubUser();
const { data: isAuthed } = useIsAuthed();
const { logout } = useAuth();
@@ -30,11 +34,9 @@ export function Sidebar() {
const [accountSettingsModalOpen, setAccountSettingsModalOpen] =
React.useState(false);
const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false);
const [startNewProjectModalIsOpen, setStartNewProjectModalIsOpen] =
const [conversationPanelIsOpen, setConversationPanelIsOpen] =
React.useState(false);
const [conversationPanelIsOpen, setConversationPanelIsOpen] = React.useState(
MULTI_CONVO_UI_IS_ENABLED,
);
React.useEffect(() => {
// If the github token is invalid, open the account settings modal again
@@ -43,6 +45,11 @@ export function Sidebar() {
}
}, [user.isError]);
const handleEndSession = () => {
dispatch(setCurrentAgentState(AgentState.LOADING));
endSession();
};
const handleAccountSettingsModalClose = () => {
// If the user closes the modal without connecting to GitHub,
// we need to log them out to clear the invalid token from the
@@ -51,22 +58,30 @@ export function Sidebar() {
setAccountSettingsModalOpen(false);
};
const handleClickLogo = () => {
if (location.pathname.startsWith("/conversations/"))
setStartNewProjectModalIsOpen(true);
};
const showSettingsModal =
isAuthed && (!settingsAreUpToDate || settingsModalIsOpen);
return (
<>
<aside className="h-[40px] md:h-auto px-1 flex flex-row md:flex-col gap-1 relative">
<aside className="h-[40px] md:h-auto px-1 flex flex-row md:flex-col gap-1">
<nav className="flex flex-row md:flex-col items-center gap-[18px]">
<div className="w-[34px] h-[34px] flex items-center justify-center">
<AllHandsLogoButton onClick={handleClickLogo} />
<div className="w-[34px] h-[34px] flex items-center justify-center mb-7">
<AllHandsLogoButton onClick={handleEndSession} />
</div>
{user.isLoading && <LoadingSpinner size="small" />}
<ExitProjectButton onClick={handleEndSession} />
{MULTI_CONVERSATION_UI && (
<TooltipButton
data-testid="toggle-conversation-panel"
tooltip="Conversations"
ariaLabel="Conversations"
onClick={() => setConversationPanelIsOpen((prev) => !prev)}
>
<FaListUl size={22} />
</TooltipButton>
)}
<DocsButton />
<SettingsButton onClick={() => setSettingsModalIsOpen(true)} />
{!user.isLoading && (
<UserActions
user={
@@ -76,33 +91,14 @@ export function Sidebar() {
onClickAccountSettings={() => setAccountSettingsModalOpen(true)}
/>
)}
<SettingsButton onClick={() => setSettingsModalIsOpen(true)} />
{MULTI_CONVO_UI_IS_ENABLED && (
<button
data-testid="toggle-conversation-panel"
type="button"
onClick={() => setConversationPanelIsOpen((prev) => !prev)}
className={cn(
conversationPanelIsOpen ? "border-b-2 border-[#FFE165]" : "",
)}
>
<FolderIcon width={28} height={28} />
</button>
)}
<DocsButton />
<ExitProjectButton
onClick={() => setStartNewProjectModalIsOpen(true)}
/>
</nav>
{conversationPanelIsOpen && (
<div
className="absolute h-full left-[calc(100%+12px)] top-0 z-20" // 12px padding (sidebar parent)
>
<ConversationPanelWrapper isOpen={conversationPanelIsOpen}>
<ConversationPanel
onClose={() => setConversationPanelIsOpen(false)}
/>
</div>
</ConversationPanelWrapper>
)}
</aside>
@@ -116,11 +112,6 @@ export function Sidebar() {
onClose={() => setSettingsModalIsOpen(false)}
/>
))}
{startNewProjectModalIsOpen && (
<ExitProjectConfirmationModal
onClose={() => setStartNewProjectModalIsOpen(false)}
/>
)}
</>
);
}
@@ -1,8 +1,8 @@
import { Tooltip } from "@nextui-org/react";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import DefaultUserAvatar from "#/icons/default-user.svg?react";
import { cn } from "#/utils/utils";
import { Avatar } from "./avatar";
import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
interface UserAvatarProps {
onClick: () => void;
@@ -11,10 +11,11 @@ interface UserAvatarProps {
}
export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
const buttonContent = (
<button
data-testid="user-avatar"
type="button"
return (
<TooltipButton
testId="user-avatar"
tooltip="Account settings"
ariaLabel="Account settings"
onClick={onClick}
className={cn(
"w-8 h-8 rounded-full flex items-center justify-center border-2 border-gray-200",
@@ -30,12 +31,6 @@ export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
/>
)}
{isLoading && <LoadingSpinner size="small" />}
</button>
);
return (
<Tooltip content="Account settings" closeDelay={100}>
{buttonContent}
</Tooltip>
</TooltipButton>
);
}
@@ -12,7 +12,7 @@ export function AllHandsLogoButton({ onClick }: AllHandsLogoButtonProps) {
ariaLabel="All Hands Logo"
onClick={onClick}
>
<AllHandsLogo width={34} height={23} />
<AllHandsLogo width={44} height={30} />
</TooltipButton>
);
}
@@ -13,7 +13,7 @@ export function ExitProjectButton({ onClick }: ExitProjectButtonProps) {
onClick={onClick}
testId="new-project-button"
>
<NewProjectIcon width={28} height={28} />
<NewProjectIcon width={26} height={26} />
</TooltipButton>
);
}
@@ -1,4 +1,4 @@
import CogTooth from "#/assets/cog-tooth";
import { FaCog } from "react-icons/fa";
import { TooltipButton } from "./tooltip-button";
interface SettingsButtonProps {
@@ -13,7 +13,7 @@ export function SettingsButton({ onClick }: SettingsButtonProps) {
ariaLabel="Settings"
onClick={onClick}
>
<CogTooth />
<FaCog size={24} />
</TooltipButton>
);
}
@@ -1,5 +1,6 @@
import { Tooltip } from "@nextui-org/react";
import { ReactNode } from "react";
import React, { ReactNode } from "react";
import { cn } from "#/utils/utils";
interface TooltipButtonProps {
children: ReactNode;
@@ -8,6 +9,7 @@ interface TooltipButtonProps {
href?: string;
ariaLabel: string;
testId?: string;
className?: React.HTMLAttributes<HTMLButtonElement>["className"];
}
export function TooltipButton({
@@ -17,6 +19,7 @@ export function TooltipButton({
href,
ariaLabel,
testId,
className,
}: TooltipButtonProps) {
const buttonContent = (
<button
@@ -24,7 +27,7 @@ export function TooltipButton({
aria-label={ariaLabel}
data-testid={testId}
onClick={onClick}
className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
className={cn("hover:opacity-80", className)}
>
{children}
</button>
@@ -35,7 +38,7 @@ export function TooltipButton({
href={href}
target="_blank"
rel="noreferrer noopener"
className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
className={cn("hover:opacity-80", className)}
aria-label={ariaLabel}
>
{children}
@@ -45,7 +48,7 @@ export function TooltipButton({
);
return (
<Tooltip content={tooltip} closeDelay={100}>
<Tooltip content={tooltip} closeDelay={100} placement="right">
{content}
</Tooltip>
);
@@ -20,7 +20,7 @@ export function AdvancedOptionSwitch({
<Switch
isDisabled={isDisabled}
name="use-advanced-options"
isSelected={showAdvancedOptions}
defaultSelected={showAdvancedOptions}
onValueChange={setShowAdvancedOptions}
classNames={{
thumb: cn(
@@ -0,0 +1,52 @@
import { useTranslation } from "react-i18next";
import { Select, SelectItem } from "@nextui-org/react";
interface RuntimeSizeSelectorProps {
isDisabled: boolean;
defaultValue?: number;
}
export function RuntimeSizeSelector({
isDisabled,
defaultValue,
}: RuntimeSizeSelectorProps) {
const { t } = useTranslation();
return (
<fieldset className="flex flex-col gap-2">
<label
htmlFor="runtime-size"
className="font-[500] text-[#A3A3A3] text-xs"
>
{t("SETTINGS_FORM$RUNTIME_SIZE_LABEL")}
</label>
<Select
id="runtime-size"
name="runtime-size"
defaultSelectedKeys={[String(defaultValue || 1)]}
isDisabled={isDisabled}
aria-label={t("SETTINGS_FORM$RUNTIME_SIZE_LABEL")}
classNames={{
trigger: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
}}
>
<SelectItem key="1" value={1}>
1x (2 core, 8G)
</SelectItem>
<SelectItem
key="2"
value={2}
isDisabled
classNames={{
description:
"whitespace-normal break-words min-w-[300px] max-w-[300px]",
base: "min-w-[300px] max-w-[300px]",
}}
description="Runtime sizes over 1 are disabled by default, please contact contact@all-hands.dev to get access to larger runtimes."
>
2x (4 core, 16G)
</SelectItem>
</Select>
</fieldset>
);
}
@@ -21,6 +21,9 @@ import { ModalBackdrop } from "../modal-backdrop";
import { ModelSelector } from "./model-selector";
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
import { RuntimeSizeSelector } from "./runtime-size-selector";
import { useConfig } from "#/hooks/query/use-config";
interface SettingsFormProps {
disabled?: boolean;
settings: Settings;
@@ -40,6 +43,7 @@ export function SettingsForm({
}: SettingsFormProps) {
const { mutateAsync: saveSettings } = useSaveSettings();
const endSession = useEndSession();
const { data: config } = useConfig();
const location = useLocation();
const { t } = useTranslation();
@@ -97,6 +101,8 @@ export function SettingsForm({
posthog.capture("settings_saved", {
LLM_MODEL: newSettings.LLM_MODEL,
LLM_API_KEY: newSettings.LLM_API_KEY ? "SET" : "UNSET",
REMOTE_RUNTIME_RESOURCE_FACTOR:
newSettings.REMOTE_RUNTIME_RESOURCE_FACTOR,
});
};
@@ -122,6 +128,8 @@ export function SettingsForm({
}
};
const isSaasMode = config?.APP_MODE === "saas";
return (
<div>
<form
@@ -164,16 +172,21 @@ export function SettingsForm({
isSet={settings.LLM_API_KEY === "SET"}
/>
{showAdvancedOptions && (
<AgentInput
isDisabled={!!disabled}
defaultValue={settings.AGENT}
agents={agents}
/>
)}
{showAdvancedOptions && (
<>
<AgentInput
isDisabled={!!disabled}
defaultValue={settings.AGENT}
agents={agents}
/>
{isSaasMode && (
<RuntimeSizeSelector
isDisabled={!!disabled}
defaultValue={settings.REMOTE_RUNTIME_RESOURCE_FACTOR}
/>
)}
<SecurityAnalyzerInput
isDisabled={!!disabled}
defaultValue={settings.SECURITY_ANALYZER}
@@ -16,7 +16,7 @@ export function SettingsModal({ onClose, settings }: SettingsModalProps) {
<ModalBackdrop onClose={onClose}>
<div
data-testid="ai-config-modal"
className="bg-root-primary w-[384px] p-6 rounded-xl flex flex-col gap-2"
className="bg-root-primary min-w-[384px] max-w-[700px] p-6 rounded-xl flex flex-col gap-2"
>
{aiConfigOptions.error && (
<p className="text-danger text-xs">{aiConfigOptions.error.message}</p>
+54 -18
View File
@@ -2,12 +2,18 @@ import posthog from "posthog-js";
import React from "react";
import { io, Socket } from "socket.io-client";
import EventLogger from "#/utils/event-logger";
import { handleAssistantMessage } from "#/services/actions";
import {
handleAssistantMessage,
handleStatusMessage,
} from "#/services/actions";
import { useRate } from "#/hooks/use-rate";
import { OpenHandsParsedEvent } from "#/types/core";
import { AgentStateChangeObservation } from "#/types/core/observations";
import {
AssistantMessageAction,
UserMessageAction,
} from "#/types/core/actions";
const isOpenHandsMessage = (event: unknown): event is OpenHandsParsedEvent =>
const isOpenHandsEvent = (event: unknown): event is OpenHandsParsedEvent =>
typeof event === "object" &&
event !== null &&
"id" in event &&
@@ -15,10 +21,26 @@ const isOpenHandsMessage = (event: unknown): event is OpenHandsParsedEvent =>
"message" in event &&
"timestamp" in event;
const isAgentStateChangeObservation = (
const isUserMessage = (
event: OpenHandsParsedEvent,
): event is AgentStateChangeObservation =>
"observation" in event && event.observation === "agent_state_changed";
): event is UserMessageAction =>
"source" in event &&
"type" in event &&
event.source === "user" &&
event.type === "message";
const isAssistantMessage = (
event: OpenHandsParsedEvent,
): event is AssistantMessageAction =>
"source" in event &&
"type" in event &&
event.source === "agent" &&
event.type === "message";
const isMessageAction = (
event: OpenHandsParsedEvent,
): event is UserMessageAction | AssistantMessageAction =>
isUserMessage(event) || isAssistantMessage(event);
export enum WsClientProviderStatus {
CONNECTED,
@@ -43,16 +65,28 @@ const WsClientContext = React.createContext<UseWsClient>({
interface WsClientProviderProps {
conversationId: string;
ghToken: string | null;
}
export function updateStatusWhenErrorMessagePresent(data: unknown) {
if (
data &&
typeof data === "object" &&
"message" in data &&
typeof data.message === "string"
) {
handleStatusMessage({
type: "error",
message: data.message,
status_update: true,
});
}
}
export function WsClientProvider({
ghToken,
conversationId,
children,
}: React.PropsWithChildren<WsClientProviderProps>) {
const sioRef = React.useRef<Socket | null>(null);
const ghTokenRef = React.useRef<string | null>(ghToken);
const [status, setStatus] = React.useState(
WsClientProviderStatus.DISCONNECTED,
);
@@ -74,7 +108,7 @@ export function WsClientProvider({
}
function handleMessage(event: Record<string, unknown>) {
if (isOpenHandsMessage(event) && !isAgentStateChangeObservation(event)) {
if (isOpenHandsEvent(event) && isMessageAction(event)) {
messageRateHandler.record(new Date().getTime());
}
setEvents((prevEvents) => [...prevEvents, event]);
@@ -85,7 +119,7 @@ export function WsClientProvider({
handleAssistantMessage(event);
}
function handleDisconnect() {
function handleDisconnect(data: unknown) {
setStatus(WsClientProviderStatus.DISCONNECTED);
const sio = sioRef.current;
if (!sio) {
@@ -93,13 +127,19 @@ export function WsClientProvider({
}
sio.io.opts.query = sio.io.opts.query || {};
sio.io.opts.query.latest_event_id = lastEventRef.current?.id;
updateStatusWhenErrorMessagePresent(data);
}
function handleError() {
posthog.capture("socket_error");
function handleError(data: unknown) {
setStatus(WsClientProviderStatus.DISCONNECTED);
updateStatusWhenErrorMessagePresent(data);
posthog.capture("socket_error");
}
React.useEffect(() => {
lastEventRef.current = null;
}, [conversationId]);
React.useEffect(() => {
if (!conversationId) {
throw new Error("No conversation ID provided");
@@ -118,9 +158,6 @@ export function WsClientProvider({
sio = io(baseUrl, {
transports: ["websocket"],
auth: {
github_token: ghToken || undefined,
},
query,
});
sio.on("connect", handleConnect);
@@ -130,7 +167,6 @@ export function WsClientProvider({
sio.on("disconnect", handleDisconnect);
sioRef.current = sio;
ghTokenRef.current = ghToken;
return () => {
sio.off("connect", handleConnect);
@@ -139,7 +175,7 @@ export function WsClientProvider({
sio.off("connect_failed", handleError);
sio.off("disconnect", handleDisconnect);
};
}, [ghToken, conversationId]);
}, [conversationId]);
React.useEffect(
() => () => {
@@ -13,13 +13,18 @@ export const useCreateConversation = () => {
const { gitHubToken } = useAuth();
const queryClient = useQueryClient();
const { selectedRepository, files } = useSelector(
const { selectedRepository, files, importedProjectZip } = useSelector(
(state: RootState) => state.initialQuery,
);
return useMutation({
mutationFn: (variables: { q?: string }) => {
if (!variables.q?.trim() && !selectedRepository && files.length === 0) {
if (
!variables.q?.trim() &&
!selectedRepository &&
files.length === 0 &&
!importedProjectZip
) {
throw new Error("No query provided");
}
@@ -0,0 +1,16 @@
import { useMutation } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { useConversation } from "#/context/conversation-context";
type UploadFilesArgs = {
files: File[];
};
export const useUploadFiles = () => {
const { conversationId } = useConversation();
return useMutation({
mutationFn: ({ files }: UploadFilesArgs) =>
OpenHands.uploadFiles(conversationId, files),
});
};
@@ -1,11 +1,11 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
export const useUserConversation = (cid: string | null) =>
useQuery({
queryKey: ["user", "conversation", cid],
queryFn: () => OpenHands.getConversation(cid!),
enabled: MULTI_CONVO_UI_IS_ENABLED && !!cid,
enabled: MULTI_CONVERSATION_UI && !!cid,
retry: false,
});
+2
View File
@@ -18,6 +18,8 @@ const getSettingsQueryFn = async () => {
CONFIRMATION_MODE: apiSettings.confirmation_mode,
SECURITY_ANALYZER: apiSettings.security_analyzer,
LLM_API_KEY: apiSettings.llm_api_key,
REMOTE_RUNTIME_RESOURCE_FACTOR:
apiSettings.remote_runtime_resource_factor,
};
}
@@ -3,8 +3,8 @@
import React from "react";
import { useSettingsUpToDate } from "#/context/settings-up-to-date-context";
import {
DEFAULT_SETTINGS,
getCurrentSettingsVersion,
DEFAULT_SETTINGS,
getLocalStorageSettings,
} from "#/services/settings";
import { useSaveSettings } from "./mutation/use-save-settings";

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