Compare commits

...

82 Commits

Author SHA1 Message Date
openhands
61439dfa39 Fix issue #6134: Document how to use custom sandbox container with Docker 2025-01-14 06:24:06 +00:00
Boxuan Li
92b8d55c2d Rename trajectories_path config to save_trajectory_path (#6216)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-14 04:32:45 +00:00
dependabot[bot]
a125b6cd43 chore(deps): bump the version-all group across 1 directory with 6 updates (#6248)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 23:33:09 +01:00
tofarr
01ac207b92 Fix remove dead code (#6249) 2025-01-13 14:15:13 -07:00
Ray Myers
6d015a5dca Don't start conversation runtime without valid API key (#6181) 2025-01-13 22:03:37 +01:00
dependabot[bot]
275512305d chore(deps): bump docker/setup-qemu-action from 3.2.0 to 3.3.0 (#6229)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 21:31:01 +01:00
mamoodi
3a4bc10b29 Release 0.20.0 (#6234)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-01-13 14:58:20 -05:00
sp.wack
bbd31b32f3 chore: Move GH requests to the server (#6217) 2025-01-13 23:12:50 +04:00
Joseph O'Connor
295c6fd629 fix(issue_definitions, issue-success-check.jinja): pass git-patch to issue-success-check (#6243)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-01-13 18:40:15 +00:00
tofarr
5a809c9b53 Feature: User id propagation (#6233) 2025-01-13 18:10:45 +00:00
sp.wack
0b74fd71d9 fix(frontend): Prevent from send a SET API key (#6235) 2025-01-13 17:50:37 +00:00
tofarr
4fa5c329d6 Fix : minor updates to log messages (#6232) 2025-01-13 17:19:51 +00:00
tofarr
5b1dcf83a6 Fix for issue where S3FileStore does not delete directory objects (#6231) 2025-01-13 16:50:58 +00:00
tofarr
b9884f7609 Fixes for minor cases where FDs were not closed (#6228) 2025-01-13 09:15:23 -07:00
dependabot[bot]
99eda0e571 chore(deps-dev): bump the eslint group in /frontend with 2 updates (#6227)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 19:44:10 +04:00
Ryan H. Tran
5832463088 Revert openhands-resolver.yml change in #5972 (#6222) 2025-01-13 15:39:54 +00:00
tofarr
045ec2b95d Fix: Timezone should be UTC (#6225) 2025-01-13 08:24:26 -07:00
tofarr
23473070b9 Revert "Config objects as Pydantic BaseModels (#6176)" (#6214) 2025-01-13 07:36:25 -07:00
mamoodi
63133c0ba9 Document changes for Micro-Agents and some formatting changes (#6155)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-01-13 09:24:10 -05:00
dependabot[bot]
2023fb767f chore(deps): bump the version-all group in /frontend with 2 updates (#6192)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-01-13 11:09:11 +00:00
Ryan H. Tran
23f40a1c01 Enable runtime image build for resolver's experimental feature (#5972) 2025-01-12 17:21:34 -05:00
Calvin Smith
873dddb4e8 Config objects as Pydantic BaseModels (#6176)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-01-12 15:09:45 -05:00
Ryan H. Tran
fe50cd1f9f Upgrade openhands-aci to 0.1.8 (#6123) 2025-01-12 07:26:57 +01:00
Boxuan Li
516e2da520 Custom runtime builder: fix NoEmptyContinuation error (#6211) 2025-01-11 15:58:08 -08:00
jmtatsch
1dd6f544bc Fix #6056 (#6203) 2025-01-11 23:32:12 +01:00
Graham Neubig
40c52feb5b fix: Handle empty lines in patch parser (#6208)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-12 06:43:08 +09:00
Xingyao Wang
f31ccad48b feat: misc bash improvements, set max value for action-exec timeout, retry on requests.ConnectionError (#6175) 2025-01-11 04:36:12 +08:00
Xingyao Wang
828d169b82 refactor: consolidate runtime startup command into an util function (#6199) 2025-01-11 04:27:13 +08:00
dependabot[bot]
a622d27016 chore(deps-dev): bump llama-index-embeddings-huggingface from 0.4.0 to 0.5.0 in the llama group (#6194)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-10 19:59:54 +01:00
sp.wack
5507b131fe hotfix(frontend): Add beta tag to new app tab (#6198) 2025-01-10 17:31:42 +00:00
sp.wack
0f102e4c71 hotfix(frontend): Get bottom right conversation card details even when multi convo is disabled (#6197) 2025-01-10 17:19:37 +00:00
sp.wack
157a1a24f6 fix(frontend): Wait for fetched settings instead of loading default ones (#6193) 2025-01-10 16:54:31 +00:00
dependabot[bot]
fcfbcb64d4 chore(deps): bump the version-all group in /frontend with 5 updates (#6170)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-01-10 13:51:03 +00:00
Xingyao Wang
931792e87a fix: UI terminal output incorrect newline (#6182) 2025-01-10 17:11:06 +04:00
Robert Brennan
ee701eacc2 fix: prevent race condition in session manager during disconnect (#6053)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-09 17:26:53 -07:00
Ray Myers
8907fed78e Provide a clearer error message when settings are missing midsession (#6158) 2025-01-09 19:09:34 +00:00
Robert Brennan
3cc20a2576 remove timeouts on remote runtime (#6171) 2025-01-09 12:39:40 -05:00
dependabot[bot]
01cf0d433c chore(deps): bump the version-all group with 5 updates (#6169)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-09 17:45:16 +01:00
sp.wack
f6bed82ae2 Add port mappings support (#5577)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: tofarr <tofarr@gmail.com>
Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: Robert Brennan <contact@rbren.io>
2025-01-09 15:02:56 +00:00
sp.wack
3eae2e2aca chore(frontend): Optimize requests made to the backend (#6168) 2025-01-09 15:00:26 +00:00
sp.wack
b45fc522c7 feat(frontend): Display current conversation info in the bottom right (#6143) 2025-01-09 14:55:33 +00:00
sp.wack
0d409c8c24 fix(frontend): Prevent saving empty custom model (#6149) 2025-01-09 13:43:39 +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
188 changed files with 7087 additions and 5141 deletions

View File

@@ -56,7 +56,7 @@ jobs:
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
uses: docker/setup-qemu-action@v3.3.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.2.0
uses: docker/setup-qemu-action@v3.3.0
with:
image: tonistiigi/binfmt:latest
- name: Login to GHCR

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

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

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.20-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.

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.

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.20-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.20-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.20
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!

View File

@@ -34,7 +34,7 @@ workspace_base = "./workspace"
# Path to store trajectories, can be a folder or a file
# If it's a folder, the session id will be used as the file name
#trajectories_path="./trajectories"
#save_trajectory_path="./trajectories"
# File store path
#file_store_path = "/tmp/file_store"

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.20-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -1,4 +1,4 @@
#
services:
openhands:
build:
@@ -7,8 +7,8 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.18-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
- "3000:3000"
@@ -16,6 +16,7 @@ services:
- "host.docker.internal:host-gateway"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ~/.openhands-state:/.openhands-state
- ${WORKSPACE_BASE:-$PWD/workspace}:/opt/workspace_base
pull_policy: build
stdin_open: true

View File

@@ -94,7 +94,7 @@ Les options de configuration de base sont définies dans la section `[core]` du
- Description : Désactiver la couleur dans la sortie du terminal
**Trajectoires**
- `trajectories_path`
- `save_trajectory_path`
- Type : `str`
- Valeur par défaut : `"./trajectories"`
- Description : Chemin pour stocker les trajectoires (peut être un dossier ou un fichier). Si c'est un dossier, les trajectoires seront enregistrées dans un fichier nommé avec l'ID de session et l'extension .json, dans ce dossier.

View File

@@ -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.20-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.20 \
python -m openhands.core.cli
```

View File

@@ -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.20-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.20 \
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```

View File

@@ -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.20-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.20-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.20
```
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).

View File

@@ -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.20-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```

View File

@@ -91,7 +91,7 @@
- 描述: 禁用终端输出中的颜色
**轨迹**
- `trajectories_path`
- `save_trajectory_path`
- 类型: `str`
- 默认值: `"./trajectories"`
- 描述: 存储轨迹的路径(可以是文件夹或文件)。如果是文件夹,轨迹将保存在该文件夹中以会话 ID 命名的 .json 文件中。

View File

@@ -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.20-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.20 \
python -m openhands.core.cli
```

View File

@@ -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.20-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.20 \
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```

View File

@@ -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.20-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.20-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.20
```
你也可以在可脚本化的[无头模式](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)。

View File

@@ -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.20-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```

View File

@@ -7,53 +7,11 @@ If you are running in [GUI Mode](https://docs.all-hands.dev/modules/usage/how-to
take precedence.
:::
---
# Table of Contents
- [Core Configuration](#core-configuration)
- [API Keys](#api-keys)
- [Workspace](#workspace)
- [Debugging and Logging](#debugging-and-logging)
- [Session Management](#session-management)
- [Trajectories](#trajectories)
- [File Store](#file-store)
- [Task Management](#task-management)
- [Sandbox Configuration](#sandbox-configuration)
- [Miscellaneous](#miscellaneous)
- [LLM Configuration](#llm-configuration)
- [AWS Credentials](#aws-credentials)
- [API Configuration](#api-configuration)
- [Custom LLM Provider](#custom-llm-provider)
- [Embeddings](#embeddings)
- [Message Handling](#message-handling)
- [Model Selection](#model-selection)
- [Retrying](#retrying)
- [Advanced Options](#advanced-options)
- [Agent Configuration](#agent-configuration)
- [Microagent Configuration](#microagent-configuration)
- [Memory Configuration](#memory-configuration)
- [LLM Configuration](#llm-configuration-2)
- [ActionSpace Configuration](#actionspace-configuration)
- [Microagent Usage](#microagent-usage)
- [Sandbox Configuration](#sandbox-configuration)
- [Execution](#execution)
- [Container Image](#container-image)
- [Networking](#networking)
- [Linting and Plugins](#linting-and-plugins)
- [Dependencies and Environment](#dependencies-and-environment)
- [Evaluation](#evaluation)
- [Security Configuration](#security-configuration)
- [Confirmation Mode](#confirmation-mode)
- [Security Analyzer](#security-analyzer)
---
## Core Configuration
The core configuration options are defined in the `[core]` section of the `config.toml` file.
**API Keys**
### API Keys
- `e2b_api_key`
- Type: `str`
- Default: `""`
@@ -69,7 +27,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `""`
- Description: API token secret for Modal
**Workspace**
### Workspace
- `workspace_base`
- Type: `str`
- Default: `"./workspace"`
@@ -80,7 +38,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `"/tmp/cache"`
- Description: Cache directory path
**Debugging and Logging**
### Debugging and Logging
- `debug`
- Type: `bool`
- Default: `false`
@@ -91,13 +49,13 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `false`
- Description: Disable color in terminal output
**Trajectories**
- `trajectories_path`
### Trajectories
- `save_trajectory_path`
- Type: `str`
- Default: `"./trajectories"`
- Description: Path to store trajectories (can be a folder or a file). If it's a folder, the trajectories will be saved in a file named with the session id name and .json extension, in that folder.
**File Store**
### File Store
- `file_store_path`
- Type: `str`
- Default: `"/tmp/file_store"`
@@ -128,7 +86,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `[".*"]`
- Description: List of allowed file extensions for uploads
**Task Management**
### Task Management
- `max_budget_per_task`
- Type: `float`
- Default: `0.0`
@@ -139,7 +97,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `100`
- Description: Maximum number of iterations
**Sandbox Configuration**
### Sandbox Configuration
- `workspace_mount_path_in_sandbox`
- Type: `str`
- Default: `"/workspace"`
@@ -155,7 +113,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `""`
- Description: Path to rewrite the workspace mount path to. You can usually ignore this, it refers to special cases of running inside another container.
**Miscellaneous**
### Miscellaneous
- `run_as_openhands`
- Type: `bool`
- Default: `true`
@@ -182,7 +140,7 @@ The LLM (Large Language Model) configuration options are defined in the `[llm]`
To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LLM_NUM_RETRIES`.
**AWS Credentials**
### AWS Credentials
- `aws_access_key_id`
- Type: `str`
- Default: `""`
@@ -198,7 +156,7 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `""`
- Description: AWS secret access key
**API Configuration**
### API Configuration
- `api_key`
- Type: `str`
- Default: `None`
@@ -224,13 +182,13 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `0.0`
- Description: Cost per output token
**Custom LLM Provider**
### Custom LLM Provider
- `custom_llm_provider`
- Type: `str`
- Default: `""`
- Description: Custom LLM provider
**Embeddings**
### Embeddings
- `embedding_base_url`
- Type: `str`
- Default: `""`
@@ -246,7 +204,7 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `"local"`
- Description: Embedding model to use
**Message Handling**
### Message Handling
- `max_message_chars`
- Type: `int`
- Default: `30000`
@@ -262,13 +220,13 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `0`
- Description: Maximum number of output tokens
**Model Selection**
### Model Selection
- `model`
- Type: `str`
- Default: `"claude-3-5-sonnet-20241022"`
- Description: Model to use
**Retrying**
### Retrying
- `num_retries`
- Type: `int`
- Default: `8`
@@ -289,7 +247,7 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `2.0`
- Description: Multiplier for exponential backoff calculation
**Advanced Options**
### Advanced Options
- `drop_params`
- Type: `bool`
- Default: `false`
@@ -329,13 +287,13 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
The agent configuration options are defined in the `[agent]` and `[agent.<agent_name>]` sections of the `config.toml` file.
**Microagent Configuration**
### Microagent Configuration
- `micro_agent_name`
- Type: `str`
- Default: `""`
- Description: Name of the micro agent to use for this agent
**Memory Configuration**
### Memory Configuration
- `memory_enabled`
- Type: `bool`
- Default: `false`
@@ -346,13 +304,13 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
- Default: `3`
- Description: The maximum number of threads indexing at the same time for embeddings
**LLM Configuration**
### LLM Configuration
- `llm_config`
- Type: `str`
- Default: `'your-llm-config-group'`
- Description: The name of the LLM config to use
**ActionSpace Configuration**
### ActionSpace Configuration
- `function_calling`
- Type: `bool`
- Default: `true`
@@ -373,7 +331,7 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
- Default: `false`
- Description: Whether Jupyter is enabled in the action space
**Microagent Usage**
### Microagent Usage
- `use_microagents`
- Type: `bool`
- Default: `true`
@@ -390,7 +348,7 @@ The sandbox configuration options are defined in the `[sandbox]` section of the
To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-e SANDBOX_TIMEOUT`.
**Execution**
### Execution
- `timeout`
- Type: `int`
- Default: `120`
@@ -401,19 +359,19 @@ To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-
- Default: `1000`
- Description: Sandbox user ID
**Container Image**
### Container Image
- `base_container_image`
- Type: `str`
- Default: `"nikolaik/python-nodejs:python3.12-nodejs22"`
- Description: Container image to use for the sandbox
**Networking**
### Networking
- `use_host_network`
- Type: `bool`
- Default: `false`
- Description: Use host network
**Linting and Plugins**
### Linting and Plugins
- `enable_auto_lint`
- Type: `bool`
- Default: `false`
@@ -424,7 +382,7 @@ To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-
- Default: `true`
- Description: Whether to initialize plugins
**Dependencies and Environment**
### Dependencies and Environment
- `runtime_extra_deps`
- Type: `str`
- Default: `""`
@@ -435,7 +393,7 @@ To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-
- Default: `{}`
- Description: Environment variables to set at the launch of the runtime
**Evaluation**
### Evaluation
- `browsergym_eval_env`
- Type: `str`
- Default: `""`
@@ -447,13 +405,13 @@ The security configuration options are defined in the `[security]` section of th
To use these with the docker command, pass in `-e SECURITY_<option>`. Example: `-e SECURITY_CONFIRMATION_MODE`.
**Confirmation Mode**
### Confirmation Mode
- `confirmation_mode`
- Type: `bool`
- Default: `false`
- Description: Enable confirmation mode
**Security Analyzer**
### Security Analyzer
- `security_analyzer`
- Type: `str`
- Default: `""`

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.20-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.20 \
python -m openhands.core.cli
```
@@ -58,7 +58,7 @@ Here are some examples of CLI commands and their expected outputs:
### Example 1: Simple Task
```bash
How can I help? >> Write a Python script that prints "Hello, World!"
>> Write a Python script that prints "Hello, World!"
```
Expected Output:
@@ -72,7 +72,7 @@ Expected Output:
### Example 2: Bash Command
```bash
How can I help? >> Create a directory named "test_dir"
>> Create a directory named "test_dir"
```
Expected Output:
@@ -86,7 +86,7 @@ Expected Output:
### Example 3: Error Handling
```bash
How can I help? >> Delete a non-existent file
>> Delete a non-existent file
```
Expected Output:

View File

@@ -58,3 +58,32 @@ sandbox_base_container_image="custom-image"
### Run
Run OpenHands by running ```make run``` in the top level directory.
## Using Docker
If you're using OpenHands via Docker, you can specify a custom sandbox container by setting the `SANDBOX_BASE_CONTAINER_IMAGE` environment variable when running the Docker container.
Here's an example command:
```bash
docker run -it --pull=always \
-e SANDBOX_BASE_CONTAINER_IMAGE=custom-image \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.20
```
Make sure to replace `custom-image` with the name of your custom Docker image. The image should be available in your local Docker environment before running this command.
> Note: This feature is available from version 0.20.0 onwards.
### Command Explanation
- `-e SANDBOX_BASE_CONTAINER_IMAGE=custom-image`: Specifies your custom sandbox container image
- `-v /var/run/docker.sock:/var/run/docker.sock`: Allows OpenHands to create and manage Docker containers
- `-v ~/.openhands-state:/.openhands-state`: Persists OpenHands state between runs
- `--add-host host.docker.internal:host-gateway`: Required for Docker-in-Docker communication

View File

@@ -76,18 +76,18 @@ When using OpenHands in online mode, the GitHub OAuth flow:
Common issues and solutions:
1. **Token Not Recognized**:
- **Token Not Recognized**:
- Ensure the token is properly saved in settings.
- Check that the token hasn't expired.
- Verify the token has the required scopes.
- Try regenerating the token.
2. **Organization Access Denied**:
- **Organization Access Denied**:
- Check if SSO is required but not enabled.
- Verify organization membership.
- Contact organization admin if token policies are blocking access.
3. **Verifying Token Works**:
- **Verifying Token Works**:
- The app will show a green checkmark if the token is valid.
- Try accessing a repository to confirm permissions.
- Check the browser console for any error messages.

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.20-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.20 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -1,16 +0,0 @@
# Persisting Session Data
Using the standard Development Workflow, the session data is stored in memory. Currently, if OpenHands' service is restarted,
previous sessions become invalid (a new secret is generated) and thus not recoverable.
## How to Persist Session Data
### Development Workflow
In the `config.toml` file, specify the following:
```
[core]
...
file_store="local"
file_store_path="/absolute/path/to/openhands/cache/directory"
jwt_secret="secretpass"
```

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.20-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.20-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.20
```
You'll find OpenHands running at http://localhost:3000!

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

View File

@@ -1,67 +0,0 @@
# Customizing Agent Behavior
OpenHands can be customized to work more effectively with specific repositories by providing repository-specific context and guidelines. This section explains how to optimize OpenHands for your project.
## Repository Configuration
You can customize OpenHands' behavior for your repository by creating a `.openhands` directory in your repository's root. At minimum, it should contain the file
`.openhands/microagents/repo.md`, which includes instructions that will
be given to the agent every time it works with this repository.
We suggest including the following information:
- **Repository Overview**: A brief description of your project's purpose and architecture.
- **Directory Structure**: Key directories and their purposes.
- **Development Guidelines**: Project-specific coding standards and practices.
- **Testing Requirements**: How to run tests and what types of tests are required.
- **Setup Instructions**: Steps needed to build and run the project.
### Example Repository Configuration
Example `.openhands/microagents/repo.md` file:
```
Repository: MyProject
Description: A web application for task management
Directory Structure:
- src/: Main application code
- tests/: Test files
- docs/: Documentation
Setup:
- Run `npm install` to install dependencies
- Use `npm run dev` for development
- Run `npm test` for testing
Guidelines:
- Follow ESLint configuration
- Write tests for all new features
- Use TypeScript for new code
```
### Customizing Prompts
When working with a repository:
- **Reference Project Standards**: Mention specific coding standards or patterns used in your project.
- **Include Context**: Reference relevant documentation or existing implementations.
- **Specify Testing Requirements**: Include project-specific testing requirements in your prompts.
Example customized prompt:
```
Add a new task completion feature to src/components/TaskList.tsx following our existing component patterns.
Include unit tests in tests/components/ and update the documentation in docs/features/.
The component should use our shared styling from src/styles/components.
```
### Best Practices for Repository Customization
- **Keep Instructions Updated**: Regularly update your `.openhands` directory as your project evolves.
- **Be Specific**: Include specific paths, patterns, and requirements unique to your project.
- **Document Dependencies**: List all tools and dependencies required for development.
- **Include Examples**: Provide examples of good code patterns from your project.
- **Specify Conventions**: Document naming conventions, file organization, and code style preferences.
By customizing OpenHands for your repository, you'll get more accurate and consistent results that align with your project's standards and requirements.
## Other Microagents
You can create other instructions in the `.openhands/microagents/` directory
that will be sent to the agent if a particular keyword is found, like `test`, `frontend`, or `migration`. See [Micro-Agents](microagents.md) for more information.

View File

@@ -0,0 +1,36 @@
# Microagents Overview
Microagents are specialized prompts that enhance OpenHands with domain-specific knowledge, repository-specific context
and task-specific workflows. They help by providing expert guidance, automating common tasks, and ensuring
consistent practices across projects.
## Microagent Types
Currently OpenHands supports the following types of microagents:
* [Repository Microagents](./microagents-repo): Repository-specific context and guidelines for OpenHands.
* [Public Microagents](./microagents-public): General guidelines triggered by keywords for all OpenHands users.
When OpenHands works with a repository, it:
1. Loads repository-specific instructions from `.openhands/microagents/` if present in the repository.
2. Loads general guidelines triggered by keywords in conversations.
See current [Public Microagents](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge).
## Microagent Format
All microagents use markdown files with YAML frontmatter that have special instructions to help OpenHands accomplish
tasks:
```
---
name: <Name of the microagent>
type: <MicroAgent type>
version: <MicroAgent version>
agent: <The agent type (Typically CodeActAgent)>
triggers:
- <Optional keywords triggering the microagent. If triggers are removed, it will always be included>
---
<Markdown with any special guidelines, instructions, and prompts that OpenHands should follow.
Check out the specific documentation for each microagent on best practices for more information.>
```

View File

@@ -0,0 +1,153 @@
# Public Microagents
## Overview
Public microagents are specialized guidelines triggered by keywords for all OpenHands users.
They are defined in markdown files under the
[`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) directory.
Public microagents:
- Monitor incoming commands for their trigger words.
- Activate when relevant triggers are detected.
- Apply their specialized knowledge and capabilities.
- Follow their specific guidelines and restrictions.
## Current Public Microagents
For more information about specific microagents, refer to their individual documentation files in
the [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/) directory.
### GitHub Agent
**File**: `github.md`
**Triggers**: `github`, `git`
The GitHub agent specializes in GitHub API interactions and repository management. It:
- Has access to a `GITHUB_TOKEN` for API authentication.
- Follows strict guidelines for repository interactions.
- Handles branch management and pull requests.
- Uses the GitHub API instead of web browser interactions.
Key features:
- Branch protection (prevents direct pushes to main/master)
- Automated PR creation
- Git configuration management
- API-first approach for GitHub operations
Usage Example:
```bash
git checkout -b feature-branch
git commit -m "Add new feature"
git push origin feature-branch
```
### NPM Agent
**File**: `npm.md`
**Triggers**: `npm`
Specializes in handling npm package management with specific focus on:
- Non-interactive shell operations.
- Automated confirmation handling using Unix 'yes' command.
- Package installation automation.
Usage Example:
```bash
yes | npm install package-name
```
## Contributing a Public Microagent
You can create your own public microagents by adding new markdown files to the
[`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/) directory.
### Public Microagents Best Practices
- **Clear Scope**: Keep the microagent focused on a specific domain or task.
- **Explicit Instructions**: Provide clear, unambiguous guidelines.
- **Useful Examples**: Include practical examples of common use cases.
- **Safety First**: Include necessary warnings and constraints.
- **Integration Awareness**: Consider how the microagent interacts with other components.
### Steps to Contribute a Public Microagent
#### 1. Plan the Public Microagent
Before creating a public microagent, consider:
- What specific problem or use case will it address?
- What unique capabilities or knowledge should it have?
- What trigger words make sense for activating it?
- What constraints or guidelines should it follow?
#### 2. Create File
Create a new markdown file in [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/)
with a descriptive name (e.g., `docker.md` for a Docker-focused agent).
Update the file with the required frontmatter [according to the required format](./microagents-overview#microagent-format)
and the required specialized guidelines while following the [best practices above](#public-microagents-best-practices).
#### 3. Testing the Public Microagent
- Test the agent with various prompts.
- Verify trigger words activate the agent correctly.
- Ensure instructions are clear and comprehensive.
- Check for potential conflicts with existing agents.
#### 4. Submission Process
Submit a pull request with:
- The new microagent file.
- Updated documentation if needed.
- Description of the agent's purpose and capabilities.
### Example Public Microagent Implementation
Here's a template for a new microagent:
```markdown
---
name: docker
agent: CodeActAgent
triggers:
- docker
- container
---
You are responsible for Docker container management and Dockerfile creation.
Key responsibilities:
1. Create and modify Dockerfiles
2. Manage container lifecycle
3. Handle Docker Compose configurations
Guidelines:
- Always use official base images when possible
- Include necessary security considerations
- Follow Docker best practices for layer optimization
Examples:
1. Creating a Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]
2. Docker Compose usage:
version: '3'
services:
web:
build: .
ports:
- "3000:3000"
Remember to:
- Validate Dockerfile syntax
- Check for security vulnerabilities
- Optimize for build time and image size
```
See the [current public micro-agents](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) for
more examples.

View File

@@ -0,0 +1,68 @@
# Repository Microagents
## Overview
OpenHands can be customized to work more effectively with specific repositories by providing repository-specific context
and guidelines. This section explains how to optimize OpenHands for your project.
## Creating a Repository Micro-Agent
You can customize OpenHands' behavior for your repository by creating a `.openhands/microagents/` directory in your repository's root.
At minimum it should contain the file
`.openhands/microagents/repo.md`, which includes instructions that will
be given to the agent every time it works with this repository.
### Repository Microagents Best Practices
- **Keep Instructions Updated**: Regularly update your `.openhands/microagents/` directory as your project evolves.
- **Be Specific**: Include specific paths, patterns, and requirements unique to your project.
- **Document Dependencies**: List all tools and dependencies required for development.
- **Include Examples**: Provide examples of good code patterns from your project.
- **Specify Conventions**: Document naming conventions, file organization, and code style preferences.
### Steps to Create a Repository Microagent
#### 1. Plan the Repository Microagent
When creating a repository-specific micro-agent, we suggest including the following information:
- **Repository Overview**: A brief description of your project's purpose and architecture.
- **Directory Structure**: Key directories and their purposes.
- **Development Guidelines**: Project-specific coding standards and practices.
- **Testing Requirements**: How to run tests and what types of tests are required.
- **Setup Instructions**: Steps needed to build and run the project.
#### 2. Create File
Create a file in your repository under `.openhands/microagents/` (Example: `.openhands/microagents/repo.md`)
Update the file with the required frontmatter [according to the required format](./microagents-overview#microagent-format)
and the required specialized guidelines for your repository.
### Example Repository Microagent
```
---
name: repo
type: repo
agent: CodeActAgent
---
Repository: MyProject
Description: A web application for task management
Directory Structure:
- src/: Main application code
- tests/: Test files
- docs/: Documentation
Setup:
- Run `npm install` to install dependencies
- Use `npm run dev` for development
- Run `npm test` for testing
Guidelines:
- Follow ESLint configuration
- Write tests for all new features
- Use TypeScript for new code
If adding a new component in src/components, always add appropriate unit tests in tests/components/.
```

View File

@@ -1,210 +0,0 @@
# Micro-Agents
OpenHands uses specialized micro-agents to handle specific tasks and contexts efficiently. These micro-agents are small, focused components that provide specialized behavior and knowledge for particular scenarios.
## Overview
Micro-agents are defined in markdown files under the `openhands/agenthub/codeact_agent/micro/` directory. Each micro-agent is configured with:
- A unique name.
- The agent type (typically CodeActAgent).
- Trigger keywords that activate the agent.
- Specific instructions and capabilities.
## Available Micro-Agents
### GitHub Agent
**File**: `github.md`
**Triggers**: `github`, `git`
The GitHub agent specializes in GitHub API interactions and repository management. It:
- Has access to a `GITHUB_TOKEN` for API authentication.
- Follows strict guidelines for repository interactions.
- Handles branch management and pull requests.
- Uses the GitHub API instead of web browser interactions.
Key features:
- Branch protection (prevents direct pushes to main/master)
- Automated PR creation
- Git configuration management
- API-first approach for GitHub operations
### NPM Agent
**File**: `npm.md`
**Triggers**: `npm`
Specializes in handling npm package management with specific focus on:
- Non-interactive shell operations.
- Automated confirmation handling using Unix 'yes' command.
- Package installation automation.
### Custom Micro-Agents
You can create your own micro-agents by adding new markdown files to the micro-agents directory.
Each file should follow this structure:
```markdown
---
name: agent_name
agent: CodeActAgent
triggers:
- trigger_word1
- trigger_word2
---
Instructions and capabilities for the micro-agent...
```
## Best Practices
When working with micro-agents:
- **Use Appropriate Triggers**: Ensure your commands include the relevant trigger words to activate the correct micro-agent.
- **Follow Agent Guidelines**: Each agent has specific instructions and limitations. Respect these for optimal results.
- **API-First Approach**: When available, use API endpoints rather than web interfaces.
- **Automation Friendly**: Design commands that work well in non-interactive environments.
## Integration
Micro-agents are automatically integrated into OpenHands' workflow. They:
- Monitor incoming commands for their trigger words.
- Activate when relevant triggers are detected.
- Apply their specialized knowledge and capabilities.
- Follow their specific guidelines and restrictions.
## Example Usage
```bash
# GitHub agent example
git checkout -b feature-branch
git commit -m "Add new feature"
git push origin feature-branch
# NPM agent example
yes | npm install package-name
```
For more information about specific agents, refer to their individual documentation files in the micro-agents directory.
## Contributing a Micro-Agent
To contribute a new micro-agent to OpenHands, follow these guidelines:
### 1. Planning Your Micro-Agent
Before creating a micro-agent, consider:
- What specific problem or use case will it address?
- What unique capabilities or knowledge should it have?
- What trigger words make sense for activating it?
- What constraints or guidelines should it follow?
### 2. File Structure
Create a new markdown file in `openhands/agenthub/codeact_agent/micro/` with a descriptive name (e.g., `docker.md` for a Docker-focused agent).
### 3. Required Components
Your micro-agent file must include:
- **Front Matter**: YAML metadata at the start of the file:
```markdown
---
name: your_agent_name
agent: CodeActAgent
triggers:
- trigger_word1
- trigger_word2
---
```
- **Instructions**: Clear, specific guidelines for the agent's behavior:
```markdown
You are responsible for [specific task/domain].
Key responsibilities:
1. [Responsibility 1]
2. [Responsibility 2]
Guidelines:
- [Guideline 1]
- [Guideline 2]
Examples of usage:
[Example 1]
[Example 2]
```
### 4. Best Practices for Micro-Agent Development
- **Clear Scope**: Keep the agent focused on a specific domain or task.
- **Explicit Instructions**: Provide clear, unambiguous guidelines.
- **Useful Examples**: Include practical examples of common use cases.
- **Safety First**: Include necessary warnings and constraints.
- **Integration Awareness**: Consider how the agent interacts with other components.
### 5. Testing Your Micro-Agent
Before submitting:
- Test the agent with various prompts.
- Verify trigger words activate the agent correctly.
- Ensure instructions are clear and comprehensive.
- Check for potential conflicts with existing agents.
### 6. Example Implementation
Here's a template for a new micro-agent:
```markdown
---
name: docker
agent: CodeActAgent
triggers:
- docker
- container
---
You are responsible for Docker container management and Dockerfile creation.
Key responsibilities:
1. Create and modify Dockerfiles
2. Manage container lifecycle
3. Handle Docker Compose configurations
Guidelines:
- Always use official base images when possible
- Include necessary security considerations
- Follow Docker best practices for layer optimization
Examples:
1. Creating a Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]
2. Docker Compose usage:
version: '3'
services:
web:
build: .
ports:
- "3000:3000"
Remember to:
- Validate Dockerfile syntax
- Check for security vulnerabilities
- Optimize for build time and image size
```
### 7. Submission Process
1. Create your micro-agent file in the correct directory.
2. Test thoroughly.
3. Submit a pull request with:
- The new micro-agent file.
- Updated documentation if needed.
- Description of the agent's purpose and capabilities.
Remember that micro-agents are a powerful way to extend OpenHands' capabilities in specific domains. Well-designed
agents can significantly improve the system's ability to handle specialized tasks.

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.20-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```

1322
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

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"
},

View File

@@ -23,15 +23,26 @@ const sidebars: SidebarsConfig = {
id: 'usage/prompting/prompting-best-practices',
},
{
type: 'doc',
label: 'Customization',
id: 'usage/prompting/customization',
},
{
type: 'doc',
type: 'category',
label: 'Microagents',
id: 'usage/prompting/microagents',
},
items: [
{
type: 'doc',
label: 'Overview',
id: 'usage/prompting/microagents-overview',
},
{
type: 'doc',
label: 'Repository',
id: 'usage/prompting/microagents-repo',
},
{
type: 'doc',
label: 'Public',
id: 'usage/prompting/microagents-public',
},
],
}
],
},
{
@@ -126,11 +137,6 @@ const sidebars: SidebarsConfig = {
label: 'Custom Sandbox',
id: 'usage/how-to/custom-sandbox-guide',
},
{
type: 'doc',
label: 'Persist Session Data',
id: 'usage/how-to/persist-session-data',
},
],
},
{

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`.

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
@@ -448,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(

View File

@@ -39,7 +39,7 @@ def get_config(
run_as_openhands=False,
max_budget_per_task=4,
max_iterations=100,
trajectories_path=os.path.join(
save_trajectory_path=os.path.join(
mount_path_on_host, f'traj_{task_short_name}.json'
),
sandbox=SandboxConfig(

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

View File

@@ -1,47 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { retrieveLatestGitHubCommit } from "../../src/api/github";
describe("retrieveLatestGitHubCommit", () => {
const { githubGetMock } = vi.hoisted(() => ({
githubGetMock: vi.fn(),
}));
vi.mock("../../src/api/github-axios-instance", () => ({
github: {
get: githubGetMock,
},
}));
it("should return the latest commit when repository has commits", async () => {
const mockCommit = {
sha: "123abc",
commit: {
message: "Initial commit",
},
};
githubGetMock.mockResolvedValueOnce({
data: [mockCommit],
});
const result = await retrieveLatestGitHubCommit("user/repo");
expect(result).toEqual(mockCommit);
});
it("should return null when repository is empty", async () => {
const error = new Error("Repository is empty");
(error as any).response = { status: 409 };
githubGetMock.mockRejectedValueOnce(error);
const result = await retrieveLatestGitHubCommit("user/empty-repo");
expect(result).toBeNull();
});
it("should throw error for other error cases", async () => {
const error = new Error("Network error");
(error as any).response = { status: 500 };
githubGetMock.mockRejectedValueOnce(error);
await expect(retrieveLatestGitHubCommit("user/repo")).rejects.toThrow();
});
});

View File

@@ -3,11 +3,13 @@ import { afterEach, describe, expect, it, test, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { formatTimeDelta } from "#/utils/format-time-delta";
import { ConversationCard } from "#/components/features/conversation-panel/conversation-card";
import { clickOnEditButton } from "./utils";
describe("ConversationCard", () => {
const onClick = vi.fn();
const onDelete = vi.fn();
const onChangeTitle = vi.fn();
const onDownloadWorkspace = vi.fn();
afterEach(() => {
vi.clearAllMocks();
@@ -17,11 +19,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 +35,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 +70,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 +94,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 +114,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 +139,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 +164,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 +195,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 +214,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"
/>,
);
@@ -238,37 +234,151 @@ describe("ConversationCard", () => {
expect(onClick).not.toHaveBeenCalled();
});
it("should call onDownloadWorkspace when the download button is clicked", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const menu = screen.getByTestId("context-menu");
const downloadButton = within(menu).getByTestId("download-button");
await user.click(downloadButton);
expect(onDownloadWorkspace).toHaveBeenCalled();
});
it("should not display the edit or delete options if the handler is not provided", async () => {
const user = userEvent.setup();
const { rerender } = render(
<ConversationCard
onClick={onClick}
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
expect(screen.queryByTestId("edit-button")).toBeInTheDocument();
expect(screen.queryByTestId("delete-button")).not.toBeInTheDocument();
// toggle to hide the context menu
await user.click(ellipsisButton);
rerender(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
await user.click(ellipsisButton);
expect(screen.queryByTestId("edit-button")).not.toBeInTheDocument();
expect(screen.queryByTestId("delete-button")).toBeInTheDocument();
});
it("should not render the ellipsis button if there are no actions", () => {
const { rerender } = render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(screen.getByTestId("ellipsis-button")).toBeInTheDocument();
rerender(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(screen.getByTestId("ellipsis-button")).toBeInTheDocument();
rerender(
<ConversationCard
onClick={onClick}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(screen.queryByTestId("ellipsis-button")).toBeInTheDocument();
rerender(
<ConversationCard
onClick={onClick}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(screen.queryByTestId("ellipsis-button")).not.toBeInTheDocument();
});
describe("state indicator", () => {
it("should render the '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");
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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);
};

View File

@@ -3,7 +3,6 @@ import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { GitHubRepositorySelector } from "#/components/features/github/github-repo-selector";
import OpenHands from "#/api/open-hands";
import * as GitHubAPI from "#/api/github";
describe("GitHubRepositorySelector", () => {
const onInputChangeMock = vi.fn();
@@ -60,8 +59,8 @@ describe("GitHubRepositorySelector", () => {
];
const searchPublicRepositoriesSpy = vi.spyOn(
GitHubAPI,
"searchPublicRepositories",
OpenHands,
"searchGitHubRepositories",
);
searchPublicRepositoriesSpy.mockResolvedValue(mockSearchedRepos);

View File

@@ -1,10 +1,12 @@
import { screen } from "@testing-library/react";
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } 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";
import OpenHands from "#/api/open-hands";
import { MOCK_USER_PREFERENCES } from "#/mocks/handlers";
const renderSidebar = () => {
const RouterStub = createRoutesStub([
@@ -18,7 +20,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 +28,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();
@@ -43,4 +45,126 @@ describe("Sidebar", () => {
).not.toBeInTheDocument();
},
);
describe("Settings", () => {
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
afterEach(() => {
vi.clearAllMocks();
});
it("should fetch settings data on mount", () => {
renderSidebar();
expect(getSettingsSpy).toHaveBeenCalledOnce();
});
it("should send all settings data when saving AI configuration", async () => {
const user = userEvent.setup();
renderSidebar();
const settingsButton = screen.getByTestId("settings-button");
await user.click(settingsButton);
const settingsModal = screen.getByTestId("ai-config-modal");
const saveButton = within(settingsModal).getByTestId(
"save-settings-button",
);
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
// the actual values are falsey (null or "") but we're checking for undefined
llm_api_key: undefined,
llm_base_url: undefined,
security_analyzer: undefined,
});
});
it("should send all settings data when saving account settings", async () => {
const user = userEvent.setup();
renderSidebar();
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
const menu = screen.getByTestId("account-settings-context-menu");
const accountSettingsButton = within(menu).getByTestId(
"account-settings-button",
);
await user.click(accountSettingsButton);
const accountSettingsModal = screen.getByTestId("account-settings-form");
const saveButton =
within(accountSettingsModal).getByTestId("save-settings");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
llm_api_key: undefined, // null or undefined
});
});
it("should not reset AI configuration when saving account settings", async () => {
const user = userEvent.setup();
renderSidebar();
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
const menu = screen.getByTestId("account-settings-context-menu");
const accountSettingsButton = within(menu).getByTestId(
"account-settings-button",
);
await user.click(accountSettingsButton);
const accountSettingsModal = screen.getByTestId("account-settings-form");
const languageInput =
within(accountSettingsModal).getByLabelText(/language/i);
await user.click(languageInput);
const norskOption = screen.getByText(/norsk/i);
await user.click(norskOption);
const tokenInput =
within(accountSettingsModal).getByLabelText(/github token/i);
await user.type(tokenInput, "new-token");
const saveButton =
within(accountSettingsModal).getByTestId("save-settings");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
language: "no",
llm_api_key: undefined, // null or undefined
});
});
it("should not send the api key if its SET", async () => {
const user = userEvent.setup();
renderSidebar();
const settingsButton = screen.getByTestId("settings-button");
await user.click(settingsButton);
const settingsModal = screen.getByTestId("ai-config-modal");
const apiKeyInput = within(settingsModal).getByLabelText(/api key/i);
await user.type(apiKeyInput, "SET");
const saveButton = within(settingsModal).getByTestId(
"save-settings-button",
);
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
llm_api_key: undefined,
llm_base_url: undefined,
security_analyzer: undefined,
});
});
});
});

View File

@@ -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");
});
});

View File

@@ -0,0 +1,76 @@
import { screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi, afterEach } from "vitest";
import { renderWithProviders } from "test-utils";
import { createRoutesStub } from "react-router";
import userEvent from "@testing-library/user-event";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { SettingsForm } from "#/components/shared/modals/settings/settings-form";
import OpenHands from "#/api/open-hands";
describe("SettingsForm", () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
const onCloseMock = vi.fn();
afterEach(() => {
vi.clearAllMocks();
});
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "123",
});
const RouterStub = createRoutesStub([
{
Component: () => (
<SettingsForm
settings={DEFAULT_SETTINGS}
models={["anthropic/claude-3-5-sonnet-20241022", "model2"]}
agents={["CodeActAgent", "agent2"]}
securityAnalyzers={["analyzer1", "analyzer2"]}
onClose={onCloseMock}
/>
),
path: "/",
},
]);
it("should not show runtime size selector by default", () => {
renderWithProviders(<RouterStub />);
expect(screen.queryByText("Runtime Size")).not.toBeInTheDocument();
});
it("should show runtime size selector when advanced options are enabled", async () => {
const user = userEvent.setup();
renderWithProviders(<RouterStub />);
const toggleAdvancedMode = screen.getByTestId("advanced-option-switch");
await user.click(toggleAdvancedMode);
await screen.findByTestId("runtime-size");
});
it("should not submit the form if required fields are empty", async () => {
const user = userEvent.setup();
renderWithProviders(<RouterStub />);
expect(screen.queryByTestId("custom-model-input")).not.toBeInTheDocument();
const toggleAdvancedMode = screen.getByTestId("advanced-option-switch");
await user.click(toggleAdvancedMode);
const customModelInput = screen.getByTestId("custom-model-input");
expect(customModelInput).toBeInTheDocument();
await user.clear(customModelInput);
const saveButton = screen.getByTestId("save-settings-button");
await user.click(saveButton);
expect(saveSettingsSpy).not.toHaveBeenCalled();
expect(onCloseMock).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,43 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import * as ChatSlice from "#/state/chat-slice";
import {
updateStatusWhenErrorMessagePresent,
} from "#/context/ws-client-provider";
describe("Propagate error message", () => {
it("should do nothing when no message was passed from server", () => {
const addErrorMessageSpy = vi.spyOn(ChatSlice, "addErrorMessage");
updateStatusWhenErrorMessagePresent(null)
updateStatusWhenErrorMessagePresent(undefined)
updateStatusWhenErrorMessagePresent({})
updateStatusWhenErrorMessagePresent({message: null})
expect(addErrorMessageSpy).not.toHaveBeenCalled();
});
it("should display error to user when present", () => {
const message = "We have a problem!"
const addErrorMessageSpy = vi.spyOn(ChatSlice, "addErrorMessage")
updateStatusWhenErrorMessagePresent({message})
expect(addErrorMessageSpy).toHaveBeenCalledWith({
message,
status_update: true,
type: 'error'
});
});
it("should display error including translation id when present", () => {
const message = "We have a problem!"
const addErrorMessageSpy = vi.spyOn(ChatSlice, "addErrorMessage")
updateStatusWhenErrorMessagePresent({message, data: {msg_id: '..id..'}})
expect(addErrorMessageSpy).toHaveBeenCalledWith({
message,
id: '..id..',
status_update: true,
type: 'error'
});
});
});

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"]} />,

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.18.0",
"version": "0.20.0",
"private": true,
"type": "module",
"engines": {
@@ -8,36 +8,36 @@
},
"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.21",
"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",
"react-textarea-autosize": "^8.5.4",
"react-textarea-autosize": "^8.5.7",
"remark-gfm": "^4.0.0",
"sirv-cli": "^3.0.0",
"socket.io-client": "^4.8.1",
@@ -78,13 +78,13 @@
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.49.1",
"@react-router/dev": "^7.1.1",
"@tailwindcss/typography": "^0.5.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.4",
"@types/react-dom": "^19.0.2",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
@@ -97,20 +97,20 @@
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
"jsdom": "^25.0.1",
"jsdom": "^26.0.0",
"lint-staged": "^15.3.0",
"msw": "^2.6.6",
"postcss": "^8.4.47",
"prettier": "^3.4.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"typescript": "^5.7.3",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^1.6.0"

View File

@@ -1,19 +1,6 @@
import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link";
import { github } from "./github-axios-instance";
import { openHands } from "./open-hands-axios";
/**
* Given the user, retrieves app installations IDs for OpenHands Github App
* Uses user access token for Github App
*/
export const retrieveGitHubAppInstallations = async (): Promise<number[]> => {
const response = await github.get<GithubAppInstallation>(
"/user/installations",
);
return response.data.installations.map((installation) => installation.id);
};
/**
* Retrieves repositories where OpenHands Github App has been installed
* @param installationIndex Pagination cursor position for app installation IDs
@@ -82,72 +69,3 @@ export const retrieveGitHubUserRepositories = async (
return { data: response.data, nextPage };
};
/**
* Given a GitHub token, retrieves the authenticated user
* @returns The authenticated user or an error response
*/
export const retrieveGitHubUser = async () => {
const response = await github.get<GitHubUser>("/user");
const { data } = response;
const user: GitHubUser = {
id: data.id,
login: data.login,
avatar_url: data.avatar_url,
company: data.company,
name: data.name,
email: data.email,
};
return user;
};
export const searchPublicRepositories = async (
query: string,
per_page = 5,
sort: "" | "updated" | "stars" | "forks" = "stars",
order: "desc" | "asc" = "desc",
): Promise<GitHubRepository[]> => {
const response = await github.get<{ items: GitHubRepository[] }>(
"/search/repositories",
{
params: {
q: query,
per_page,
sort,
order,
},
},
);
return response.data.items;
};
export const retrieveLatestGitHubCommit = async (
repository: string,
): Promise<GitHubCommit | null> => {
try {
const response = await github.get<GitHubCommit[]>(
`/repos/${repository}/commits`,
{
params: {
per_page: 1,
},
},
);
return response.data[0] || null;
} catch (error) {
if (!error || typeof error !== "object") {
throw new Error("Unknown error occurred");
}
const axiosError = error as { response?: { status: number } };
if (axiosError.response?.status === 409) {
// Repository is empty, no commits yet
return null;
}
throw new Error(
error instanceof Error ? error.message : "Unknown error occurred",
);
}
};

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(
@@ -312,6 +315,45 @@ class OpenHands {
const data = await openHands.post("/api/settings", settings);
return data.status === 200;
}
static async getGitHubUser(): Promise<GitHubUser> {
const response = await openHands.get<GitHubUser>("/api/github/user");
const { data } = response;
const user: GitHubUser = {
id: data.id,
login: data.login,
avatar_url: data.avatar_url,
company: data.company,
name: data.name,
email: data.email,
};
return user;
}
static async getGitHubUserInstallationIds(): Promise<number[]> {
const response = await openHands.get<number[]>("/api/github/installations");
return response.data;
}
static async searchGitHubRepositories(
query: string,
per_page = 5,
): Promise<GitHubRepository[]> {
const response = await openHands.get<{ items: GitHubRepository[] }>(
"/api/github/search/repositories",
{
params: {
query,
per_page,
},
},
);
return response.data.items;
}
}
export default OpenHands;

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;
}

View File

@@ -27,7 +27,10 @@ export function AccountSettingsContextMenu({
ref={ref}
className="absolute left-full -top-1 z-10"
>
<ContextMenuListItem onClick={onClickAccountSettings}>
<ContextMenuListItem
testId="account-settings-button"
onClick={onClickAccountSettings}
>
{t(I18nKey.ACCOUNT_SETTINGS$SETTINGS)}
</ContextMenuListItem>
<ContextMenuSeparator />

View File

@@ -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>

View File

@@ -1,39 +1,30 @@
import { useParams } from "react-router";
import React from "react";
import { useSelector } from "react-redux";
import posthog from "posthog-js";
import { AgentControlBar } from "./agent-control-bar";
import { AgentStatusBar } from "./agent-status-bar";
import { ProjectMenuCard } from "../project-menu/ProjectMenuCard";
import { useAuth } from "#/context/auth-context";
import { RootState } from "#/store";
import { SecurityLock } from "./security-lock";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
import { ConversationCard } from "../conversation-panel/conversation-card";
import { DownloadModal } from "#/components/shared/download-modal";
interface ControlsProps {
setSecurityOpen: (isOpen: boolean) => void;
showSecurityLock: boolean;
lastCommitData: GitHubCommit | null;
}
export function Controls({
setSecurityOpen,
showSecurityLock,
lastCommitData,
}: ControlsProps) {
const { gitHubToken } = useAuth();
const { selectedRepository } = useSelector(
(state: RootState) => state.initialQuery,
export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
const params = useParams();
const { data: conversation } = useUserConversation(
params.conversationId ?? null,
);
const projectMenuCardData = React.useMemo(
() =>
selectedRepository && lastCommitData
? {
repoName: selectedRepository,
lastCommit: lastCommitData,
avatar: null, // TODO: fetch repo avatar
}
: null,
[selectedRepository, lastCommitData],
);
const [downloading, setDownloading] = React.useState(false);
const handleDownloadWorkspace = () => {
posthog.capture("download_workspace_button_clicked");
setDownloading(true);
};
return (
<div className="flex items-center justify-between">
@@ -46,9 +37,19 @@ export function Controls({
)}
</div>
<ProjectMenuCard
isConnectedToGitHub={!!gitHubToken}
githubData={projectMenuCardData}
<ConversationCard
variant="compact"
onDownloadWorkspace={handleDownloadWorkspace}
title={conversation?.title ?? ""}
lastUpdatedAt={conversation?.created_at ?? ""}
selectedRepository={conversation?.selected_repository ?? null}
status={conversation?.status}
/>
<DownloadModal
initialPath=""
onClose={() => setDownloading(false)}
isOpen={downloading}
/>
</div>
);

View File

@@ -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>

View File

@@ -0,0 +1,50 @@
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
import { cn } from "#/utils/utils";
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;
onDownload?: (event: React.MouseEvent<HTMLButtonElement>) => void;
position?: "top" | "bottom";
}
export function ConversationCardContextMenu({
onClose,
onDelete,
onEdit,
onDownload,
position = "bottom",
}: ConversationCardContextMenuProps) {
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
return (
<ContextMenu
ref={ref}
testId="context-menu"
className={cn(
"right-0 absolute",
position === "top" && "bottom-full",
position === "bottom" && "top-full",
)}
>
{onDelete && (
<ContextMenuListItem testId="delete-button" onClick={onDelete}>
Delete
</ContextMenuListItem>
)}
{onEdit && (
<ContextMenuListItem testId="edit-button" onClick={onEdit}>
Edit Title
</ContextMenuListItem>
)}
{onDownload && (
<ContextMenuListItem testId="download-button" onClick={onDownload}>
Download Workspace
</ContextMenuListItem>
)}
</ContextMenu>
);
}

View File

@@ -2,101 +2,156 @@ 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";
import { cn } from "#/utils/utils";
interface ProjectCardProps {
onClick: () => void;
onDelete: () => void;
onChangeTitle: (title: string) => void;
name: string;
repo: string | null;
lastUpdated: string; // ISO 8601
state?: ProjectState;
interface ConversationCardProps {
onClick?: () => void;
onDelete?: () => void;
onChangeTitle?: (title: string) => void;
onDownloadWorkspace?: () => void;
isActive?: boolean;
title: string;
selectedRepository: string | null;
lastUpdatedAt: string; // ISO 8601
status?: ProjectStatus;
variant?: "compact" | "default";
}
export function ConversationCard({
onClick,
onDelete,
onChangeTitle,
name,
repo,
lastUpdated,
state = "cold",
}: ProjectCardProps) {
onDownloadWorkspace,
isActive,
title,
selectedRepository,
lastUpdatedAt,
status = "STOPPED",
variant = "default",
}: ConversationCardProps) {
const [contextMenuVisible, setContextMenuVisible] = React.useState(false);
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
const inputRef = React.useRef<HTMLInputElement>(null);
const handleBlur = () => {
if (inputRef.current?.value) {
const trimmed = inputRef.current.value.trim();
onChangeTitle(trimmed);
onChangeTitle?.(trimmed);
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();
onDelete?.();
};
const handleEdit = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
setTitleMode("edit");
setContextMenuVisible(false);
};
const handleDownload = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
onDownloadWorkspace?.();
};
React.useEffect(() => {
if (titleMode === "edit") {
inputRef.current?.focus();
}
}, [titleMode]);
const hasContextMenu = !!(onDelete || onChangeTitle || onDownloadWorkspace);
return (
<div
data-testid="conversation-card"
onClick={onClick}
className="h-[100px] w-full px-[18px] py-4 border-b border-neutral-600"
className={cn(
"h-[100px] w-full px-[18px] py-4 border-b border-neutral-600 cursor-pointer",
variant === "compact" &&
"h-auto w-fit rounded-xl border border-[#525252]",
)}
>
<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} />
<EllipsisButton
onClick={(event) => {
event.stopPropagation();
setContextMenuVisible((prev) => !prev);
}}
/>
<ConversationStateIndicator status={status} />
{hasContextMenu && (
<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>
<ConversationCardContextMenu
onClose={() => setContextMenuVisible(false)}
onDelete={onDelete && handleDelete}
onEdit={onChangeTitle && handleEdit}
onDownload={onDownloadWorkspace && handleDownload}
position={variant === "compact" ? "top" : "bottom"}
/>
)}
</div>
</div>
{repo && (
<ConversationRepoLink
repo={repo}
onClick={(e) => e.stopPropagation()}
/>
)}
<p className="text-xs text-neutral-400">
<time>{formatTimeDelta(new Date(lastUpdated))} ago</time>
</p>
<div
className={cn(
variant === "compact" && "flex items-center justify-between mt-1",
)}
>
{selectedRepository && (
<ConversationRepoLink
selectedRepository={selectedRepository}
onClick={(e) => e.stopPropagation()}
/>
)}
<p className="text-xs text-neutral-400">
<time>{formatTimeDelta(new Date(lastUpdatedAt))} ago</time>
</p>
</div>
</div>
);
}

View File

@@ -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,
);
}

View File

@@ -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 && (

View File

@@ -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>
);
}

View File

@@ -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>
);

View File

@@ -59,7 +59,7 @@ export function GitHubRepositoriesSuggestionBox({
<GitHubRepositorySelector
onInputChange={setSearchQuery}
onSelect={handleSubmit}
publicRepositories={searchedRepos}
publicRepositories={searchedRepos || []}
userRepositories={repositories}
/>
) : (

View File

@@ -1,92 +0,0 @@
import React from "react";
import posthog from "posthog-js";
import { useTranslation } from "react-i18next";
import EllipsisH from "#/icons/ellipsis-h.svg?react";
import { ProjectMenuCardContextMenu } from "./project.menu-card-context-menu";
import { ProjectMenuDetailsPlaceholder } from "./project-menu-details-placeholder";
import { ProjectMenuDetails } from "./project-menu-details";
import { ConnectToGitHubModal } from "#/components/shared/modals/connect-to-github-modal";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { DownloadModal } from "#/components/shared/download-modal";
import { I18nKey } from "#/i18n/declaration";
interface ProjectMenuCardProps {
isConnectedToGitHub: boolean;
githubData: {
avatar: string | null;
repoName: string;
lastCommit: GitHubCommit;
} | null;
}
export function ProjectMenuCard({
isConnectedToGitHub,
githubData,
}: ProjectMenuCardProps) {
const { t } = useTranslation();
const [contextMenuIsOpen, setContextMenuIsOpen] = React.useState(false);
const [connectToGitHubModalOpen, setConnectToGitHubModalOpen] =
React.useState(false);
const [downloading, setDownloading] = React.useState(false);
const toggleMenuVisibility = () => {
setContextMenuIsOpen((prev) => !prev);
};
const handleDownloadWorkspace = () => {
posthog.capture("download_workspace_button_clicked");
setDownloading(true);
};
const handleDownloadClose = () => {
setDownloading(false);
};
return (
<div className="px-4 py-[10px] w-[337px] rounded-xl border border-[#525252] flex justify-between items-center relative">
{!downloading && contextMenuIsOpen && (
<ProjectMenuCardContextMenu
isConnectedToGitHub={isConnectedToGitHub}
onConnectToGitHub={() => setConnectToGitHubModalOpen(true)}
onDownloadWorkspace={handleDownloadWorkspace}
onClose={() => setContextMenuIsOpen(false)}
/>
)}
{githubData && (
<ProjectMenuDetails
repoName={githubData.repoName}
avatar={githubData.avatar}
lastCommit={githubData.lastCommit}
/>
)}
{!githubData && (
<ProjectMenuDetailsPlaceholder
isConnectedToGitHub={isConnectedToGitHub}
onConnectToGitHub={() => setConnectToGitHubModalOpen(true)}
/>
)}
<DownloadModal
initialPath=""
onClose={handleDownloadClose}
isOpen={downloading}
/>
{!downloading && (
<button
type="button"
onClick={toggleMenuVisibility}
aria-label={t(I18nKey.PROJECT_MENU_CARD$OPEN)}
>
<EllipsisH width={36} height={36} />
</button>
)}
{connectToGitHubModalOpen && (
<ModalBackdrop onClose={() => setConnectToGitHubModalOpen(false)}>
<ConnectToGitHubModal
onClose={() => setConnectToGitHubModalOpen(false)}
/>
</ModalBackdrop>
)}
</div>
);
}

View File

@@ -1,41 +0,0 @@
import { useTranslation } from "react-i18next";
import { cn } from "#/utils/utils";
import CloudConnection from "#/icons/cloud-connection.svg?react";
import { I18nKey } from "#/i18n/declaration";
interface ProjectMenuDetailsPlaceholderProps {
isConnectedToGitHub: boolean;
onConnectToGitHub: () => void;
}
export function ProjectMenuDetailsPlaceholder({
isConnectedToGitHub,
onConnectToGitHub,
}: ProjectMenuDetailsPlaceholderProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col">
<span className="text-sm leading-6 font-semibold">
{t(I18nKey.PROJECT_MENU_DETAILS_PLACEHOLDER$NEW_PROJECT_LABEL)}
</span>
<button
type="button"
onClick={onConnectToGitHub}
disabled={isConnectedToGitHub}
>
<span
className={cn(
"text-xs leading-4 text-[#A3A3A3] flex items-center gap-2",
"hover:underline hover:underline-offset-2",
)}
>
{!isConnectedToGitHub
? t(I18nKey.PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECT_TO_GITHUB)
: t(I18nKey.PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECTED)}
<CloudConnection width={12} height={12} />
</span>
</button>
</div>
);
}

View File

@@ -1,44 +0,0 @@
import { useTranslation } from "react-i18next";
import ExternalLinkIcon from "#/icons/external-link.svg?react";
import { formatTimeDelta } from "#/utils/format-time-delta";
import { I18nKey } from "#/i18n/declaration";
interface ProjectMenuDetailsProps {
repoName: string;
avatar: string | null;
lastCommit: GitHubCommit;
}
export function ProjectMenuDetails({
repoName,
avatar,
lastCommit,
}: ProjectMenuDetailsProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col">
<a
href={`https://github.com/${repoName}`}
target="_blank"
rel="noreferrer noopener"
className="flex items-center gap-2"
>
{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} />
</a>
<a
href={lastCommit.html_url}
target="_blank"
rel="noreferrer noopener"
className="text-xs text-[#A3A3A3] hover:underline hover:underline-offset-2"
>
<span>{lastCommit.sha.slice(-7)}</span> <span>&middot;</span>{" "}
<span>
{formatTimeDelta(new Date(lastCommit.commit.author.date))}{" "}
{t(I18nKey.PROJECT_MENU_DETAILS$AGO_LABEL)}
</span>
</a>
</div>
);
}

View File

@@ -1,37 +0,0 @@
import { useTranslation } from "react-i18next";
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
import { ContextMenu } from "../context-menu/context-menu";
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
import { I18nKey } from "#/i18n/declaration";
interface ProjectMenuCardContextMenuProps {
isConnectedToGitHub: boolean;
onConnectToGitHub: () => void;
onDownloadWorkspace: () => void;
onClose: () => void;
}
export function ProjectMenuCardContextMenu({
isConnectedToGitHub,
onConnectToGitHub,
onDownloadWorkspace,
onClose,
}: ProjectMenuCardContextMenuProps) {
const menuRef = useClickOutsideElement<HTMLUListElement>(onClose);
const { t } = useTranslation();
return (
<ContextMenu
ref={menuRef}
className="absolute right-0 bottom-[calc(100%+8px)]"
>
{!isConnectedToGitHub && (
<ContextMenuListItem onClick={onConnectToGitHub}>
{t(I18nKey.PROJECT_MENU_CARD_CONTEXT_MENU$CONNECT_TO_GITHUB_LABEL)}
</ContextMenuListItem>
)}
<ContextMenuListItem onClick={onDownloadWorkspace}>
{t(I18nKey.PROJECT_MENU_CARD_CONTEXT_MENU$DOWNLOAD_FILES_LABEL)}
</ContextMenuListItem>
</ContextMenu>
);
}

View File

@@ -0,0 +1,19 @@
interface PathFormProps {
ref: React.RefObject<HTMLFormElement | null>;
onBlur: () => void;
defaultValue: string;
}
export function PathForm({ ref, onBlur, defaultValue }: PathFormProps) {
return (
<form ref={ref} onSubmit={(e) => e.preventDefault()} className="flex-1">
<input
name="url"
type="text"
defaultValue={defaultValue}
className="w-full bg-transparent"
onBlur={onBlur}
/>
</form>
);
}

View File

@@ -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,30 +11,37 @@ 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 { useCurrentSettings } from "#/context/settings-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();
const { data: settings, isError: settingsIsError } = useSettings();
const { isUpToDate: settingsAreUpToDate } = useSettingsUpToDate();
const {
data: settings,
isError: settingsIsError,
isSuccess: settingsSuccessfulyFetched,
} = useSettings();
const { isUpToDate: settingsAreUpToDate } = useCurrentSettings();
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 +50,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 +63,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
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 +96,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>
@@ -110,17 +111,12 @@ export function Sidebar() {
<AccountSettingsModal onClose={handleAccountSettingsModalClose} />
)}
{settingsIsError ||
(showSettingsModal && (
(showSettingsModal && settingsSuccessfulyFetched && (
<SettingsModal
settings={settings}
onClose={() => setSettingsModalIsOpen(false)}
/>
))}
{startNewProjectModalIsOpen && (
<ExitProjectConfirmationModal
onClose={() => setStartNewProjectModalIsOpen(false)}
/>
)}
</>
);
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1,15 @@
import { useActiveHost } from "#/hooks/query/use-active-host";
export function ServedAppLabel() {
const { activeHost } = useActiveHost();
return (
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">App</div>
<span className="border rounded-md text- px-1 font-bold">BETA</span>
</div>
{activeHost && <div className="w-2 h-2 bg-green-500 rounded-full" />}
</div>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);

View File

@@ -18,9 +18,10 @@ export function AdvancedOptionSwitch({
return (
<Switch
data-testid="advanced-option-switch"
isDisabled={isDisabled}
name="use-advanced-options"
isSelected={showAdvancedOptions}
defaultSelected={showAdvancedOptions}
onValueChange={setShowAdvancedOptions}
classNames={{
thumb: cn(

View File

@@ -22,7 +22,9 @@ export function CustomModelInput({
{t(I18nKey.SETTINGS_FORM$CUSTOM_MODEL_LABEL)}
</label>
<Input
data-testid="custom-model-input"
isDisabled={isDisabled}
isRequired
id="custom-model"
name="custom-model"
defaultValue={defaultValue}

View File

@@ -13,7 +13,7 @@ import { ModalButton } from "../../buttons/modal-button";
import { CustomInput } from "../../custom-input";
import { FormFieldset } from "../../form-fieldset";
import { useConfig } from "#/hooks/query/use-config";
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
import { useCurrentSettings } from "#/context/settings-context";
interface AccountSettingsFormProps {
onClose: () => void;
@@ -30,10 +30,10 @@ export function AccountSettingsForm({
}: AccountSettingsFormProps) {
const { gitHubToken, setGitHubToken, logout } = useAuth();
const { data: config } = useConfig();
const { mutate: saveSettings } = useSaveSettings();
const { saveUserSettings } = useCurrentSettings();
const { t } = useTranslation();
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
@@ -50,7 +50,7 @@ export function AccountSettingsForm({
({ label }) => label === language,
)?.value;
if (languageKey) saveSettings({ LANGUAGE: languageKey });
if (languageKey) await saveUserSettings({ LANGUAGE: languageKey });
}
handleCaptureConsent(analytics);
@@ -61,7 +61,7 @@ export function AccountSettingsForm({
};
return (
<ModalBody>
<ModalBody testID="account-settings-form">
<form className="flex flex-col w-full gap-6" onSubmit={handleSubmit}>
<div className="w-full flex flex-col gap-2">
<BaseModalTitle title="Account Settings" />
@@ -137,6 +137,7 @@ export function AccountSettingsForm({
<div className="flex flex-col gap-2 w-full">
<ModalButton
testId="save-settings"
type="submit"
intent="account"
text={t(I18nKey.ACCOUNT_SETTINGS_MODAL$SAVE)}

View File

@@ -18,7 +18,7 @@ export function AccountSettingsModal({ onClose }: AccountSettingsModalProps) {
<ModalBackdrop onClose={onClose}>
<AccountSettingsForm
onClose={onClose}
selectedLanguage={settings.LANGUAGE}
selectedLanguage={settings?.LANGUAGE || "en"}
gitHubError={user.isError}
analyticsConsent={analyticsConsent}
/>

View File

@@ -0,0 +1,53 @@
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
data-testid="runtime-size"
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>
);
}

View File

@@ -19,7 +19,10 @@ import { CustomModelInput } from "../../inputs/custom-model-input";
import { SecurityAnalyzerInput } from "../../inputs/security-analyzers-input";
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";
import { useCurrentSettings } from "#/context/settings-context";
interface SettingsFormProps {
disabled?: boolean;
@@ -38,8 +41,9 @@ export function SettingsForm({
securityAnalyzers,
onClose,
}: SettingsFormProps) {
const { mutateAsync: saveSettings } = useSaveSettings();
const { saveUserSettings } = useCurrentSettings();
const endSession = useEndSession();
const { data: config } = useConfig();
const location = useLocation();
const { t } = useTranslation();
@@ -91,17 +95,21 @@ export function SettingsForm({
const newSettings = extractSettings(formData);
saveSettingsView(isUsingAdvancedOptions ? "advanced" : "basic");
await saveSettings(newSettings, { onSuccess: onClose });
await saveUserSettings(newSettings);
onClose();
resetOngoingSession();
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,
});
};
const handleConfirmResetSettings = async () => {
await saveSettings(getDefaultSettings(), { onSuccess: onClose });
await saveUserSettings(getDefaultSettings());
onClose();
resetOngoingSession();
posthog.capture("settings_reset");
};
@@ -122,6 +130,8 @@ export function SettingsForm({
}
};
const isSaasMode = config?.APP_MODE === "saas";
return (
<div>
<form
@@ -164,16 +174,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}
@@ -191,6 +206,7 @@ export function SettingsForm({
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<ModalButton
testId="save-settings-button"
disabled={disabled}
type="submit"
text={t(I18nKey.SETTINGS_FORM$SAVE_LABEL)}

View File

@@ -0,0 +1,75 @@
import React from "react";
import {
LATEST_SETTINGS_VERSION,
Settings,
settingsAreUpToDate,
} from "#/services/settings";
import { useSettings } from "#/hooks/query/use-settings";
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
interface SettingsContextType {
isUpToDate: boolean;
setIsUpToDate: (value: boolean) => void;
saveUserSettings: (newSettings: Partial<Settings>) => Promise<void>;
settings: Settings | undefined;
}
const SettingsContext = React.createContext<SettingsContextType | undefined>(
undefined,
);
interface SettingsProviderProps {
children: React.ReactNode;
}
export function SettingsProvider({ children }: SettingsProviderProps) {
const { data: userSettings } = useSettings();
const { mutateAsync: saveSettings } = useSaveSettings();
const [isUpToDate, setIsUpToDate] = React.useState(settingsAreUpToDate());
const saveUserSettings = async (newSettings: Partial<Settings>) => {
const updatedSettings: Partial<Settings> = {
...userSettings,
...newSettings,
};
if (updatedSettings.LLM_API_KEY === "SET") {
delete updatedSettings.LLM_API_KEY;
}
await saveSettings(updatedSettings, {
onSuccess: () => {
if (!isUpToDate) {
localStorage.setItem(
"SETTINGS_VERSION",
LATEST_SETTINGS_VERSION.toString(),
);
setIsUpToDate(true);
}
},
});
};
const value = React.useMemo(
() => ({
isUpToDate,
setIsUpToDate,
saveUserSettings,
settings: userSettings,
}),
[isUpToDate, setIsUpToDate, saveUserSettings, userSettings],
);
return <SettingsContext value={value}>{children}</SettingsContext>;
}
export function useCurrentSettings() {
const context = React.useContext(SettingsContext);
if (context === undefined) {
throw new Error(
"useCurrentSettings must be used within a SettingsProvider",
);
}
return context;
}

View File

@@ -1,40 +0,0 @@
import React from "react";
import { settingsAreUpToDate } from "#/services/settings";
interface SettingsUpToDateContextType {
isUpToDate: boolean;
setIsUpToDate: (value: boolean) => void;
}
const SettingsUpToDateContext = React.createContext<
SettingsUpToDateContextType | undefined
>(undefined);
interface SettingsUpToDateProviderProps {
children: React.ReactNode;
}
export function SettingsUpToDateProvider({
children,
}: SettingsUpToDateProviderProps) {
const [isUpToDate, setIsUpToDate] = React.useState(settingsAreUpToDate());
const value = React.useMemo(
() => ({ isUpToDate, setIsUpToDate }),
[isUpToDate, setIsUpToDate],
);
return (
<SettingsUpToDateContext value={value}>{children}</SettingsUpToDateContext>
);
}
export function useSettingsUpToDate() {
const context = React.useContext(SettingsUpToDateContext);
if (context === undefined) {
throw new Error(
"useSettingsUpToDate must be used within a SettingsUpToDateProvider",
);
}
return context;
}

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,45 @@ const WsClientContext = React.createContext<UseWsClient>({
interface WsClientProviderProps {
conversationId: string;
ghToken: string | null;
}
interface ErrorArg {
message?: string;
data?: ErrorArgData | unknown;
}
interface ErrorArgData {
msg_id: string;
}
export function updateStatusWhenErrorMessagePresent(data: ErrorArg | unknown) {
const isObject = (val: unknown): val is object =>
!!val && typeof val === "object";
const isString = (val: unknown): val is string => typeof val === "string";
if (isObject(data) && "message" in data && isString(data.message)) {
let msgId: string | undefined;
if (
"data" in data &&
isObject(data.data) &&
"msg_id" in data.data &&
isString(data.data.msg_id)
) {
msgId = data.data.msg_id;
}
handleStatusMessage({
type: "error",
message: data.message,
id: msgId,
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 +125,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 +136,7 @@ export function WsClientProvider({
handleAssistantMessage(event);
}
function handleDisconnect() {
function handleDisconnect(data: unknown) {
setStatus(WsClientProviderStatus.DISCONNECTED);
const sio = sioRef.current;
if (!sio) {
@@ -93,13 +144,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 +175,6 @@ export function WsClientProvider({
sio = io(baseUrl, {
transports: ["websocket"],
auth: {
github_token: ghToken || undefined,
},
query,
});
sio.on("connect", handleConnect);
@@ -130,7 +184,6 @@ export function WsClientProvider({
sio.on("disconnect", handleDisconnect);
sioRef.current = sio;
ghTokenRef.current = ghToken;
return () => {
sio.off("connect", handleConnect);
@@ -139,7 +192,7 @@ export function WsClientProvider({
sio.off("connect_failed", handleError);
sio.off("disconnect", handleDisconnect);
};
}, [ghToken, conversationId]);
}, [conversationId]);
React.useEffect(
() => () => {

View File

@@ -20,7 +20,7 @@ import toast from "react-hot-toast";
import store from "./store";
import { useConfig } from "./hooks/query/use-config";
import { AuthProvider } from "./context/auth-context";
import { SettingsUpToDateProvider } from "./context/settings-up-to-date-context";
import { SettingsProvider } from "./context/settings-context";
function PosthogInit() {
const { data: config } = useConfig();
@@ -50,13 +50,20 @@ async function prepareApp() {
}
}
const QUERY_KEYS_TO_IGNORE = ["authenticated", "hosts"];
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
if (!query.queryKey.includes("authenticated")) toast.error(error.message);
if (!QUERY_KEYS_TO_IGNORE.some((key) => query.queryKey.includes(key))) {
toast.error(error.message);
}
},
}),
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
},
mutations: {
onError: (error) => {
toast.error(error.message);
@@ -72,12 +79,12 @@ prepareApp().then(() =>
<StrictMode>
<Provider store={store}>
<AuthProvider>
<SettingsUpToDateProvider>
<QueryClientProvider client={queryClient}>
<QueryClientProvider client={queryClient}>
<SettingsProvider>
<HydratedRouter />
<PosthogInit />
</QueryClientProvider>
</SettingsUpToDateProvider>
</SettingsProvider>
</QueryClientProvider>
</AuthProvider>
</Provider>
</StrictMode>,

View File

@@ -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");
}

View File

@@ -1,12 +1,6 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
ApiSettings,
DEFAULT_SETTINGS,
LATEST_SETTINGS_VERSION,
Settings,
} from "#/services/settings";
import { ApiSettings, DEFAULT_SETTINGS, Settings } from "#/services/settings";
import OpenHands from "#/api/open-hands";
import { useSettingsUpToDate } from "#/context/settings-up-to-date-context";
const saveSettingsMutationFn = async (settings: Partial<Settings>) => {
const apiSettings: Partial<ApiSettings> = {
@@ -24,19 +18,11 @@ const saveSettingsMutationFn = async (settings: Partial<Settings>) => {
export const useSaveSettings = () => {
const queryClient = useQueryClient();
const { isUpToDate, setIsUpToDate } = useSettingsUpToDate();
return useMutation({
mutationFn: saveSettingsMutationFn,
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["settings"] });
if (!isUpToDate) {
localStorage.setItem(
"SETTINGS_VERSION",
LATEST_SETTINGS_VERSION.toString(),
);
setIsUpToDate(true);
}
},
});
};

View File

@@ -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),
});
};

View File

@@ -0,0 +1,51 @@
import { useQueries, useQuery } from "@tanstack/react-query";
import axios from "axios";
import React from "react";
import { useSelector } from "react-redux";
import { openHands } from "#/api/open-hands-axios";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { RootState } from "#/store";
import { useConversation } from "#/context/conversation-context";
export const useActiveHost = () => {
const { curAgentState } = useSelector((state: RootState) => state.agent);
const [activeHost, setActiveHost] = React.useState<string | null>(null);
const { conversationId } = useConversation();
const { data } = useQuery({
queryKey: [conversationId, "hosts"],
queryFn: async () => {
const response = await openHands.get<{ hosts: string[] }>(
`/api/conversations/${conversationId}/web-hosts`,
);
return { hosts: Object.keys(response.data.hosts) };
},
enabled: !RUNTIME_INACTIVE_STATES.includes(curAgentState),
initialData: { hosts: [] },
});
const apps = useQueries({
queries: data.hosts.map((host) => ({
queryKey: [conversationId, "hosts", host],
queryFn: async () => {
try {
await axios.get(host);
return host;
} catch (e) {
return "";
}
},
refetchInterval: 3000,
})),
});
const appsData = apps.map((app) => app.data);
React.useEffect(() => {
const successfulApp = appsData.find((app) => app);
setActiveHost(successfulApp || "");
}, [appsData]);
return { activeHost };
};

View File

@@ -1,7 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "#/context/auth-context";
import { useConfig } from "./use-config";
import { retrieveGitHubAppInstallations } from "#/api/github";
import OpenHands from "#/api/open-hands";
export const useAppInstallations = () => {
const { data: config } = useConfig();
@@ -9,10 +9,7 @@ export const useAppInstallations = () => {
return useQuery({
queryKey: ["installations", gitHubToken, config?.GITHUB_CLIENT_ID],
queryFn: async () => {
const data = await retrieveGitHubAppInstallations();
return data;
},
queryFn: OpenHands.getGitHubUserInstallationIds,
enabled:
!!gitHubToken &&
!!config?.GITHUB_CLIENT_ID &&

View File

@@ -1,9 +1,9 @@
import { useQuery } from "@tanstack/react-query";
import React from "react";
import posthog from "posthog-js";
import { retrieveGitHubUser } from "#/api/github";
import { useAuth } from "#/context/auth-context";
import { useConfig } from "./use-config";
import OpenHands from "#/api/open-hands";
export const useGitHubUser = () => {
const { gitHubToken, setUserId } = useAuth();
@@ -11,7 +11,7 @@ export const useGitHubUser = () => {
const user = useQuery({
queryKey: ["user", gitHubToken],
queryFn: retrieveGitHubUser,
queryFn: OpenHands.getGitHubUser,
enabled: !!gitHubToken && !!config?.APP_MODE,
retry: false,
});

View File

@@ -1,17 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { retrieveLatestGitHubCommit } from "#/api/github";
import { useAuth } from "#/context/auth-context";
interface UseLatestRepoCommitConfig {
repository: string | null;
}
export const useLatestRepoCommit = (config: UseLatestRepoCommitConfig) => {
const { gitHubToken } = useAuth();
return useQuery({
queryKey: ["latest_commit", gitHubToken, config.repository],
queryFn: () => retrieveLatestGitHubCommit(config.repository!),
enabled: !!gitHubToken && !!config.repository,
});
};

View File

@@ -1,12 +1,11 @@
import { useQuery } from "@tanstack/react-query";
import { searchPublicRepositories } from "#/api/github";
import OpenHands from "#/api/open-hands";
export function useSearchRepositories(query: string) {
return useQuery({
queryKey: ["repositories", query],
queryFn: () => searchPublicRepositories(query, 3),
queryFn: () => OpenHands.searchGitHubRepositories(query, 3),
enabled: !!query,
select: (data) => data.map((repo) => ({ ...repo, is_public: true })),
initialData: [],
});
}

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,
};
}
@@ -37,7 +39,6 @@ export const useSettings = () => {
const query = useQuery({
queryKey: ["settings"],
queryFn: getSettingsQueryFn,
initialData: DEFAULT_SETTINGS,
});
React.useEffect(() => {

View File

@@ -1,11 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
export const useUserConversation = (cid: string | null) =>
useQuery({
queryKey: ["user", "conversation", cid],
queryFn: () => OpenHands.getConversation(cid!),
enabled: MULTI_CONVO_UI_IS_ENABLED && !!cid,
enabled: !!cid,
retry: false,
});

View File

@@ -1,10 +1,10 @@
// Sometimes we ship major changes, like a new default agent.
import React from "react";
import { useSettingsUpToDate } from "#/context/settings-up-to-date-context";
import { useCurrentSettings } from "#/context/settings-context";
import {
DEFAULT_SETTINGS,
getCurrentSettingsVersion,
DEFAULT_SETTINGS,
getLocalStorageSettings,
} from "#/services/settings";
import { useSaveSettings } from "./mutation/use-save-settings";
@@ -12,7 +12,7 @@ import { useSaveSettings } from "./mutation/use-save-settings";
// In this case, we may want to override a previous choice made by the user.
export const useMaybeMigrateSettings = () => {
const { mutateAsync: saveSettings } = useSaveSettings();
const { isUpToDate } = useSettingsUpToDate();
const { isUpToDate } = useCurrentSettings();
const maybeMigrateSettings = async () => {
const currentVersion = getCurrentSettingsVersion();

View File

@@ -131,7 +131,9 @@ export const useTerminal = ({
content = content.replaceAll(secret, "*".repeat(10));
});
terminal.current?.writeln(parseTerminalOutput(content));
terminal.current?.writeln(
parseTerminalOutput(content.replaceAll("\n", "\r\n").trim()),
);
if (type === "output") {
terminal.current.write(`\n$ `);

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