Compare commits

..

125 Commits

Author SHA1 Message Date
openhands
93287ef9ac Fix microagent test filenames to match expected names
- Change test filenames from 'test.md' to match expected microagent names
- Use 'default.md' for tests expecting 'default' name
- Use 'custom_name.md' for test expecting 'custom_name' name
- Use 'test_agent.md' for test expecting 'test_agent' name
- This properly tests the filename-based naming behavior
2025-06-24 14:20:34 +00:00
openhands
e70595f46f Fix microagent tests and remove debug prints
- Update test assertions to expect filename as microagent name instead of 'default'
- Remove debug print statements from microagent.py
- Revert pytest-asyncio dependency addition as requested
- All tests now pass with the new filename-based naming behavior
2025-06-24 14:16:20 +00:00
openhands
1d3ff66987 Fix failing tests: add missing newlines and pytest-asyncio dependency
- Add missing newlines at end of microagent files (fixed by pre-commit)
- Add pytest-asyncio dependency to fix async test execution
- All non-Docker tests now pass
2025-06-24 14:01:12 +00:00
Xingyao Wang
1a95f86802 fix all remaining issue' 2025-06-23 17:49:02 -04:00
Xingyao Wang
eee12bfd94 fix test 2025-06-23 16:09:32 -04:00
Xingyao Wang
8c2d4dbe8b Merge branch 'main' into update-microagent-docs 2025-06-23 14:22:56 -04:00
மனோஜ்குமார் பழனிச்சாமி
f5ae1759b6 Add model name (#8718) 2025-06-23 14:21:47 -04:00
Ikuo Matsumura
9ec94737ed feat(cli): Add vi mode support (#9287)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-06-23 17:39:38 +00:00
llamantino
63c7815823 docs: rewrite local LLMs page (#9307) 2025-06-24 01:20:03 +08:00
baii
95ae47307c Fix the issue where the shttp_services configuration from config.toml fails to load correctly. (#9175) 2025-06-23 13:02:56 -04:00
Graham Neubig
035050252b Better timeout prompt (#9140)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-06-23 16:42:15 +00:00
Tommaso Bendinelli
5b48aee0c9 Fix openhands.core.exceptions.FunctionCallConversionError fn_call_converter for GPT-o4-mini when the agent generates images (#9152)
Co-authored-by: tommaso <tommaso@t7144.csem.local>
2025-06-23 16:01:36 +00:00
Xingyao Wang
1a89dbb738 docs: Add Success Stories tab to documentation (#9120)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-23 23:39:39 +08:00
Rohit Malhotra
bba62c26fd Make sandbox api key configurable via user settings (#8803)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-23 11:25:10 -04:00
Graham Neubig
9b4ad4e6e3 Fix SambaNova context length exception handling (#9252)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-23 07:06:31 -04:00
Graham Neubig
1e33624951 Simplify max_output_tokens handling in LLM classes (#9296)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-23 06:48:45 -04:00
Graham Neubig
8b90d610c6 Fix CLI model selection to allow custom model names (#9205)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-23 04:03:00 +00:00
mamoodi
834abc0eee More doc updates (#9289) 2025-06-22 22:46:47 -04:00
Tim O'Farrell
c9bb0fc168 Conversation Manager small refactor (#9286)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-22 19:27:03 -06:00
Graham Neubig
5d69e606eb feat: Add Windows PowerShell support to CLI runtime (#9211)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-22 20:17:40 -04:00
Engel Nyst
081880248c Fix lint (#9290) 2025-06-22 13:40:14 -04:00
Chase
4ee269c3f7 Add ability to customize configuration model on per-agent basis (#8576) 2025-06-22 14:43:17 +02:00
Xingyao Wang
711315c3b9 docs: Update documentation based on llamantino feedback (#9119)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-21 21:57:14 -04:00
mamoodi
c2e6244b86 Small doc updates. Fix FAQs (#9270) 2025-06-21 15:52:29 -07:00
Xingyao Wang
a1479adfd3 feat(agent): Add configurable system_prompt_filename to AgentConfig (#9265)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-22 06:21:52 +08:00
dependabot[bot]
99fd3f7bb2 chore(deps): bump ubuntu from 22.04 to 24.04 in /containers/e2b-sandbox (#9042)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 12:23:42 -07:00
dependabot[bot]
c617881b3c chore(deps): bump the version-all group in /frontend with 4 updates (#9234)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 12:22:43 -07:00
dependabot[bot]
7ca3607dcd chore(deps): bump the version-all group with 3 updates (#9256)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-21 12:21:57 -07:00
mamoodi
89999a8e09 Update free credits lines (#9269) 2025-06-21 15:35:04 +00:00
Ray Myers
3d9761df7e Release branch for 0.45.0 (#9264) 2025-06-20 21:14:23 +00:00
Xingyao Wang
ea3c4f9366 Fix(CLI): duplicated Command Action display in CLI (#9260)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-21 04:24:16 +08:00
Graham Neubig
bda0a64a3d Fix empty image URLs in multimodal browsing causing litellm.BadRequestError (#9214)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-20 19:44:11 +00:00
Graham Neubig
8badcb7b35 Fix feedback UI localization in LikertScale component (#9253)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-20 18:50:39 +00:00
Xingyao Wang
078534c2ab Fix httpx deprecation warning during LLM API calls (#9261)
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-20 18:36:31 +00:00
Rohit Malhotra
ba885cd04c Remove Bitbucket login button from SAAS auth modal (#9258)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-20 16:29:57 +00:00
Rohit Malhotra
ee64a6662a (Hotfix): tokens go stale for restarted convos in cloud openhands (#9111)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-20 12:16:42 -04:00
solah soyalp
075ef4db9f Add Japanese translations (#9244) 2025-06-20 00:45:08 +00:00
Xingyao Wang
a526f73ea6 Add FAQ page to documentation (#9132) 2025-06-19 13:37:03 -07:00
Xingyao Wang
516f9fa635 Add o4-mini model and Mistral provider support to OpenHands CLI (#9217)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-19 18:47:27 +00:00
Xingyao Wang
8c5995a5d8 Update citation in README.md (#9243) 2025-06-19 18:01:30 +00:00
dependabot[bot]
afe130f6db chore(deps): bump the version-all group across 1 directory with 15 updates (#9239)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-19 18:48:47 +02:00
Xingyao Wang
cc2f96c6c4 Fix search_events signature mismatches after get_events replacement (#9238)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-19 17:57:44 +02:00
Rohit Malhotra
b7a6190133 Add max_budget_per_task to settings (#8812)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-06-18 20:25:01 -04:00
brettstewart
54af9ff3fe feat(runtime): add kubernetes support (#8814)
Co-authored-by: Corey White <corey.white@ziffdavis.com>
Co-authored-by: luke_schulz <luke.schulz@ziffmedia.com>
2025-06-18 21:25:50 +00:00
Xingyao Wang
ef582a6335 Increase max iterations from 250 to 500 (#9203)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-18 21:32:15 +02:00
Xingyao Wang
0ca3188afa Merge branch 'main' into update-microagent-docs 2025-06-18 14:23:58 -04:00
Xingyao Wang
d5f5e34ead Fix deprecation warnings in OpenHands CLI (#9199)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-19 02:22:55 +08:00
Graham Neubig
91e6d359c2 Update repo.md with better "openhands with openhands" directions (#9216) 2025-06-18 12:38:51 -04:00
Mislav Lukach
a9f26a13a6 feat(chat): support file upload (#8945)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-06-18 20:13:07 +04:00
dependabot[bot]
a92d6904fc chore(deps): bump the version-all group in /frontend with 2 updates (#9215)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 15:28:28 +00:00
dependabot[bot]
306777626f chore(deps): bump the version-all group across 1 directory with 9 updates (#9182)
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-06-18 12:05:15 +00:00
Rohit Malhotra
1807efad0b Add Bitbucket integration documentation for local usage (#9206)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-18 05:56:36 -04:00
Graham Neubig
e074b2d36f Add Bitbucket microagent and backend implementation (#9021)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-06-18 00:04:29 -04:00
Ray Myers
b7efeb11d9 Bump version to 0.44.0 (#9163)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-06-17 21:13:17 +00:00
Graham Neubig
7d0aadf8ed Rename ~/.openhands-state to ~/.openhands (#9135)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-06-17 20:44:52 +00:00
Mislav Lukach
78af1de870 chore(analytics): improve label clarity (#9161)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-06-17 20:33:52 +00:00
llamantino
6a9065960d fix(devcontainer): mark workspace as safe dir (#9136) 2025-06-18 04:22:42 +08:00
Maxim Evtush
653a8a7ce2 Refactor: Improve Consistency in Function Signatures and Regex Usage in compute_ism_pm_score.py (#9145) 2025-06-18 04:22:16 +08:00
Graham Neubig
3591c7a79f Add uvx installation option to CLI documentation (#9186)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-18 04:19:18 +08:00
Ivan Dagelic
bae6bd77f4 fix: daytona runtime sandbox handling (#9187)
Signed-off-by: Ivan Dagelic <dagelic.ivan@gmail.com>
2025-06-18 04:18:46 +08:00
Rohit Malhotra
30c71776e7 [Fix]: Loading microagents for integrations (#9189)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-17 16:16:19 -04:00
Robert Brennan
147ffb7e42 Suppress pydub warning about ffmpeg/avconv not found (#8940)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-17 14:44:32 -04:00
Tim O'Farrell
237037cee9 Fix remote runtime status (#9190) 2025-06-18 02:34:41 +08:00
Xingyao Wang
567af43a71 Fix deprecation warning: Replace get_events with search_events (#9188)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-18 00:54:29 +08:00
Rohit Malhotra
65071550b6 Fix grammar issues in Slack documentation (#9180)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-17 23:53:55 +08:00
Alexander
d81d2f62cb docs: local serving with ollama documented (#8807)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-17 07:18:18 -04:00
Ryan H. Tran
ddaa186971 [GAIA] Add prompt improvement to alleviate solution parsing issue & support Tavily search tools (#9057) 2025-06-17 13:16:50 +07:00
Graham Neubig
e6e0f4673f docs: Add "Running OpenHands with OpenHands" section for recursive development (#9146)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-16 20:57:52 -04:00
Graham Neubig
7d78b65a1a docs: Add Python version requirement to CLI documentation (#9164)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-16 20:14:10 +00:00
openhands
283f503870 Exclude name field in MicroagentMetadata as it's deprecated 2025-06-08 22:07:33 +00:00
openhands
0691e5c0d0 Remove type: field from all microagent markdown files 2025-06-08 19:48:01 +00:00
openhands
fc16da8fd2 Update microagent documentation to clarify that type field is optional 2025-06-08 19:39:17 +00:00
openhands
bd3ff43c67 Remove name field from microagent files 2025-06-08 19:35:06 +00:00
openhands
0fe5b808af Update microagent code to use filename as name when not specified 2025-06-08 19:34:59 +00:00
openhands
6c49686ff0 Add MCP tools documentation and update microagent field requirements 2025-06-08 19:30:21 +00:00
openhands
17212bb2f2 Remove unused fields from microagent code and update all microagent files 2025-06-08 19:26:56 +00:00
openhands
9d9f931e95 Remove unused fields from microagent documentation and example 2025-06-08 19:23:47 +00:00
openhands
6fe9680474 Consolidate task microagent documentation into keyword-triggered microagents 2025-06-08 19:19:44 +00:00
Xingyao Wang
53c80d1c92 Merge branch 'main' into update-microagent-docs 2025-06-08 15:17:37 -04:00
openhands
401262f353 Update documentation for task microagents with user input support 2025-06-08 19:15:31 +00:00
Xingyao Wang
58845b01a3 rename more files 2025-06-08 14:30:37 -04:00
Xingyao Wang
469d184157 address engel comment 2025-06-08 14:28:22 -04:00
Xingyao Wang
4837c4dc74 Update microagents/get_test_to_pass.md
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-09 02:24:23 +08:00
Xingyao Wang
6763f21cc3 Merge branch 'main' into add-back-microagents 2025-06-07 16:47:00 -04:00
Xingyao Wang
32e610ac1d revert unnecessary change 2025-06-07 16:30:55 -04:00
Xingyao Wang
85c65391ca revert changes 2025-06-03 13:53:27 -04:00
Xingyao Wang
c444dbfbbf remove fe changes 2025-06-03 12:04:37 -04:00
Xingyao Wang
dd988d0f14 revert fe 2025-06-03 12:03:00 -04:00
Xingyao Wang
6f1a74e286 merge main 2025-06-03 11:37:51 -04:00
Xingyao Wang
7b956b6103 revert docs to look like main 2025-06-03 11:35:57 -04:00
openhands
34b097115d Fix linting issues in frontend and Python code 2025-05-19 01:39:48 +00:00
openhands
3e4ab4f379 Fix docstring formatting in KnowledgeMicroagent class 2025-05-19 01:29:23 +00:00
openhands
54cd9f7e44 Fix unlocalized strings in microagent-dropdown.tsx 2025-05-19 01:26:33 +00:00
openhands
802b765f98 Add microagent button and dropdown to trajectory actions 2025-05-17 12:05:13 +00:00
openhands
18c88f99ff Merge from main to resolve conflicts 2025-05-17 06:56:11 +00:00
openhands
f3934be07b Fix microagent suggestions using tippy.js for better popup handling 2025-05-12 12:55:00 +00:00
openhands
6ce9f49d1e Fix linting issues in TipTap editor component 2025-05-12 11:06:15 +00:00
openhands
fc07622b20 Implement microagent suggestions using TipTap 2025-05-12 11:00:08 +00:00
Xingyao Wang
da935f9d8f Merge branch 'main' into add-back-microagents 2025-05-03 00:04:17 +08:00
openhands
642cc52a1a Fix linting issues in handlers.ts 2025-05-02 13:06:21 +00:00
openhands
4c361ab9e5 Add mock handler for microagents endpoint 2025-05-02 09:23:25 +00:00
openhands
5dfa1bb6eb Fix microagent suggestions UI and TypeScript errors 2025-05-02 09:21:15 +00:00
Xingyao Wang
a07cf972a5 Merge commit '6032d2620d6ec252d3c80695a6de1fc88da9c87a' into add-back-microagents 2025-05-02 09:03:17 +00:00
openhands
f2e3bc3254 Fix microagent suggestions feature 2025-05-02 08:52:19 +00:00
openhands
3790ec7d60 Add tests for microagent suggestions component 2025-05-02 03:31:41 +00:00
openhands
3c0719309e Add microagent suggestions feature to chat input 2025-05-02 02:57:57 +00:00
Xingyao Wang
0236e0943e fix test 2025-05-02 02:09:27 +00:00
Xingyao Wang
cd464c0022 rename files 2025-05-01 10:38:04 +08:00
Xingyao Wang
4519a7f4f3 fix test 2025-05-01 02:29:52 +00:00
Xingyao Wang
fdc591330b add remain 2025-05-01 02:25:38 +00:00
Xingyao Wang
98e454e82c fix lint and missing imports 2025-05-01 02:25:24 +00:00
Xingyao Wang
e088d2d24a simplify microagent 2025-05-01 02:13:46 +00:00
Xingyao Wang
58c574af1e revert changes 2025-05-01 02:13:00 +00:00
Xingyao Wang
405f0069f8 revert some changes 2025-05-01 02:03:06 +00:00
Xingyao Wang
f26d770d03 remove hardcoded last line 2025-05-01 02:01:51 +00:00
Xingyao Wang
bf2c3de219 cleanup tests 2025-04-30 11:11:23 +08:00
Xingyao Wang
7c35ce16e5 Merge branch 'main' into add-back-microagents 2025-04-30 11:07:17 +08:00
Xingyao Wang
f4024ccd94 Update microagents/update_pr_description.md
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-04-30 10:43:37 +08:00
Xingyao Wang
b55bfed831 Update microagents/address_pr_comments.md
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-04-30 10:38:22 +08:00
OpenHands Bot
cb0994027f 🤖 Auto-fix Python linting issues 2025-04-29 16:02:04 +00:00
openhands
bcc9bd0b9a Move task microagent tests to test_microagent_task.py 2025-04-29 02:12:14 +00:00
openhands
6c144e6b5a Add back microagent files with special handling for user inputs 2025-04-29 02:06:42 +00:00
openhands
e90b841b0d Update microagent files to match original ones with added triggers and variable prompts 2025-04-29 01:48:10 +00:00
openhands
a1e6ed4dff Add special handling for microagents that require user input 2025-04-29 01:47:18 +00:00
openhands
ad6311d3cd Add back microagent files and add special handling for user input variables 2025-04-29 01:33:23 +00:00
252 changed files with 9514 additions and 3899 deletions

View File

@@ -1,5 +1,9 @@
#!/bin/bash
# Mark the current repository as safe for Git to prevent "dubious ownership" errors,
# which can occur in containerized environments when directory ownership doesn't match the current user.
git config --global --add safe.directory "$(realpath .)"
# Install `nc`
sudo apt update && sudo apt install netcat -y

View File

@@ -45,6 +45,13 @@ body:
description: What version of OpenHands are you using?
placeholder: ex. 0.9.8, main, etc.
- type: input
id: model-name
attributes:
label: Model Name
description: What model are you using?
placeholder: ex. gpt-4o, claude-3-5-sonnet, openrouter/deepseek-r1, etc.
- type: dropdown
id: os
attributes:

View File

@@ -5,6 +5,14 @@ This repository contains the code for OpenHands, an automated AI software engine
To set up the entire repo, including frontend and backend, run `make build`.
You don't need to do this unless the user asks you to, or if you're trying to run the entire application.
## Running OpenHands with OpenHands:
To run the full application to debug issues:
```bash
export INSTALL_DOCKER=0
export RUNTIME=local
make build && make run FRONTEND_PORT=12000 FRONTEND_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0 &> /tmp/openhands-log.txt &
```
IMPORTANT: Before making any changes to the codebase, ALWAYS run `make install-pre-commit-hooks` to ensure pre-commit hooks are properly installed.
Before pushing any changes, you MUST ensure that any lint errors or simple test errors have been fixed.

View File

@@ -103,6 +103,29 @@ components or interface enhancements.
make start-frontend
```
### 5. Running OpenHands with OpenHands
You can use OpenHands to develop and improve OpenHands itself! This is a powerful way to leverage AI assistance for contributing to the project.
#### Quick Start
1. **Build and run OpenHands:**
```bash
export INSTALL_DOCKER=0
export RUNTIME=local
make build && make run
```
2. **Access the interface:**
- Local development: http://localhost:3001
- Remote/cloud environments: Use the appropriate external URL
3. **Configure for external access (if needed):**
```bash
# For external access (e.g., cloud environments)
make run FRONTEND_PORT=12000 FRONTEND_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0
```
### 6. LLM Debugging
If you encounter any issues with the Language Model (LM) or you're simply curious, export DEBUG=1 in the environment and restart the backend.
@@ -136,7 +159,7 @@ poetry run pytest ./tests/unit/test_*.py
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.44-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.45-nikolaik`
## Develop inside Docker container

View File

@@ -12,6 +12,7 @@ DEFAULT_MODEL = "gpt-4o"
CONFIG_FILE = config.toml
PRE_COMMIT_CONFIG_PATH = "./dev_config/python/.pre-commit-config.yaml"
PYTHON_VERSION = 3.12
KIND_CLUSTER_NAME = "local-hands"
# ANSI color codes
GREEN=$(shell tput -Txterm setaf 2)
@@ -199,6 +200,40 @@ lint:
@$(MAKE) -s lint-frontend
@$(MAKE) -s lint-backend
kind:
@echo "$(YELLOW)Checking if kind is installed...$(RESET)"
@if ! command -v kind > /dev/null; then \
echo "$(RED)kind is not installed. Please install kind with `brew install kind` to continue$(RESET)"; \
exit 1; \
else \
echo "$(BLUE)kind $(shell kind version) is already installed.$(RESET)"; \
fi
@echo "$(YELLOW)Checking if kind cluster '$(KIND_CLUSTER_NAME)' already exists...$(RESET)"
@if kind get clusters | grep -q "^$(KIND_CLUSTER_NAME)$$"; then \
echo "$(BLUE)Kind cluster '$(KIND_CLUSTER_NAME)' already exists.$(RESET)"; \
kubectl config use-context kind-$(KIND_CLUSTER_NAME); \
else \
echo "$(YELLOW)Creating kind cluster '$(KIND_CLUSTER_NAME)'...$(RESET)"; \
kind create cluster --name $(KIND_CLUSTER_NAME) --config kind/cluster.yaml; \
fi
@echo "$(YELLOW)Checking if mirrord is installed...$(RESET)"
@if ! command -v mirrord > /dev/null; then \
echo "$(RED)mirrord is not installed. Please install mirrord with `brew install metalbear-co/mirrord/mirrord` to continue$(RESET)"; \
exit 1; \
else \
echo "$(BLUE)mirrord $(shell mirrord --version) is already installed.$(RESET)"; \
fi
@echo "$(YELLOW)Installing k8s mirrord resources...$(RESET)"
@kubectl apply -f kind/manifests
@echo "$(GREEN)Mirrord resources installed successfully.$(RESET)"
@echo "$(YELLOW)Waiting for Mirrord pod to be ready.$(RESET)"
@sleep 5
@kubectl wait --for=condition=Available deployment/ubuntu-dev
@echo "$(YELLOW)Waiting for Nginx to be ready.$(RESET)"
@kubectl -n ingress-nginx wait --for=condition=Available deployment/ingress-nginx-controller
@echo "$(YELLOW)Running make run inside of mirrord.$(RESET)"
@mirrord exec --target deployment/ubuntu-dev -- make run
test-frontend:
@echo "$(YELLOW)Running tests for frontend...$(RESET)"
@cd frontend && npm run test
@@ -333,3 +368,4 @@ help:
# Phony targets
.PHONY: build check-dependencies check-system check-python check-npm check-nodejs check-docker check-poetry install-python-dependencies install-frontend-dependencies install-pre-commit-hooks lint-backend lint-frontend lint test-frontend test build-frontend start-backend start-frontend _run_setup run run-wsl setup-config setup-config-prompts setup-config-basic openhands-cloud-run docker-dev docker-run clean help
.PHONY: kind

View File

@@ -48,7 +48,7 @@ Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or [sign up for
## ☁️ OpenHands Cloud
The easiest way to get started with OpenHands is on [OpenHands Cloud](https://app.all-hands.dev),
which comes with $50 in free credits for new users.
which comes with $20 in free credits for new users.
## 💻 Running OpenHands Locally
@@ -62,19 +62,21 @@ system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.44-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.44-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.44
docker.all-hands.dev/all-hands-ai/openhands:0.45
```
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
When you open the application, you'll be asked to choose an LLM provider and add an API key.
@@ -145,13 +147,12 @@ For a list of open source projects and licenses used in OpenHands, please see ou
## 📚 Cite
```
@misc{openhands,
title={{OpenHands: An Open Platform for AI Software Developers as Generalist Agents}},
author={Xingyao Wang and Boxuan Li and Yufan Song and Frank F. Xu and Xiangru Tang and Mingchen Zhuge and Jiayi Pan and Yueqi Song and Bowen Li and Jaskirat Singh and Hoang H. Tran and Fuqiang Li and Ren Ma and Mingzhang Zheng and Bill Qian and Yanjun Shao and Niklas Muennighoff and Yizhe Zhang and Binyuan Hui and Junyang Lin and Robert Brennan and Hao Peng and Heng Ji and Graham Neubig},
year={2024},
eprint={2407.16741},
archivePrefix={arXiv},
primaryClass={cs.SE},
url={https://arxiv.org/abs/2407.16741},
@inproceedings{
wang2025openhands,
title={OpenHands: An Open Platform for {AI} Software Developers as Generalist Agents},
author={Xingyao Wang and Boxuan Li and Yufan Song and Frank F. Xu and Xiangru Tang and Mingchen Zhuge and Jiayi Pan and Yueqi Song and Bowen Li and Jaskirat Singh and Hoang H. Tran and Fuqiang Li and Ren Ma and Mingzhang Zheng and Bill Qian and Yanjun Shao and Niklas Muennighoff and Yizhe Zhang and Binyuan Hui and Junyang Lin and Robert Brennan and Hao Peng and Heng Ji and Graham Neubig},
booktitle={The Thirteenth International Conference on Learning Representations},
year={2025},
url={https://openreview.net/forum?id=OJd3ayDDoF}
}
```

View File

@@ -51,19 +51,21 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.44-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.44-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.44
docker.all-hands.dev/all-hands-ai/openhands:0.45
```
> **注意**: 如果您在0.44版本之前使用过OpenHands您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands
打开应用程序时您将被要求选择一个LLM提供商并添加API密钥。

60
README_JA.md Normal file
View File

@@ -0,0 +1,60 @@
<a name="readme-top"></a>
<div align="center">
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
<h1 align="center">OpenHands: コードを減らして、もっと作ろう</h1>
</div>
<div align="center">
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Slackコミュニティに参加"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Discordコミュニティに参加"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="クレジット"></a>
<br/>
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="ドキュメントを見る"></a>
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Arxiv論文"></a>
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="評価ベンチマークスコア"></a>
<hr>
</div>
OpenHands旧OpenDevinへようこそ。これはAIが駆動するソフトウェア開発エージェントのプラットフォームです。
OpenHandsのエージェントは人間の開発者ができることは何でもこなします。コードを修正し、コマンドを実行し、ウェブを閲覧し、APIを呼び出し、StackOverflowからコードスニペットをコピーすることさえできます。
詳細は[docs.all-hands.dev](https://docs.all-hands.dev)をご覧いただくか、[OpenHands Cloud](https://app.all-hands.dev)に登録して始めましょう。
> [!IMPORTANT]
> 仕事でOpenHandsを使っていますかぜひお話を聞かせてください。[こちらの短いフォーム](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)にご記入いただき、Design Partnerプログラムにご参加ください。商用機能の早期アクセスや製品ロードマップへのフィードバックの機会を提供します。
![アプリのスクリーンショット](./docs/static/img/screenshot.png)
## ☁️ OpenHands Cloud
OpenHandsを始める最も簡単な方法は[OpenHands Cloud](https://app.all-hands.dev)を利用することです。新規ユーザーには50ドル分の無料クレジットが付与されます。
## 💻 OpenHandsをローカルで実行する
OpenHandsはDockerを利用してローカル環境でも実行できます。システム要件や詳細については[Running OpenHands](https://docs.all-hands.dev/usage/installation)ガイドをご覧ください。
> [!WARNING]
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.45
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
OpenHandsは[http://localhost:3000](http://localhost:3000)で起動します!

View File

@@ -64,7 +64,7 @@
#max_budget_per_task = 0.0
# Maximum number of iterations
#max_iterations = 250
#max_iterations = 500
# Path to mount the workspace in the sandbox
#workspace_mount_path_in_sandbox = "/workspace"
@@ -415,3 +415,47 @@ type = "noop"
# Configuration for the evaluation, please refer to the specific evaluation
# plugin for the available options
##############################################################################
########################### Kubernetes #######################################
# Kubernetes configuration when using the Kubernetes runtime
##############################################################################
[kubernetes]
# The Kubernetes namespace to use for OpenHands resources
#namespace = "default"
# Domain for ingress resources
#ingress_domain = "localhost"
# Size of the persistent volume claim
#pvc_storage_size = "2Gi"
# Storage class for persistent volume claims
#pvc_storage_class = "standard"
# CPU request for runtime pods
#resource_cpu_request = "1"
# Memory request for runtime pods
#resource_memory_request = "1Gi"
# Memory limit for runtime pods
#resource_memory_limit = "2Gi"
# Optional name of image pull secret for private registries
#image_pull_secret = ""
# Optional name of TLS secret for ingress
#ingress_tls_secret = ""
# Optional node selector key for pod scheduling
#node_selector_key = ""
# Optional node selector value for pod scheduling
#node_selector_val = ""
# Optional YAML string defining pod tolerations
#tolerations_yaml = ""
# Run the runtime sandbox container in privileged mode for use with docker-in-docker
#privileged = false

View File

@@ -44,7 +44,7 @@ ENV WORKSPACE_BASE=/opt/workspace_base
ENV OPENHANDS_BUILD_VERSION=$OPENHANDS_BUILD_VERSION
ENV SANDBOX_USER_ID=0
ENV FILE_STORE=local
ENV FILE_STORE_PATH=/.openhands-state
ENV FILE_STORE_PATH=/.openhands
RUN mkdir -p $FILE_STORE_PATH
RUN mkdir -p $WORKSPACE_BASE

View File

@@ -12,7 +12,7 @@ services:
- SANDBOX_API_HOSTNAME=host.docker.internal
- DOCKER_HOST_ADDR=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.44-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.45-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -1,4 +1,4 @@
FROM ubuntu:22.04
FROM ubuntu:24.04
# install basic packages
RUN apt-get update && apt-get install -y \

View File

@@ -7,6 +7,7 @@ repos:
- id: end-of-file-fixer
exclude: docs/modules/python
- id: check-yaml
args: ["--allow-multiple-documents"]
- id: debug-statements
- repo: https://github.com/tox-dev/pyproject-fmt

View File

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

17
docs/README_JA.md Normal file
View File

@@ -0,0 +1,17 @@
# セットアップ
```
npm install -g mint
```
または
```
yarn global add mint
```
# プレビュー
```
mint dev
```

View File

@@ -26,6 +26,7 @@
"usage/installation",
"usage/getting-started",
"usage/key-features",
"usage/faqs",
{
"group": "OpenHands Cloud",
"pages": [
@@ -43,7 +44,7 @@
]
},
{
"group": "Running OpenHands on Your Own",
"group": "Run OpenHands on Your Own",
"pages": [
"usage/local-setup",
"usage/how-to/gui-mode",
@@ -103,8 +104,9 @@
]
},
{
"group": "Customization",
"group": "Customizations & Settings",
"pages": [
"usage/common-settings",
"usage/prompting/repository",
{
"group": "Microagents",
@@ -149,6 +151,12 @@
}
]
},
{
"tab": "Success Stories",
"pages": [
"success-stories/index"
]
},
{
"tab": "API Reference",
"openapi": "/openapi.json"

View File

@@ -0,0 +1,217 @@
---
title: "Success Stories"
description: "Real-world examples of what you can achieve with OpenHands"
---
Discover how developers and teams are using OpenHands to automate their software development workflows. From quick fixes to complex projects, see what's possible with AI-powered development assistance.
Check out the [#success-stories](https://www.linen.dev/s/openhands/c/success-stories) channel on our Slack for more!
<Update label="2025-06-13 OpenHands helps frontline support" description="@Joe Pelletier">
## One of the cool things about OpenHands, and especially the Slack Integration, is the ability to empower folks who are on the front lines with customers.
For example, often times Support and Customer Success teams will field bug reports, doc questions, and other nits from customers. They tend to have few options to deal with this, other than file a feedback ticket with product teams and hope it gets prioritized in an upcoming sprint.
Instead, with tools like OpenHands and the Slack integration, they can request OpenHands to make fixes proactively and then have someone on the engineering team (like a lead engineer, a merge engineer, or even technical product manager) review the PR and approve it — thus reducing the cycle time for quick wins from weeks to just a few hours.
Here's how we do that with the OpenHands project:
<iframe
width="560"
height="560"
src="https://www.linen.dev/s/openhands/t/29118545/seems-mcp-config-from-config-toml-is-being-overwritten-hence#629f8e2b-cde8-427e-920c-390557a06cc9"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
[Original Slack thread](https://www.linen.dev/s/openhands/t/29124350/one-of-the-cool-things-about-openhands-and-especially-the-sl#25029f37-7b0d-4535-9187-83b3e06a4011)
</Update>
<Update label="2025-06-13 Ask OpenHands to show me some love" description="@Graham Neubig">
## Asked openhands to “show me some love” and...
Asked openhands to “show me some love” and it coded up this app for me, actually kinda genuinely feel loved
<video
controls
autoplay
className="w-full aspect-video"
src="/success-stories/stories/2025-06-13-show-love/v1.mp4"
></video>
[Original Slack thread](https://www.linen.dev/s/openhands/t/29100731/asked-openhands-to-show-me-some-love-and-it-coded-up-this-ap#1e08af6b-b7d5-4167-8a53-17e6806555e0)
</Update>
<Update label="2025-06-11 OpenHands does 100% of my infra IAM research for me" description="@Xingyao Wang">
## Now, OpenHands does 100% of my infra IAM research for me
Got an IAM error on GCP? Send a screenshot to OH... and it just works!!!
Can't imagine going back to the early days without OH: I'd spend an entire afternoon figuring how to get IAM right
[Original Slack thread](https://www.linen.dev/s/openhands/t/29100732/now-openhands-does-100-of-my-infra-iam-research-for-me-sweat#20482a73-4e2e-4edd-b6d1-c9e8442fccd1)
![](/success-stories/stories/2025-06-11-infra-iam/s1.png)
![](/success-stories/stories/2025-06-11-infra-iam/s2.png)
</Update>
<Update label="2025-06-08 OpenHands builds an interactive map for me" description="@Rodrigo Argenton Freire (ODLab)">
## Very simple example, but baby steps....
I am a professor of architecture and urban design. We built, me and some students, an interactive map prototype to help visitors and new students to find important places in the campus. Considering that we lack a lot of knowledge in programming, that was really nice to build and a smooth process.
We first created the main components with all-hands and then adjusted some details locally. Definitely, saved us a lot of time and money.
That's a prototype but we will have all the info by tuesday.
https://buriti-emau.github.io/Mapa-UFU/
[Original Slack thread](https://www.linen.dev/s/openhands/t/29100736/very-simple-example-but-baby-steps-i-am-a-professor-of-archi#8f2e3f3f-44e6-44ea-b9a8-d53487470179)
![](/success-stories/stories/2025-06-08-map/s1.png)
</Update>
<Update label="2025-06-06 Web Search Saves the Day" description="@Ian Walker">
## Tavily adapter helps solve persistent debugging issue
Big congratulations to the new [Tavily adapter](https://www.all-hands.dev/blog/building-a-provably-versatile-agent)... OpenHands and I have been beavering away at a Lightstreamer client library for most of this week but were getting a persistent (and unhelpful) "unexpected error" from the server.
Coming back to the problem today, after trying several unsuccessful fixes prompted by me, OH decided all by itself to search the web, and found the cause of the problem (of course it was simply CRLF line endings...). I was on the verge of giving up - good thing OH has more stamina than me!
This demonstrates how OpenHands' web search capabilities can help solve debugging issues that would otherwise require extensive manual research.
<iframe
width="560"
height="560"
src="https://www.linen.dev/s/openhands/t/29100737/big-congratulations-to-the-new-tavily-adapter-openhands-and-#87b027e5-188b-425e-8aa9-719dcb4929f4"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
[Original Slack thread](https://www.linen.dev/s/openhands/t/29100737/big-congratulations-to-the-new-tavily-adapter-openhands-and-#76f1fb26-6ef7-4709-b9ea-fb99105e47e4)
</Update>
<Update label="2025-06-05 OpenHands updates my personal website for a new paper" description="@Xingyao Wang">
## I asked OpenHands to update my personal website for the "OpenHands Versa" paper.
It is an extremely trivial task: You just need to browse to arxiv, copy the author names, format them for BibTeX, and then modify the papers.bib file. But now I'm getting way too lazy to even open my IDE and actually do this one-file change!
[Original Tweet/X thread](https://x.com/xingyaow_/status/1930796287919542410)
[Original Slack thread](https://www.linen.dev/s/openhands/t/29100738/i-asked-openhands-to-update-my-personal-website-for-the-open#f0324022-b12b-4d34-b12b-bdbc43823f69)
</Update>
<Update label="2025-06-02 OpenHands makes an animated gif of swe-bench verified scores over time" description="@Graham Neubig">
## I asked OpenHands to make an animated gif of swe-bench verified scores over time.
It took a bit of prompting but ended up looking pretty nice I think
<video width="560" height="315" autoPlay loop muted src="/success-stories/stories/2025-06-02-swebench-score/s1.mp4"></video>
[Original Slack thread](https://www.linen.dev/s/openhands/t/29100744/i-asked-openhands-to-make-an-animated-gif-of-swe-bench-verif#fb3b82c9-6222-4311-b97b-b2ac1cfe6dff)
</Update>
<Update label="2025-05-30 AWS Troubleshooting" description="@Graham Neubig">
## Quick AWS security group fix
I really don't like trying to fix issues with AWS, especially security groups and other finicky things like this. But I started up an instance and wasn't able to ssh in. So I asked OpenHands:
> Currently, the following ssh command is timing out:
>
> $ ssh -i gneubig.pem ubuntu@XXX.us-east-2.compute.amazonaws.com
> ssh: connect to host XXX.us-east-2.compute.amazonaws.com port 22: Operation timed out
>
> Use the provided AWS credentials to take a look at i-XXX and examine why
And 2 minutes later I was able to SSH in!
This shows how OpenHands can quickly diagnose and fix AWS infrastructure issues that would normally require manual investigation.
[Original Slack thread](https://www.linen.dev/s/openhands/t/29100747/i-really-don-t-like-trying-to-fix-issues-with-aws-especially#d92a66d2-3bc1-4467-9d09-dc983004d083)
</Update>
<Update label="2025-05-04 Chrome Extension Development" description="@Xingyao Wang">
## OpenHands builds Chrome extension for GitHub integration
I asked OpenHands to write a Chrome extension based on our [OpenHands Cloud API](https://docs.all-hands.dev/modules/usage/cloud/cloud-api). Once installed, you can now easily launch an OpenHands cloud session from your GitHub webpage/PR!
This demonstrates OpenHands' ability to create browser extensions and integrate with external APIs, enabling seamless workflows between GitHub and OpenHands Cloud.
![Chrome extension](/success-stories/stories/2025-05-04-chrome-extension/s1.png)
![Chrome extension](/success-stories/stories/2025-05-04-chrome-extension/s2.png)
[GitHub Repository](https://github.com/xingyaoww/openhands-chrome-extension)
[Original Slack thread](https://www.linen.dev/s/openhands/t/29100755/i-asked-openhands-to-write-a-chrome-extension-based-on-our-h#88f14b7f-f8ff-40a6-83c2-bd64e95924c5)
</Update>
<Update label="2025-04-11 Visual UI Testing" description="@Xingyao Wang">
## OpenHands tests UI automatically with visual browsing
Thanks to visual browsing -- OpenHands can actually test some simple UI by serving the website, clicking the button in the browser and looking at screenshots now!
Prompt is just:
```
I want to create a Hello World app in Javascript that:
* Displays Hello World in the middle.
* Has a button that when clicked, changes the greeting with a bouncing animation to fun versions of Hello.
* Has a counter for how many times the button has been clicked.
* Has another button that changes the app's background color.
```
Eager-to-work Sonnet 3.7 will test stuff for you without you asking!
This showcases OpenHands' visual browsing capabilities, enabling it to create, serve, and automatically test web applications through actual browser interactions and screenshot analysis.
![Visual UI testing](/success-stories/stories/2025-04-11-visual-ui/s1.png)
[Original Slack thread](https://www.linen.dev/s/openhands/t/29100764/thanks-to-u07k0p3bdb9-s-visual-browsing-openhands-can-actual#21beb9bc-1a04-4272-87e9-4d3e3b9925e7)
</Update>
<Update label="2025-03-07 Proactive Error Handling" description="@Graham Neubig">
## OpenHands fixes crashes before you notice them
Interesting story, I asked OpenHands to start an app on port 12000, it showed up on the app pane. I started using the app, and then it crashed... But because it crashed in OpenHands, OpenHands immediately saw the error message and started fixing the problem without me having to do anything. It was already fixing the problem before I even realized what was going wrong.
This demonstrates OpenHands' proactive monitoring capabilities - it doesn't just execute commands, but actively watches for errors and begins remediation automatically, often faster than human reaction time.
</Update>
<Update label="2024-12-03 Creative Design Acceleration" description="@Rohit Malhotra">
## Pair programming for interactive design projects
Used OpenHands as a pair programmer to do heavy lifting for a creative/interactive design project in p5js.
I usually take around 2 days for high fidelity interactions (planning strategy + writing code + circling back with designer), did this in around 5hrs instead with the designer watching curiously the entire time.
This showcases how OpenHands can accelerate creative and interactive design workflows, reducing development time by 75% while maintaining high quality output.
[Original Tweet](https://x.com/rohit_malh5/status/1863995531657425225)
</Update>

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

View File

@@ -1,7 +1,7 @@
---
title: Cloud UI
description: The Cloud UI provides a web interface for interacting with OpenHands. This page explains how to use the
OpenHands Cloud UI.
description: The Cloud UI provides a web interface for interacting with OpenHands. This page provides references on
how to use the OpenHands Cloud UI.
---
## Landing Page
@@ -19,10 +19,12 @@ The landing page is where you can:
The Settings page allows you to:
- [Configure GitHub repository access](/usage/cloud/github-installation#modifying-repository-access) for OpenHands.
- [Install the OpenHands Slack app](/usage/cloud/slack-installation).
- Set application settings like your preferred language, notifications and other preferences.
- Add credits to your account.
- Generate custom secrets.
- Create API keys to work with OpenHands programmatically.
- [Generate custom secrets](/usage/common-settings#secrets-management).
- [Create API keys to work with OpenHands programmatically](/usage/cloud/cloud-api).
- Change your email address.
## Key Features

View File

@@ -35,7 +35,7 @@ You can grant OpenHands access to specific GitHub repositories:
You can modify GitHub repository access at any time by:
- Selecting `Add GitHub repos` on the landing page or
- Visiting the Settings page and selecting `Configure GitHub Repositories` under the `Git` tab
- Visiting the Settings page and selecting `Configure GitHub Repositories` under the `Integrations` tab
## Working With GitHub Repos in Openhands Cloud

View File

@@ -5,7 +5,7 @@ description: This guide walks you through installing the OpenHands Slack app.
## Prerequisites
- Access to OpenHands Cloud
- Access to OpenHands Cloud.
## Installation Steps
@@ -23,15 +23,15 @@ description: This guide walks you through installing the OpenHands Slack app.
<Accordion title="Authorize Slack App (for all Slack workspace members)">
**Make sure your Slack workspace admin/owner has installed OpenHands Slack App first**
**Make sure your Slack workspace admin/owner has installed OpenHands Slack App first.**
Every user in the slack workspace (including admins/owners) must link their Cloud OpenHands account to the OpenHands Slack App. To do this
Every user in the Slack workspace (including admins/owners) must link their OpenHands Cloud account to the OpenHands Slack App. To do this:
1. Visit [integrations settings](https://app.all-hands.dev/settings/integrations) in OpenHands Cloud.
2. Click the button "Install Slack App".
2. Click `Install OpenHands Slack App`.
3. In the top right corner, select the workspace to install the OpenHands Slack app.
4. Review permissions and click allow.
Depending on the workspace settings, you may need approval from your slack admin to authorize the Slack App.
Depending on the workspace settings, you may need approval from your Slack admin to authorize the Slack App.
</Accordion>
@@ -68,6 +68,6 @@ You can mention a repo name when starting a new conversation in the following fo
2. "All-Hands-AI/OpenHands" (e.g `@openhands in All-Hands-AI/OpenHands ...`)
The repo match is case insensitive. If a repo name match is made, it will kick off the conversation.
If the repo name partially matches against, multiple repos, you'll be asked to select a repo from the filtered list.
If the repo name partially matches against multiple repos, you'll be asked to select a repo from the filtered list.
![slack-pro-tip.png](/static/img/slack-pro-tip.png)

View File

@@ -0,0 +1,52 @@
---
title: OpenHands Settings
description: Overview of some of the settings available in OpenHands.
---
## Openhands Cloud vs Running on Your Own
There are some differences between the settings available in OpenHands Cloud and those available when running OpenHands
on your own:
* [OpenHands Cloud settings](/usage/cloud/cloud-ui#settings)
* [Settings available when running on your own](/usage/how-to/gui-mode#settings)
Refer to these pages for more detailed information.
## Secrets Management
OpenHands provides a secrets manager that allows you to securely store and manage sensitive information that can be
accessed by the agent during runtime, such as API keys. These secrets are automatically exported as environment
variables in the agent's runtime environment.
### Accessing the Secrets Manager
In the Settings page, navigate to the `Secrets` tab. Here, you'll see a list of all your existing custom secrets.
### Adding a New Secret
1. Click `Add a new secret`.
2. Fill in the following fields:
- **Name**: A unique identifier for your secret (e.g., `AWS_ACCESS_KEY`). This will be the environment variable name.
- **Value**: The sensitive information you want to store.
- **Description** (optional): A brief description of what the secret is used for, which is also provided to the agent.
3. Click `Add secret` to save.
### Editing a Secret
1. Click the `Edit` button next to the secret you want to modify.
2. You can update the name and description of the secret.
<Note>
For security reasons, you cannot view or edit the value of an existing secret. If you need to change the
value, delete the secret and create a new one.
</Note>
### Deleting a Secret
1. Click the `Delete` button next to the secret you want to remove.
2. Select `Confirm` to delete the secret.
### Using Secrets in the Agent
- All custom secrets are automatically exported as environment variables in the agent's runtime environment.
- You can access them in your code using standard environment variable access methods
(e.g., `os.environ['SECRET_NAME']` in Python).
- Example: If you create a secret named `OPENAI_API_KEY`, you can access it in your code as
`process.env.OPENAI_API_KEY` in JavaScript or `os.environ['OPENAI_API_KEY']` in Python.

96
docs/usage/faqs.mdx Normal file
View File

@@ -0,0 +1,96 @@
---
title: FAQs
description: Frequently asked questions about OpenHands
icon: question
---
## Getting Started
### I'm new to OpenHands. Where should I start?
1. **Quick start**: Use [OpenHands Cloud](/usage/cloud/openhands-cloud) to get started quickly with
[GitHub](/usage/cloud/github-installation), [GitLab](/usage/cloud/gitlab-installation),
and [Slack](/usage/cloud/slack-installation) integrations.
2. **Run on your own**: If you prefer to run it on your own hardware, follow our [Getting Started guide](/usage/local-setup).
3. **First steps**: Complete the [start building tutorial](/usage/getting-started) to learn the basics.
### Can I use OpenHands for production workloads?
OpenHands is meant to be run by a single user on their local workstation. It is not appropriate for multi-tenant
deployments where multiple users share the same instance. There is no built-in authentication, isolation, or scalability.
If you're interested in running OpenHands in a multi-tenant environment, check out the source-available,
commercially-licensed [OpenHands Cloud Helm Chart](https://github.com/all-Hands-AI/OpenHands-cloud).
<Info>
Using OpenHands for work? We'd love to chat! Fill out
[this short form](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
to join our Design Partner program, where you'll get early access to commercial features and the opportunity to provide
input on our product roadmap.
</Info>
## Safety and Security
### It's doing stuff without asking, is that safe?
**Generally yes, but with important considerations.** OpenHands runs all code in a secure, isolated Docker container
(called a "sandbox") that is separate from your host system. However, the safety depends on your configuration:
**What's protected:**
- Your host system files and programs (unless you mount them using [this feature](/usage/runtimes/docker#connecting-to-your-filesystem))
- Host system resources
- Other containers and processes
**Potential risks to consider:**
- The agent can access the internet from within the container.
- If you provide credentials (API keys, tokens), the agent can use them.
- Mounted files and directories can be modified or deleted.
- Network requests can be made to external services.
For detailed security information, see our [Runtime Architecture](/usage/architecture/runtime),
[Security Configuration](/usage/configuration-options#security-configuration),
and [Hardened Docker Installation](/usage/runtimes/docker#hardened-docker-installation) documentation.
## File Storage and Access
### Where are my files stored?
Your files are stored in different locations depending on how you've configured OpenHands:
**Default behavior (no file mounting):**
- Files created by the agent are stored inside the runtime Docker container.
- These files are temporary and will be lost when the container is removed.
- The agent works in the `/workspace` directory inside the runtime container.
**When you mount your local filesystem (following [this](/usage/runtimes/docker#connecting-to-your-filesystem)):**
- Your local files are mounted into the container's `/workspace` directory.
- Changes made by the agent are reflected in your local filesystem.
- Files persist after the container is stopped.
<Warning>
Be careful when mounting your filesystem - the agent can modify or delete any files in the mounted directory.
</Warning>
## Development Tools and Environment
### How do I get the dev tools I need?
OpenHands comes with a basic runtime environment that includes Python and Node.js.
It also has the ability to install any tools it needs, so usually it's sufficient to ask it to set up its environment.
If you would like to set things up more systematically, you can:
- **Use setup.sh**: Add a [setup.sh file](/usage/prompting/repository#setup-script) file to
your repository, which will be run every time the agent starts.
- **Use a custom sandbox**: Use a [custom docker image](/usage/how-to/custom-sandbox-guide) to initialize the sandbox.
### Something's not working. Where can I get help?
1. **Search existing issues**: Check our [GitHub issues](https://github.com/All-Hands-AI/OpenHands/issues) to see if
others have encountered the same problem.
2. **Join our community**: Get help from other users and developers:
- [Slack community](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A)
- [Discord server](https://discord.gg/ESHStjSjD4)
3. **Check our troubleshooting guide**: Common issues and solutions are documented in
[Troubleshooting](/usage/troubleshooting/troubleshooting).
4. **Report bugs**: If you've found a bug, please [create an issue](https://github.com/All-Hands-AI/OpenHands/issues/new)
and fill in as much detail as possible.

View File

@@ -1,6 +1,6 @@
---
title: Start Building
description: So you've [run OpenHands](./installation) and have [set up your LLM](./installation#setup). Now what?
description: So you've [run OpenHands](/usage/installation). Now what?
icon: code
---

View File

@@ -11,10 +11,18 @@ for scripting.
### Running with Python
**Note** - OpenHands requires Python version 3.12 or higher (Python 3.14 is not currently supported)
1. Install OpenHands using pip:
```bash
pip install openhands-ai
```
Or if you prefer not to manage your own Python environment, you can use `uvx`:
```bash
uvx --python 3.12 --from openhands-ai openhands
```
2. Launch an interactive OpenHands conversation from the command line:
@@ -47,19 +55,21 @@ poetry run python -m openhands.cli.main
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.44-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
-e LLM_MODEL=$LLM_MODEL \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-v ~/.openhands:/.openhands \
--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.44 \
docker.all-hands.dev/all-hands-ai/openhands:0.45 \
python -m openhands.cli.main --override-cli-mode true
```
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
This launches the CLI in Docker, allowing you to interact with OpenHands as described above.
The `-e SANDBOX_USER_ID=$(id -u)` ensures files created by the agent in your workspace have the correct permissions.

View File

@@ -25,9 +25,9 @@ You can use the Settings page at any time to:
- Setup the LLM provider and model for OpenHands.
- [Setup the search engine](/usage/search-engine-setup).
- [Configure MCP servers](/usage/mcp).
- [Connect to GitHub](/usage/how-to/gui-mode#github-setup) and [connect to GitLab](/usage/how-to/gui-mode#gitlab-setup)
- [Connect to GitHub](/usage/how-to/gui-mode#github-setup) and [connect to GitLab](/usage/how-to/gui-mode#gitlab-setup).
- Set application settings like your preferred language, notifications and other preferences.
- [Manage custom secrets](/usage/how-to/gui-mode#secrets-management).
- [Manage custom secrets](/usage/common-settings#secrets-management).
#### GitHub Setup
@@ -45,7 +45,7 @@ OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if pro
- All Repositories (You can select specific repositories, but this will impact what returns in repo search)
- Minimal Permissions (Select `Meta Data = Read-only` read for search, `Pull Requests = Read and Write` and `Content = Read and Write` for branch creation)
2. **Enter Token in OpenHands**:
- In the Settings page, navigate to the `Git` tab.
- In the Settings page, navigate to the `Integrations` tab.
- Paste your token in the `GitHub Token` field.
- Click `Save Changes` to apply the changes.
@@ -97,7 +97,7 @@ OpenHands automatically exports a `GITLAB_TOKEN` to the shell environment if pro
- `write_repository` (Write repository)
- Set an expiration date or leave it blank for a non-expiring token.
2. **Enter Token in OpenHands**:
- In the Settings page, navigate to the `Git` tab.
- In the Settings page, navigate to the `Integrations` tab.
- Paste your token in the `GitLab Token` field.
- Click `Save Changes` to apply the changes.
@@ -122,35 +122,40 @@ OpenHands automatically exports a `GITLAB_TOKEN` to the shell environment if pro
</Accordion>
</AccordionGroup>
#### Secrets Management
#### BitBucket Setup (Coming soon ...)
<AccordionGroup>
<Accordion title="Setting Up a BitBucket Password">
1. **Generate an App Password**:
- On BitBucket, go to Personal Settings > App Password.
- Create a new password with the following scopes:
- `repository: read`
- `repository: write`
- `pull requests: read`
- `pull requests: write`
- `issues: read`
- `issues: write`
- App passwords are non-expiring token. OpenHands will migrate to using API tokens in the future.
2. **Enter Token in OpenHands**:
- In the Settings page, navigate to the `Integrations` tab.
- Paste your token in the `BitBucket Token` field.
- Click `Save Changes` to apply the changes.
</Accordion>
OpenHands provides a secrets manager that allows you to securely store and manage sensitive information that can be accessed by the agent during runtime, such as API keys. These secrets are automatically exported as environment variables in the agent's runtime environment.
<Accordion title="Troubleshooting">
Common issues and solutions:
1. **Accessing the Secrets Manager**:
- In the Settings page, navigate to the `Secrets` tab.
- You'll see a list of all your existing custom secrets (if any).
- **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.
2. **Adding a New Secret**:
- Click the `Add New Secret` button.
- Fill in the following fields:
- **Name**: A unique identifier for your secret (e.g., `AWS_ACCESS_KEY`). This will be the environment variable name.
- **Value**: The sensitive information you want to store.
- **Description** (optional): A brief description of what the secret is used for, which is also provided to the agent.
- Click `Add Secret` to save.
- **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.
</Accordion>
3. **Editing a Secret**:
- Click the `Edit` button next to the secret you want to modify.
- You can update the name and description of the secret.
- Note: For security reasons, you cannot view or edit the value of an existing secret. If you need to change the value, delete the secret and create a new one.
4. **Deleting a Secret**:
- Click the `Delete` button next to the secret you want to remove.
- Confirm the deletion when prompted.
5. **Using Secrets in the Agent**:
- All custom secrets are automatically exported as environment variables in the agent's runtime environment.
- You can access them in your code using standard environment variable access methods (e.g., `os.environ['SECRET_NAME']` in Python).
- Example: If you create a secret named `OPENAI_API_KEY`, you can access it in your code as `process.env.OPENAI_API_KEY` in JavaScript or `os.environ['OPENAI_API_KEY']` in Python.
</AccordionGroup>
#### Advanced Settings
@@ -172,11 +177,11 @@ section of the documentation.
The status indicator located in the bottom left of the screen will cycle through a number of states as a new conversation
is loaded. Typically these include:
* `Disconnected` : The frontend is not connected to any conversation
* `Disconnected` : The frontend is not connected to any conversation.
* `Connecting` : The frontend is connecting a websocket to a conversation.
* `Building Runtime...` : The server is building a runtime. This is typically in development mode only while building a docker image.
* `Starting Runtime...` : The server is starting a new runtime instance - probably a new docker container or remote runtime.
* `Initializing Agent...` : The server is starting the agent loop. (This step does not appear at present with Nested runtimes)
* `Initializing Agent...` : The server is starting the agent loop (This step does not appear at present with Nested runtimes).
* `Setting up workspace...` : Usually this means a `git clone ...` operation.
* `Setting up git hooks` : Setting up the git pre commit hooks for the workspace.
* `Agent is awaiting user input...` : Ready to go!

View File

@@ -32,19 +32,20 @@ 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.44-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
-e LLM_MODEL=$LLM_MODEL \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-v ~/.openhands:/.openhands \
--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.44 \
docker.all-hands.dev/all-hands-ai/openhands:0.45 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
The `-e SANDBOX_USER_ID=$(id -u)` is passed to the Docker command to ensure the sandbox user matches the host users
permissions. This prevents the agent from creating root-owned files in the mounted workspace.

View File

@@ -1,12 +1,12 @@
---
title: Quick Start
description: Running OpenHands Cloud or running on your local system.
description: Running OpenHands Cloud or running on your own.
icon: rocket
---
## OpenHands Cloud
The easiest way to get started with OpenHands is on OpenHands Cloud, which comes with $50 in free credits for new users.
The easiest way to get started with OpenHands is on OpenHands Cloud, which comes with $20 in free credits for new users.
To get started with OpenHands Cloud, visit [app.all-hands.dev](https://app.all-hands.dev).

View File

@@ -6,73 +6,85 @@ description: When using a Local LLM, OpenHands may have limited functionality. I
## News
- 2025/05/21: We collaborated with Mistral AI and released [Devstral Small](https://mistral.ai/news/devstral) that achieves [46.8% on SWE-Bench Verified](https://github.com/SWE-bench/experiments/pull/228)!
- 2025/03/31: We released an open model OpenHands LM v0.1 32B that achieves 37.1% on SWE-Bench Verified
- 2025/03/31: We released an open model OpenHands LM 32B v0.1 that achieves 37.1% on SWE-Bench Verified
([blog](https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model), [model](https://huggingface.co/all-hands/openhands-lm-32b-v0.1)).
## Quickstart: Running OpenHands with a Local LLM using LM Studio
## Quickstart: Running OpenHands on Your Macbook
This guide explains how to serve a local Devstral LLM using [LM Studio](https://lmstudio.ai/) and have OpenHands connect to it.
### Serve the model on your Macbook
We recommend:
- **LM Studio** as the local model server, which handles metadata downloads automatically and offers a simple, user-friendly interface for configuration.
- **Devstral Small 2505** as the LLM for software development, trained on real GitHub issues and optimized for agent-style workflows like OpenHands.
We recommend using [LMStudio](https://lmstudio.ai/) for serving these models locally.
### Hardware Requirements
1. Download [LM Studio](https://lmstudio.ai/) and install it
Running Devstral requires a recent GPU with at least 16GB of VRAM, or a Mac with Apple Silicon (M1, M2, etc.) with at least 32GB of RAM.
2. Download the model:
- Option 1: Directly download the LLM from [this link](https://lmstudio.ai/model/devstral-small-2505-mlx) or by searching for the name `Devstral-Small-2505` in LM Studio
- Option 2: Download a LLM in GGUF format. For example, to download [Devstral Small 2505 GGUF](https://huggingface.co/mistralai/Devstral-Small-2505_gguf), using `huggingface-cli download mistralai/Devstral-Small-2505_gguf --local-dir mistralai/Devstral-Small-2505_gguf`. Then in bash terminal, run `lms import {model_name}` in the directory where you've downloaded the model checkpoint (e.g. run `lms import devstralQ4_K_M.gguf` in `mistralai/Devstral-Small-2505_gguf`)
### 1. Install LM Studio
3. Open LM Studio application, you should first switch to `power user` mode, and then open the developer tab:
Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstudio.ai/).
![image](./screenshots/1_select_power_user.png)
### 2. Download Devstral Small
4. Then click `Select a model to load` on top of the application:
1. Make sure to set the User Interface Complexity Level to "Power User", by clicking on the appropriate label at the bottom of the window.
2. Click the "Discover" button (Magnifying Glass icon) on the left navigation bar to open the Models download page.
![image](./screenshots/2_select_model.png)
![image](./screenshots/01_lm_studio_open_model_hub.png)
5. And choose the model you want to use, holding `option` on mac to enable advanced loading options:
3. Search for the "Devstral Small 2505" model, confirm it's the official Mistral AI (mistralai) model, then proceed to download.
![image](./screenshots/3_select_devstral.png)
![image](./screenshots/02_lm_studio_download_devstral.png)
6. You should then pick an appropriate context window for OpenHands based on your hardware configuration (larger than 32768 is recommended for using OpenHands, but too large may cause you to run out of memory); Flash attention is also recommended if it works on your machine.
4. Wait for the download to finish.
![image](./screenshots/4_set_context_window.png)
### 3. Load the Model
7. And you should start the server (if it is not already in `Running` status), un-toggle `Serve on Local Network` and remember the port number of the LMStudio URL (`1234` is the port number for `http://127.0.0.1:1234` in this example):
1. Click the "Developer" button (Console icon) on the left navigation bar to open the Developer Console.
2. Click the "Select a model to load" dropdown at the top of the application window.
![image](./screenshots/5_copy_url.png)
![image](./screenshots/03_lm_studio_open_load_model.png)
8. Finally, you can click the `copy` button near model name to copy the model name (`imported-models/uncategorized/devstralq4_k_m.gguf` in this example):
3. Enable the "Manually choose model load parameters" switch.
4. Select 'Devstral Small 2505' from the model list.
![image](./screenshots/6_copy_to_get_model_name.png)
![image](./screenshots/04_lm_studio_setup_devstral_part_1.png)
### Start OpenHands with locally served model
5. Enable the "Show advanced settings" switch at the bottom of the Model settings flyout to show all the available settings.
6. Set "Context Length" to at least 32768 and enable Flash Attention.
7. Click "Load Model" to start loading the model.
Check [the installation guide](/usage/local-setup) to make sure you have all the prerequisites for running OpenHands.
![image](./screenshots/05_lm_studio_setup_devstral_part_2.png)
### 4. Start the LLM server
1. Enable the switch next to "Status" at the top-left of the Window.
2. Take note of the Model API Identifier shown on the sidebar on the right.
![image](./screenshots/06_lm_studio_start_server.png)
### 5. Start OpenHands
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
```bash
export LMSTUDIO_MODEL_NAME="imported-models/uncategorized/devstralq4_k_m.gguf" # <- Replace this with the model name you copied from LMStudio
export LMSTUDIO_URL="http://host.docker.internal:1234" # <- Replace this with the port from LMStudio
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.44-nikolaik
mkdir -p ~/.openhands-state && echo '{"language":"en","agent":"CodeActAgent","max_iterations":null,"security_analyzer":null,"confirmation_mode":false,"llm_model":"lm_studio/'$LMSTUDIO_MODEL_NAME'","llm_api_key":"dummy","llm_base_url":"'$LMSTUDIO_URL/v1'","remote_runtime_resource_factor":null,"github_token":null,"enable_default_condenser":true,"user_consents_to_analytics":true}' > ~/.openhands-state/settings.json
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.44-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.44
docker.all-hands.dev/all-hands-ai/openhands:0.45
```
Once your server is running -- you can visit `http://localhost:3000` in your browser to use OpenHands with local Devstral model:
2. Wait until the server is running (see log below):
```
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.44
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.45
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
@@ -82,53 +94,88 @@ INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:3000 (Press CTRL+C to quit)
```
3. Visit `http://localhost:3000` in your browser.
## Advanced: Serving LLM on GPUs
### 6. Configure OpenHands to use the LLM server
### Download model checkpoints
Once you open OpenHands in your browser, you'll need to configure it to use the local LLM server you just started.
<Note>
The model checkpoints downloaded here should NOT be in GGUF format.
</Note>
When started for the first time, OpenHands will prompt you to set up the LLM provider.
For example, to download [OpenHands LM 32B v0.1](https://huggingface.co/all-hands/openhands-lm-32b-v0.1):
1. Click "see advanced settings" to open the LLM Settings page.
![image](./screenshots/07_openhands_open_advanced_settings.png)
2. Enable the "Advanced" switch at the top of the page to show all the available settings.
3. Set the following values:
- **Custom Model**: `openai/mistralai/devstral-small-2505` (the Model API identifier from LM Studio, prefixed with "openai/")
- **Base URL**: `http://host.docker.internal:1234/v1`
- **API Key**: `local-llm`
4. Click "Save Settings" to save the configuration.
![image](./screenshots/08_openhands_configure_local_llm_parameters.png)
That's it! You can now start using OpenHands with the local LLM server.
If you encounter any issues, let us know on [Slack](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) or [Discord](https://discord.gg/ESHStjSjD4).
## Advanced: Alternative LLM Backends
This section describes how to run local LLMs with OpenHands using alternative backends like Ollama, SGLang, or vLLM — without relying on LM Studio.
### Create an OpenAI-Compatible Endpoint with Ollama
- Install Ollama following [the official documentation](https://ollama.com/download).
- Example launch command for Devstral Small 2505:
```bash
huggingface-cli download all-hands/openhands-lm-32b-v0.1 --local-dir all-hands/openhands-lm-32b-v0.1
# ⚠️ WARNING: OpenHands requires a large context size to work properly.
# When using Ollama, set OLLAMA_CONTEXT_LENGTH to at least 32768.
# The default (4096) is way too small — not even the system prompt will fit, and the agent will not behave correctly.
OLLAMA_CONTEXT_LENGTH=32768 OLLAMA_HOST=0.0.0.0:11434 OLLAMA_KEEP_ALIVE=-1 nohup ollama serve &
ollama pull devstral:latest
```
### Create an OpenAI-Compatible Endpoint With SGLang
### Create an OpenAI-Compatible Endpoint with vLLM or SGLang
First, download the model checkpoints. For [Devstral Small 2505](https://huggingface.co/mistralai/Devstral-Small-2505):
```bash
huggingface-cli download mistralai/Devstral-Small-2505 --local-dir mistralai/Devstral-Small-2505
```
#### Serving the model using SGLang
- Install SGLang following [the official documentation](https://docs.sglang.ai/start/install.html).
- Example launch command for OpenHands LM 32B (with at least 2 GPUs):
- Example launch command for Devstral Small 2505 (with at least 2 GPUs):
```bash
SGLANG_ALLOW_OVERWRITE_LONGER_CONTEXT_LEN=1 python3 -m sglang.launch_server \
--model all-hands/openhands-lm-32b-v0.1 \
--served-model-name openhands-lm-32b-v0.1 \
--model mistralai/Devstral-Small-2505 \
--served-model-name Devstral-Small-2505 \
--port 8000 \
--tp 2 --dp 1 \
--host 0.0.0.0 \
--api-key mykey --context-length 131072
```
### Create an OpenAI-Compatible Endpoint with vLLM
#### Serving the model using vLLM
- Install vLLM following [the official documentation](https://docs.vllm.ai/en/latest/getting_started/installation.html).
- Example launch command for OpenHands LM 32B (with at least 2 GPUs):
- Example launch command for Devstral Small 2505 (with at least 2 GPUs):
```bash
vllm serve all-hands/openhands-lm-32b-v0.1 \
vllm serve mistralai/Devstral-Small-2505 \
--host 0.0.0.0 --port 8000 \
--api-key mykey \
--tensor-parallel-size 2 \
--served-model-name openhands-lm-32b-v0.1
--served-model-name Devstral-Small-2505 \
--enable-prefix-caching
```
## Advanced: Run and Configure OpenHands
### Run OpenHands
### Run OpenHands (Alternative Backends)
#### Using Docker
@@ -137,24 +184,20 @@ Run OpenHands using [the official docker run command](../installation#start-the-
#### Using Development Mode
Use the instructions in [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md) to build OpenHands.
Ensure `config.toml` exists by running `make setup-config` which will create one for you. In the `config.toml`, enter the following:
```
[core]
workspace_base="/path/to/your/workspace"
[llm]
model="openhands-lm-32b-v0.1"
ollama_base_url="http://localhost:8000"
```
Start OpenHands using `make run`.
### Configure OpenHands
### Configure OpenHands (Alternative Backends)
Once OpenHands is running, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
1. Enable `Advanced` options.
2. Set the following:
- `Custom Model` to `openai/<served-model-name>` (e.g. `openai/openhands-lm-32b-v0.1`)
- `Base URL` to `http://host.docker.internal:8000`
- `API key` to the same string you set when serving the model (e.g. `mykey`)
Once OpenHands is running, open the Settings page in the UI and go to the `LLM` tab.
1. Click **"see advanced settings"** to access the full configuration panel.
2. Enable the **Advanced** toggle at the top of the page.
3. Set the following parameters, if you followed the examples above:
- **Custom Model**: `openai/<served-model-name>`
e.g. `openai/devstral` if you're using Ollama, or `openai/Devstral-Small-2505` for SGLang or vLLM.
- **Base URL**: `http://host.docker.internal:<port>/v1`
Use port `11434` for Ollama, or `8000` for SGLang and vLLM.
- **API Key**:
- For **Ollama**: any placeholder value (e.g. `dummy`, `local-llm`)
- For **SGLang** or **vLLM**: use the same key provided when starting the server (e.g. `mykey`)

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 646 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -67,19 +67,21 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
### Start the App
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.44-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.44-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.44
docker.all-hands.dev/all-hands-ai/openhands:0.45
```
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
You'll find OpenHands running at http://localhost:3000!
### Setup
@@ -151,8 +153,6 @@ To enable search functionality in OpenHands:
For more details, see the [Search Engine Setup](/usage/search-engine-setup) guide.
Now you're ready to [get started with OpenHands](/usage/getting-started).
### Versions
The [docker command above](/usage/local-setup#start-the-app) pulls the most recent stable release of OpenHands. You have other options as well:

View File

@@ -5,26 +5,111 @@ description: Keyword-triggered microagents provide OpenHands with specific instr
## Usage
These microagents are only loaded when a prompt includes one of the trigger words.
Keyword-triggered microagents are only loaded when a prompt includes one of the trigger words. There are two types of keyword-triggered microagents:
1. **Standard Keyword Microagents**: Triggered by keywords embedded in text
2. **Command-Style Microagents**: Triggered by command-style inputs (e.g., `/fix_test`) that can prompt for user input
Additionally, there's a special type of microagent that's always active:
3. **Repository Microagents**: Always active for a specific repository, providing repository-specific context and tools
## Frontmatter Syntax
Frontmatter is required for keyword-triggered microagents. It must be placed at the top of the file,
above the guidelines.
above the guidelines. Enclose the frontmatter in triple dashes (---).
Enclose the frontmatter in triple dashes (---) and include the following fields:
### Standard Keyword Microagents
For standard keyword microagents, include the following fields:
| Field | Description | Required | Default |
|------------|--------------------------------------------------|----------|------------------|
| `name` | The name of the microagent | No | Filename |
| `type` | The type of microagent (`knowledge`) | No | Inferred |
| `triggers` | A list of keywords that activate the microagent. | Yes | None |
| `agent` | The agent this microagent applies to. | No | 'CodeActAgent' |
### Command-Style Microagents
## Example
For command-style microagents that require user input, include the following fields:
Keyword-triggered microagent file example located at `.openhands/microagents/yummy.md`:
```
| Field | Description | Required | Default |
|------------|------------------------------------------------------------|----------|------------------|
| `name` | The name of the microagent | No | Filename |
| `type` | The type of microagent (`task`) | No | Inferred |
| `triggers` | A list of command triggers (e.g., `/fix_test`) | No | `/[name]` |
| `inputs` | A list of input variables the microagent requires | Yes | None |
### Repository Microagents
Repository microagents are always active for a specific repository. They provide repository-specific context and tools.
| Field | Description | Required | Default |
|------------|------------------------------------------------------------|----------|------------------|
| `name` | The name of the microagent | No | Filename |
| `type` | The type of microagent (`repo`) | No | Inferred |
#### Repository Microagent Example
Here's an example of a repository microagent:
```yaml
---
# The type field is optional and will be inferred as 'repo' when no triggers are present
---
# Repository Guidelines
This repository follows these coding standards:
1. Use PEP 8 for Python code
2. Use ESLint for JavaScript code
3. Write unit tests for all new features
```
This microagent is always active when working with the repository and provides repository-specific guidelines.
### MCP Tools Support
Microagents can also provide additional MCP (Model-Code-Prompt) tools to the agent. This is useful for extending the agent's capabilities with custom tools.
| Field | Description | Required | Default |
|--------------|-----------------------------------------------------------|----------|------------------|
| `mcp_tools` | Configuration for additional MCP tools | No | None |
#### MCP Tools Example
Here's an example of a microagent that provides an additional MCP tool (the `fetch` tool for accessing web content):
```yaml
---
# The type field is optional and will be inferred as 'repo' when no triggers are present
mcp_tools:
stdio_servers:
- name: "fetch"
command: uvx
args:
- mcp-server-fetch
---
```
This microagent is a repository microagent (always active) that adds the `fetch` tool to the agent's capabilities.
Each input in the `inputs` list requires:
| Field | Description | Required |
|---------------|--------------------------------------------------|----------|
| `name` | The name of the input variable | Yes |
| `description` | A description of what the input should contain | Yes |
## Examples
### Standard Keyword Microagent Example
Standard keyword microagent file example located at `.openhands/microagents/yummy.md`:
```yaml
---
# The type field is optional and will be inferred as 'knowledge' when triggers are present
triggers:
- yummyhappy
- happyyummy
@@ -33,4 +118,58 @@ triggers:
The user has said the magic word. Respond with "That was delicious!"
```
[See examples of microagents triggered by keywords in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)
### Command-Style Microagent Example
Command-style microagent file example located at `.openhands/microagents/fix_test.md`:
```yaml
---
# The type field is optional and will be inferred as 'task' when inputs are present
triggers:
- /fix_test
inputs:
- name: BRANCH_NAME
description: "Branch for the agent to work on"
- name: TEST_COMMAND_TO_RUN
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
- name: FUNCTION_TO_FIX
description: "The name of function to fix"
- name: FILE_FOR_FUNCTION
description: "The path of the file that contains the function"
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
PLEASE DO NOT modify the tests by yourself -- Let me know if you think some of the tests are incorrect.
```
## Using Command-Style Microagents
Command-style microagents are designed to streamline common development tasks by providing structured templates for specific operations. They are triggered using a command-style format and will prompt the user for any required inputs.
### How to Use
1. Type `/` in the chat input to see available command-style microagents
2. Select a microagent from the dropdown or type its name (e.g., `/fix_test`)
3. The agent will prompt you for any required inputs
4. Provide the requested information
5. The agent will execute the task with your inputs
### Template Variables
In the body of a command-style microagent, you can reference input variables using the `{{ VARIABLE_NAME }}` syntax. These will be replaced with the user-provided values when the microagent is triggered.
### Available Command-Style Microagents
OpenHands includes several built-in command-style microagents:
| Command | Description |
|----------------------|-------------------------------------------------------|
| `/fix_test` | Fix failing tests by modifying a specific function |
| `/update_test` | Update tests for a new implementation |
| `/update_pr` | Update a pull request description |
| `/address_pr_comments` | Address comments on a pull request |
| `/add_repo_instruction` | Add instructions to the repository microagent |
[See examples of microagents in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)

View File

@@ -8,7 +8,7 @@ description: Microagents are specialized prompts that enhance OpenHands with dom
Currently OpenHands supports the following types of microagents:
- [General Microagents](./microagents-repo): General guidelines for OpenHands about the repository.
- [Keyword-Triggered Microagents](./microagents-keyword): Guidelines activated by specific keywords in prompts.
- [Keyword-Triggered Microagents](./microagents-keyword): Guidelines activated by specific keywords in prompts, including command-style microagents that prompt for user inputs.
To customize OpenHands' behavior, create a .openhands/microagents/ directory in the root of your repository and
add `<microagent_name>.md` files inside. For repository-specific guidelines, you can ask OpenHands to analyze your repository and create a comprehensive `repo.md` file (see [General Microagents](./microagents-repo) for details).
@@ -34,7 +34,7 @@ some-repository/
Each microagent file may include frontmatter that provides additional information. In some cases, this frontmatter
is required:
| Microagent Type | Required |
|---------------------------------|----------|
| `General Microagents` | No |
| `Keyword-Triggered Microagents` | Yes |
| Microagent Type | Required |
|------------------------------------------------|----------|
| `General Microagents` | No |
| `Keyword-Triggered Microagents (all types)` | Yes |

View File

@@ -128,3 +128,7 @@ docker network create openhands-network
docker run # ... \
--network openhands-network \
```
<Note>
**Docker Desktop Required**: Network isolation features, including custom networks and `host.docker.internal` routing, require Docker Desktop. Docker Engine alone does not support these features on localhost across custom networks. If you're using Docker Engine without Docker Desktop, network isolation may not work as expected.
</Note>

View File

@@ -31,9 +31,9 @@ On initial prompt, an error is seen with `Permission Denied` or `PermissionError
**Resolution**
* Check if the `~/.openhands-state` is owned by `root`. If so, you can:
* Change the directory's ownership: `sudo chown <user>:<user> ~/.openhands-state`.
* or update permissions on the directory: `sudo chmod 777 ~/.openhands-state`
* Check if the `~/.openhands` is owned by `root`. If so, you can:
* Change the directory's ownership: `sudo chown <user>:<user> ~/.openhands`.
* or update permissions on the directory: `sudo chmod 777 ~/.openhands`
* or delete it if you dont need previous data. OpenHands will recreate it. You'll need to re-enter LLM settings.
* If mounting a local directory, ensure your `WORKSPACE_BASE` has the necessary permissions for the user running
OpenHands.
@@ -56,13 +56,16 @@ To fix this:
-e SANDBOX_VSCODE_PORT=41234 \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:latest \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
-p 41234:41234 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:latest
```
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
2. Make sure to expose the same port with `-p 41234:41234` in your Docker command.
3. If running with the development workflow, you can set this in your `config.toml` file:
```toml

View File

@@ -133,13 +133,66 @@ This guide provides step-by-step instructions for running OpenHands on a Windows
> **Note**: If you're running the frontend in development mode (using `npm run dev`), use port 3001 instead: `http://localhost:3001`
## Installing and Running the CLI
To install and run the OpenHands CLI on Windows without WSL, follow these steps:
### 1. Install uv (Python Package Manager)
Open PowerShell as Administrator and run:
```powershell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```
### 2. Install .NET SDK (Required)
The OpenHands CLI **requires** the .NET Core runtime for PowerShell integration. Without it, the CLI will fail to start with a `coreclr` error. Install the .NET SDK which includes the runtime:
```powershell
winget install Microsoft.DotNet.SDK.8
```
Alternatively, you can download and install the .NET SDK from the [official Microsoft website](https://dotnet.microsoft.com/download).
After installation, restart your PowerShell session to ensure the environment variables are updated.
### 3. Install and Run OpenHands
After installing the prerequisites, you can install and run OpenHands with:
```powershell
uvx --python 3.12 --from openhands-ai openhands
```
### Troubleshooting CLI Issues
#### CoreCLR Error
If you encounter an error like `Failed to load CoreCLR` or `pythonnet.load('coreclr')` when running OpenHands CLI, this indicates that the .NET Core runtime is missing or not properly configured. To fix this:
1. Install the .NET SDK as described in step 2 above
2. Verify that your system PATH includes the .NET SDK directories
3. Restart your PowerShell session completely after installing the .NET SDK
4. Make sure you're using PowerShell 7 (pwsh) rather than Windows PowerShell
To verify your .NET installation, run:
```powershell
dotnet --info
```
This should display information about your installed .NET SDKs and runtimes. If this command fails, the .NET SDK is not properly installed or not in your PATH.
If the issue persists after installing the .NET SDK, try installing the specific .NET Runtime version 6.0 or later from the [.NET download page](https://dotnet.microsoft.com/download).
## Limitations on Windows
When running OpenHands on Windows without WSL or Docker, be aware of the following limitations:
1. **Browser Tool Not Supported**: The browser tool is not currently supported on Windows.
2. **.NET Core Requirement**: The PowerShell integration requires .NET Core Runtime to be installed. If .NET Core is not available, OpenHands will automatically fall back to a more limited PowerShell implementation with reduced functionality.
2. **.NET Core Requirement**: The PowerShell integration requires .NET Core Runtime to be installed. The CLI implementation attempts to load the CoreCLR at startup with `pythonnet.load('coreclr')` and will fail with an error if .NET Core is not properly installed.
3. **Interactive Shell Commands**: Some interactive shell commands may not work as expected. The PowerShell session implementation has limitations compared to the bash session used on Linux/macOS.

1
evaluation/benchmarks/gaia/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
data/

View File

@@ -6,6 +6,13 @@ This folder contains evaluation harness for evaluating agents on the [GAIA bench
Please follow instruction [here](../../README.md#setup) to setup your local development environment and LLM.
To enable the Tavily MCP Server, you can add the Tavily API key under the `core` section of your `config.toml` file, like below:
```toml
[core]
search_api_key = "tvly-******"
```
## Run the evaluation
We are using the GAIA dataset hosted on [Hugging Face](https://huggingface.co/datasets/gaia-benchmark/GAIA).

View File

@@ -1,4 +1,5 @@
import asyncio
import copy
import functools
import os
import re
@@ -6,6 +7,7 @@ import re
import huggingface_hub
import pandas as pd
from datasets import load_dataset
from pydantic import SecretStr
from evaluation.benchmarks.gaia.scorer import question_scorer
from evaluation.utils.shared import (
@@ -24,6 +26,7 @@ from openhands.core.config import (
OpenHandsConfig,
get_llm_config_arg,
get_parser,
load_from_toml,
)
from openhands.core.config.utils import get_agent_config_arg
from openhands.core.logger import openhands_logger as logger
@@ -41,7 +44,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have solved the question, please first send your answer to user through message and then exit.\n'
'CodeActAgent': 'When you think you have solved the question, please use the finish tool and include your final answer in the message parameter of the finish tool. Your final answer MUST be encapsulated within <solution> and </solution>.\n'
}
@@ -49,7 +52,7 @@ def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
sandbox_config.base_container_image = 'nikolaik/python-nodejs:python3.12-nodejs22'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
@@ -67,6 +70,11 @@ def get_config(
logger.info('Agent config not provided, using default settings')
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.enable_prompt_extensions = False
config_copy = copy.deepcopy(config)
load_from_toml(config_copy)
if config_copy.search_api_key:
config.search_api_key = SecretStr(config_copy.search_api_key)
return config
@@ -134,16 +142,26 @@ def process_instance(
dest_file = None
# Prepare instruction
instruction = f'{instance["Question"]}\n'
instruction = """You have one question to answer. It is paramount that you provide a correct answer.
Give it all you can: I know for a fact that you have access to all the relevant tools to solve it and find the correct answer (the answer does exist). Failure or 'I cannot answer' or 'None found' will not be tolerated, success will be rewarded.
You must make sure you find the correct answer! You MUST strictly follow the task-specific formatting instructions for your final answer.
Here is the task:
{task_question}
""".format(
task_question=instance['Question'],
)
logger.info(f'Instruction: {instruction}')
if dest_file:
instruction += f'\n\nThe mentioned file is provided in the workspace at: {dest_file.split("/")[-1]}'
instruction += 'IMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\n'
instruction += 'Please encapsulate your final answer (answer ONLY) within <solution> and </solution>.\n'
instruction += """IMPORTANT: When seeking information from a website, REFRAIN from arbitrary URL navigation. You should utilize the designated search engine tool with precise keywords to obtain relevant URLs or use the specific website's search interface. DO NOT navigate directly to specific URLs as they may not exist.\n\nFor example: if you want to search for a research paper on Arxiv, either use the search engine tool with specific keywords or navigate to arxiv.org and then use its interface.\n"""
instruction += 'IMPORTANT: You should NEVER ask for Human Help.\n'
instruction += 'IMPORTANT: Please encapsulate your final answer (answer ONLY) within <solution> and </solution>. Your answer will be evaluated using string matching approaches so it important that you STRICTLY adhere to the output formatting instructions specified in the task (e.g., alphabetization, sequencing, units, rounding, decimal places, etc.)\n'
instruction += (
'For example: The answer to the question is <solution> 42 </solution>.\n'
)
instruction += "IMPORTANT: Your final answer should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, express it numerically (i.e., with digits rather than words), do not use commas, and do not include units such as $ or percent signs unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities). If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.\n"
# NOTE: You can actually set slightly different instruction for different agents
instruction += AGENT_CLS_TO_INST_SUFFIX.get(metadata.agent_class, '')
logger.info(f'Instruction:\n{instruction}', extra={'msg_type': 'OBSERVATION'})
@@ -175,7 +193,7 @@ def process_instance(
for event in reversed(state.history):
if event.source == 'agent':
if isinstance(event, AgentFinishAction):
model_answer_raw = event.thought
model_answer_raw = event.final_thought
break
elif isinstance(event, CmdRunAction):
model_answer_raw = event.thought
@@ -222,6 +240,7 @@ def process_instance(
error=state.last_error if state and state.last_error else None,
test_result=test_result,
)
runtime.close()
return output
@@ -253,6 +272,8 @@ if __name__ == '__main__':
if llm_config is None:
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
toml_config = OpenHandsConfig()
load_from_toml(toml_config)
metadata = make_metadata(
llm_config=llm_config,
dataset_name='gaia',
@@ -261,7 +282,10 @@ if __name__ == '__main__':
eval_note=args.eval_note,
eval_output_dir=args.eval_output_dir,
data_split=args.data_split,
details={'gaia-level': args.level},
details={
'gaia-level': args.level,
'mcp-servers': ['tavily'] if toml_config.search_api_key else [],
},
agent_config=agent_config,
)

View File

@@ -39,7 +39,7 @@ echo "LEVELS: $LEVELS"
COMMAND="poetry run python ./evaluation/benchmarks/gaia/run_infer.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--max-iterations 30 \
--max-iterations 60 \
--level $LEVELS \
--data-split validation \
--eval-num-workers $NUM_WORKERS \

View File

@@ -116,7 +116,7 @@ def get_token_per_line(code: str):
return identifiers_per_line
def get_ISM(answer_code: str, model_output_list: list, asnwer_name: str) -> list:
def get_ISM(answer_code: str, model_output_list: list, answer_name: str) -> list:
"""
计算ISM返回一个有序的得分列表
:return:
@@ -126,13 +126,13 @@ def get_ISM(answer_code: str, model_output_list: list, asnwer_name: str) -> list
if '```python' in code:
code = code.replace('```python', '')
code = code.replace('```', '')
if not re.search(rf'\b{re.escape(asnwer_name)}\b', code) or not is_code_valid(
if not re.search(rf'\b{re.escape(answer_name)}\b', code) or not is_code_valid(
code
):
score_list.append(0)
continue
# if asnwer_name not in code:
# if answer_name not in code:
# score_list.append(0)
# continue
@@ -155,7 +155,7 @@ def get_ISM(answer_code: str, model_output_list: list, asnwer_name: str) -> list
def get_ISM_without_verification(
answer_code: str, model_output_list: list, asnwer_name: str
answer_code: str, model_output_list: list, answer_name: str
) -> list:
"""
计算ISM返回一个有序的得分列表
@@ -163,11 +163,11 @@ def get_ISM_without_verification(
"""
score_list = []
for code in model_output_list:
if asnwer_name not in code:
if answer_name not in code:
score_list.append(0)
continue
# if asnwer_name not in code:
# if answer_name not in code:
# score_list.append(0)
# continue
@@ -215,7 +215,7 @@ def longest_common_prefix_with_lengths(list1, list2):
return max_length, len_list1, len_list2
def get_PM(answer_code: str, model_output_list: list, asnwer_name: str) -> list:
def get_PM(answer_code: str, model_output_list: list, answer_name: str) -> list:
"""
计算PM返回一个有序的得分列表
:return:
@@ -225,14 +225,14 @@ def get_PM(answer_code: str, model_output_list: list, asnwer_name: str) -> list:
if '```python' in code:
code = code.replace('```python', '')
code = code.replace('```', '')
if not re.search(rf'\b{re.escape(asnwer_name)}\b', code) or not is_code_valid(
if not re.search(rf'\b{re.escape(answer_name)}\b', code) or not is_code_valid(
code
):
# if asnwer_name not in code or is_code_valid(code) == False:
# if answer_name not in code or is_code_valid(code) == False:
score_list.append(0)
continue
# if asnwer_name not in code:
# if answer_name not in code:
# score_list.append(0)
# continue

View File

@@ -193,9 +193,9 @@ describe("ChatInput", () => {
it("should handle image paste correctly", () => {
const onSubmit = vi.fn();
const onImagePaste = vi.fn();
const onFilesPaste = vi.fn();
render(<ChatInput onSubmit={onSubmit} onImagePaste={onImagePaste} />);
render(<ChatInput onSubmit={onSubmit} onFilesPaste={onFilesPaste} />);
const input = screen.getByTestId("chat-input").querySelector("textarea");
expect(input).toBeTruthy();
@@ -213,8 +213,8 @@ describe("ChatInput", () => {
},
});
// Verify image paste was handled
expect(onImagePaste).toHaveBeenCalledWith([file]);
// Verify file paste was handled
expect(onFilesPaste).toHaveBeenCalledWith([file]);
});
it("should use the default maxRows value", () => {

View File

@@ -23,6 +23,7 @@ describe("ConversationPanel", () => {
preloadedState: {
metrics: {
cost: null,
max_budget_per_task: null,
usage: null,
},
},
@@ -273,6 +274,7 @@ describe("ConversationPanel", () => {
preloadedState: {
metrics: {
cost: null,
max_budget_per_task: null,
usage: null,
},
},

View File

@@ -50,13 +50,13 @@ const renderRepoConnector = () => {
const MOCK_RESPOSITORIES: GitRepository[] = [
{
id: 1,
id: "1",
full_name: "rbren/polaris",
git_provider: "github",
is_public: true,
},
{
id: 2,
id: "2",
full_name: "All-Hands-AI/OpenHands",
git_provider: "github",
is_public: true,

View File

@@ -94,13 +94,13 @@ describe("RepositorySelectionForm", () => {
it("shows loading indicator when repositories are being fetched", () => {
const MOCK_REPOS: GitRepository[] = [
{
id: 1,
id: "1",
full_name: "user/repo1",
git_provider: "github",
is_public: true,
},
{
id: 2,
id: "2",
full_name: "user/repo2",
git_provider: "github",
is_public: true,
@@ -122,13 +122,13 @@ describe("RepositorySelectionForm", () => {
it("shows dropdown when repositories are loaded", async () => {
const MOCK_REPOS: GitRepository[] = [
{
id: 1,
id: "1",
full_name: "user/repo1",
git_provider: "github",
is_public: true,
},
{
id: 2,
id: "2",
full_name: "user/repo2",
git_provider: "github",
is_public: true,
@@ -166,13 +166,13 @@ describe("RepositorySelectionForm", () => {
it("should call the search repos API when searching a URL", async () => {
const MOCK_REPOS: GitRepository[] = [
{
id: 1,
id: "1",
full_name: "user/repo1",
git_provider: "github",
is_public: true,
},
{
id: 2,
id: "2",
full_name: "user/repo2",
git_provider: "github",
is_public: true,
@@ -181,7 +181,7 @@ describe("RepositorySelectionForm", () => {
const MOCK_SEARCH_REPOS: GitRepository[] = [
{
id: 3,
id: "3",
full_name: "kubernetes/kubernetes",
git_provider: "github",
is_public: true,
@@ -228,7 +228,7 @@ describe("RepositorySelectionForm", () => {
it("should call onRepoSelection when a searched repository is selected", async () => {
const MOCK_SEARCH_REPOS: GitRepository[] = [
{
id: 3,
id: "3",
full_name: "kubernetes/kubernetes",
git_provider: "github",
is_public: true,

View File

@@ -19,10 +19,10 @@ const MOCK_TASK_1: SuggestedTask = {
};
const MOCK_RESPOSITORIES: GitRepository[] = [
{ id: 1, full_name: "repo1", git_provider: "github", is_public: true },
{ id: 2, full_name: "repo2", git_provider: "github", is_public: true },
{ id: 3, full_name: "repo3", git_provider: "gitlab", is_public: true },
{ id: 4, full_name: "repo4", git_provider: "gitlab", is_public: true },
{ id: "1", full_name: "repo1", git_provider: "github", is_public: true },
{ id: "2", full_name: "repo2", git_provider: "github", is_public: true },
{ id: "3", full_name: "repo3", git_provider: "gitlab", is_public: true },
{ id: "4", full_name: "repo4", git_provider: "gitlab", is_public: true },
];
const renderTaskCard = (task = MOCK_TASK_1) => {

View File

@@ -1,4 +1,4 @@
import { render, screen, within, fireEvent } from "@testing-library/react";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
@@ -92,7 +92,7 @@ describe("InteractiveChatBox", () => {
await user.type(textarea, "Hello, world!");
await user.keyboard("{Enter}");
expect(onSubmitMock).toHaveBeenCalledWith("Hello, world!", [file]);
expect(onSubmitMock).toHaveBeenCalledWith("Hello, world!", [file], []);
// clear images after submission
expect(screen.queryAllByTestId("image-preview")).toHaveLength(0);
@@ -144,7 +144,7 @@ describe("InteractiveChatBox", () => {
onStop={onStop}
onChange={onChange}
value="test message"
/>
/>,
);
// Upload an image via the upload button - this should NOT clear the text input
@@ -161,7 +161,7 @@ describe("InteractiveChatBox", () => {
await user.click(submitButton);
// Verify onSubmit was called with the message and image
expect(onSubmit).toHaveBeenCalledWith("test message", [file]);
expect(onSubmit).toHaveBeenCalledWith("test message", [file], []);
// Verify onChange was called to clear the text input
expect(onChange).toHaveBeenCalledWith("");
@@ -173,7 +173,7 @@ describe("InteractiveChatBox", () => {
onStop={onStop}
onChange={onChange}
value=""
/>
/>,
);
// Verify the text input was cleared

View File

@@ -0,0 +1,95 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { LikertScale } from "#/components/features/feedback/likert-scale";
import { I18nKey } from "#/i18n/declaration";
// Mock the mutation hook
vi.mock("#/hooks/mutation/use-submit-conversation-feedback", () => ({
useSubmitConversationFeedback: () => ({
mutate: vi.fn(),
}),
}));
describe("LikertScale", () => {
const user = userEvent.setup();
afterEach(() => {
vi.clearAllMocks();
});
it("should render with proper localized text for rating prompt", () => {
renderWithProviders(<LikertScale eventId={1} />);
// Check that the rating prompt is displayed with proper translation key
expect(screen.getByText(I18nKey.FEEDBACK$RATE_AGENT_PERFORMANCE)).toBeInTheDocument();
});
it("should show localized feedback reasons when rating is 3 or below", async () => {
renderWithProviders(<LikertScale eventId={1} />);
// Click on a rating of 3 (which should show reasons)
const threeStarButton = screen.getAllByRole("button")[2]; // 3rd button (rating 3)
await user.click(threeStarButton);
// Wait for reasons to appear
await waitFor(() => {
expect(screen.getByText(I18nKey.FEEDBACK$SELECT_REASON)).toBeInTheDocument();
});
// Check that all feedback reasons are properly localized
expect(screen.getByText(I18nKey.FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION)).toBeInTheDocument();
expect(screen.getByText(I18nKey.FEEDBACK$REASON_FORGOT_CONTEXT)).toBeInTheDocument();
expect(screen.getByText(I18nKey.FEEDBACK$REASON_UNNECESSARY_CHANGES)).toBeInTheDocument();
expect(screen.getByText(I18nKey.FEEDBACK$REASON_OTHER)).toBeInTheDocument();
});
it("should show countdown message with proper localization", async () => {
renderWithProviders(<LikertScale eventId={1} />);
// Click on a rating of 2 (which should show reasons and countdown)
const twoStarButton = screen.getAllByRole("button")[1]; // 2nd button (rating 2)
await user.click(twoStarButton);
// Wait for countdown to appear
await waitFor(() => {
expect(screen.getByText(I18nKey.FEEDBACK$SELECT_REASON_COUNTDOWN)).toBeInTheDocument();
});
});
it("should show thank you message after submission", () => {
renderWithProviders(
<LikertScale eventId={1} initiallySubmitted={true} initialRating={4} />
);
// Check that thank you message is displayed with proper translation key
expect(screen.getByText(I18nKey.FEEDBACK$THANK_YOU_FOR_FEEDBACK)).toBeInTheDocument();
});
it("should render all 5 star rating buttons", () => {
renderWithProviders(<LikertScale eventId={1} />);
// Check that all 5 star buttons are rendered
const starButtons = screen.getAllByRole("button");
expect(starButtons).toHaveLength(5);
// Check that each button has proper aria-label
for (let i = 1; i <= 5; i++) {
expect(screen.getByLabelText(`Rate ${i} stars`)).toBeInTheDocument();
}
});
it("should not show reasons for ratings above 3", async () => {
renderWithProviders(<LikertScale eventId={1} />);
// Click on a rating of 5 (which should NOT show reasons)
const fiveStarButton = screen.getAllByRole("button")[4]; // 5th button (rating 5)
await user.click(fiveStarButton);
// Wait a bit to ensure reasons don't appear
await waitFor(() => {
expect(screen.queryByText(I18nKey.FEEDBACK$SELECT_REASON)).not.toBeInTheDocument();
});
});
});

View File

@@ -41,19 +41,6 @@ describe("UploadImageInput", () => {
expect(onUploadMock).toHaveBeenNthCalledWith(1, files);
});
it("should not upload any file that is not an image", async () => {
render(<UploadImageInput onUpload={onUploadMock} />);
const file = new File(["(⌐□_□)"], "chucknorris.txt", {
type: "text/plain",
});
const input = screen.getByTestId("upload-image-input");
await user.upload(input, file);
expect(onUploadMock).not.toHaveBeenCalled();
});
it("should render custom labels", () => {
const { rerender } = render(<UploadImageInput onUpload={onUploadMock} />);
expect(screen.getByTestId("default-label")).toBeInTheDocument();

View File

@@ -89,6 +89,9 @@ describe("Content", () => {
await screen.findByTestId("gitlab-token-input");
await screen.findByTestId("gitlab-token-help-anchor");
await screen.findByTestId("bitbucket-token-input");
await screen.findByTestId("bitbucket-token-help-anchor");
getConfigSpy.mockResolvedValue(VALID_SAAS_CONFIG);
queryClient.invalidateQueries();
rerender();
@@ -107,6 +110,13 @@ describe("Content", () => {
expect(
screen.queryByTestId("gitlab-token-help-anchor"),
).not.toBeInTheDocument();
expect(
screen.queryByTestId("bitbucket-token-input"),
).not.toBeInTheDocument();
expect(
screen.queryByTestId("bitbucket-token-help-anchor"),
).not.toBeInTheDocument();
});
});
@@ -229,6 +239,7 @@ describe("Content", () => {
describe("Form submission", () => {
it("should save the GitHub token", async () => {
const saveProvidersSpy = vi.spyOn(SecretsService, "addGitProvider");
saveProvidersSpy.mockImplementation(() => Promise.resolve(true));
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
@@ -243,15 +254,49 @@ describe("Form submission", () => {
expect(saveProvidersSpy).toHaveBeenCalledWith({
github: { token: "test-token", host: "" },
gitlab: { token: "", host: "" },
bitbucket: { token: "", host: "" },
});
});
it("should save GitLab tokens", async () => {
const saveProvidersSpy = vi.spyOn(SecretsService, "addGitProvider");
saveProvidersSpy.mockImplementation(() => Promise.resolve(true));
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
renderGitSettingsScreen();
const gitlabInput = await screen.findByTestId("gitlab-token-input");
const submit = await screen.findByTestId("submit-button");
await userEvent.type(gitlabInput, "test-token");
await userEvent.click(submit);
expect(saveProvidersSpy).toHaveBeenCalledWith({
github: { token: "test-token", host: "" },
github: { token: "", host: "" },
gitlab: { token: "test-token", host: "" },
bitbucket: { token: "", host: "" },
});
});
it("should save the Bitbucket token", async () => {
const saveProvidersSpy = vi.spyOn(SecretsService, "addGitProvider");
saveProvidersSpy.mockImplementation(() => Promise.resolve(true));
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
renderGitSettingsScreen();
const bitbucketInput = await screen.findByTestId("bitbucket-token-input");
const submit = await screen.findByTestId("submit-button");
await userEvent.type(bitbucketInput, "test-token");
await userEvent.click(submit);
expect(saveProvidersSpy).toHaveBeenCalledWith({
github: { token: "", host: "" },
gitlab: { token: "", host: "" },
bitbucket: { token: "test-token", host: "" },
});
});

View File

@@ -45,13 +45,13 @@ const renderHomeScreen = () =>
const MOCK_RESPOSITORIES: GitRepository[] = [
{
id: 1,
id: "1",
full_name: "octocat/hello-world",
git_provider: "github",
is_public: true,
},
{
id: 2,
id: "2",
full_name: "octocat/earth",
git_provider: "github",
is_public: true,

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
{
"name": "openhands-frontend",
"version": "0.44.0",
"version": "0.45.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@heroui/react": "^2.8.0-beta.7",
"@heroui/react": "^2.8.0-beta.9",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.6.2",
@@ -18,27 +18,27 @@
"@stripe/stripe-js": "^7.3.1",
"@tailwindcss/postcss": "^4.1.10",
"@tailwindcss/vite": "^4.1.10",
"@tanstack/react-query": "^5.80.7",
"@tanstack/react-query": "^5.80.10",
"@vitejs/plugin-react": "^4.5.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.9.0",
"axios": "^1.10.0",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.17.3",
"framer-motion": "^12.18.1",
"i18next": "^25.2.1",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.28",
"jose": "^6.0.11",
"lucide-react": "^0.514.0",
"lucide-react": "^0.519.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.251.0",
"posthog-js": "^1.255.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.5.1",
"react-i18next": "^15.5.1",
"react-i18next": "^15.5.3",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
@@ -83,8 +83,8 @@
"@babel/parser": "^7.27.1",
"@babel/traverse": "^7.27.1",
"@babel/types": "^7.27.0",
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.53.0",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.53.1",
"@react-router/dev": "^7.6.2",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.78.0",
@@ -92,7 +92,7 @@
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.0.1",
"@types/node": "^24.0.3",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-highlight": "^0.12.8",
@@ -109,13 +109,13 @@
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.4.1",
"eslint-plugin-prettier": "^5.5.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.1.4",
"husky": "^9.1.7",
"jsdom": "^26.1.0",
"lint-staged": "^16.1.0",
"lint-staged": "^16.1.2",
"msw": "^2.6.6",
"prettier": "^3.5.3",
"stripe": "^18.2.1",

View File

@@ -1,6 +1,7 @@
import { openHands } from "../open-hands-axios";
import { GetFilesResponse, GetFileResponse } from "./file-service.types";
import { getConversationUrl } from "../conversation.utils";
import { FileUploadSuccessResponse } from "../open-hands.types";
export class FileService {
/**
@@ -35,4 +36,31 @@ export class FileService {
return data.code;
}
/**
* Upload multiple files to the workspace
* @param conversationId ID of the conversation
* @param files List of files.
* @returns list of uploaded files, list of skipped files
*/
static async uploadFiles(
conversationId: string,
files: File[],
): Promise<FileUploadSuccessResponse> {
const formData = new FormData();
for (const file of files) {
formData.append("files", file);
}
const url = `${getConversationUrl(conversationId)}/upload-files`;
const response = await openHands.post<FileUploadSuccessResponse>(
url,
formData,
{
headers: {
"Content-Type": "multipart/form-data",
},
},
);
return response.data;
}
}

View File

@@ -293,9 +293,11 @@ class OpenHands {
static async startConversation(
conversationId: string,
providers?: Provider[],
): Promise<Conversation | null> {
const { data } = await openHands.post<Conversation | null>(
`/api/conversations/${conversationId}/start`,
providers ? { providers_set: providers } : {},
);
return data;

View File

@@ -10,7 +10,6 @@ export interface SaveFileSuccessResponse {
}
export interface FileUploadSuccessResponse {
message: string;
uploaded_files: string[];
skipped_files: { name: string; reason: string }[];
}

View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Bitbucket</title><path d="M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891zM14.52 15.53H9.522L8.17 8.466h7.561z"/></svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@@ -20,19 +20,22 @@ export function ActionSuggestions({
const providersAreSet = providers.length > 0;
const isGitLab = providers.includes("gitlab");
const isBitbucket = providers.includes("bitbucket");
const pr = isGitLab ? "merge request" : "pull request";
const prShort = isGitLab ? "MR" : "PR";
const getProviderName = () => {
if (isGitLab) return "GitLab";
if (isBitbucket) return "Bitbucket";
return "GitHub";
};
const terms = {
pr,
prShort,
pushToBranch: `Please push the changes to a remote branch on ${
isGitLab ? "GitLab" : "GitHub"
}, but do NOT create a ${pr}. Please use the exact SAME branch name as the one you are currently on.`,
createPR: `Please push the changes to ${
isGitLab ? "GitLab" : "GitHub"
} and open a ${pr}. Please create a meaningful branch name that describes the changes. If a ${pr} template exists in the repository, please follow it when creating the ${prShort} description.`,
pushToBranch: `Please push the changes to a remote branch on ${getProviderName()}, but do NOT create a ${pr}. Please use the exact SAME branch name as the one you are currently on.`,
createPR: `Please push the changes to ${getProviderName()} and open a ${pr}. Please create a meaningful branch name that describes the changes. If a ${pr} template exists in the repository, please follow it when creating the ${prShort} description.`,
pushToPR: `Please push the latest changes to the existing ${pr}.`,
};

View File

@@ -18,7 +18,7 @@ interface ChatInputProps {
onChange?: (message: string) => void;
onFocus?: () => void;
onBlur?: () => void;
onImagePaste?: (files: File[]) => void;
onFilesPaste?: (files: File[]) => void;
className?: React.HTMLAttributes<HTMLDivElement>["className"];
buttonClassName?: React.HTMLAttributes<HTMLButtonElement>["className"];
}
@@ -35,7 +35,7 @@ export function ChatInput({
onChange,
onFocus,
onBlur,
onImagePaste,
onFilesPaste,
className,
buttonClassName,
}: ChatInputProps) {
@@ -45,15 +45,11 @@ export function ChatInput({
const handlePaste = (event: React.ClipboardEvent<HTMLTextAreaElement>) => {
// Only handle paste if we have an image paste handler and there are files
if (onImagePaste && event.clipboardData.files.length > 0) {
const files = Array.from(event.clipboardData.files).filter((file) =>
file.type.startsWith("image/"),
);
if (onFilesPaste && event.clipboardData.files.length > 0) {
const files = Array.from(event.clipboardData.files);
// Only prevent default if we found image files to handle
if (files.length > 0) {
event.preventDefault();
onImagePaste(files);
}
event.preventDefault();
onFilesPaste(files);
}
// For text paste, let the default behavior handle it
};
@@ -73,12 +69,10 @@ export function ChatInput({
const handleDrop = (event: React.DragEvent<HTMLTextAreaElement>) => {
event.preventDefault();
setIsDraggingOver(false);
if (onImagePaste && event.dataTransfer.files.length > 0) {
const files = Array.from(event.dataTransfer.files).filter((file) =>
file.type.startsWith("image/"),
);
if (onFilesPaste && event.dataTransfer.files.length > 0) {
const files = Array.from(event.dataTransfer.files);
if (files.length > 0) {
onImagePaste(files);
onFilesPaste(files);
}
}
};

View File

@@ -29,6 +29,7 @@ import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
import { useWSErrorMessage } from "#/hooks/use-ws-error-message";
import { ErrorMessageBanner } from "./error-message-banner";
import { shouldRenderEvent } from "./event-content-helpers/should-render-event";
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
import { useConfig } from "#/hooks/query/use-config";
function getEntryPoint(
@@ -69,13 +70,18 @@ export function ChatInterface() {
);
const params = useParams();
const { mutate: getTrajectory } = useGetTrajectory();
const { mutateAsync: uploadFiles } = useUploadFiles();
const optimisticUserMessage = getOptimisticUserMessage();
const errorMessage = getErrorMessage();
const events = parsedEvents.filter(shouldRenderEvent);
const handleSendMessage = async (content: string, files: File[]) => {
const handleSendMessage = async (
content: string,
images: File[],
files: File[],
) => {
if (events.length === 0) {
posthog.capture("initial_query_submitted", {
entry_point: getEntryPoint(
@@ -91,11 +97,23 @@ export function ChatInterface() {
current_message_length: content.length,
});
}
const promises = files.map((file) => convertImageToBase64(file));
const promises = images.map((image) => convertImageToBase64(image));
const imageUrls = await Promise.all(promises);
const timestamp = new Date().toISOString();
send(createChatMessage(content, imageUrls, timestamp));
const { skipped_files: skippedFiles, uploaded_files: uploadedFiles } =
files.length > 0
? await uploadFiles({ conversationId: params.conversationId!, files })
: { skipped_files: [], uploaded_files: [] };
skippedFiles.forEach((f) => displayErrorToast(f.reason));
const filePrompt = `${t("CHAT_INTERFACE$AUGMENTED_PROMPT_FILES_TITLE")}: ${uploadedFiles.join("\n\n")}`;
const prompt =
uploadedFiles.length > 0 ? `${content}\n\n${filePrompt}` : content;
send(createChatMessage(prompt, imageUrls, uploadedFiles, timestamp));
setOptimisticUserMessage(content);
setMessageToSend(null);
};
@@ -177,7 +195,7 @@ export function ChatInterface() {
events.length > 0 &&
!optimisticUserMessage && (
<ActionSuggestions
onSuggestionsClick={(value) => handleSendMessage(value, [])}
onSuggestionsClick={(value) => handleSendMessage(value, [], [])}
/>
)}
</div>

View File

@@ -0,0 +1,19 @@
import {
AssistantMessageAction,
UserMessageAction,
} from "#/types/core/actions";
import i18n from "#/i18n";
import { isUserMessage } from "#/types/core/guards";
export const parseMessageFromEvent = (
event: UserMessageAction | AssistantMessageAction,
): string => {
const m = isUserMessage(event) ? event.args.content : event.message;
if (!event.args.file_urls || event.args.file_urls.length === 0) {
return m;
}
const delimiter = i18n.t("CHAT_INTERFACE$AUGMENTED_PROMPT_FILES_TITLE");
const parts = m.split(delimiter);
return parts[0];
};

View File

@@ -19,6 +19,8 @@ import { MCPObservationContent } from "./mcp-observation-content";
import { getObservationResult } from "./event-content-helpers/get-observation-result";
import { getEventContent } from "./event-content-helpers/get-event-content";
import { GenericEventMessage } from "./generic-event-message";
import { FileList } from "../files/file-list";
import { parseMessageFromEvent } from "./event-content-helpers/parse-message-from-event";
import { LikertScale } from "../feedback/likert-scale";
import { useConfig } from "#/hooks/query/use-config";
@@ -91,14 +93,16 @@ export function EventMessage({
}
if (isUserMessage(event) || isAssistantMessage(event)) {
const message = parseMessageFromEvent(event);
return (
<ChatMessage
type={event.source}
message={isUserMessage(event) ? event.args.content : event.message}
>
<ChatMessage type={event.source} message={message}>
{event.args.image_urls && event.args.image_urls.length > 0 && (
<ImageCarousel size="small" images={event.args.image_urls} />
)}
{event.args.file_urls && event.args.file_urls.length > 0 && (
<FileList files={event.args.file_urls} />
)}
{shouldShowConfirmationButtons && <ConfirmationButtons />}
</ChatMessage>
);

View File

@@ -3,11 +3,13 @@ import { ChatInput } from "./chat-input";
import { cn } from "#/utils/utils";
import { ImageCarousel } from "../images/image-carousel";
import { UploadImageInput } from "../images/upload-image-input";
import { FileList } from "../files/file-list";
import { isFileImage } from "#/utils/is-file-image";
interface InteractiveChatBoxProps {
isDisabled?: boolean;
mode?: "stop" | "submit";
onSubmit: (message: string, images: File[]) => void;
onSubmit: (message: string, images: File[], files: File[]) => void;
onStop: () => void;
value?: string;
onChange?: (message: string) => void;
@@ -22,21 +24,35 @@ export function InteractiveChatBox({
onChange,
}: InteractiveChatBoxProps) {
const [images, setImages] = React.useState<File[]>([]);
const [files, setFiles] = React.useState<File[]>([]);
const handleUpload = (files: File[]) => {
setImages((prevImages) => [...prevImages, ...files]);
const handleUpload = (selectedFiles: File[]) => {
setFiles((prevFiles) => [
...prevFiles,
...selectedFiles.filter((f) => !isFileImage(f)),
]);
setImages((prevImages) => [
...prevImages,
...selectedFiles.filter((f) => isFileImage(f)),
]);
};
const removeElementByIndex = (array: Array<File>, index: number) => {
const newArray = [...array];
newArray.splice(index, 1);
return newArray;
};
const handleRemoveFile = (index: number) => {
setFiles(removeElementByIndex(files, index));
};
const handleRemoveImage = (index: number) => {
setImages((prevImages) => {
const newImages = [...prevImages];
newImages.splice(index, 1);
return newImages;
});
setImages(removeElementByIndex(images, index));
};
const handleSubmit = (message: string) => {
onSubmit(message, images);
onSubmit(message, images, files);
setFiles([]);
setImages([]);
if (message) {
onChange?.("");
@@ -55,6 +71,12 @@ export function InteractiveChatBox({
onRemove={handleRemoveImage}
/>
)}
{files.length > 0 && (
<FileList
files={files.map((f) => f.name)}
onRemove={handleRemoveFile}
/>
)}
<div
className={cn(
@@ -72,7 +94,7 @@ export function InteractiveChatBox({
onSubmit={handleSubmit}
onStop={onStop}
value={value}
onImagePaste={handleUpload}
onFilesPaste={handleUpload}
className="py-[10px]"
buttonClassName="py-[10px]"
/>

View File

@@ -0,0 +1,34 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { BudgetProgressBar } from "./budget-progress-bar";
import { BudgetUsageText } from "./budget-usage-text";
interface BudgetDisplayProps {
cost: number | null;
maxBudgetPerTask: number | null;
}
export function BudgetDisplay({ cost, maxBudgetPerTask }: BudgetDisplayProps) {
const { t } = useTranslation();
// Don't render anything if cost is not available
if (cost === null) {
return null;
}
return (
<div className="border-b border-neutral-700">
{maxBudgetPerTask !== null && maxBudgetPerTask > 0 ? (
<>
<BudgetProgressBar currentCost={cost} maxBudget={maxBudgetPerTask} />
<BudgetUsageText currentCost={cost} maxBudget={maxBudgetPerTask} />
</>
) : (
<span className="text-xs text-neutral-400">
{t(I18nKey.CONVERSATION$NO_BUDGET_LIMIT)}
</span>
)}
</div>
);
}

View File

@@ -0,0 +1,27 @@
import React from "react";
interface BudgetProgressBarProps {
currentCost: number;
maxBudget: number;
}
export function BudgetProgressBar({
currentCost,
maxBudget,
}: BudgetProgressBarProps) {
const usagePercentage = (currentCost / maxBudget) * 100;
const isNearLimit = usagePercentage > 80;
return (
<div className="w-full h-1.5 bg-neutral-700 rounded-full overflow-hidden mt-1">
<div
className={`h-full transition-all duration-300 ${
isNearLimit ? "bg-red-500" : "bg-blue-500"
}`}
style={{
width: `${Math.min(100, usagePercentage)}%`,
}}
/>
</div>
);
}

View File

@@ -0,0 +1,25 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface BudgetUsageTextProps {
currentCost: number;
maxBudget: number;
}
export function BudgetUsageText({
currentCost,
maxBudget,
}: BudgetUsageTextProps) {
const { t } = useTranslation();
const usagePercentage = (currentCost / maxBudget) * 100;
return (
<div className="flex justify-end">
<span className="text-xs text-neutral-400">
${currentCost.toFixed(4)} / ${maxBudget.toFixed(4)} (
{usagePercentage.toFixed(2)}% {t(I18nKey.CONVERSATION$USED)})
</span>
</div>
);
}

View File

@@ -9,6 +9,7 @@ import { EllipsisButton } from "./ellipsis-button";
import { ConversationCardContextMenu } from "./conversation-card-context-menu";
import { SystemMessageModal } from "./system-message-modal";
import { MicroagentsModal } from "./microagents-modal";
import { BudgetDisplay } from "./budget-display";
import { cn } from "#/utils/utils";
import { BaseModal } from "../../shared/modals/base-modal/base-modal";
import { RootState } from "#/store";
@@ -285,7 +286,7 @@ export function ConversationCard({
<div className="rounded-md p-3">
<div className="grid gap-3">
{metrics?.cost !== null && (
<div className="flex justify-between items-center border-b border-neutral-700 pb-2">
<div className="flex justify-between items-center pb-2">
<span className="text-lg font-semibold">
{t(I18nKey.CONVERSATION$TOTAL_COST)}
</span>
@@ -294,6 +295,10 @@ export function ConversationCard({
</span>
</div>
)}
<BudgetDisplay
cost={metrics?.cost ?? null}
maxBudgetPerTask={metrics?.max_budget_per_task ?? null}
/>
{metrics?.usage !== null && (
<>

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useContext } from "react";
import { useTranslation } from "react-i18next";
import { cn } from "#/utils/utils";
import i18n from "#/i18n";
import { I18nKey } from "#/i18n/declaration";
import { useSubmitConversationFeedback } from "#/hooks/mutation/use-submit-conversation-feedback";
import { ScrollContext } from "#/context/scroll-context";
@@ -14,19 +15,14 @@ interface LikertScaleProps {
initialReason?: string;
}
const FEEDBACK_REASONS = [
i18n.t("FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION"),
i18n.t("FEEDBACK$REASON_FORGOT_CONTEXT"),
i18n.t("FEEDBACK$REASON_UNNECESSARY_CHANGES"),
i18n.t("FEEDBACK$REASON_OTHER"),
];
export function LikertScale({
eventId,
initiallySubmitted = false,
initialRating,
initialReason,
}: LikertScaleProps) {
const { t } = useTranslation();
const [selectedRating, setSelectedRating] = useState<number | null>(
initialRating || null,
);
@@ -43,6 +39,14 @@ export function LikertScale({
// Get scroll context
const scrollContext = useContext(ScrollContext);
// Define feedback reasons using the translation hook
const FEEDBACK_REASONS = [
t(I18nKey.FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION),
t(I18nKey.FEEDBACK$REASON_FORGOT_CONTEXT),
t(I18nKey.FEEDBACK$REASON_UNNECESSARY_CHANGES),
t(I18nKey.FEEDBACK$REASON_OTHER),
];
// If scrollContext is undefined, we're not inside a ScrollProvider
const scrollToBottom = scrollContext?.scrollDomToBottom;
const autoScroll = scrollContext?.autoScroll;
@@ -188,8 +192,8 @@ export function LikertScale({
<div className="mt-3 flex flex-col gap-1">
<div className="text-sm text-gray-500 mb-1">
{isSubmitted
? i18n.t("FEEDBACK$THANK_YOU_FOR_FEEDBACK")
: i18n.t("FEEDBACK$RATE_AGENT_PERFORMANCE")}
? t(I18nKey.FEEDBACK$THANK_YOU_FOR_FEEDBACK)
: t(I18nKey.FEEDBACK$RATE_AGENT_PERFORMANCE)}
</div>
<div className="flex flex-col gap-1">
<span className="flex gap-2 items-center flex-wrap">
@@ -220,11 +224,11 @@ export function LikertScale({
{showReasons && !isSubmitted && (
<div className="mt-1 flex flex-col gap-1">
<div className="text-xs text-gray-500 mb-1">
{i18n.t("FEEDBACK$SELECT_REASON")}
{t(I18nKey.FEEDBACK$SELECT_REASON)}
</div>
{countdown > 0 && (
<div className="text-xs text-gray-400 mb-1 italic">
{i18n.t("FEEDBACK$SELECT_REASON_COUNTDOWN", {
{t(I18nKey.FEEDBACK$SELECT_REASON_COUNTDOWN, {
countdown,
})}
</div>

View File

@@ -0,0 +1,20 @@
import { FaFile } from "react-icons/fa";
import { RemoveButton } from "#/components/shared/buttons/remove-button";
interface FileItemProps {
filename: string;
onRemove?: () => void;
}
export function FileItem({ filename, onRemove }: FileItemProps) {
return (
<div
data-testid="file-item"
className="flex flex-row gap-x-1 items-center justify-start"
>
<FaFile className="h-4 w-4" />
<code className="text-sm flex-1 text-white truncate">{filename}</code>
{onRemove && <RemoveButton onClick={onRemove} />}
</div>
);
}

View File

@@ -0,0 +1,25 @@
import React from "react";
import { cn } from "#/utils/utils";
import { FileItem } from "./file-item";
interface FileListProps {
files: string[];
onRemove?: (index: number) => void;
}
export function FileList({ files, onRemove }: FileListProps) {
return (
<div
data-testid="file-list"
className={cn("flex flex-col gap-y-1.5 justify-start")}
>
{files.map((f, index) => (
<FileItem
key={index}
filename={f}
onRemove={onRemove ? () => onRemove?.(index) : undefined}
/>
))}
</div>
);
}

View File

@@ -93,9 +93,7 @@ export function RepositorySelectionForm({
}));
const handleRepoSelection = (key: React.Key | null) => {
const selectedRepo = allRepositories?.find(
(repo) => repo.id.toString() === key,
);
const selectedRepo = allRepositories?.find((repo) => repo.id === key);
if (selectedRepo) onRepoSelection(selectedRepo.full_name);
setSelectedRepository(selectedRepo || null);

View File

@@ -54,6 +54,10 @@ export function TaskCard({ task }: TaskCardProps) {
const issueType =
task.task_type === "OPEN_ISSUE" ? "issues" : "merge_requests";
href = `https://gitlab.com/${task.repo}/-/${issueType}/${task.issue_number}`;
} else if (task.git_provider === "bitbucket") {
const issueType =
task.task_type === "OPEN_ISSUE" ? "issues" : "pull-requests";
href = `https://bitbucket.org/${task.repo}/${issueType}/${task.issue_number}`;
} else {
const hrefType = task.task_type === "OPEN_ISSUE" ? "issues" : "pull";
href = `https://github.com/${task.repo}/${hrefType}/${task.issue_number}`;

View File

@@ -60,7 +60,7 @@ export function ImageCarousel({
key={index}
size={size}
src={src}
onRemove={onRemove && (() => onRemove(index))}
onRemove={onRemove ? () => onRemove?.(index) : undefined}
/>
))}
</div>

View File

@@ -15,7 +15,12 @@ export function ImagePreview({
return (
<div data-testid="image-preview" className="relative w-fit shrink-0">
<Thumbnail src={src} size={size} />
{onRemove && <RemoveButton onClick={onRemove} />}
{onRemove && (
<RemoveButton
onClick={onRemove}
className="absolute right-[3px] top-[3px]"
/>
)}
</div>
);
}

View File

@@ -8,10 +8,7 @@ interface UploadImageInputProps {
export function UploadImageInput({ onUpload, label }: UploadImageInputProps) {
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
const validFiles = Array.from(event.target.files).filter((file) =>
file.type.startsWith("image/"),
);
onUpload(validFiles);
onUpload(Array.from(event.target.files));
}
};
@@ -21,7 +18,6 @@ export function UploadImageInput({ onUpload, label }: UploadImageInputProps) {
<input
data-testid="upload-image-input"
type="file"
accept="image/*"
multiple
hidden
onChange={handleUpload}

View File

@@ -0,0 +1,30 @@
import { Trans } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function BitbucketTokenHelpAnchor() {
return (
<p data-testid="bitbucket-token-help-anchor" className="text-xs">
<Trans
i18nKey={I18nKey.BITBUCKET$TOKEN_HELP_TEXT}
components={[
<a
key="bitbucket-token-help-anchor-link"
aria-label="Bitbucket token help link"
href="https://bitbucket.org/account/settings/app-passwords/new?scopes=repository:write,pullrequest:write,issue:write"
target="_blank"
className="underline underline-offset-2"
rel="noopener noreferrer"
/>,
<a
key="bitbucket-token-help-anchor-link-2"
aria-label="Bitbucket token see more link"
href="https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/"
target="_blank"
className="underline underline-offset-2"
rel="noopener noreferrer"
/>,
]}
/>
</p>
);
}

View File

@@ -0,0 +1,64 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { SettingsInput } from "../settings-input";
import { BitbucketTokenHelpAnchor } from "./bitbucket-token-help-anchor";
import { KeyStatusIcon } from "../key-status-icon";
interface BitbucketTokenInputProps {
onChange: (value: string) => void;
onBitbucketHostChange: (value: string) => void;
isBitbucketTokenSet: boolean;
name: string;
bitbucketHostSet: string | null | undefined;
}
export function BitbucketTokenInput({
onChange,
onBitbucketHostChange,
isBitbucketTokenSet,
name,
bitbucketHostSet,
}: BitbucketTokenInputProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col gap-6">
<SettingsInput
testId={name}
name={name}
onChange={onChange}
label={t(I18nKey.BITBUCKET$TOKEN_LABEL)}
type="password"
className="w-full max-w-[680px]"
placeholder={isBitbucketTokenSet ? "<hidden>" : "username:app_password"}
startContent={
isBitbucketTokenSet && (
<KeyStatusIcon
testId="bb-set-token-indicator"
isSet={isBitbucketTokenSet}
/>
)
}
/>
<SettingsInput
onChange={onBitbucketHostChange || (() => {})}
name="bitbucket-host-input"
testId="bitbucket-host-input"
label={t(I18nKey.BITBUCKET$HOST_LABEL)}
type="text"
className="w-full max-w-[680px]"
placeholder="bitbucket.org"
defaultValue={bitbucketHostSet || undefined}
startContent={
bitbucketHostSet &&
bitbucketHostSet.trim() !== "" && (
<KeyStatusIcon testId="bb-set-host-indicator" isSet />
)
}
/>
<BitbucketTokenHelpAnchor />
</div>
);
}

View File

@@ -3,19 +3,20 @@ import CloseIcon from "#/icons/close.svg?react";
interface RemoveButtonProps {
onClick: () => void;
className?: React.HTMLAttributes<HTMLDivElement>["className"];
}
export function RemoveButton({ onClick }: RemoveButtonProps) {
export function RemoveButton({ onClick, className }: RemoveButtonProps) {
return (
<button
type="button"
onClick={onClick}
className={cn(
"bg-neutral-400 rounded-full w-3 h-3 flex items-center justify-center",
"absolute right-[3px] top-[3px]",
"bg-neutral-400 rounded-full w-5 h-5 flex items-center justify-center",
className,
)}
>
<CloseIcon width={10} height={10} />
<CloseIcon width={18} height={18} />
</button>
);
}

View File

@@ -1,126 +0,0 @@
import React from "react";
import { useNavigation } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "#/store";
import { addFile, removeFile } from "#/state/initial-query-slice";
import { SuggestionBubble } from "#/components/features/suggestions/suggestion-bubble";
import { SUGGESTIONS } from "#/utils/suggestions";
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
import { ChatInput } from "#/components/features/chat/chat-input";
import { getRandomKey } from "#/utils/get-random-key";
import { cn } from "#/utils/utils";
import { AttachImageLabel } from "../features/images/attach-image-label";
import { ImageCarousel } from "../features/images/image-carousel";
import { UploadImageInput } from "../features/images/upload-image-input";
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
import { LoadingSpinner } from "./loading-spinner";
interface TaskFormProps {
ref: React.RefObject<HTMLFormElement | null>;
}
export function TaskForm({ ref }: TaskFormProps) {
const dispatch = useDispatch();
const navigation = useNavigation();
const { files } = useSelector((state: RootState) => state.initialQuery);
const [text, setText] = React.useState("");
const [suggestion, setSuggestion] = React.useState(() => {
const key = getRandomKey(SUGGESTIONS["non-repo"]);
return { key, value: SUGGESTIONS["non-repo"][key] };
});
const [inputIsFocused, setInputIsFocused] = React.useState(false);
const { mutate: createConversation, isPending } = useCreateConversation();
const onRefreshSuggestion = () => {
const suggestions = SUGGESTIONS["non-repo"];
// remove current suggestion to avoid refreshing to the same suggestion
const suggestionCopy = { ...suggestions };
delete suggestionCopy[suggestion.key];
const key = getRandomKey(suggestionCopy);
setSuggestion({ key, value: suggestions[key] });
};
const onClickSuggestion = () => {
setText(suggestion.value);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const q = formData.get("q")?.toString();
createConversation({ q });
};
return (
<div className="flex flex-col gap-1 w-full">
<form
ref={ref}
onSubmit={handleSubmit}
className="flex flex-col items-center gap-2"
>
<SuggestionBubble
suggestion={suggestion}
onClick={onClickSuggestion}
onRefresh={onRefreshSuggestion}
/>
<div
className={cn(
"border border-neutral-600 px-4 rounded-lg text-[17px] leading-5 w-full transition-colors duration-200",
inputIsFocused ? "bg-neutral-600" : "bg-tertiary",
"hover:border-neutral-500 focus-within:border-neutral-500",
)}
>
{isPending ? (
<div className="flex justify-center py-[17px]">
<LoadingSpinner size="small" />
</div>
) : (
<ChatInput
name="q"
onSubmit={() => {
if (typeof ref !== "function") ref?.current?.requestSubmit();
}}
onChange={(message) => setText(message)}
onFocus={() => setInputIsFocused(true)}
onBlur={() => setInputIsFocused(false)}
onImagePaste={async (imageFiles) => {
const promises = imageFiles.map(convertImageToBase64);
const base64Images = await Promise.all(promises);
base64Images.forEach((base64) => {
dispatch(addFile(base64));
});
}}
value={text}
maxRows={15}
showButton={!!text}
className="text-[17px] leading-5 py-[17px]"
buttonClassName="pb-[17px]"
disabled={navigation.state === "submitting"}
/>
)}
</div>
</form>
<UploadImageInput
onUpload={async (uploadedFiles) => {
const promises = uploadedFiles.map(convertImageToBase64);
const base64Images = await Promise.all(promises);
base64Images.forEach((base64) => {
dispatch(addFile(base64));
});
}}
label={<AttachImageLabel />}
/>
{files.length > 0 && (
<ImageCarousel
size="large"
images={files}
onRemove={(index) => dispatch(removeFile(index))}
/>
)}
</div>
);
}

View File

@@ -348,6 +348,7 @@ export function WsClientProvider({
conversation?.url,
conversation?.status,
conversation?.runtime_status,
providers,
]);
React.useEffect(

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