Compare commits

..

59 Commits

Author SHA1 Message Date
Engel Nyst
70ad153c2c Add comprehensive unit tests for Ctrl+C behavior
- test_single_ctrl_c_stops_agent: Verifies first Ctrl+C stops agent gracefully with helpful message
- test_double_ctrl_c_raises_keyboard_interrupt: Verifies second Ctrl+C within 2 seconds raises KeyboardInterrupt for CLI cleanup
- test_ctrl_p_pauses_agent: Verifies Ctrl+P still pauses agent as expected

Tests use proper mocking of prompt_toolkit's create_input, raw_mode, and attach context managers.
All tests pass and validate the improved Ctrl+C behavior implementation.

Co-authored-by: OpenHands-Claude <openhands-claude@all-hands.dev>
2025-06-28 16:17:48 +02:00
Engel Nyst
bded599449 Fix Ctrl+C behavior: use KeyboardInterrupt instead of signals
The previous approach using os.kill(os.getpid(), signal.SIGTERM) was too
aggressive and caused runtime crashes. The proper solution is to raise
KeyboardInterrupt and let the CLI main function handle it gracefully.

Key insights:
- CLI main function already has proper KeyboardInterrupt handling
- shutdown_listener is designed for server mode (uvicorn) primarily
- Raw input mode intercepts Ctrl+C before it becomes SIGINT
- Raising KeyboardInterrupt allows normal CLI shutdown flow

This approach:
- First Ctrl+C: stops agent gracefully with helpful message
- Second Ctrl+C: raises KeyboardInterrupt for clean application exit
- No more runtime crashes or 'system crashed and restarted' errors

Co-authored-by: OpenHands-Claude <openhands@all-hands.dev>
2025-06-28 16:02:22 +02:00
Engel Nyst
0bb43193d0 Use proper signal mechanism for double Ctrl+C shutdown
Instead of directly setting shutdown_listener._should_exit = True,
use os.kill(os.getpid(), signal.SIGTERM) to trigger the proper
shutdown signal handler.

This follows the established pattern where:
- shutdown_listener registers signal handlers for SIGINT/SIGTERM
- Signal handler sets _should_exit = True and calls shutdown listeners
- Components check should_continue()/should_exit() for coordinated shutdown

Benefits:
- Follows OpenHands' established shutdown architecture
- Proper signal handling instead of direct flag manipulation
- Consistent with how other shutdown scenarios work
- Cleaner separation of concerns

Co-authored-by: OpenHands-Claude <openhands-claude@all-hands.dev>
2025-06-28 15:41:02 +02:00
Engel Nyst
72b1aa6154 Fix double Ctrl+C to use global shutdown mechanism
Instead of raising KeyboardInterrupt which interrupts pending actions
and causes 'runtime system crashed' errors, use the existing global
shutdown_listener mechanism that gracefully shuts down all components.

This prevents:
- [Errno 21] Is a directory errors
- 'runtime system crashed and restarted' messages
- Delayed action execution after restart
- Missing 'Force quitting...' message

The shutdown_listener._should_exit flag is checked by EventStream
and other components for clean shutdown coordination.
2025-06-28 15:24:03 +02:00
Engel Nyst
db5f7a5744 Implement double Ctrl+C behavior for graceful vs force quit
Changed to a more user-friendly approach:
- First Ctrl+C: Stops agent gracefully (sets STOPPED state)
- Second Ctrl+C within 2 seconds: Force quits application

This provides better UX by allowing users to:
1. Stop the current agent task without quitting the CLI
2. Force quit if they really want to exit the application

Messages shown:
- First: 'Stopping agent... (press Ctrl+C again within 2 seconds to force quit)'
- Second: 'Force quitting...'

Co-authored-by: OpenHands-Claude <openhands@all-hands.dev>
2025-06-28 14:46:54 +02:00
Engel Nyst
fbe253f9e9 Fix Ctrl+C to cleanly stop agent instead of hard interrupt
Changed approach from raising KeyboardInterrupt to setting agent state
to STOPPED, which allows for clean shutdown without race conditions.

Changes:
- Ctrl+C now sets AgentState.STOPPED and signals done event
- Shows 'Keyboard interrupt, shutting down...' message
- Avoids 'cannot schedule new futures after interpreter shutdown' error
- Ctrl+P and Ctrl+D continue to pause the agent as before

This approach prevents the race condition where background tasks try to
schedule futures after the interpreter begins shutdown.

Co-authored-by: OpenHands-Claude <openhands@all-hands.dev>
2025-06-28 14:30:01 +02:00
Engel Nyst
f70f07e19a Fix Ctrl+C to raise KeyboardInterrupt instead of ignoring it
The previous fix removed Ctrl+C handling entirely, which caused it to be
consumed by the input handler but do nothing. This fix makes Ctrl+C
explicitly raise KeyboardInterrupt, which will properly terminate the
application.

Changes:
- Ctrl+C now raises KeyboardInterrupt in process_agent_pause
- Ctrl+P and Ctrl+D continue to pause the agent as before
- Application will properly terminate when Ctrl+C is pressed

Co-authored-by: OpenHands-Claude <openhands@all-hands.dev>
2025-06-28 14:21:48 +02:00
Engel Nyst
c9a2dec194 Fix Ctrl+C behavior in CLI to properly interrupt and stop application
Previously, Ctrl+C was intercepted by the process_agent_pause function
and treated the same as Ctrl+P (pause agent). This prevented normal
KeyboardInterrupt handling and made it impossible to stop the application
with Ctrl+C.

Changes:
- Remove Keys.ControlC from process_agent_pause function
- Now only Ctrl+P and Ctrl+D pause the agent
- Ctrl+C properly propagates as KeyboardInterrupt to main function
- Application can now be terminated normally with Ctrl+C

Co-authored-by: OpenHands-Claude <openhands@all-hands.dev>
2025-06-28 14:08:01 +02:00
Graham Neubig
2c2a721937 Fix unit tests to be environment-independent for cloud deployment (#9425)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-27 20:43:09 -04:00
AutoLTX
7abad5844a [Feature] Support .cursorrules (#9327)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-28 02:33:19 +02:00
dependabot[bot]
4781e9a424 chore(deps): bump the version-all group across 1 directory with 20 updates (#9421)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-27 20:32:51 -04:00
llamantino
a24d7e636e fix(cli): avoid race condition from multiple process_agent_pause tasks (#9423) 2025-06-27 23:22:43 +00:00
Peter Hamilton
66b95adbc9 Fix: Retry on Bedrock ServiceUnavailableError (#9419)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-27 22:17:50 +02:00
mamoodi
d617d6842a Release 0.47.0 (#9405)
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-27 13:59:36 -04:00
Xingyao Wang
0eb7f956a9 fix(CLI): Reduce severity of pending action timeout messages (#9415)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-27 16:28:31 +00:00
Graham Neubig
d3154c4bae Fix CLI import error with broken third-party runtime dependencies (#9413)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-27 12:00:38 -04:00
Calvin Smith
04a15b1467 Condensation request signal in event stream (#9097)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-06-27 09:57:39 -06:00
Xingyao Wang
b74da7d4c3 feat(CLI): Enhance --file option to prompt agent to read and understand file first (#9398)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-27 15:57:29 +00:00
Graham Neubig
70ad469fb2 Fix typing 2025-06-26 23:47:54 -04:00
Graham Neubig
a85f6af9c2 Fix typing in memory module 2025-06-26 23:46:37 -04:00
Graham Neubig
5e213963dc Fix typing 2025-06-26 23:43:13 -04:00
openhands
051c579855 Fix mypy type error in memory.py with reference to GitHub issue #18440 2025-06-27 03:38:50 +00:00
openhands
6d66b8503c Fix mypy type error in memory.py by adding type ignore annotations 2025-06-27 03:20:20 +00:00
Engel Nyst
0fb1a712d5 feat: Add user directory support for microagents (#9333)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-26 22:31:59 -04:00
Ray Myers
94fe052561 chore - Add pydantic lib to type checking (#9086)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-26 18:31:41 +00:00
Robert Brennan
612bc3fa60 Fix prompt for pushing to a branch to check for main/master (#9397)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-26 16:48:13 +00:00
Engel Nyst
668906f079 Fix swe bench modal (#9242)
Co-authored-by: Hoang Tran <descience.thh10@gmail.com>
2025-06-27 00:10:24 +08:00
Graham Neubig
c7dff3e4d2 Remove third-party runtimes (daytona, modal, e2b, runloop) from main codebase (#9213)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-06-26 07:39:39 -04:00
Graham Neubig
6efb992bae Fix incomplete localization issue #9282 (#9283)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-26 03:09:48 +00:00
Ray Myers
fafbe81d51 chore - Don't build ubuntu image on PR (#9379) 2025-06-25 22:55:13 -04:00
Robert Brennan
dfe6f2d8cc Fix terminal truncation to trim middle of long outputs instead of suffix (#9365)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-26 07:19:23 +08:00
Xingyao Wang
743c814ee8 Add important warning about not pushing/creating PRs unless explicitly asked (#9357) 2025-06-25 19:09:48 -04:00
Tim O'Farrell
feb529b1d5 Fix alignment on typing indicator (#9367) 2025-06-25 15:40:34 -06:00
Robert Brennan
8f566a4247 Update Slack invite links across all documentation (#9372)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-25 21:29:46 +00:00
Graham Neubig
0e4aeba47c Add GitLab alternative directory support for microagents (#9331)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-25 21:08:01 +00:00
Robert Brennan
d37e40caf8 Fix Bitbucket pagination and sorting to fetch ALL repositories (#9356)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-25 21:06:01 +00:00
Xingyao Wang
8e4a8a65f8 Revert "Simplify max_output_tokens handling in LLM classes" (#9364) 2025-06-25 20:01:23 +00:00
Graham Neubig
e9027e2ae8 Add YouTube video tutorial to CLI documentation (#9351)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-25 19:09:18 +00:00
Engel Nyst
1fd0aefd20 Revert "chore(deps): bump the version-all group across 1 directory with 12 updates" (#9347) 2025-06-26 01:24:07 +08:00
dependabot[bot]
722fabfa97 chore(deps-dev): bump the eslint group across 1 directory with 3 updates (#9348)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-25 12:16:12 -04:00
dependabot[bot]
24f12eed12 chore(deps): bump the version-all group across 1 directory with 12 updates (#9326)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-25 08:34:40 -04:00
Ryan H. Tran
dfa54673d2 [OH-Versa] Add remaining browsing & GAIA eval improvement (#9015)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-25 12:36:15 +07:00
Xingyao Wang
76914e3c26 Add new feedback reason: The agent should have asked me first before doing it (#9332)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-24 22:12:20 -04:00
mamoodi
b0b820f8b2 Release 0.46.0 (#9328) 2025-06-24 16:47:17 -04:00
Rohit Malhotra
5c8bdd364e [Feat]: BitBucket integration for Cloud OpenHands (#9225)
Co-authored-by: chuckbutkus <chuck@all-hands.dev>
2025-06-24 15:40:58 -04:00
Engel Nyst
0c1c570dac Microagents doc (for LLMs) (#9324)
Co-authored-by: OpenHands-Claude <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-06-24 17:04:36 +02:00
mindflow-cn
fa75b22cc0 Enhanced llm editor (#9174)
Co-authored-by: jianchuanli <jianchuanli@langcode.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-06-24 13:57:18 +00:00
Graham Neubig
8aeb4dd632 Fix org repo deletion to run in runtime (#9319) 2025-06-24 21:43:45 +08:00
mamoodi
4c34a5f0f5 Make some doc changes for consistency (#9309)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-24 08:34:11 -04:00
mamoodi
848f692033 Update CLI docs (#9074)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-23 21:58:51 +00:00
Xingyao Wang
2df4536420 Show Likert scale feedback form on AWAITING_USER_INPUT and ERROR agent states (#9292)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-23 21:18:19 +00:00
Robert Brennan
d66bcf5021 Update README.md with OpenHands Cloud chart (#9194) 2025-06-23 16:59:26 -04:00
Graham Neubig
4f5e146783 Better translation of "let's start building" in Japanese (#9310)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-23 20:15:42 +00:00
sp.wack
0c38fb0ceb chore(frontend): OpenHands design library scaffold (#9224) 2025-06-23 15:19:35 -04:00
Graham Neubig
7b0f880860 Fix Pydantic class-based config deprecation warnings (#9279)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-23 13:10:38 -06:00
Mizote Hikaru
a156d5d243 fix: create metadata.json when joining conversation if it doesn't exist (#8986) 2025-06-23 15:05:26 -04:00
Graham Neubig
c29b5e9757 Fix automatic lowercasing of model names in LLM integration (#9271)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-23 18:59:06 +00:00
Graham Neubig
5e5168ffd4 Fix Pydantic model_fields instance access deprecation warnings (#9278)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-23 18:54:13 +00:00
MXDI
6aad23d35c feat: Add support for Mistral AI models with customizable safety sett… (#8802)
Co-authored-by: Mahdiglm <mahdiglm@users.noreply.github.com>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-23 18:37:06 +00:00
217 changed files with 5330 additions and 2560 deletions

View File

@@ -40,9 +40,7 @@ jobs:
# Only build nikolaik on PRs, otherwise build both nikolaik and ubuntu.
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
json=$(jq -n -c '[
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
{ image: "ubuntu:24.04", tag: "ubuntu" }
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" }
]')
else
json=$(jq -n -c '[

View File

@@ -121,7 +121,7 @@ A specialized prompt that enhances OpenHands with domain-specific knowledge, rep
A central repository of available microagents and their configurations.
#### Public Microagent
A general-purpose microagent available to all OpenHands users, triggered by specific keywords.
A general-purpose microagent available to all OpenHands users, triggered by specific keywords. Located in `microagents/`.
#### Repository Microagent
A type of microagent that provides repository-specific context and guidelines, stored in the `.openhands/microagents/` directory.

View File

@@ -68,6 +68,29 @@ If you are starting a pull request (PR), please follow the template in `.github/
These details may or may not be useful for your current task.
### Microagents
Microagents are specialized prompts that enhance OpenHands with domain-specific knowledge and task-specific workflows. They are Markdown files that can include frontmatter for configuration.
#### Types:
- **Public Microagents**: Located in `microagents/`, available to all users
- **Repository Microagents**: Located in `.openhands/microagents/`, specific to this repository
#### Loading Behavior:
- **Without frontmatter**: Always loaded into LLM context
- **With triggers in frontmatter**: Only loaded when user's message matches the specified trigger keywords
#### Structure:
```yaml
---
triggers:
- keyword1
- keyword2
---
# Microagent Content
Your specialized knowledge and instructions here...
```
### Frontend
#### Action Handling:

View File

@@ -159,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.45-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.47-nikolaik`
## Develop inside Docker container

View File

@@ -11,7 +11,7 @@
<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="Join our Slack community"></a>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></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="Join our Discord community"></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="Credits"></a>
<br/>
@@ -62,17 +62,17 @@ system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-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
docker.all-hands.dev/all-hands-ai/openhands:0.47
```
> **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.
@@ -85,15 +85,14 @@ works best, but you have [many options](https://docs.all-hands.dev/usage/llms).
## 💡 Other ways to run OpenHands
> [!CAUTION]
> [!WARNING]
> 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, please
> [get in touch with us](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> for advanced deployment options.
> 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)
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem),
You can [connect OpenHands to your local filesystem](https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/usage/how-to/headless-mode),
interact with it via a [friendly CLI](https://docs.all-hands.dev/usage/how-to/cli-mode),
or run it on tagged issues with [a github action](https://docs.all-hands.dev/usage/how-to/github-action).
@@ -118,7 +117,7 @@ troubleshooting resources, and advanced configuration options.
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github:
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) - Here we talk about research, architecture, and future development.
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA) - Here we talk about research, architecture, and future development.
- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
- [Read or post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas.

View File

@@ -12,7 +12,7 @@
<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://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA"><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/>
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-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
docker.all-hands.dev/all-hands-ai/openhands:0.47
```
> **注意**: 如果您在0.44版本之前使用过OpenHands您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
@@ -107,7 +107,7 @@ docker run -it --rm --pull=always \
OpenHands是一个社区驱动的项目我们欢迎每个人的贡献。我们大部分沟通
通过Slack进行因此这是开始的最佳场所但我们也很乐意您通过Discord或Github与我们联系
- [加入我们的Slack工作空间](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) - 这里我们讨论研究、架构和未来发展。
- [加入我们的Slack工作空间](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA) - 这里我们讨论研究、架构和未来发展。
- [加入我们的Discord服务器](https://discord.gg/ESHStjSjD4) - 这是一个社区运营的服务器,用于一般讨论、问题和反馈。
- [阅读或发布Github问题](https://github.com/All-Hands-AI/OpenHands/issues) - 查看我们正在处理的问题,或添加您自己的想法。

View File

@@ -10,7 +10,7 @@
<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://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA"><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/>
@@ -42,17 +42,17 @@ OpenHandsはDockerを利用してローカル環境でも実行できます。
> 公共ネットワークで実行していますか?[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 pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-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
docker.all-hands.dev/all-hands-ai/openhands:0.47
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。

View File

@@ -10,18 +10,7 @@
# General core configurations
##############################################################################
[core]
# API key for E2B
#e2b_api_key = ""
# API key for Modal
#modal_api_token_id = ""
#modal_api_token_secret = ""
# API key for Daytona
#daytona_api_key = ""
# Daytona Target
#daytona_target = ""
# API keys and configuration for core services
# Base path for the workspace
#workspace_base = "./workspace"
@@ -201,6 +190,27 @@ model = "gpt-4o"
#native_tool_calling = None
# Safety settings for models that support them (e.g., Mistral AI, Gemini)
# Example for Mistral AI:
# safety_settings = [
# { "category" = "hate", "threshold" = "low" },
# { "category" = "harassment", "threshold" = "low" },
# { "category" = "sexual", "threshold" = "low" },
# { "category" = "dangerous", "threshold" = "low" }
# ]
#
# Example for Gemini:
# safety_settings = [
# { "category" = "HARM_CATEGORY_HARASSMENT", "threshold" = "BLOCK_NONE" },
# { "category" = "HARM_CATEGORY_HATE_SPEECH", "threshold" = "BLOCK_NONE" },
# { "category" = "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold" = "BLOCK_NONE" },
# { "category" = "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold" = "BLOCK_NONE" }
# ]
#safety_settings = []
[llm.draft_editor]
# The number of times llm_editor tries to fix an error when editing.
correct_num = 5
[llm.gpt4o-mini]
api_key = ""
@@ -250,6 +260,9 @@ enable_finish = true
# length limit
enable_history_truncation = true
# Whether the condensation request tool is enabled
enable_condensation_request = false
[agent.RepoExplorerAgent]
# Example: use a cheaper model for RepoExplorerAgent to reduce cost, especially
# useful when an agent doesn't demand high quality but uses a lot of tokens
@@ -318,6 +331,9 @@ classpath = "my_package.my_module.MyCustomAgent"
# Enable GPU support in the runtime
#enable_gpu = false
# When there are multiple cards, you can specify the GPU by ID
#cuda_visible_devices = ''
# Additional Docker runtime kwargs
#docker_runtime_kwargs = {}

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.45-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.47-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -3,9 +3,9 @@ repos:
rev: v5.0.0
hooks:
- id: trailing-whitespace
exclude: docs/modules/python
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/)
- id: end-of-file-fixer
exclude: docs/modules/python
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/)
- id: check-yaml
args: ["--allow-multiple-documents"]
- id: debug-statements
@@ -28,17 +28,19 @@ repos:
entry: ruff check --config dev_config/python/ruff.toml
types_or: [python, pyi, jupyter]
args: [--fix, --unsafe-fixes]
exclude: third_party/
# Run the formatter.
- id: ruff-format
entry: ruff format --config dev_config/python/ruff.toml
types_or: [python, pyi, jupyter]
exclude: third_party/
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
hooks:
- id: mypy
additional_dependencies:
[types-requests, types-setuptools, types-pyyaml, types-toml, types-docker, lxml]
[types-requests, types-setuptools, types-pyyaml, types-toml, types-docker, pydantic, lxml]
# To see gaps add `--html-report mypy-report/`
entry: mypy --config-file dev_config/python/mypy.ini openhands/
always_run: true

View File

@@ -7,3 +7,9 @@ warn_unreachable = True
warn_redundant_casts = True
no_implicit_optional = True
strict_optional = True
# Exclude third-party runtime directory from type checking
exclude = third_party/
[mypy-openhands.memory.condenser.impl.*]
disable_error_code = override

View File

@@ -1,3 +1,6 @@
# Exclude third-party runtime directory from linting
exclude = ["third_party/"]
[lint]
select = [
"E",

View File

@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.47-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:

View File

@@ -196,7 +196,7 @@
},
"footer": {
"socials": {
"slack": "https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A",
"slack": "https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA",
"github": "https://github.com/All-Hands-AI/OpenHands",
"discord": "https://discord.gg/ESHStjSjD4"
}

View File

@@ -3,6 +3,15 @@ title: Slack Integration (Beta)
description: This guide walks you through installing the OpenHands Slack app.
---
<iframe
className="w-full aspect-video"
src="https://www.youtube.com/embed/hbloGmfZsJ4"
title="OpenHands Slack Integration Tutorial"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen>
</iframe>
## Prerequisites
- Access to OpenHands Cloud.

View File

@@ -1,28 +1,17 @@
---
title: Configuration Options
description: This page outlines all available configuration options for OpenHands, allowing you to customize its behavior and integrate it with other services. In GUI Mode, any settings applied through the Settings UI will take precedence.
description: This page outlines all available configuration options for OpenHands, allowing you to customize its
behavior and integrate it with other services.
---
<Note>
In GUI Mode, any settings applied through the Settings UI will take precedence.
</Note>
## Core Configuration
The core configuration options are defined in the `[core]` section of the `config.toml` file.
### API Keys
- `e2b_api_key`
- Type: `str`
- Default: `""`
- Description: API key for E2B
- `modal_api_token_id`
- Type: `str`
- Default: `""`
- Description: API token ID for Modal
- `modal_api_token_secret`
- Type: `str`
- Default: `""`
- Description: API token secret for Modal
### Workspace
- `workspace_base` **(Deprecated)**
- Type: `str`

View File

@@ -88,7 +88,7 @@ If you would like to set things up more systematically, you can:
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)
- [Slack community](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA)
- [Discord server](https://discord.gg/ESHStjSjD4)
3. **Check our troubleshooting guide**: Common issues and solutions are documented in
[Troubleshooting](/usage/troubleshooting/troubleshooting).

View File

@@ -7,6 +7,15 @@ description: The Command-Line Interface (CLI) provides a powerful interface that
This mode is different from the [headless mode](/usage/how-to/headless-mode), which is non-interactive and better
for scripting.
<iframe
className="w-full aspect-video"
src="https://www.youtube.com/embed/PfvIx4y8h7w"
title="OpenHands CLI Tutorial"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen>
</iframe>
## Getting Started
### Running with Python
@@ -14,34 +23,34 @@ for scripting.
**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`:
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:
```bash
openhands
```
<Note>
If you have cloned the repository, you can also run the CLI directly using Poetry:
poetry run python -m openhands.cli.main
</Note>
3. Set your model, API key, and other preferences using the UI (or alternatively environment variables, below).
This command opens an interactive prompt where you can type tasks or commands and get responses from OpenHands.
The first time you run the CLI, it will take you through configuring the required LLM
settings. These will be saved for future sessions.
#### For Developers
If you have cloned the repository, you can run the CLI directly using Poetry:
```bash
poetry run python -m openhands.cli.main
```
The conversation history will be saved in `~/.openhands/sessions`.
### Running with Docker
@@ -55,7 +64,7 @@ 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.45-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -64,16 +73,21 @@ docker run -it \
-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.45 \
docker.all-hands.dev/all-hands-ai/openhands:0.47 \
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.
<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.
</Note>
This launches the CLI in Docker, allowing you to interact with OpenHands as described above.
This launches the CLI in Docker, allowing you to interact with OpenHands.
The `-e SANDBOX_USER_ID=$(id -u)` ensures files created by the agent in your workspace have the correct permissions.
The conversation history will be saved in `~/.openhands/sessions`.
## Interactive CLI Overview
### What is CLI Mode?

View File

@@ -1,6 +1,7 @@
---
title: Custom Sandbox
description: This guide is for users that would like to use their own custom Docker image for the runtime. For example, with certain tools or programming languages pre-installed.
description: This guide is for users that would like to use their own custom Docker image for the runtime.
For example, with certain tools or programming languages pre-installed.
---
The sandbox is where the agent performs its tasks. Instead of running commands directly on your computer

View File

@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -42,7 +42,7 @@ docker run -it \
-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.45 \
docker.all-hands.dev/all-hands-ai/openhands:0.47 \
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.

View File

@@ -73,6 +73,15 @@ We have a few guides for running OpenHands with specific model providers:
- [OpenAI](/usage/llms/openai-llms)
- [OpenRouter](/usage/llms/openrouter)
## Model Customization
LLM providers have specific settings that can be customized to optimize their performance with OpenHands, such as:
- **Custom Tokenizers**: For specialized models, you can add a suitable tokenizer
- **Native Tool Calling**: Toggle native function/tool calling capabilities
For detailed information about model customization, see [LLM Configuration Options](configuration-options#llm-customization).
### API retries and rate limits
LLM providers typically have rate limits, sometimes very low, and may require retries. OpenHands will automatically

View File

@@ -68,23 +68,23 @@ Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstud
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-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
docker.all-hands.dev/all-hands-ai/openhands:0.47
```
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.45
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.47
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
@@ -119,7 +119,7 @@ When started for the first time, OpenHands will prompt you to set up the LLM pro
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).
If you encounter any issues, let us know on [Slack](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA) or [Discord](https://discord.gg/ESHStjSjD4).
## Advanced: Alternative LLM Backends

View File

@@ -67,17 +67,17 @@ 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.45-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-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
docker.all-hands.dev/all-hands-ai/openhands:0.47
```
> **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.

View File

@@ -1,6 +1,7 @@
---
title: Model Context Protocol (MCP)
description: This page outlines how to configure and use the Model Context Protocol (MCP) in OpenHands, allowing you to extend the agent's capabilities with custom tools.
description: This page outlines how to configure and use the Model Context Protocol (MCP) in OpenHands, allowing you
to extend the agent's capabilities with custom tools.
---
## Overview

View File

@@ -5,111 +5,26 @@ description: Keyword-triggered microagents provide OpenHands with specific instr
## Usage
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
These microagents are only loaded when a prompt includes one of the trigger words.
## Frontmatter Syntax
Frontmatter is required for keyword-triggered microagents. It must be placed at the top of the file,
above the guidelines. Enclose the frontmatter in triple dashes (---).
above the guidelines.
### Standard Keyword Microagents
For standard keyword microagents, include the following fields:
Enclose the frontmatter in triple dashes (---) and 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
For command-style microagents that require user input, include the following fields:
## Example
| 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
Keyword-triggered microagent file example located at `.openhands/microagents/yummy.md`:
```
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
@@ -118,58 +33,4 @@ triggers:
The user has said the magic word. Respond with "That was delicious!"
```
### 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)
[See examples of microagents triggered by keywords in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)

View File

@@ -11,6 +11,8 @@ accordingly. However, they are applied to all repositories belonging to the orga
Add a `.openhands` repository under the organization or user and create a `microagents` directory and place the
microagents in that directory.
For GitLab organizations, use `openhands-config` as the repository name instead of `.openhands`, since GitLab doesn't support repository names starting with non-alphanumeric characters.
## Example
General microagent file example for organization `Great-Co` located inside the `.openhands` repository:
@@ -20,3 +22,5 @@ General microagent file example for organization `Great-Co` located inside the `
* Document interfaces and public APIs; use implementation comments only for non-obvious logic.
* Follow the same naming convention for variables, classes, constants, etc. already used in each repository.
```
For GitLab organizations, the same microagent would be located inside the `openhands-config` repository.

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, including command-style microagents that prompt for user inputs.
- [Keyword-Triggered Microagents](./microagents-keyword): Guidelines activated by specific keywords in prompts.
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 (all types)` | Yes |
| Microagent Type | Required |
|---------------------------------|----------|
| `General Microagents` | No |
| `Keyword-Triggered Microagents` | Yes |

View File

@@ -3,7 +3,6 @@ title: Daytona Runtime
description: You can use [Daytona](https://www.daytona.io/) as a runtime provider.
---
## Step 1: Retrieve Your Daytona API Key
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
2. Click **"Create Key"**.

View File

@@ -3,8 +3,6 @@ title: Docker Runtime
description: This is the default Runtime that's used when you start OpenHands.
---
This is the default Runtime that's used when you start OpenHands.
## Image
The `SANDBOX_RUNTIME_CONTAINER_IMAGE` from nikolaik is a pre-built runtime image
that contains our Runtime server, as well as some basic utilities for Python and NodeJS.

View File

@@ -3,7 +3,8 @@ title: E2B Runtime
description: E2B is an open-source secure cloud environment (sandbox) made for running AI-generated code and agents.
---
[E2B](https://e2b.dev) offers [Python](https://pypi.org/project/e2b/) and [JS/TS](https://www.npmjs.com/package/e2b) SDK to spawn and control these sandboxes.
[E2B](https://e2b.dev) offers [Python](https://pypi.org/project/e2b/) and [JS/TS](https://www.npmjs.com/package/e2b)
SDK to spawn and control these sandboxes.
## Getting started
@@ -18,9 +19,13 @@ description: E2B is an open-source secure cloud environment (sandbox) made for r
Full CLI API is [here](https://e2b.dev/docs/cli/installation).
## OpenHands sandbox
You can use the E2B CLI to create a custom sandbox with a Dockerfile. Read the full guide [here](https://e2b.dev/docs/guide/custom-sandbox). The premade OpenHands sandbox for E2B is set up in the `containers` directory. and it's called `openhands`.
You can use the E2B CLI to create a custom sandbox with a Dockerfile. Read the full guide
[here](https://e2b.dev/docs/guide/custom-sandbox). The premade OpenHands sandbox for E2B is set up in the `containers`
directory. and it's called `openhands`.
## Debugging
You can connect to a running E2B sandbox with E2B CLI in your terminal.
- List all running sandboxes (based on your API key)
@@ -34,5 +39,6 @@ You can connect to a running E2B sandbox with E2B CLI in your terminal.
```
## Links
- [E2B Docs](https://e2b.dev/docs)
- [E2B GitHub](https://github.com/e2b-dev/e2b)

View File

@@ -1,6 +1,8 @@
---
title: Local Runtime
description: The Local Runtime allows the OpenHands agent to execute actions directly on your local machine without using Docker. This runtime is primarily intended for controlled environments like CI pipelines or testing scenarios where Docker is not available.
description: The Local Runtime allows the OpenHands agent to execute actions directly on your local machine without
using Docker. This runtime is primarily intended for controlled environments like CI pipelines or testing scenarios
where Docker is not available.
---
<Warning>

View File

@@ -9,8 +9,6 @@ commands.
By default, OpenHands uses a [Docker-based runtime](/usage/runtimes/docker), running on your local computer.
This means you only have to pay for the LLM you're using, and your code is only ever sent to the LLM.
We also support other runtimes, which are typically managed by third-parties.
Additionally, we provide a [Local Runtime](/usage/runtimes/local) that runs directly on your machine without Docker,
which can be useful in controlled environments like CI pipelines.
@@ -21,6 +19,18 @@ OpenHands supports several different runtime environments:
- [Docker Runtime](/usage/runtimes/docker) - The default runtime that uses Docker containers for isolation (recommended for most users).
- [OpenHands Remote Runtime](/usage/runtimes/remote) - Cloud-based runtime for parallel execution (beta).
- [Local Runtime](/usage/runtimes/local) - Direct execution on your local machine without Docker.
- And more third-party runtimes:
- [Modal Runtime](/usage/runtimes/modal) - Runtime provided by our partners at Modal.
- [Daytona Runtime](/usage/runtimes/daytona) - Runtime provided by Daytona.
### Third-Party Runtimes
The following third-party runtimes are available when you install the `third_party_runtimes` extra:
```bash
pip install openhands-ai[third_party_runtimes]
```
- [E2B Runtime](/usage/runtimes/e2b) - Open source runtime using E2B sandboxes.
- [Modal Runtime](/usage/runtimes/modal) - Serverless runtime using Modal infrastructure.
- [Runloop Runtime](/usage/runtimes/runloop) - Cloud runtime using Runloop infrastructure.
- [Daytona Runtime](/usage/runtimes/daytona) - Development environment runtime using Daytona.
**Note**: These third-party runtimes are supported by their respective developers, not by the OpenHands team. For issues specific to these runtimes, please refer to their documentation or contact their support teams.

View File

@@ -1,7 +1,11 @@
---
title: Remote Runtime
description: This runtime is specifically designed for agent evaluation purposes only through the [OpenHands evaluation harness](https://github.com/All-Hands-AI/OpenHands/tree/main/evaluation). It should not be used to launch production OpenHands applications.
description: This runtime is specifically designed for agent evaluation purposes only through the
[OpenHands evaluation harness](https://github.com/All-Hands-AI/OpenHands/tree/main/evaluation). It should not be
used to launch production OpenHands applications.
---
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details), it allows you to launch runtimes
in parallel in the cloud. Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply if you want to try this out!
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details),
it allows you to launch runtimes in parallel in the cloud. Fill out
[this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to
apply if you want to try this out!

View File

@@ -1,6 +1,7 @@
---
title: Runloop Runtime
description: Runloop provides a fast, secure and scalable AI sandbox (Devbox). Check out the [runloop docs](https://docs.runloop.ai/overview/what-is-runloop) for more detail.
description: Runloop provides a fast, secure and scalable AI sandbox (Devbox). Check out the
[runloop docs](https://docs.runloop.ai/overview/what-is-runloop) for more detail.
---
## Access

View File

@@ -1,6 +1,6 @@
---
title: Search Engine Setup
description: Configure OpenHands to use Tavily as a search engine
description: Configure OpenHands to use Tavily as a search engine.
---
## Setting Up Search Engine in OpenHands
@@ -11,10 +11,10 @@ OpenHands can be configured to use [Tavily](https://tavily.com/) as a search eng
To use the search functionality in OpenHands, you'll need to obtain a Tavily API key:
1. Visit [Tavily's website](https://tavily.com/) and sign up for an account
2. Navigate to the API section in your dashboard
3. Generate a new API key
4. Copy the API key (it should start with `tvly-`)
1. Visit [Tavily's website](https://tavily.com/) and sign up for an account.
2. Navigate to the API section in your dashboard.
3. Generate a new API key.
4. Copy the API key (it should start with `tvly-`).
### Configuring Search in OpenHands
@@ -22,13 +22,12 @@ Once you have your Tavily API key, you can configure OpenHands to use it:
#### In the OpenHands UI
1. Open OpenHands and navigate to the Settings page by clicking the gear icon
2. In the LLM settings tab, locate the "Search API Key (Tavily)" field
3. Enter your Tavily API key (starting with `tvly-`)
4. Click "Save" to apply the changes
1. Open OpenHands and navigate to the Settings page.
2. Under the `LLM` tab, enter your Tavily API key (starting with `tvly-`) in the `Search API Key (Tavily)` field.
3. Click `Save` to apply the changes.
<Note>
The search API key field is optional. If you don't provide a key, the search functionality will not be available to the agent.
The search API key field is optional. If you don't provide a key, the search functionality will not be available to the agent.
</Note>
#### Using Configuration Files
@@ -45,22 +44,23 @@ search_api_key = "tvly-your-api-key-here"
When the search engine is configured:
1. The agent can decide to search the web when it needs external information
2. Search queries are sent to Tavily's API via [Tavily's MCP server](https://github.com/tavily-ai/tavily-mcp) which includes a variety of [tools](https://docs.tavily.com/documentation/api-reference/introduction) (search, extract, crawl, map).
3. Results are returned and incorporated into the agent's context
4. The agent can use this information to provide more accurate and up-to-date responses
- The agent can decide to search the web when it needs external information.
- Search queries are sent to Tavily's API via [Tavily's MCP server](https://github.com/tavily-ai/tavily-mcp) which
includes a variety of [tools](https://docs.tavily.com/documentation/api-reference/introduction) (search, extract, crawl, map).
- Results are returned and incorporated into the agent's context.
- The agent can use this information to provide more accurate and up-to-date responses.
### Limitations
- Search results depend on Tavily's coverage and freshness
- Usage may be subject to Tavily's rate limits and pricing tiers
- The agent will only search when it determines that external information is needed
- Search results depend on Tavily's coverage and freshness.
- Usage may be subject to Tavily's rate limits and pricing tiers.
- The agent will only search when it determines that external information is needed.
### Troubleshooting
If you encounter issues with the search functionality:
- Verify that your API key is correct and active
- Check that your API key starts with `tvly-`
- Ensure you have an active internet connection
- Check Tavily's status page for any service disruptions
- Verify that your API key is correct and active.
- Check that your API key starts with `tvly-`.
- Ensure you have an active internet connection.
- Check Tavily's status page for any service disruptions.

View File

@@ -3,13 +3,20 @@ import copy
import functools
import os
import re
import shutil
import zipfile
import huggingface_hub
import pandas as pd
from datasets import load_dataset
from PIL import Image
from pydantic import SecretStr
from evaluation.benchmarks.gaia.scorer import question_scorer
from evaluation.benchmarks.gaia.utils import (
image_to_jpg_base64_url,
image_to_png_base64_url,
)
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
@@ -97,27 +104,44 @@ def initialize_runtime(
if instance['file_name'] != '':
# if this question comes with a file, we need to save it to the workspace
assert metadata.data_split is not None
extension_name = instance['file_name'].split('.')[-1]
src_file = os.path.join(
DATASET_CACHE_DIR, '2023', metadata.data_split, instance['file_name']
)
assert os.path.exists(src_file)
dest_file = os.path.join('/workspace', instance['file_name'])
runtime.copy_to(src_file, dest_file)
if extension_name == 'zip':
temp_dir = os.path.join(
DATASET_CACHE_DIR, '2023', metadata.data_split, 'tmp_file'
)
os.makedirs(temp_dir, exist_ok=True)
with zipfile.ZipFile(src_file, 'r') as zip_ref:
zip_ref.extractall(temp_dir)
for root, dirs, files in os.walk(temp_dir):
for file in files:
dest_file = '/workspace'
runtime.copy_to(os.path.join(root, file), dest_file)
shutil.rmtree(temp_dir)
elif extension_name not in ['jpg', 'png']:
dest_file = '/workspace'
runtime.copy_to(src_file, dest_file)
# rename to file.extension_name
extension_name = instance['file_name'].split('.')[-1]
action = CmdRunAction(
command=f'mv /workspace/{instance["file_name"]} /workspace/file.{extension_name}'
)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
# rename to file.extension_name
action = CmdRunAction(
command=f'mv /workspace/{instance["file_name"]} /workspace/file.{extension_name}'
)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
action = CmdRunAction(command='cd /workspace')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
action = CmdRunAction(
command='apt-get update && apt-get install -y ffmpeg && apt-get install -y ffprobe'
)
runtime.run_action(action)
logger.info(f'{"-" * 50} END Runtime Initialization Fn {"-" * 50}')
@@ -151,8 +175,31 @@ Here is the task:
task_question=instance['Question'],
)
logger.info(f'Instruction: {instruction}')
image_urls = []
if dest_file:
instruction += f'\n\nThe mentioned file is provided in the workspace at: {dest_file.split("/")[-1]}'
if extension_name not in ['jpg', 'png', 'zip']:
instruction += f'To solve this task you will have to use the attached file provided in the workspace at location: {dest_file}\n\n'
elif extension_name == 'zip':
filenames = []
src_file = os.path.join(
DATASET_CACHE_DIR, '2023', metadata.data_split, instance['file_name']
)
with zipfile.ZipFile(src_file, 'r') as zip_ref:
filenames = zip_ref.namelist()
filenames = [f'/workspace/{file}' for file in filenames]
filenames = ', '.join(filenames)
instruction += f'To solve this task you will have to use the attached files provided in the workspace at locations: {filenames}\n\n'
else: # Image files: jpg, png
src_file = os.path.join(
DATASET_CACHE_DIR, '2023', metadata.data_split, instance['file_name']
)
instruction += 'Image: To solve this task you will have to use the image shown below.\n\n'
image = Image.open(src_file)
if extension_name == 'jpg':
image_urls.append(image_to_jpg_base64_url(image))
else:
image_urls.append(image_to_png_base64_url(image))
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'
@@ -174,7 +221,9 @@ Here is the task:
state: State | None = asyncio.run(
run_controller(
config=config,
initial_user_action=MessageAction(content=instruction),
initial_user_action=MessageAction(
content=instruction, image_urls=image_urls
),
runtime=runtime,
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
metadata.agent_class

View File

@@ -0,0 +1,43 @@
import base64
import io
import numpy as np
from PIL import Image
def image_to_png_base64_url(
image: np.ndarray | Image.Image, add_data_prefix: bool = True
):
"""Convert a numpy array to a base64 encoded png image url."""
if isinstance(image, np.ndarray):
image = Image.fromarray(image)
if image.mode in ('RGBA', 'LA'):
image = image.convert('RGB')
buffered = io.BytesIO()
image.save(buffered, format='PNG')
image_base64 = base64.b64encode(buffered.getvalue()).decode()
return (
f'data:image/png;base64,{image_base64}'
if add_data_prefix
else f'{image_base64}'
)
def image_to_jpg_base64_url(
image: np.ndarray | Image.Image, add_data_prefix: bool = True
):
"""Convert a numpy array to a base64 encoded jpeg image url."""
if isinstance(image, np.ndarray):
image = Image.fromarray(image)
if image.mode in ('RGBA', 'LA'):
image = image.convert('RGB')
buffered = io.BytesIO()
image.save(buffered, format='JPEG')
image_base64 = base64.b64encode(buffered.getvalue()).decode()
return (
f'data:image/jpeg;base64,{image_base64}'
if add_data_prefix
else f'{image_base64}'
)

View File

@@ -109,7 +109,7 @@ def codeact_user_response(
) -> str:
encaps_str = (
(
'Please encapsulate your final answer (answer ONLY) within <solution> and </solution>.\n'
'Your final answer MUST be encapsulated within <solution> and </solution>.\n'
'For example: The answer to the question is <solution> 42 </solution>.\n'
)
if encapsulate_solution
@@ -117,7 +117,7 @@ def codeact_user_response(
)
msg = (
'Please continue working on the task on whatever approach you think is suitable.\n'
'If you think you have solved the task, please first send your answer to user through message and then finish the interaction.\n'
'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.\n'
f'{encaps_str}'
'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP.\n'
)

View File

@@ -0,0 +1,194 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { screen } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import { EventMessage } from "#/components/features/chat/event-message";
vi.mock("#/hooks/query/use-config", () => ({
useConfig: () => ({
data: { APP_MODE: "saas" },
}),
}));
vi.mock("#/hooks/query/use-feedback-exists", () => ({
useFeedbackExists: (eventId: number | undefined) => ({
data: { exists: false },
isLoading: false,
}),
}));
describe("EventMessage", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("should render LikertScale for finish action when it's the last message", () => {
const finishEvent = {
id: 123,
source: "agent" as const,
action: "finish" as const,
args: {
final_thought: "Task completed successfully",
task_completed: "success" as const,
outputs: {},
thought: "Task completed successfully",
},
message: "Task completed successfully",
timestamp: new Date().toISOString(),
};
renderWithProviders(
<EventMessage
event={finishEvent}
hasObservationPair={false}
isAwaitingUserConfirmation={false}
isLastMessage={true}
isInLast10Actions={true}
/>
);
expect(screen.getByLabelText("Rate 1 stars")).toBeInTheDocument();
expect(screen.getByLabelText("Rate 5 stars")).toBeInTheDocument();
});
it("should render LikertScale for assistant message when it's the last message", () => {
const assistantMessageEvent = {
id: 456,
source: "agent" as const,
action: "message" as const,
args: {
thought: "I need more information to proceed.",
image_urls: null,
file_urls: [],
wait_for_response: true,
},
message: "I need more information to proceed.",
timestamp: new Date().toISOString(),
};
renderWithProviders(
<EventMessage
event={assistantMessageEvent}
hasObservationPair={false}
isAwaitingUserConfirmation={false}
isLastMessage={true}
isInLast10Actions={true}
/>
);
expect(screen.getByLabelText("Rate 1 stars")).toBeInTheDocument();
expect(screen.getByLabelText("Rate 5 stars")).toBeInTheDocument();
});
it("should render LikertScale for error observation when it's the last message", () => {
const errorEvent = {
id: 789,
source: "user" as const,
observation: "error" as const,
content: "An error occurred",
extras: {
error_id: "test-error-123",
},
message: "An error occurred",
timestamp: new Date().toISOString(),
cause: 123,
};
renderWithProviders(
<EventMessage
event={errorEvent}
hasObservationPair={false}
isAwaitingUserConfirmation={false}
isLastMessage={true}
isInLast10Actions={true}
/>
);
expect(screen.getByLabelText("Rate 1 stars")).toBeInTheDocument();
expect(screen.getByLabelText("Rate 5 stars")).toBeInTheDocument();
});
it("should NOT render LikertScale when not the last message", () => {
const finishEvent = {
id: 101,
source: "agent" as const,
action: "finish" as const,
args: {
final_thought: "Task completed successfully",
task_completed: "success" as const,
outputs: {},
thought: "Task completed successfully",
},
message: "Task completed successfully",
timestamp: new Date().toISOString(),
};
renderWithProviders(
<EventMessage
event={finishEvent}
hasObservationPair={false}
isAwaitingUserConfirmation={false}
isLastMessage={false}
isInLast10Actions={false}
/>
);
expect(screen.queryByLabelText("Rate 1 stars")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Rate 5 stars")).not.toBeInTheDocument();
});
it("should render LikertScale for error observation when in last 10 actions but not last message", () => {
const errorEvent = {
id: 999,
source: "user" as const,
observation: "error" as const,
content: "An error occurred",
extras: {
error_id: "test-error-456",
},
message: "An error occurred",
timestamp: new Date().toISOString(),
cause: 123,
};
renderWithProviders(
<EventMessage
event={errorEvent}
hasObservationPair={false}
isAwaitingUserConfirmation={false}
isLastMessage={false}
isInLast10Actions={true}
/>
);
expect(screen.getByLabelText("Rate 1 stars")).toBeInTheDocument();
expect(screen.getByLabelText("Rate 5 stars")).toBeInTheDocument();
});
it("should NOT render LikertScale for error observation when not in last 10 actions", () => {
const errorEvent = {
id: 888,
source: "user" as const,
observation: "error" as const,
content: "An error occurred",
extras: {
error_id: "test-error-789",
},
message: "An error occurred",
timestamp: new Date().toISOString(),
cause: 123,
};
renderWithProviders(
<EventMessage
event={errorEvent}
hasObservationPair={false}
isAwaitingUserConfirmation={false}
isLastMessage={false}
isInLast10Actions={false}
/>
);
expect(screen.queryByLabelText("Rate 1 stars")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Rate 5 stars")).not.toBeInTheDocument();
});
});

View File

@@ -128,7 +128,7 @@ describe("RepoConnector", () => {
renderRepoConnector();
await screen.findByText("Add GitHub repos");
await screen.findByText("HOME$ADD_GITHUB_REPOS");
});
it("should not render the 'add git(hub|lab) repos' links if oss mode", async () => {

View File

@@ -53,7 +53,7 @@ describe("TaskSuggestions", () => {
it("should render an empty message if there are no tasks", async () => {
getSuggestedTasksSpy.mockResolvedValue([]);
renderTaskSuggestions();
await screen.findByText(/No tasks available/i);
await screen.findByText("TASKS$NO_TASKS_AVAILABLE");
});
it("should render the task groups with the correct titles", async () => {

View File

@@ -473,7 +473,7 @@ describe("Secret actions", () => {
// make POST request
expect(createSecretSpy).not.toHaveBeenCalled();
expect(screen.queryByText(/secret already exists/i)).toBeInTheDocument();
expect(screen.queryByText("SECRETS$SECRET_ALREADY_EXISTS")).toBeInTheDocument();
await userEvent.clear(nameInput);
await userEvent.type(nameInput, "My_Custom_Secret");
@@ -557,7 +557,7 @@ describe("Secret actions", () => {
// make POST request
expect(createSecretSpy).not.toHaveBeenCalled();
expect(screen.queryByText(/secret already exists/i)).toBeInTheDocument();
expect(screen.queryByText("SECRETS$SECRET_ALREADY_EXISTS")).toBeInTheDocument();
expect(nameInput).toHaveValue(MOCK_GET_SECRETS_RESPONSE[0].name);
expect(valueInput).toHaveValue("my-custom-secret-value");

View File

@@ -0,0 +1,54 @@
import { describe, it, expect } from "vitest";
import { extractSettings } from "#/utils/settings-utils";
describe("Model name case preservation", () => {
it("should preserve the original case of model names in extractSettings", () => {
// Create FormData with proper casing
const formData = new FormData();
formData.set("llm-provider-input", "SambaNova");
formData.set("llm-model-input", "Meta-Llama-3.1-8B-Instruct");
formData.set("agent", "CodeActAgent");
formData.set("language", "en");
const settings = extractSettings(formData);
// Test that model names maintain their original casing
expect(settings.LLM_MODEL).toBe("SambaNova/Meta-Llama-3.1-8B-Instruct");
});
it("should preserve openai model case", () => {
const formData = new FormData();
formData.set("llm-provider-input", "openai");
formData.set("llm-model-input", "gpt-4o");
formData.set("agent", "CodeActAgent");
formData.set("language", "en");
const settings = extractSettings(formData);
expect(settings.LLM_MODEL).toBe("openai/gpt-4o");
});
it("should preserve anthropic model case", () => {
const formData = new FormData();
formData.set("llm-provider-input", "anthropic");
formData.set("llm-model-input", "claude-sonnet-4-20250514");
formData.set("agent", "CodeActAgent");
formData.set("language", "en");
const settings = extractSettings(formData);
expect(settings.LLM_MODEL).toBe("anthropic/claude-sonnet-4-20250514");
});
it("should not automatically lowercase model names", () => {
const formData = new FormData();
formData.set("llm-provider-input", "SambaNova");
formData.set("llm-model-input", "Meta-Llama-3.1-8B-Instruct");
formData.set("agent", "CodeActAgent");
formData.set("language", "en");
const settings = extractSettings(formData);
// Test that camelCase and PascalCase are preserved
expect(settings.LLM_MODEL).not.toBe("sambanova/meta-llama-3.1-8b-instruct");
expect(settings.LLM_MODEL).toBe("SambaNova/Meta-Llama-3.1-8B-Instruct");
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +1,39 @@
{
"name": "openhands-frontend",
"version": "0.45.0",
"version": "0.47.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@heroui/react": "^2.8.0-beta.9",
"@heroui/react": "^2.8.0-beta.10",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.6.2",
"@react-router/serve": "^7.6.2",
"@react-router/node": "^7.6.3",
"@react-router/serve": "^7.6.3",
"@react-types/shared": "^3.29.1",
"@reduxjs/toolkit": "^2.8.2",
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.3.1",
"@tailwindcss/postcss": "^4.1.10",
"@tailwindcss/vite": "^4.1.10",
"@tanstack/react-query": "^5.80.10",
"@vitejs/plugin-react": "^4.5.2",
"@stripe/stripe-js": "^7.4.0",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.81.4",
"@vitejs/plugin-react": "^4.6.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.10.0",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.18.1",
"framer-motion": "^12.19.2",
"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.519.0",
"lucide-react": "^0.525.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.255.0",
"posthog-js": "^1.255.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-highlight": "^0.15.0",
@@ -42,14 +42,14 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.6.2",
"react-router": "^7.6.3",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"remark-gfm": "^4.0.1",
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"vite": "^6.3.5",
"vite": "^7.0.0",
"web-vitals": "^5.0.3",
"ws": "^8.18.2"
},
@@ -80,19 +80,19 @@
]
},
"devDependencies": {
"@babel/parser": "^7.27.1",
"@babel/traverse": "^7.27.1",
"@babel/parser": "^7.27.7",
"@babel/traverse": "^7.27.7",
"@babel/types": "^7.27.0",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.53.1",
"@react-router/dev": "^7.6.2",
"@react-router/dev": "^7.6.3",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.78.0",
"@tanstack/eslint-plugin-query": "^5.81.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.0.3",
"@types/node": "^24.0.5",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-highlight": "^0.12.8",
@@ -107,9 +107,9 @@
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.5.0",
"eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.1.4",
@@ -117,7 +117,7 @@
"jsdom": "^26.1.0",
"lint-staged": "^16.1.2",
"msw": "^2.6.6",
"prettier": "^3.5.3",
"prettier": "^3.6.2",
"stripe": "^18.2.1",
"tailwindcss": "^4.1.8",
"typescript": "^5.8.3",

View File

@@ -56,8 +56,6 @@ const NON_TEXT_ATTRIBUTES = [
"type",
"href",
"src",
"alt",
"placeholder",
"rel",
"target",
"style",
@@ -65,7 +63,6 @@ const NON_TEXT_ATTRIBUTES = [
"onChange",
"onSubmit",
"data-testid",
"aria-label",
"aria-labelledby",
"aria-describedby",
"aria-hidden",
@@ -139,6 +136,7 @@ function isLikelyCode(str) {
}
function isCommonDevelopmentString(str) {
// Technical patterns that are definitely not UI strings
const technicalPatterns = [
// URLs and paths
@@ -191,7 +189,7 @@ function isCommonDevelopmentString(str) {
// CSS units and values
const cssUnitsPattern =
/(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$/;
/\b\d+(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$|^(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$/;
const cssValuesPattern =
/(rgb|rgba|hsl|hsla|#[0-9a-fA-F]+|solid|absolute|relative|sticky|fixed|static|block|inline|flex|grid|none|auto|hidden|visible)/;
@@ -394,6 +392,7 @@ function isCommonDevelopmentString(str) {
}
function isLikelyUserFacingText(str) {
// Basic validation - skip very short strings or strings without letters
if (!str || str.length <= 2 || !/[a-zA-Z]/.test(str)) {
return false;
@@ -540,8 +539,8 @@ function isInTranslationContext(path) {
}
function scanFileForUnlocalizedStrings(filePath) {
// Skip all suggestion files as they contain special strings
if (filePath.includes("suggestions")) {
// Skip suggestion content files as they contain special strings that are already properly localized
if (filePath.includes("utils/suggestions/") || filePath.includes("mocks/task-suggestions-handlers.ts")) {
return [];
}

View File

@@ -34,7 +34,7 @@ export function ActionSuggestions({
const terms = {
pr,
prShort,
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.`,
pushToBranch: `Please push the changes to a remote branch on ${getProviderName()}, but do NOT create a ${pr}. Check your current branch name first - if it's main, master, deploy, or another common default branch name, create a new branch with a descriptive name related to your changes. Otherwise, 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

@@ -35,6 +35,7 @@ interface EventMessageProps {
hasObservationPair: boolean;
isAwaitingUserConfirmation: boolean;
isLastMessage: boolean;
isInLast10Actions: boolean;
}
export function EventMessage({
@@ -42,24 +43,52 @@ export function EventMessage({
hasObservationPair,
isAwaitingUserConfirmation,
isLastMessage,
isInLast10Actions,
}: EventMessageProps) {
const shouldShowConfirmationButtons =
isLastMessage && event.source === "agent" && isAwaitingUserConfirmation;
const { data: config } = useConfig();
// Use our query hook to check if feedback exists and get rating/reason
const {
data: feedbackData = { exists: false },
isLoading: isCheckingFeedback,
} = useFeedbackExists(isFinishAction(event) ? event.id : undefined);
} = useFeedbackExists(event.id);
const renderLikertScale = () => {
if (config?.APP_MODE !== "saas" || isCheckingFeedback) {
return null;
}
// For error observations, show if in last 10 actions
// For other events, show only if it's the last message
const shouldShow = isErrorObservation(event)
? isInLast10Actions
: isLastMessage;
if (!shouldShow) {
return null;
}
return (
<LikertScale
eventId={event.id}
initiallySubmitted={feedbackData.exists}
initialRating={feedbackData.rating}
initialReason={feedbackData.reason}
/>
);
};
if (isErrorObservation(event)) {
return (
<ErrorMessage
errorId={event.extras.error_id}
defaultMessage={event.message}
/>
<>
<ErrorMessage
errorId={event.extras.error_id}
defaultMessage={event.message}
/>
{renderLikertScale()}
</>
);
}
@@ -70,24 +99,11 @@ export function EventMessage({
return null;
}
const showLikertScale =
config?.APP_MODE === "saas" &&
isFinishAction(event) &&
isLastMessage &&
!isCheckingFeedback;
if (isFinishAction(event)) {
return (
<>
<ChatMessage type="agent" message={getEventContent(event).details} />
{showLikertScale && (
<LikertScale
eventId={event.id}
initiallySubmitted={feedbackData.exists}
initialRating={feedbackData.rating}
initialReason={feedbackData.reason}
/>
)}
{renderLikertScale()}
</>
);
}
@@ -96,15 +112,20 @@ export function EventMessage({
const message = parseMessageFromEvent(event);
return (
<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>
<>
<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>
{isAssistantMessage(event) &&
event.action === "message" &&
renderLikertScale()}
</>
);
}

View File

@@ -39,6 +39,7 @@ export const Messages: React.FC<MessagesProps> = React.memo(
hasObservationPair={actionHasObservationPair(message)}
isAwaitingUserConfirmation={isAwaitingUserConfirmation}
isLastMessage={messages.length - 1 === index}
isInLast10Actions={messages.length - 1 - index < 10}
/>
))}

View File

@@ -2,15 +2,15 @@ export function TypingIndicator() {
return (
<div className="flex items-center space-x-1.5 bg-tertiary px-3 py-1.5 rounded-full">
<span
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[-2px]"
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[1px]"
style={{ animationDelay: "0ms" }}
/>
<span
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[-2px]"
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[1px]"
style={{ animationDelay: "75ms" }}
/>
<span
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[-2px]"
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[1px]"
style={{ animationDelay: "150ms" }}
/>
</div>

View File

@@ -44,6 +44,7 @@ export function LikertScale({
t(I18nKey.FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION),
t(I18nKey.FEEDBACK$REASON_FORGOT_CONTEXT),
t(I18nKey.FEEDBACK$REASON_UNNECESSARY_CHANGES),
t(I18nKey.FEEDBACK$REASON_SHOULD_ASK_FIRST),
t(I18nKey.FEEDBACK$REASON_OTHER),
];

View File

@@ -1,6 +1,9 @@
import { useTranslation } from "react-i18next";
import { useConfig } from "#/hooks/query/use-config";
import { I18nKey } from "#/i18n/declaration";
export function RepoProviderLinks() {
const { t } = useTranslation();
const { data: config } = useConfig();
const githubHref = config
@@ -10,7 +13,7 @@ export function RepoProviderLinks() {
return (
<div className="flex flex-col text-sm underline underline-offset-2 text-content-2 gap-4 w-fit">
<a href={githubHref} target="_blank" rel="noopener noreferrer">
Add GitHub repos
{t(I18nKey.HOME$ADD_GITHUB_REPOS)}
</a>
</div>
);

View File

@@ -1,5 +1,7 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { SettingsDropdownInput } from "../../settings/settings-dropdown-input";
import { I18nKey } from "#/i18n/declaration";
export interface BranchDropdownProps {
items: { key: React.Key; label: string }[];
@@ -16,11 +18,13 @@ export function BranchDropdown({
isDisabled,
selectedKey,
}: BranchDropdownProps) {
const { t } = useTranslation();
return (
<SettingsDropdownInput
testId="branch-dropdown"
name="branch-dropdown"
placeholder="Select a branch"
placeholder={t(I18nKey.REPOSITORY$SELECT_BRANCH)}
items={items}
wrapperClassName="max-w-[500px]"
onSelectionChange={onSelectionChange}

View File

@@ -1,5 +1,7 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { SettingsDropdownInput } from "../../settings/settings-dropdown-input";
import { I18nKey } from "#/i18n/declaration";
export interface RepositoryDropdownProps {
items: { key: React.Key; label: string }[];
@@ -14,11 +16,13 @@ export function RepositoryDropdown({
onInputChange,
defaultFilter,
}: RepositoryDropdownProps) {
const { t } = useTranslation();
return (
<SettingsDropdownInput
testId="repo-dropdown"
name="repo-dropdown"
placeholder="Select a repo"
placeholder={t(I18nKey.REPOSITORY$SELECT_REPO)}
items={items}
wrapperClassName="max-w-[500px]"
onSelectionChange={onSelectionChange}

View File

@@ -1,13 +1,16 @@
import { useTranslation } from "react-i18next";
import { TaskGroup } from "./task-group";
import { useSuggestedTasks } from "#/hooks/query/use-suggested-tasks";
import { TaskSuggestionsSkeleton } from "./task-suggestions-skeleton";
import { cn } from "#/utils/utils";
import { I18nKey } from "#/i18n/declaration";
interface TaskSuggestionsProps {
filterFor?: string | null;
}
export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) {
const { t } = useTranslation();
const { data: tasks, isLoading } = useSuggestedTasks();
const suggestedTasks = filterFor
? tasks?.filter((task) => task.title === filterFor)
@@ -20,11 +23,13 @@ export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) {
data-testid="task-suggestions"
className={cn("flex flex-col w-full", !hasSuggestedTasks && "gap-6")}
>
<h2 className="heading">Suggested Tasks</h2>
<h2 className="heading">{t(I18nKey.TASKS$SUGGESTED_TASKS)}</h2>
<div className="flex flex-col gap-6">
{isLoading && <TaskSuggestionsSkeleton />}
{!hasSuggestedTasks && !isLoading && <p>No tasks available</p>}
{!hasSuggestedTasks && !isLoading && (
<p>{t(I18nKey.TASKS$NO_TASKS_AVAILABLE)}</p>
)}
{suggestedTasks?.map((taskGroup, index) => (
<TaskGroup
key={index}

View File

@@ -64,7 +64,7 @@ export function PaymentForm() {
onChange={handleTopUpInputChange}
type="number"
label={t(I18nKey.PAYMENT$ADD_FUNDS)}
placeholder="Specify an amount in USD to add - min $10"
placeholder={t(I18nKey.PAYMENT$SPECIFY_AMOUNT_USD)}
className="w-[680px]"
min={10}
max={25000}

View File

@@ -1,7 +1,9 @@
import { Trans } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function BitbucketTokenHelpAnchor() {
const { t } = useTranslation();
return (
<p data-testid="bitbucket-token-help-anchor" className="text-xs">
<Trans
@@ -9,7 +11,7 @@ export function BitbucketTokenHelpAnchor() {
components={[
<a
key="bitbucket-token-help-anchor-link"
aria-label="Bitbucket token help link"
aria-label={t(I18nKey.GIT$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"
@@ -17,7 +19,7 @@ export function BitbucketTokenHelpAnchor() {
/>,
<a
key="bitbucket-token-help-anchor-link-2"
aria-label="Bitbucket token see more link"
aria-label={t(I18nKey.GIT$BITBUCKET_TOKEN_SEE_MORE_LINK)}
href="https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/"
target="_blank"
className="underline underline-offset-2"

View File

@@ -1,7 +1,9 @@
import { Trans } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function GitHubTokenHelpAnchor() {
const { t } = useTranslation();
return (
<p data-testid="github-token-help-anchor" className="text-xs">
<Trans
@@ -9,7 +11,7 @@ export function GitHubTokenHelpAnchor() {
components={[
<a
key="github-token-help-anchor-link"
aria-label="GitHub token help link"
aria-label={t(I18nKey.GIT$GITHUB_TOKEN_HELP_LINK)}
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
target="_blank"
className="underline underline-offset-2"
@@ -17,7 +19,7 @@ export function GitHubTokenHelpAnchor() {
/>,
<a
key="github-token-help-anchor-link-2"
aria-label="GitHub token see more link"
aria-label={t(I18nKey.GIT$GITHUB_TOKEN_SEE_MORE_LINK)}
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"
target="_blank"
className="underline underline-offset-2"

View File

@@ -1,7 +1,9 @@
import { Trans } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function GitLabTokenHelpAnchor() {
const { t } = useTranslation();
return (
<p data-testid="gitlab-token-help-anchor" className="text-xs">
<Trans
@@ -9,7 +11,7 @@ export function GitLabTokenHelpAnchor() {
components={[
<a
key="gitlab-token-help-anchor-link"
aria-label="Gitlab token help link"
aria-label={t(I18nKey.GIT$GITLAB_TOKEN_HELP_LINK)}
href="https://gitlab.com/-/user_settings/personal_access_tokens?name=openhands-app&scopes=api,read_user,read_repository,write_repository"
target="_blank"
className="underline underline-offset-2"
@@ -17,7 +19,7 @@ export function GitLabTokenHelpAnchor() {
/>,
<a
key="gitlab-token-help-anchor-link-2"
aria-label="GitLab token see more link"
aria-label={t(I18nKey.GIT$GITLAB_TOKEN_SEE_MORE_LINK)}
href="https://docs.gitlab.com/user/profile/personal_access_tokens/"
target="_blank"
className="underline underline-offset-2"

View File

@@ -111,7 +111,7 @@ export function SecretForm({
(secret) => secret.name === name && secret.name !== selectedSecret,
);
if (isNameAlreadyUsed) {
setError("Secret already exists");
setError(t("SECRETS$SECRET_ALREADY_EXISTS"));
return;
}
@@ -144,7 +144,7 @@ export function SecretForm({
className="w-full max-w-[350px]"
required
defaultValue={mode === "edit" && selectedSecret ? selectedSecret : ""}
placeholder="e.g. OpenAI_API_Key"
placeholder={t("SECRETS$API_KEY_EXAMPLE")}
pattern="^\S*$"
/>
{error && <p className="text-red-500 text-sm">{error}</p>}

View File

@@ -7,6 +7,7 @@ import { ModalBody } from "#/components/shared/modals/modal-body";
import { BrandButton } from "../settings/brand-button";
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
import BitbucketLogo from "#/assets/branding/bitbucket-logo.svg?react";
import { useAuthUrl } from "#/hooks/use-auth-url";
import { GetConfigResponse } from "#/api/open-hands.types";
@@ -23,6 +24,11 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
identityProvider: "gitlab",
});
const bitbucketAuthUrl = useAuthUrl({
appMode: appMode || null,
identityProvider: "bitbucket",
});
const handleGitHubAuth = () => {
if (githubAuthUrl) {
// Always start the OIDC flow, let the backend handle TOS check
@@ -37,6 +43,13 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
}
};
const handleBitbucketAuth = () => {
if (bitbucketAuthUrl) {
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = bitbucketAuthUrl;
}
};
return (
<ModalBackdrop>
<ModalBody className="border border-tertiary">
@@ -67,6 +80,16 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
>
{t(I18nKey.GITLAB$CONNECT_TO_GITLAB)}
</BrandButton>
<BrandButton
type="button"
variant="primary"
onClick={handleBitbucketAuth}
className="w-full"
startContent={<BitbucketLogo width={20} height={20} />}
>
{t(I18nKey.BITBUCKET$CONNECT_TO_BITBUCKET)}
</BrandButton>
</div>
</ModalBody>
</ModalBackdrop>

View File

@@ -28,7 +28,7 @@ export function CustomModelInput({
id="custom-model"
name="custom-model"
defaultValue={defaultValue}
aria-label="Custom Model"
aria-label={t(I18nKey.MODEL$CUSTOM_MODEL)}
classNames={{
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
}}

View File

@@ -198,46 +198,46 @@ function SecurityInvariant() {
{t(I18nKey.INVARIANT$ASK_CONFIRMATION_RISK_SEVERITY_LABEL)}
</p>
<Select
placeholder="Select risk severity"
placeholder={t(I18nKey.SECURITY$SELECT_RISK_SEVERITY)}
value={selectedRisk}
onChange={(e) =>
setSelectedRisk(Number(e.target.value) as ActionSecurityRisk)
}
className={getRiskColor(selectedRisk)}
selectedKeys={new Set([selectedRisk.toString()])}
aria-label="Select risk severity"
aria-label={t(I18nKey.SECURITY$SELECT_RISK_SEVERITY)}
>
<SelectItem
key={ActionSecurityRisk.UNKNOWN}
aria-label="Unknown Risk"
aria-label={t(I18nKey.SECURITY$UNKNOWN_RISK)}
className={getRiskColor(ActionSecurityRisk.UNKNOWN)}
>
{getRiskText(ActionSecurityRisk.UNKNOWN)}
</SelectItem>
<SelectItem
key={ActionSecurityRisk.LOW}
aria-label="Low Risk"
aria-label={t(I18nKey.SECURITY$LOW_RISK)}
className={getRiskColor(ActionSecurityRisk.LOW)}
>
{getRiskText(ActionSecurityRisk.LOW)}
</SelectItem>
<SelectItem
key={ActionSecurityRisk.MEDIUM}
aria-label="Medium Risk"
aria-label={t(I18nKey.SECURITY$MEDIUM_RISK)}
className={getRiskColor(ActionSecurityRisk.MEDIUM)}
>
{getRiskText(ActionSecurityRisk.MEDIUM)}
</SelectItem>
<SelectItem
key={ActionSecurityRisk.HIGH}
aria-label="High Risk"
aria-label={t(I18nKey.SECURITY$HIGH_RISK)}
className={getRiskColor(ActionSecurityRisk.HIGH)}
>
{getRiskText(ActionSecurityRisk.HIGH)}
</SelectItem>
<SelectItem
key={ActionSecurityRisk.HIGH + 1}
aria-label="Don't ask for confirmation"
aria-label={t(I18nKey.SECURITY$DONT_ASK_CONFIRMATION)}
>
{t(I18nKey.INVARIANT$DONT_ASK_FOR_CONFIRMATION_LABEL)}
</SelectItem>

View File

@@ -34,10 +34,7 @@ export const useAuthCallback = () => {
const loginMethod = searchParams.get("login_method");
// Set the login method if it's valid
if (
loginMethod === LoginMethod.GITHUB ||
loginMethod === LoginMethod.GITLAB
) {
if (Object.values(LoginMethod).includes(loginMethod as LoginMethod)) {
setLoginMethod(loginMethod as LoginMethod);
// Clean up the URL by removing the login_method parameter

View File

@@ -602,7 +602,30 @@ export enum I18nKey {
FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION = "FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION",
FEEDBACK$REASON_FORGOT_CONTEXT = "FEEDBACK$REASON_FORGOT_CONTEXT",
FEEDBACK$REASON_UNNECESSARY_CHANGES = "FEEDBACK$REASON_UNNECESSARY_CHANGES",
FEEDBACK$REASON_SHOULD_ASK_FIRST = "FEEDBACK$REASON_SHOULD_ASK_FIRST",
FEEDBACK$REASON_OTHER = "FEEDBACK$REASON_OTHER",
FEEDBACK$THANK_YOU_FOR_FEEDBACK = "FEEDBACK$THANK_YOU_FOR_FEEDBACK",
FEEDBACK$FAILED_TO_SUBMIT = "FEEDBACK$FAILED_TO_SUBMIT",
HOME$ADD_GITHUB_REPOS = "HOME$ADD_GITHUB_REPOS",
REPOSITORY$SELECT_BRANCH = "REPOSITORY$SELECT_BRANCH",
REPOSITORY$SELECT_REPO = "REPOSITORY$SELECT_REPO",
TASKS$SUGGESTED_TASKS = "TASKS$SUGGESTED_TASKS",
TASKS$NO_TASKS_AVAILABLE = "TASKS$NO_TASKS_AVAILABLE",
PAYMENT$SPECIFY_AMOUNT_USD = "PAYMENT$SPECIFY_AMOUNT_USD",
GIT$BITBUCKET_TOKEN_HELP_LINK = "GIT$BITBUCKET_TOKEN_HELP_LINK",
GIT$BITBUCKET_TOKEN_SEE_MORE_LINK = "GIT$BITBUCKET_TOKEN_SEE_MORE_LINK",
GIT$GITHUB_TOKEN_HELP_LINK = "GIT$GITHUB_TOKEN_HELP_LINK",
GIT$GITHUB_TOKEN_SEE_MORE_LINK = "GIT$GITHUB_TOKEN_SEE_MORE_LINK",
GIT$GITLAB_TOKEN_HELP_LINK = "GIT$GITLAB_TOKEN_HELP_LINK",
GIT$GITLAB_TOKEN_SEE_MORE_LINK = "GIT$GITLAB_TOKEN_SEE_MORE_LINK",
SECRETS$SECRET_ALREADY_EXISTS = "SECRETS$SECRET_ALREADY_EXISTS",
SECRETS$API_KEY_EXAMPLE = "SECRETS$API_KEY_EXAMPLE",
MODEL$CUSTOM_MODEL = "MODEL$CUSTOM_MODEL",
SECURITY$SELECT_RISK_SEVERITY = "SECURITY$SELECT_RISK_SEVERITY",
SECURITY$DONT_ASK_CONFIRMATION = "SECURITY$DONT_ASK_CONFIRMATION",
SETTINGS$MAXIMUM_BUDGET_USD = "SETTINGS$MAXIMUM_BUDGET_USD",
GIT$DISCONNECT_TOKENS = "GIT$DISCONNECT_TOKENS",
API$TAVILY_KEY_EXAMPLE = "API$TAVILY_KEY_EXAMPLE",
API$TVLY_KEY_EXAMPLE = "API$TVLY_KEY_EXAMPLE",
SECRETS$CONNECT_GIT_PROVIDER = "SECRETS$CONNECT_GIT_PROVIDER",
}

View File

@@ -833,10 +833,10 @@
},
"HOME$LETS_START_BUILDING": {
"en": "Let's Start Building!",
"ja": "構築を始めましょう!",
"zh-CN": "让我们开始构建",
"zh-TW": "讓我們開始構建",
"ko-KR": "구축을 시작합시다!",
"ja": "開発を始めましょう!",
"zh-CN": "让我们开始开发",
"zh-TW": "讓我們開始開發",
"ko-KR": "개발을 시작합시다!",
"no": "La oss begynne å bygge!",
"it": "Iniziamo a costruire!",
"pt": "Vamos começar a construir!",
@@ -849,7 +849,7 @@
},
"HOME$OPENHANDS_DESCRIPTION": {
"en": "OpenHands makes it easy to build and maintain software using AI-driven development.",
"ja": "OpenHandsはAI駆動の開発を使用してソフトウェアの構築と維持を容易にします。",
"ja": "OpenHandsはAI駆動の開発を使用してソフトウェアの開発と維持を容易にします。",
"zh-CN": "OpenHands使用AI驱动的开发方式轻松构建和维护软件。",
"zh-TW": "OpenHands使用AI驅動的開發方式輕鬆構建和維護軟件。",
"ko-KR": "OpenHands는 AI 기반 개발을 사용하여 소프트웨어를 쉽게 구축하고 유지할 수 있게 합니다.",
@@ -9631,6 +9631,22 @@
"de": "Der Agent hat unnötige Änderungen vorgenommen",
"uk": "Агент зробив непотрібні зміни"
},
"FEEDBACK$REASON_SHOULD_ASK_FIRST": {
"en": "The agent should've asked me first before doing it!",
"ja": "エージェントは実行する前に私に確認すべきでした!",
"zh-CN": "代理应该先问我再做这件事!",
"zh-TW": "代理應該先問我再做這件事!",
"ko-KR": "에이전트는 먼저 나에게 물어봤어야 했습니다!",
"no": "Agenten burde ha spurt meg først før den gjorde det!",
"it": "L'agente avrebbe dovuto chiedermelo prima di farlo!",
"pt": "O agente deveria ter me perguntado primeiro antes de fazer isso!",
"es": "¡El agente debería haberme preguntado primero antes de hacerlo!",
"ar": "كان يجب على الوكيل أن يسألني أولاً قبل القيام بذلك!",
"fr": "L'agent aurait dû me demander d'abord avant de le faire !",
"tr": "Ajan bunu yapmadan önce bana sormalıydı!",
"de": "Der Agent hätte mich vorher fragen sollen!",
"uk": "Агент повинен був спочатку запитати мене, перш ніж це робити!"
},
"FEEDBACK$REASON_OTHER": {
"en": "Other",
"ja": "その他",
@@ -9678,5 +9694,357 @@
"tr": "Geri bildirim gönderilemedi",
"de": "Feedback konnte nicht gesendet werden",
"uk": "Не вдалося надіслати відгук"
},
"HOME$ADD_GITHUB_REPOS": {
"en": "Add GitHub repos",
"ja": "GitHubリポジトリを追加",
"zh-CN": "添加GitHub仓库",
"zh-TW": "新增GitHub儲存庫",
"ko-KR": "GitHub 저장소 추가",
"no": "Legg til GitHub-repositorier",
"it": "Aggiungi repository GitHub",
"pt": "Adicionar repositórios GitHub",
"es": "Agregar repositorios de GitHub",
"ar": "إضافة مستودعات GitHub",
"fr": "Ajouter des dépôts GitHub",
"tr": "GitHub depoları ekle",
"de": "GitHub-Repositories hinzufügen",
"uk": "Додати репозиторії GitHub"
},
"REPOSITORY$SELECT_BRANCH": {
"en": "Select a branch",
"ja": "ブランチを選択",
"zh-CN": "选择分支",
"zh-TW": "選擇分支",
"ko-KR": "브랜치 선택",
"no": "Velg en gren",
"it": "Seleziona un ramo",
"pt": "Selecionar um branch",
"es": "Seleccionar una rama",
"ar": "اختر فرع",
"fr": "Sélectionner une branche",
"tr": "Bir dal seç",
"de": "Einen Branch auswählen",
"uk": "Вибрати гілку"
},
"REPOSITORY$SELECT_REPO": {
"en": "Select a repo",
"ja": "リポジトリを選択",
"zh-CN": "选择仓库",
"zh-TW": "選擇儲存庫",
"ko-KR": "저장소 선택",
"no": "Velg et repositorium",
"it": "Seleziona un repository",
"pt": "Selecionar um repositório",
"es": "Seleccionar un repositorio",
"ar": "اختر مستودع",
"fr": "Sélectionner un dépôt",
"tr": "Bir depo seç",
"de": "Ein Repository auswählen",
"uk": "Вибрати репозиторій"
},
"TASKS$SUGGESTED_TASKS": {
"en": "Suggested Tasks",
"ja": "推奨タスク",
"zh-CN": "建议任务",
"zh-TW": "建議任務",
"ko-KR": "추천 작업",
"no": "Foreslåtte oppgaver",
"it": "Attività suggerite",
"pt": "Tarefas sugeridas",
"es": "Tareas sugeridas",
"ar": "المهام المقترحة",
"fr": "Tâches suggérées",
"tr": "Önerilen görevler",
"de": "Vorgeschlagene Aufgaben",
"uk": "Запропоновані завдання"
},
"TASKS$NO_TASKS_AVAILABLE": {
"en": "No tasks available",
"ja": "利用可能なタスクがありません",
"zh-CN": "没有可用任务",
"zh-TW": "沒有可用任務",
"ko-KR": "사용 가능한 작업이 없습니다",
"no": "Ingen oppgaver tilgjengelig",
"it": "Nessuna attività disponibile",
"pt": "Nenhuma tarefa disponível",
"es": "No hay tareas disponibles",
"ar": "لا توجد مهام متاحة",
"fr": "Aucune tâche disponible",
"tr": "Mevcut görev yok",
"de": "Keine Aufgaben verfügbar",
"uk": "Немає доступних завдань"
},
"PAYMENT$SPECIFY_AMOUNT_USD": {
"en": "Specify an amount in USD to add - min $10",
"ja": "追加するUSD金額を指定してください - 最小$10",
"zh-CN": "指定要添加的美元金额 - 最少$10",
"zh-TW": "指定要新增的美元金額 - 最少$10",
"ko-KR": "추가할 USD 금액을 지정하세요 - 최소 $10",
"no": "Spesifiser et beløp i USD å legge til - min $10",
"it": "Specifica un importo in USD da aggiungere - min $10",
"pt": "Especifique um valor em USD para adicionar - mín $10",
"es": "Especifique una cantidad en USD para agregar - mín $10",
"ar": "حدد مبلغًا بالدولار الأمريكي لإضافته - الحد الأدنى 10 دولارات",
"fr": "Spécifiez un montant en USD à ajouter - min 10 $",
"tr": "Eklenecek USD tutarını belirtin - min $10",
"de": "Geben Sie einen USD-Betrag zum Hinzufügen an - min $10",
"uk": "Вкажіть суму в доларах США для додавання - мін $10"
},
"GIT$BITBUCKET_TOKEN_HELP_LINK": {
"en": "Bitbucket token help link",
"ja": "Bitbucketトークンヘルプリンク",
"zh-CN": "Bitbucket令牌帮助链接",
"zh-TW": "Bitbucket令牌幫助連結",
"ko-KR": "Bitbucket 토큰 도움말 링크",
"no": "Bitbucket token hjelpelenke",
"it": "Link di aiuto per il token Bitbucket",
"pt": "Link de ajuda do token Bitbucket",
"es": "Enlace de ayuda del token de Bitbucket",
"ar": "رابط مساعدة رمز Bitbucket",
"fr": "Lien d'aide pour le jeton Bitbucket",
"tr": "Bitbucket token yardım bağlantısı",
"de": "Bitbucket-Token-Hilfe-Link",
"uk": "Посилання на довідку токена Bitbucket"
},
"GIT$BITBUCKET_TOKEN_SEE_MORE_LINK": {
"en": "Bitbucket token see more link",
"ja": "Bitbucketトークン詳細リンク",
"zh-CN": "Bitbucket令牌查看更多链接",
"zh-TW": "Bitbucket令牌查看更多連結",
"ko-KR": "Bitbucket 토큰 더 보기 링크",
"no": "Bitbucket token se mer lenke",
"it": "Link per vedere di più sul token Bitbucket",
"pt": "Link para ver mais sobre o token Bitbucket",
"es": "Enlace para ver más del token de Bitbucket",
"ar": "رابط لرؤية المزيد حول رمز Bitbucket",
"fr": "Lien pour en voir plus sur le jeton Bitbucket",
"tr": "Bitbucket token daha fazla görme bağlantısı",
"de": "Bitbucket-Token mehr sehen Link",
"uk": "Посилання для перегляду більше про токен Bitbucket"
},
"GIT$GITHUB_TOKEN_HELP_LINK": {
"en": "GitHub token help link",
"ja": "GitHubトークンヘルプリンク",
"zh-CN": "GitHub令牌帮助链接",
"zh-TW": "GitHub令牌幫助連結",
"ko-KR": "GitHub 토큰 도움말 링크",
"no": "GitHub token hjelpelenke",
"it": "Link di aiuto per il token GitHub",
"pt": "Link de ajuda do token GitHub",
"es": "Enlace de ayuda del token de GitHub",
"ar": "رابط مساعدة رمز GitHub",
"fr": "Lien d'aide pour le jeton GitHub",
"tr": "GitHub token yardım bağlantısı",
"de": "GitHub-Token-Hilfe-Link",
"uk": "Посилання на довідку токена GitHub"
},
"GIT$GITHUB_TOKEN_SEE_MORE_LINK": {
"en": "GitHub token see more link",
"ja": "GitHubトークン詳細リンク",
"zh-CN": "GitHub令牌查看更多链接",
"zh-TW": "GitHub令牌查看更多連結",
"ko-KR": "GitHub 토큰 더 보기 링크",
"no": "GitHub token se mer lenke",
"it": "Link per vedere di più sul token GitHub",
"pt": "Link para ver mais sobre o token GitHub",
"es": "Enlace para ver más del token de GitHub",
"ar": "رابط لرؤية المزيد حول رمز GitHub",
"fr": "Lien pour en voir plus sur le jeton GitHub",
"tr": "GitHub token daha fazla görme bağlantısı",
"de": "GitHub-Token mehr sehen Link",
"uk": "Посилання для перегляду більше про токен GitHub"
},
"GIT$GITLAB_TOKEN_HELP_LINK": {
"en": "Gitlab token help link",
"ja": "GitLabトークンヘルプリンク",
"zh-CN": "GitLab令牌帮助链接",
"zh-TW": "GitLab令牌幫助連結",
"ko-KR": "GitLab 토큰 도움말 링크",
"no": "GitLab token hjelpelenke",
"it": "Link di aiuto per il token GitLab",
"pt": "Link de ajuda do token GitLab",
"es": "Enlace de ayuda del token de GitLab",
"ar": "رابط مساعدة رمز GitLab",
"fr": "Lien d'aide pour le jeton GitLab",
"tr": "GitLab token yardım bağlantısı",
"de": "GitLab-Token-Hilfe-Link",
"uk": "Посилання на довідку токена GitLab"
},
"GIT$GITLAB_TOKEN_SEE_MORE_LINK": {
"en": "GitLab token see more link",
"ja": "GitLabトークン詳細リンク",
"zh-CN": "GitLab令牌查看更多链接",
"zh-TW": "GitLab令牌查看更多連結",
"ko-KR": "GitLab 토큰 더 보기 링크",
"no": "GitLab token se mer lenke",
"it": "Link per vedere di più sul token GitLab",
"pt": "Link para ver mais sobre o token GitLab",
"es": "Enlace para ver más del token de GitLab",
"ar": "رابط لرؤية المزيد حول رمز GitLab",
"fr": "Lien pour en voir plus sur le jeton GitLab",
"tr": "GitLab token daha fazla görme bağlantısı",
"de": "GitLab-Token mehr sehen Link",
"uk": "Посилання для перегляду більше про токен GitLab"
},
"SECRETS$SECRET_ALREADY_EXISTS": {
"en": "Secret already exists",
"ja": "シークレットは既に存在します",
"zh-CN": "密钥已存在",
"zh-TW": "密鑰已存在",
"ko-KR": "시크릿이 이미 존재합니다",
"no": "Hemmelighet eksisterer allerede",
"it": "Il segreto esiste già",
"pt": "Segredo já existe",
"es": "El secreto ya existe",
"ar": "السر موجود بالفعل",
"fr": "Le secret existe déjà",
"tr": "Gizli anahtar zaten mevcut",
"de": "Geheimnis existiert bereits",
"uk": "Секрет вже існує"
},
"SECRETS$API_KEY_EXAMPLE": {
"en": "e.g. OpenAI_API_Key",
"ja": "例: OpenAI_API_Key",
"zh-CN": "例如 OpenAI_API_Key",
"zh-TW": "例如 OpenAI_API_Key",
"ko-KR": "예: OpenAI_API_Key",
"no": "f.eks. OpenAI_API_Key",
"it": "es. OpenAI_API_Key",
"pt": "ex. OpenAI_API_Key",
"es": "ej. OpenAI_API_Key",
"ar": "مثل OpenAI_API_Key",
"fr": "ex. OpenAI_API_Key",
"tr": "örn. OpenAI_API_Key",
"de": "z.B. OpenAI_API_Key",
"uk": "наприклад OpenAI_API_Key"
},
"MODEL$CUSTOM_MODEL": {
"en": "Custom Model",
"ja": "カスタムモデル",
"zh-CN": "自定义模型",
"zh-TW": "自訂模型",
"ko-KR": "사용자 정의 모델",
"no": "Tilpasset modell",
"it": "Modello personalizzato",
"pt": "Modelo personalizado",
"es": "Modelo personalizado",
"ar": "نموذج مخصص",
"fr": "Modèle personnalisé",
"tr": "Özel model",
"de": "Benutzerdefiniertes Modell",
"uk": "Користувацька модель"
},
"SECURITY$SELECT_RISK_SEVERITY": {
"en": "Select risk severity",
"ja": "リスクの重要度を選択",
"zh-CN": "选择风险严重程度",
"zh-TW": "選擇風險嚴重程度",
"ko-KR": "위험 심각도 선택",
"no": "Velg risikoalvorlighet",
"it": "Seleziona gravità del rischio",
"pt": "Selecionar gravidade do risco",
"es": "Seleccionar gravedad del riesgo",
"ar": "اختر شدة المخاطر",
"fr": "Sélectionner la gravité du risque",
"tr": "Risk ciddiyetini seç",
"de": "Risikoschweregrad auswählen",
"uk": "Вибрати ступінь ризику"
},
"SECURITY$DONT_ASK_CONFIRMATION": {
"en": "Don't ask for confirmation",
"ja": "確認を求めない",
"zh-CN": "不要求确认",
"zh-TW": "不要求確認",
"ko-KR": "확인을 요청하지 않음",
"no": "Ikke spør om bekreftelse",
"it": "Non chiedere conferma",
"pt": "Não pedir confirmação",
"es": "No pedir confirmación",
"ar": "لا تطلب التأكيد",
"fr": "Ne pas demander de confirmation",
"tr": "Onay isteme",
"de": "Nicht nach Bestätigung fragen",
"uk": "Не запитувати підтвердження"
},
"SETTINGS$MAXIMUM_BUDGET_USD": {
"en": "Maximum budget per conversation in USD",
"ja": "会話あたりの最大予算USD",
"zh-CN": "每次对话的最大预算(美元)",
"zh-TW": "每次對話的最大預算(美元)",
"ko-KR": "대화당 최대 예산(USD)",
"no": "Maksimalt budsjett per samtale i USD",
"it": "Budget massimo per conversazione in USD",
"pt": "Orçamento máximo por conversa em USD",
"es": "Presupuesto máximo por conversación en USD",
"ar": "الحد الأقصى للميزانية لكل محادثة بالدولار الأمريكي",
"fr": "Budget maximum par conversation en USD",
"tr": "Konuşma başına maksimum bütçe (USD)",
"de": "Maximales Budget pro Gespräch in USD",
"uk": "Максимальний бюджет на розмову в доларах США"
},
"GIT$DISCONNECT_TOKENS": {
"en": "Disconnect Tokens",
"ja": "トークンを切断",
"zh-CN": "断开令牌连接",
"zh-TW": "中斷令牌連接",
"ko-KR": "토큰 연결 해제",
"no": "Koble fra tokens",
"it": "Disconnetti token",
"pt": "Desconectar tokens",
"es": "Desconectar tokens",
"ar": "قطع اتصال الرموز",
"fr": "Déconnecter les jetons",
"tr": "Token bağlantısını kes",
"de": "Token trennen",
"uk": "Відключити токени"
},
"API$TAVILY_KEY_EXAMPLE": {
"en": "sk-tavily-...",
"ja": "sk-tavily-...",
"zh-CN": "sk-tavily-...",
"zh-TW": "sk-tavily-...",
"ko-KR": "sk-tavily-...",
"no": "sk-tavily-...",
"it": "sk-tavily-...",
"pt": "sk-tavily-...",
"es": "sk-tavily-...",
"ar": "sk-tavily-...",
"fr": "sk-tavily-...",
"tr": "sk-tavily-...",
"de": "sk-tavily-...",
"uk": "sk-tavily-..."
},
"API$TVLY_KEY_EXAMPLE": {
"en": "tvly-...",
"ja": "tvly-...",
"zh-CN": "tvly-...",
"zh-TW": "tvly-...",
"ko-KR": "tvly-...",
"no": "tvly-...",
"it": "tvly-...",
"pt": "tvly-...",
"es": "tvly-...",
"ar": "tvly-...",
"fr": "tvly-...",
"tr": "tvly-...",
"de": "tvly-...",
"uk": "tvly-..."
},
"SECRETS$CONNECT_GIT_PROVIDER": {
"en": "Connect a Git provider to manage secrets",
"ja": "シークレットを管理するためにGitプロバイダーに接続",
"zh-CN": "连接Git提供商以管理密钥",
"zh-TW": "連接Git提供商以管理密鑰",
"ko-KR": "시크릿 관리를 위해 Git 제공자에 연결",
"no": "Koble til en Git-leverandør for å administrere hemmeligheter",
"it": "Connetti un provider Git per gestire i segreti",
"pt": "Conectar um provedor Git para gerenciar segredos",
"es": "Conectar un proveedor Git para gestionar secretos",
"ar": "اتصل بمزود Git لإدارة الأسرار",
"fr": "Connecter un fournisseur Git pour gérer les secrets",
"tr": "Gizli anahtarları yönetmek için bir Git sağlayıcısına bağlan",
"de": "Git-Anbieter verbinden, um Geheimnisse zu verwalten",
"uk": "Підключити провайдера Git для управління секретами"
}
}

View File

@@ -111,6 +111,7 @@ const openHandsHandlers = [
"gpt-4o-mini",
"anthropic/claude-3.5",
"anthropic/claude-sonnet-4-20250514",
"sambanova/Meta-Llama-3.1-8B-Instruct",
]),
),

View File

@@ -189,7 +189,7 @@ function AppSettingsScreen() {
label={t(I18nKey.SETTINGS$MAX_BUDGET_PER_CONVERSATION)}
defaultValue={settings.MAX_BUDGET_PER_TASK?.toString() || ""}
onChange={checkIfMaxBudgetPerTaskHasChanged}
placeholder="Maximum budget per conversation in USD"
placeholder={t(I18nKey.SETTINGS$MAXIMUM_BUDGET_USD)}
min={1}
step={1}
className="w-[680px]" // Match the width of the language field

View File

@@ -185,7 +185,7 @@ function GitSettingsScreen() {
!isGitHubTokenSet && !isGitLabTokenSet && !isBitbucketTokenSet
}
>
Disconnect Tokens
{t(I18nKey.GIT$DISCONNECT_TOKENS)}
</BrandButton>
<BrandButton
testId="submit-button"

View File

@@ -23,6 +23,7 @@ import { isCustomModel } from "#/utils/is-custom-model";
import { LlmSettingsInputsSkeleton } from "#/components/features/settings/llm-settings/llm-settings-inputs-skeleton";
import { KeyStatusIcon } from "#/components/features/settings/key-status-icon";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { getProviderId } from "#/utils/map-provider";
function LlmSettingsScreen() {
const { t } = useTranslation();
@@ -93,13 +94,15 @@ function LlmSettingsScreen() {
};
const basicFormAction = (formData: FormData) => {
const provider = formData.get("llm-provider-input")?.toString();
const providerDisplay = formData.get("llm-provider-input")?.toString();
const provider = providerDisplay
? getProviderId(providerDisplay)
: undefined;
const model = formData.get("llm-model-input")?.toString();
const apiKey = formData.get("llm-api-key-input")?.toString();
const searchApiKey = formData.get("search-api-key-input")?.toString();
const fullLlmModel =
provider && model && `${provider}/${model}`.toLowerCase();
const fullLlmModel = provider && model && `${provider}/${model}`;
saveSettings(
{
@@ -315,7 +318,7 @@ function LlmSettingsScreen() {
className="w-full max-w-[680px]"
defaultValue={settings.SEARCH_API_KEY || ""}
onChange={handleSearchApiKeyIsDirty}
placeholder="sk-tavily-..."
placeholder={t(I18nKey.API$TAVILY_KEY_EXAMPLE)}
startContent={
settings.SEARCH_API_KEY_SET && (
<KeyStatusIcon isSet={settings.SEARCH_API_KEY_SET} />
@@ -390,7 +393,7 @@ function LlmSettingsScreen() {
className="w-full max-w-[680px]"
defaultValue={settings.SEARCH_API_KEY || ""}
onChange={handleSearchApiKeyIsDirty}
placeholder="tvly-..."
placeholder={t(I18nKey.API$TVLY_KEY_EXAMPLE)}
startContent={
settings.SEARCH_API_KEY_SET && (
<KeyStatusIcon isSet={settings.SEARCH_API_KEY_SET} />

View File

@@ -13,6 +13,7 @@ import { BrandButton } from "#/components/features/settings/brand-button";
import { ConfirmationModal } from "#/components/shared/modals/confirmation-modal";
import { GetSecretsResponse } from "#/api/secrets-service.types";
import { useUserProviders } from "#/hooks/use-user-providers";
import { I18nKey } from "#/i18n/declaration";
import { useConfig } from "#/hooks/query/use-config";
function SecretsSettingsScreen() {
@@ -90,7 +91,7 @@ function SecretsSettingsScreen() {
type="button"
>
<BrandButton type="button" variant="secondary">
Connect a Git provider to manage secrets
{t(I18nKey.SECRETS$CONNECT_GIT_PROVIDER)}
</BrandButton>
</Link>
)}

View File

@@ -13,8 +13,10 @@ export function handleObservationMessage(message: ObservationMessage) {
let { content } = message;
if (content.length > 5000) {
const head = content.slice(0, 5000);
content = `${head}\r\n\n... (truncated ${message.content.length - 5000} characters) ...`;
const halfLength = 2500;
const head = content.slice(0, halfLength);
const tail = content.slice(content.length - halfLength);
content = `${head}\r\n\n... (truncated ${message.content.length - 5000} characters) ...\r\n\n${tail}`;
}
store.dispatch(appendOutput(content));

View File

@@ -0,0 +1,81 @@
import { describe, it, expect } from "vitest";
import { execSync } from "child_process";
import path from "path";
import fs from "fs";
describe("Localization Fix Tests", () => {
it("should not find any unlocalized strings in the frontend code", () => {
const scriptPath = path.join(
__dirname,
"../../scripts/check-unlocalized-strings.cjs",
);
// Run the localization check script
const result = execSync(`node ${scriptPath}`, {
cwd: path.join(__dirname, "../.."),
encoding: "utf8",
});
// The script should output success message and exit with code 0
expect(result).toContain(
"✅ No unlocalized strings found in frontend code.",
);
});
it("should properly detect user-facing attributes like placeholder, alt, and aria-label", () => {
// This test verifies that our fix to include placeholder, alt, and aria-label
// attributes in the localization check is working correctly by testing the regex patterns
const scriptPath = path.join(
__dirname,
"../../scripts/check-unlocalized-strings.cjs",
);
const scriptContent = fs.readFileSync(scriptPath, "utf8");
// Verify that these attributes are now being checked for localization
// by ensuring they're not excluded from text extraction
const nonTextAttributesMatch = scriptContent.match(
/const NON_TEXT_ATTRIBUTES = \[(.*?)\]/s,
);
expect(nonTextAttributesMatch).toBeTruthy();
const nonTextAttributes = nonTextAttributesMatch![1];
expect(nonTextAttributes).not.toContain('"placeholder"');
expect(nonTextAttributes).not.toContain('"alt"');
expect(nonTextAttributes).not.toContain('"aria-label"');
// Verify that the script contains the correct attributes that should be excluded
expect(nonTextAttributes).toContain('"className"');
expect(nonTextAttributes).toContain('"testId"');
expect(nonTextAttributes).toContain('"href"');
});
it("should not incorrectly flag CSS units as unlocalized strings", () => {
// This test verifies that our fix to the CSS units regex pattern
// prevents false positives like "Suggested Tasks" being flagged
const testStrings = [
"Suggested Tasks",
"No tasks available",
"Select a branch",
"Select a repo",
"Custom Models",
"API Keys",
"Git Settings",
];
// These strings should not be flagged as CSS units
const cssUnitsPattern =
/\b\d+(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$|^(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$/;
testStrings.forEach((str) => {
expect(cssUnitsPattern.test(str)).toBe(false);
});
// But actual CSS units should still be detected
const actualCssUnits = ["10px", "2rem", "100vh", "px", "rem", "s"];
actualCssUnits.forEach((unit) => {
expect(cssUnitsPattern.test(unit)).toBe(true);
});
});
});

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { parseMaxBudgetPerTask } from "../settings-utils";
import { parseMaxBudgetPerTask, extractSettings } from "../settings-utils";
describe("parseMaxBudgetPerTask", () => {
it("should return null for empty string", () => {
@@ -47,3 +47,45 @@ describe("parseMaxBudgetPerTask", () => {
expect(parseMaxBudgetPerTask("5e-1")).toBeNull(); // 0.5, which is < 1
});
});
describe("extractSettings", () => {
it("should preserve model name case when extracting settings", () => {
// Test cases with various model name formats
const testCases = [
{ provider: "sambanova", model: "Meta-Llama-3.1-8B-Instruct" },
{ provider: "openai", model: "GPT-4o" },
{ provider: "anthropic", model: "Claude-3-5-Sonnet" },
{ provider: "openrouter", model: "CamelCaseModel" },
];
testCases.forEach(({ provider, model }) => {
const formData = new FormData();
formData.set("llm-provider-input", provider);
formData.set("llm-model-input", model);
const settings = extractSettings(formData);
// Verify that the model name case is preserved
const expectedModel = `${provider}/${model}`;
expect(settings.LLM_MODEL).toBe(expectedModel);
// Only test that it's not lowercased if the original has uppercase letters
if (expectedModel !== expectedModel.toLowerCase()) {
expect(settings.LLM_MODEL).not.toBe(expectedModel.toLowerCase());
}
});
});
it("should handle custom model without lowercasing", () => {
const formData = new FormData();
formData.set("llm-provider-input", "sambanova");
formData.set("llm-model-input", "Meta-Llama-3.1-8B-Instruct");
formData.set("use-advanced-options", "true");
formData.set("custom-model", "Custom-Model-Name");
const settings = extractSettings(formData);
// Custom model should take precedence and preserve case
expect(settings.LLM_MODEL).toBe("Custom-Model-Name");
expect(settings.LLM_MODEL).not.toBe("custom-model-name");
});
});

View File

@@ -29,3 +29,10 @@ export const mapProvider = (provider: string) =>
Object.keys(MAP_PROVIDER).includes(provider)
? MAP_PROVIDER[provider as keyof typeof MAP_PROVIDER]
: provider;
export const getProviderId = (displayName: string): string => {
const entry = Object.entries(MAP_PROVIDER).find(
([, value]) => value === displayName,
);
return entry ? entry[0] : displayName;
};

View File

@@ -1,10 +1,12 @@
import { Settings } from "#/types/settings";
import { getProviderId } from "#/utils/map-provider";
const extractBasicFormData = (formData: FormData) => {
const provider = formData.get("llm-provider-input")?.toString();
const providerDisplay = formData.get("llm-provider-input")?.toString();
const provider = providerDisplay ? getProviderId(providerDisplay) : undefined;
const model = formData.get("llm-model-input")?.toString();
const LLM_MODEL = `${provider}/${model}`.toLowerCase();
const LLM_MODEL = `${provider}/${model}`;
const LLM_API_KEY = formData.get("llm-api-key-input")?.toString();
const AGENT = formData.get("agent")?.toString();
const LANGUAGE = formData.get("language")?.toString();

View File

@@ -1,16 +1,20 @@
---
name: add_agent
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- new agent
- new microagent
- create agent
- create an agent
- create microagent
- create a microagent
- add agent
- add an agent
- add microagent
- add a microagent
- microagent template
- new agent
- new microagent
- create agent
- create an agent
- create microagent
- create a microagent
- add agent
- add an agent
- add microagent
- add a microagent
- microagent template
---
This agent helps create new microagents in the `.openhands/microagents` directory by providing guidance and templates.

View File

@@ -1,9 +1,13 @@
---
inputs:
- description: Branch for the agent to work on
name: REPO_FOLDER_NAME
name: add_repo_inst
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /add_repo_inst
inputs:
- name: REPO_FOLDER_NAME
description: "Branch for the agent to work on"
---
Please browse the current repository under /workspace/{{ REPO_FOLDER_NAME }}, look at the documentation and relevant code, and understand the purpose of this repository.
@@ -14,6 +18,7 @@ Here's an example:
```markdown
---
name: repo
type: repo
agent: CodeActAgent
---

View File

@@ -1,11 +1,15 @@
---
inputs:
- description: URL of the pull request
name: PR_URL
- description: Branch name corresponds to the pull request
name: BRANCH_NAME
name: address_pr_comments
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /address_pr_comments
inputs:
- name: PR_URL
description: "URL of the pull request"
- name: BRANCH_NAME
description: "Branch name corresponds to the pull request"
---
First, check the branch {{ BRANCH_NAME }} and read the diff against the main branch to understand the purpose.

View File

@@ -1,4 +1,8 @@
---
name: agent_memory
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- /remember
---

View File

@@ -1,8 +1,15 @@
---
# This is a repo microagent that is always activated
# to include necessary default tools implemented with MCP
name: default-tools
type: repo
version: 1.0.0
agent: CodeActAgent
mcp_tools:
stdio_servers:
- args:
- mcp-server-fetch
command: uvx
name: fetch
- name: "fetch"
command: "uvx"
args: ["mcp-server-fetch"]
# We leave the body empty because MCP tools will automatically add the
# tool description for LLMs in tool calls, so there's no need to add extra descriptions.
---

View File

@@ -1,4 +1,8 @@
---
name: docker
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- docker
- container

View File

@@ -1,16 +1,19 @@
---
inputs:
- description: Branch for the agent to work on
name: BRANCH_NAME
- description: The test command you want the agent to work on. For example, `pytest
tests/unit/test_bash_parsing.py`
name: TEST_COMMAND_TO_RUN
- description: The name of function to fix
name: FUNCTION_TO_FIX
- description: The path of the file that contains the function
name: FILE_FOR_FUNCTION
name: fix_test
version: 1.0.0
author: openhands
agent: CodeActAgent
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 }}.

View File

@@ -1,4 +1,8 @@
---
name: flarglebargle
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- flarglebargle
---

View File

@@ -1,4 +1,8 @@
---
name: github
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- github
- git

View File

@@ -1,4 +1,8 @@
---
name: gitlab
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- gitlab
- git

View File

@@ -1,4 +1,8 @@
---
name: kubernetes
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- kubernetes
- k8s

View File

@@ -1,4 +1,8 @@
---
name: npm
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- npm
---

View File

@@ -1,4 +1,8 @@
---
name: pdflatex
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- pdflatex
---

View File

@@ -1,12 +1,15 @@
---
name: security
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- security
- vulnerability
- authentication
- authorization
- permissions
- security
- vulnerability
- authentication
- authorization
- permissions
---
This document provides guidance on security best practices
You should always be considering security implications when developing.

View File

@@ -1,12 +1,16 @@
---
name: SSH Microagent
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- ssh
- remote server
- remote machine
- remote host
- remote connection
- secure shell
- ssh keys
- ssh
- remote server
- remote machine
- remote host
- remote connection
- secure shell
- ssh keys
---
# SSH Microagent

View File

@@ -1,8 +1,12 @@
---
triggers:
- swift-linux
- swift-debian
- swift-installation
name: swift-linux
type: knowledge
agent: CodeActAgent
version: 1.0.0
triggers:
- swift-linux
- swift-debian
- swift-installation
---
# Swift Installation Guide for Debian Linux

View File

@@ -1,15 +1,19 @@
---
inputs:
- description: URL of the pull request
name: PR_URL
type: string
validation:
pattern: ^https://github.com/.+/.+/pull/[0-9]+$
- description: Branch name corresponds to the pull request
name: BRANCH_NAME
type: string
name: update_pr_description
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /update_pr_description
inputs:
- name: PR_URL
description: "URL of the pull request"
type: string
validation:
pattern: "^https://github.com/.+/.+/pull/[0-9]+$"
- name: BRANCH_NAME
description: "Branch name corresponds to the pull request"
type: string
---
Please check the branch "{{ BRANCH_NAME }}" and look at the diff against the main branch. This branch belongs to this PR "{{ PR_URL }}".

View File

@@ -1,12 +1,15 @@
---
inputs:
- description: Branch for the agent to work on
name: BRANCH_NAME
- description: The test command you want the agent to work on. For example, `pytest
tests/unit/test_bash_parsing.py`
name: TEST_COMMAND_TO_RUN
name: update_test
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /update_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`"
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.

34
openhands-ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

15
openhands-ui/README.md Normal file
View File

@@ -0,0 +1,15 @@
# openhands-ui
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.16. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

25
openhands-ui/bun.lock Normal file
View File

@@ -0,0 +1,25 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "openhands-ui",
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
"@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="],
"bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
}
}

1
openhands-ui/index.ts Normal file
View File

@@ -0,0 +1 @@
console.log("Hello via Bun!");

17
openhands-ui/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "@openhands/ui",
"version": "0.1.0",
"description": "OpenHands UI Components",
"keywords": [
"openhands",
"ui",
"components"
],
"type": "module",
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.8.3"
}
}

View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

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