Compare commits

..

310 Commits

Author SHA1 Message Date
openhands
a9a04020ca Fix import order in test_get_repository_microagents.py 2025-07-31 15:15:28 +00:00
openhands
f8346cbdae Fix failing tests in PR #10008 2025-07-31 15:12:19 +00:00
chuckbutkus
c1c610c98d Merge branch 'main' into enterprise-sso 2025-07-31 10:00:10 -04:00
mamoodi
43555fa13b Release 0.51.0 (#9993) 2025-07-31 09:55:05 -04:00
Chuck Butkus
7ca7d6fd84 Remove debug console logging 2025-07-31 02:53:41 -04:00
Chuck Butkus
4fb27a23ea Fix bug 2025-07-31 01:52:46 -04:00
chuckbutkus
b92bebb71e Merge branch 'main' into enterprise-sso 2025-07-31 01:45:24 -04:00
Chuck Butkus
87aa7cdd36 Fixes 2025-07-31 01:41:43 -04:00
openhands
e9d58b4a02 Fix logout button for enterprise_sso users without provider tokens 2025-07-31 04:34:43 +00:00
Hiep Le
10ae481b91 refactor: improve the get microagents API (#9958)
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-07-31 00:33:02 -04:00
Chuck Butkus
fbf350887f Lint fix 2025-07-31 00:28:06 -04:00
openhands
42f684daeb Enable logout button for authenticated users without provider tokens 2025-07-31 04:22:08 +00:00
openhands
ebe62088a3 Add Enterprise SSO translation key and fix auth modal
- Add ENTERPRISE_SSO$CONNECT_TO_ENTERPRISE_SSO translation key to i18n declaration
- Add 'Login with Enterprise SSO' translation in all supported languages
- Fix auth-modal.tsx to use correct Enterprise SSO translation key instead of Bitbucket key
2025-07-31 02:52:37 +00:00
Chuck Butkus
a88d75a2ca Update 2025-07-30 22:45:14 -04:00
Chuck Butkus
d2b5c3c777 Missed a change 2025-07-30 22:41:57 -04:00
Chuck Butkus
7321b17242 Add enterprise SSO config 2025-07-30 22:40:32 -04:00
Xingyao Wang
c2e860fe92 Improve LLM call metadata (#10004)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-31 07:02:49 +08:00
Xingyao Wang
c2fc84e6ea Remove task completion status message from finish action display (#9977)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-07-31 04:33:45 +08:00
Xingyao Wang
6f44b7352e Add search API key settings to CLI (#9976)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-31 02:03:29 +08:00
dependabot[bot]
16106e6262 chore(deps): bump the version-all group in /frontend with 3 updates (#9997)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-30 15:20:33 +00:00
Xingyao Wang
6cea73b6da Add qwen-3-coder-480b to OpenHands provider (#9985)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-30 23:12:31 +08:00
llamantino
fdf9a49e28 feat(frontend): improve conversation card context menu (#9917) 2025-07-30 19:09:56 +04:00
Erkin Alp Güney
e348634dbd Fix user input commands being echoed twice in terminal (#9959)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-30 17:47:21 +04:00
Ryan H. Tran
b67db15f8a [CLI] Fix errno 21 warning when reading directory (#9990) 2025-07-30 21:38:45 +08:00
Engel Nyst
a32a623078 perf(gemini): Apply Gemini 2.5 Pro performance optimizations from PR 9913 (#9925)
Co-authored-by: OpenHands-Claude <openhands@all-hands.dev>
2025-07-29 23:28:50 +00:00
Rohit Malhotra
03c8312f5f Add maintenance banner feature (#9981)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-07-29 17:35:10 -04:00
Graham Neubig
b75a61bce9 Fix make lint dependencies to work out of the box (#9983)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-29 21:14:00 +00:00
Tim O'Farrell
2c36e2447c Fix for app/worker urls (#9980) 2025-07-29 14:49:22 -06:00
Graham Neubig
f87c827fe6 Improve OpenHands authentication error message (#9780)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-07-29 20:22:47 +00:00
Xingyao Wang
3f395e3cee feat: show export trajectory button in SaaS mode for debugging (#9979)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-30 03:26:21 +08:00
Xingyao Wang
7a45ebf0f4 Fix MCP config priority logic in sessions.py (#9237)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-07-29 18:47:19 +00:00
Rohit Malhotra
5b13cfc2a0 Add experiment for agent config (#9861)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-29 17:56:28 +00:00
Tim O'Farrell
5553584056 Fix git changes panel (#9967) 2025-07-29 11:21:49 -06:00
Rohit Malhotra
e951612ff4 Add IP whitelisting information for Bitbucket Cloud integration (#9894)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-29 11:54:54 -04:00
dependabot[bot]
426e16b17d chore(deps): bump the version-all group in /frontend with 7 updates (#9960)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-29 14:59:27 +00:00
Tim O'Farrell
d9a595c9b1 Replace bash scripts with Python for git operations (#9914)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-29 07:34:52 -06:00
Engel Nyst
8fb3728391 Do not override user's git config in CLI mode or local machine (#9905) 2025-07-28 20:12:28 +02:00
dependabot[bot]
d4c94dce83 chore(deps): bump the version-all group in /frontend with 7 updates (#9947)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-28 17:16:13 +00:00
Rohit Malhotra
74d6633e9b Update Slack OAuth URL for the 'Install OpenHands Slack App' button (#9908)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-28 17:08:25 +00:00
Mislav Lukach
eecad803b1 feat(ds): avoid building tailwind (#9945) 2025-07-28 21:04:19 +04:00
Rohit Malhotra
da7a31a6fa Update Slack integration 'Add to Slack' button link (#9906)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-28 12:43:01 -04:00
C9luster
c677f7284e Fix the BUG in the __init__ file of openhands to obtain the version (#9840)
Co-authored-by: yinjiaqi <yinjiaqi@baidu.com>
2025-07-28 16:13:21 +00:00
sp.wack
60e8e55311 fix: keep tabs visible when agent is stopped (#9941)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-28 22:01:54 +08:00
Xingyao Wang
18557e8654 fix: Properly handle AgentRuntimeTimeoutError in runtime base (#9923)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-28 13:33:19 +00:00
llamantino
39c67e2b92 fix(ci): fix fe unit tests workflow failure due to invalid node-version value (#9928) 2025-07-28 12:13:10 +00:00
Carlos Freund
b5146e3188 fix: use poetry run for pre-commit in husky hook (#9934)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-28 16:08:29 +04:00
Erkin Alp Güney
a59a6f3041 Optimize pre commit hooks (#9939)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-28 16:07:22 +04:00
llamantino
056d3e4933 fix(tests): fix tests missed by failing frontend test workflow and other flaky tests (#9943) 2025-07-28 16:00:14 +04:00
Engel Nyst
2b4a5a73a4 Fix configuration precedence in CLI mode (#9911)
Co-authored-by: OpenHands-Claude <openhands@all-hands.dev>
2025-07-27 22:42:22 +02:00
Carlos Freund
46504ab0da Fix deprecation message to reference SANDBOX_VOLUMES instead of non-existent RUNTIME_MOUNT (#9931)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-27 18:36:12 +02:00
Ray Myers
412f6ce58d chore - remove stripe and minio python dependencies (#9921) 2025-07-27 10:26:18 -05:00
Xingyao Wang
c8f9e6b9fc feat(llm) : add qwen to fn call supported model (#9929) 2025-07-27 04:53:55 +00:00
Graham Neubig
588e838dc4 Fix CLI runtime invalid path error handling (#9814)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-26 08:36:46 +00:00
jpelletier1
2550c08749 docs: Add Known Issues section for Gemini 2.5 Pro (#9909)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-25 14:22:39 -05:00
llamantino
0651c51901 fix(llm_config): extend retry delays to respect rate limit windows (#9489) 2025-07-25 17:26:39 +00:00
bojackli
3ce19993bc Fix typo and remove redundant code in storage module. (#9862) 2025-07-25 18:24:18 +02:00
dependabot[bot]
26a9abbe82 chore(deps): bump the version-all group across 1 directory with 10 updates (#9901)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-25 18:22:11 +02:00
Ivan Dagelic
240017add1 feat: daytona envs for state management (#9893)
Signed-off-by: Ivan Dagelic <dagelic.ivan@gmail.com>
2025-07-25 17:49:10 +02:00
dependabot[bot]
b5958b069e chore(deps): bump the version-all group in /frontend with 5 updates (#9903)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-25 19:37:58 +04:00
Mislav Lukach
59b8009d7a fix(ds): add test id support (#9904) 2025-07-25 19:37:25 +04:00
Ryan H. Tran
b8b4f58a79 Update swebench version (#9897) 2025-07-25 22:33:59 +07:00
Engel Nyst
fcb190281c microagent: Add Git best practices (#9335)
Co-authored-by: OpenHands <openhands@all-hands.dev>
2025-07-25 21:45:00 +08:00
Mislav Lukach
9fcf900a23 feat(toast): custom toast component (#9898) 2025-07-25 12:24:17 +00:00
Tim O'Farrell
06ad5e30c9 feat: Optimize git change detection with performance improvement and multi-repository support (#9870)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-24 19:44:25 -06:00
llamantino
739044087b fix(mcp): workaround for ASGI error caused by duplicate http start in mcp (#9891)
Co-authored-by: Xingyao Wang <xingyaoww@gmail.com>
2025-07-24 17:44:03 +00:00
Hiep Le
fa041537c3 feat: Support the “Learn this repo” Button for the Microagent Management Page. (#9873) 2025-07-24 20:30:46 +04:00
dependabot[bot]
079f423a4b chore(deps): bump the version-all group in /frontend with 3 updates (#9883)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-24 18:50:37 +04:00
Vasi
f6060f9c53 feat: [CLI] 9392 cli improve confirmation ux - revisited (#9824)
Co-authored-by: bavg <bavg@ubuntu-server.fritz.box>
2025-07-24 16:13:19 +02:00
Graham Neubig
b7f234641c Fix system prompts to exclude tests for documentation changes (#9880)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-24 09:28:34 -04:00
mamoodi
4ac0af699f Release 0.50.0 (#9868) 2025-07-24 08:59:16 -04:00
Graham Neubig
fb9a941722 docs: Add MCP Cloud availability note and improve document structure (#9801)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-07-23 21:40:35 -04:00
Rohit Malhotra
c05339cb2d Update summary prompt to avoid repetition in consecutive summaries (#9834)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-23 20:59:06 -04:00
Cansu
2ef518f063 feat: Add configurable runtime support for issue resolver and fix: Kubernetes pod naming limits (#9877) 2025-07-24 00:12:36 +02:00
Ryan H. Tran
fbd9280239 Add MCP support for CLI (#9519)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-07-23 17:06:01 +00:00
Mislav Lukach
45ac6b839c fix(button): improve font-weight styling (#9819)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-07-23 15:37:45 +00:00
Hiep Le
8b59143174 feat: Support the “Learn something new” Button in Microagent Details View. (#9866) 2025-07-23 19:08:36 +04:00
dependabot[bot]
c7b8f5d0d1 chore(deps): bump the version-all group in /frontend with 7 updates (#9869)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-23 15:02:35 +00:00
dependabot[bot]
09533d3cb9 chore(deps): bump the version-all group across 1 directory with 30 updates (#9852)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-23 10:49:51 -04:00
Graham Neubig
00582a487c Refactor get_microagents_from_org_or_user error handling (#9865)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-23 14:35:48 +00:00
Graham Neubig
7a168b9b5f Fix Docker runtime port allocation race condition (#9810)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-22 18:12:31 -04:00
Hiep Le
556ec9ab1a feat(frontend): add responsive UI support for the microagent management page (#9847) 2025-07-22 22:47:40 +04:00
Hiep Le
d567d22748 feat: Handle Click Events for Microagents and Conversations on the Microagent Management Page. (#9853) 2025-07-22 22:01:49 +04:00
Tim O'Farrell
e045b757fa Moved monitoring of last_execution_time to system_stats (#9851)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-22 11:32:59 -06:00
Hiep Le
38ffc85470 feat(frontend): Integrate with the API to add a new microagent. (#9821) 2025-07-22 16:57:05 +00:00
Xingyao Wang
58ea7b5248 Add make lint to pre-commit hook (#9795)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-22 12:36:54 -04:00
bojackli
f62ed911d2 Fix: Resolve cross-platform path splitting bug in search (#9732)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-07-22 18:09:50 +02:00
dependabot[bot]
d13e32bcec chore(deps-dev): bump @types/node from 24.0.15 to 24.1.0 in /frontend in the version-all group (#9848)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 19:20:21 +04:00
Xingyao Wang
b978b71c47 Enhance run-eval workflow: Add release triggers and manual dispatch (#9742)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-22 23:11:59 +08:00
llamantino
dc2f5cd1b0 fix(cli): filter out LiteLLM coroutine not awaited warning at shutdown (#9842) 2025-07-22 21:53:58 +08:00
mamoodi
07041e057d fix(frontend): Add context menu state management to Controls component (#9841)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-22 09:49:41 -04:00
mamoodi
6e91d19f80 Fix: Prevent LLM settings from being accessible in SaaS mode via double-click (#9831)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-22 09:49:31 -04:00
dependabot[bot]
936510e219 chore(deps): bump the version-all group in /frontend with 2 updates (#9829)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 17:41:02 +04:00
Boxuan Li
7af35ab827 Evaluation: disable browser when NOT run_with_browsing (#9837) 2025-07-22 01:45:52 +00:00
Xingyao Wang
a7245f2de2 fix(CLI): alias persistence issue (#9828)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-22 05:45:14 +08:00
Tim O'Farrell
6d7ab8a022 Fix for issue where some cases use WORK_PORT and some use APP_PORT (#9830) 2025-07-21 20:24:24 +00:00
Hiep Le
bbfa37fd97 feat(frontend): Allow searching/filtering repositories. (#9791) 2025-07-21 16:05:32 +00:00
dependabot[bot]
d0cf12e474 chore(deps-dev): bump the eslint group in /frontend with 3 updates (#9825)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-21 16:02:35 +00:00
sp.wack
78306b1ee7 hotfix(frontend): Fix context menu closing (#9822) 2025-07-21 19:44:08 +04:00
sp.wack
f6d99234f1 fix(frontend): Fix auth modal tests by adding required providersConfigured prop (#9823) 2025-07-21 19:40:54 +04:00
Boxuan Li
19ca52f954 Skip browser dependency build in Dockerfile when browser is disabled (#9815) 2025-07-21 08:34:11 -07:00
Hiep Le
df75116184 feat(frontend): Integrate with API to display repositories and their associated microagents. (#9784)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-07-21 19:19:34 +04:00
Xingyao Wang
acfb01d743 Fix timestamp corruption in EventStream secret replacement (#9793)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-07-21 22:23:52 +08:00
Regis David Souza Mesquita
0daaf21607 Add support for the groq hosted kimi-k2-instruct model in the functio… (#9759) 2025-07-21 15:14:09 +02:00
llamantino
95bda09cd9 fix(settings): improve MCP editor layout and controls (#9771)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-07-20 19:15:45 +00:00
Rohit Malhotra
1243612694 [Fix]: instruct agent to use token when accessing provider apis (#9807) 2025-07-19 22:04:07 +00:00
Graham Neubig
aff8aba5e6 Fix: Use correct git provider in Push & Create PR button (#9803)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-19 11:13:29 -04:00
mamoodi
aea37e52f7 Update gitlab integration docs (#9025)
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-07-18 16:42:56 -04:00
Xingyao Wang
f5674d7c76 feat(agent): Add import placement guidance to CodeAct agent system prompts (#9794)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-18 19:59:09 +00:00
Vasi
9c68146b04 feat: [CLI] 9392 cli improve confirmation ux (#9758) 2025-07-18 19:42:17 +00:00
Boxuan Li
ee14f1ea41 Remove poetry dependency in Jupyter Plugin (#9789) 2025-07-18 18:54:53 +00:00
Xingyao Wang
b96301061d Bump version in pyproject.toml (#9790) 2025-07-18 14:48:37 -04:00
dependabot[bot]
1281f2d6c2 chore(deps): bump @vitejs/plugin-react from 4.6.0 to 4.7.0 in /frontend in the version-all group (#9785)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-18 21:45:40 +04:00
Hiep Le
dc41e0e90c feat(backend): Develop an API to fetch conversations by repository and conversation_trigger. (#9764) 2025-07-18 15:44:24 +00:00
Graham Neubig
793786130a Clarify GitHub integration docs regarding @openhands mentions in pull requests (#9314)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-18 10:58:39 -04:00
Hiep Le
59f03122c7 feat(frontend): Build Add Microagent Modal UI (#9735) 2025-07-18 18:28:13 +04:00
Hiep Le
67edc66da7 feat(backend): Support CreateMicroagent in the “Create New Conversation” API (#9765) 2025-07-18 01:31:09 -04:00
Graham Neubig
cb910e6863 Fix MCP tool timeout causing agent to stall indefinitely (#9779)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-17 18:53:09 -04:00
mamoodi
4c39e92351 Docs for OpenHands LLM Provider (#9751) 2025-07-17 18:51:34 +00:00
Engel Nyst
e65e0a98f0 Remove/reduce unused content in a CmdOutputObservation (#7404)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-17 19:34:46 +02:00
Hiep Le
eecc00fa4a feat(backend): API to get the microagents for the selected repository. (#9749) 2025-07-17 21:00:45 +04:00
sp.wack
5654e251a8 chore: bump to 1.0.0-beta.5 (#9770) 2025-07-17 16:44:01 +00:00
Rohit Malhotra
d9694aabcd Add conditional rendering of auth providers based on server config (#9752)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-17 16:42:57 +00:00
Ray Myers
bc8ef37192 fix - Avoid building debug log message when not logged (#9600) 2025-07-17 11:42:06 -05:00
Ray Myers
5f141f7712 Fix type hint: add | None to first element of create_default_mcp_server_config return tuple (#9754)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-17 12:10:16 -04:00
Hiep Le
30e3011cb0 feat(backend): Include owner_type in the Get Repositories API response. (#9763) 2025-07-17 11:45:05 -04:00
Xingyao Wang
3475d8021b Fix file duplication in system prompt (#9741)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-17 15:29:44 +00:00
dependabot[bot]
32cd50db2f chore(deps): bump the version-all group in /frontend with 6 updates (#9762)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-17 15:13:44 +00:00
Graham Neubig
f0a6db936c Fix: Add navigation to conversation page after clicking Launch button on task suggestions (#9760)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-17 17:43:13 +04:00
Peter Hamilton
11c37d8d70 Update llm constants to match on unpinned claude-sonnet-4 (#9681) 2025-07-17 13:48:35 +02:00
Hiep Le
7e1367057a feat(frontend): Build Microagent Management Sidebar UI. (#9717) 2025-07-17 15:45:24 +04:00
dependabot[bot]
3bbb0c6279 chore(deps): bump the version-all group in /frontend with 2 updates (#9739)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-17 11:40:08 +00:00
Xingyao Wang
eed71c21bd Add kimi-k2-0711-preview model to OpenHands provider (#9755)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-17 15:25:31 +04:00
Graham Neubig
4f46826de9 Add Moonshot AI Kimi-K2 model to recommended models (#9706)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-07-17 04:43:03 +00:00
juanmichelini
ea50fe4e3c Fix: Continue evaluation when an instance fails after max retries (#8868)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyaoww@gmail.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-07-16 22:42:44 +00:00
Tim O'Farrell
b057af8d63 Feat: Add current working directory to LLM instructions (#9718) 2025-07-16 21:10:03 +00:00
Engel Nyst
fba2218760 Fix integration tests (#9746) 2025-07-16 22:16:40 +02:00
mamoodi
6147cbdc18 Update OpenHands Cloud with Bitbucket docs (#9740) 2025-07-16 15:10:12 -04:00
Mislav Lukach
802acb3c7e feat(ui): select component (#9712)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-07-16 17:28:01 +00:00
Xingyao Wang
376dc21e34 (llm): Add Kimi K2 to function calling supported model (#9747) 2025-07-16 17:19:10 +00:00
Mislav Lukach
387318385c feat(ui): tab component (#9673)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-07-16 16:38:51 +00:00
Mislav Lukach
553f0a0918 feat(ui): toast component (#9632)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-07-16 16:33:31 +00:00
mamoodi
0d1e21ae45 Release 0.49.0 (#9691)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-07-16 08:46:41 -04:00
Xingyao Wang
a885e9e4d2 Fix newline display in frontend UI (#9729) 2025-07-15 20:59:56 -04:00
Graham Neubig
4c10848e8d Fix dictionary changed size during iteration error in override_provider_tokens_with_custom_secret (#9728)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-15 19:03:28 -04:00
Tim O'Farrell
1d95b01514 Fix: Keep the existing behavior in the docker command. (#9724) 2025-07-15 19:34:00 +00:00
Xingyao Wang
cd32b5508c Add OpenAI o3 model support to verified models and OpenHands provider (#9720)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-15 18:19:44 +00:00
Xingyao Wang
9a3bf0f2aa chore(cli): make sonnet first in openhands provider model choice (#9719) 2025-07-15 17:38:08 +00:00
Ryan H. Tran
1d04a83e08 docs: Add SHTTP transport documentation to MCP usage guide (#9701)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-15 23:18:05 +07:00
Hiep Le
17e9b0fd6a chore(Microagent Management UI): Set up the feature flag for the Microagent Management page. (#9704) 2025-07-15 19:49:35 +04:00
dependabot[bot]
54986c9841 chore(deps): bump the version-all group in /frontend with 3 updates (#9709)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-15 14:50:20 +00:00
Xingyao Wang
c419277326 Fix Likert Scale displaying "Star Rating" text instead of star icons (#9708)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-15 22:38:02 +08:00
Hiep Le
35b945b9d1 refactor(frontend): Display TOS and Privacy policy links on Sign In page (#9697)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-07-15 14:11:51 +00:00
Boxuan Li
5c3619bc48 Add README for terminal_bench evaluation harness (#9700) 2025-07-15 09:48:34 -04:00
Tim O'Farrell
641d0a0bcb Set vscode to use the correct workspace directory (#9698) 2025-07-14 17:40:32 -06:00
dependabot[bot]
fbadea9a6f chore(deps): bump the version-all group in /frontend with 3 updates (#9696)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 22:34:26 +04:00
Xingyao Wang
6e25d4bbb6 Add OpenHands provider for LLM through OH Cloud (#9526)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-15 01:44:49 +08:00
sp.wack
127220dc39 chore(ui): npm package config (#9535)
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-07-14 20:50:44 +04:00
Tim O'Farrell
9a291e385b Introduced config field to determine whether to init a git repo (#9693) 2025-07-14 10:17:26 -06:00
Tim O'Farrell
95ccec82d9 refactor: make /events endpoint lightweight without requiring active conversation (#9685)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-13 17:14:15 -06:00
Xingyao Wang
4aaa2ccd39 Add CLI alias setup for first-time users (#9542)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-13 15:36:06 +00:00
Tim O'Farrell
bfe0aa08b6 Fix issue where user gets stuck on TOS page (#9676) 2025-07-11 19:28:13 -06:00
Tim O'Farrell
7fb47761c6 Fix: VSCode using Temp Directory in Nested Mode (#9672) 2025-07-11 18:53:05 +00:00
Xuhui Zhou
415931b4dc Update system prompt for interactional system (#9284)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-11 18:07:56 +00:00
Hiep Le
6d57eeb3ed feat: Allow the users to edit the conversation's title. (#9648) 2025-07-11 21:46:51 +04:00
Hiep Le
c03d390772 fix(frontend): The conversation page cannot be used on mobile devices and tablets. (#9558) 2025-07-11 21:43:53 +04:00
dependabot[bot]
a266d4274a chore(deps): bump the version-all group in /frontend with 3 updates (#9669)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-11 21:20:21 +04:00
Engel Nyst
a19cd193d9 Log vscode error in a visible way (#9668)
Co-authored-by: OpenHands Bot <openhands@all-hands.dev>
2025-07-11 15:41:21 +00:00
Ivan Dagelic
4f3e648379 chore: update daytona sdk and proxy endpoint (#9664)
Signed-off-by: Ivan Dagelic <dagelic.ivan@gmail.com>
2025-07-11 17:33:12 +02:00
Tim O'Farrell
b99150c616 Fixes or vscode token / url not being present (#9661) 2025-07-11 09:31:39 -06:00
OpenHands
8937b3fbfc Fix issue #9655: [Bug]: CodeActAgent is incompatible with xAI Grok-4 due to hardcoded stop parameter (#9666)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-07-11 15:31:11 +00:00
juanmichelini
fb5a39a150 Fix libgl1 package for mswebench base images (#9071) 2025-07-11 10:30:33 -05:00
sp.wack
fc11c15b75 hotfix(ui): Agent message that includes codeblocks overflows (#9667) 2025-07-11 14:35:55 +00:00
Engel Nyst
50a8741d50 Build from vsix first (#9656)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-11 04:53:06 +02:00
xhguo7
9388fef0ef feat(eval): loc acc evaluation (#8515)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-07-11 03:22:35 +08:00
Tim O'Farrell
050e80cc34 Add warm server functionality to local runtime (#9033)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-10 12:12:39 -06:00
Ray Myers
5cc47ee592 Optimize dockerfile by consolidating and reordering steps (#9549) 2025-07-10 12:20:36 -05:00
dependabot[bot]
a09346672f chore(deps): bump the version-all group in /frontend with 3 updates (#9651)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-10 19:09:44 +04:00
ManOwnFire
9e72b69cf8 fix (cli): issue 9386 - show settings.json path in /settings (#9481) 2025-07-10 14:59:06 +00:00
sp.wack
da1f3a5a7b chore(frontend): Ugprade Node requirement to v22 LTS (#9639) 2025-07-10 17:21:03 +04:00
Hiep Le
5c27a452ac refactor(frontend): Make the API keys table styling consistent. (#9630) 2025-07-10 16:07:35 +04:00
Hiep Le
8cb1c738ff refactor(frontend): Make the secrets table styling consistent. (#9628) 2025-07-10 16:05:24 +04:00
Tim O'Farrell
cf276b2e96 All Runtime Status Codes should be in the RuntimeStatus enum (#9601)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-09 15:34:46 -06:00
sp.wack
1f416f616c chore(ui): Fix late redirects in settings page (#9596)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-09 17:26:54 +00:00
sp.wack
52775acd4d chore(eslint): Extend eslint rules to error on i18next/on no-literal-string (#9616)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-09 20:30:24 +04:00
Engel Nyst
be0596abd6 add log-level (#9637) 2025-07-09 11:19:10 -04:00
dependabot[bot]
e77957aa92 chore(deps): bump the version-all group in /frontend with 3 updates (#9635)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-09 18:57:28 +04:00
Eleanor Berger
d04c4c493e Update OpenAI model selection for better agentic coding support (#9597) 2025-07-09 14:44:02 +00:00
Mislav Lukach
5cb534217a feat(ui): spinner component (#9590) 2025-07-09 18:42:29 +04:00
Tim O'Farrell
9331f5e8a7 Fixes for docker nested runtime (#9634) 2025-07-09 08:39:42 -06:00
Hiep Le
8d16567428 refactor(frontend): The Jupyter tab is not showing "Waiting for runtime to start..." when connecting to an agent (#9626) 2025-07-09 18:33:09 +04:00
Xingyao Wang
acc69b74c5 docs: Add CLI installation options with shell aliases and local installation (#9575)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-09 03:42:24 +08:00
mamoodi
28d174a7ce Small documentation updates (#9622)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-07-08 15:33:22 -04:00
Xingyao Wang
cff5697456 eval: remove gemini-specific swebench template (#9623) 2025-07-08 18:34:23 +00:00
sp.wack
794eedf503 feat(frontend): Memory UI (#8592)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
Co-authored-by: tofarr <tofarr@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-07-08 16:24:07 +00:00
Hiep Le
a6ffb2f799 refactor(frontend): Remove the border bottom of the last element on the suggested tasks. (#9610) 2025-07-08 19:13:51 +04:00
Mislav Lukach
3be3779f68 feat(ui): dialog component (#9591) 2025-07-08 19:06:46 +04:00
sp.wack
222f5fdd51 chore: Update codeowners (#9619) 2025-07-08 15:01:00 +00:00
Mislav Lukach
2066f90654 feat(ui): accordion component (#9537) 2025-07-08 18:57:31 +04:00
dependabot[bot]
9ee2f976a1 chore(deps): bump vite from 7.0.2 to 7.0.3 in /frontend in the version-all group (#9618)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 14:54:36 +00:00
Hiep Le
be62df5277 fix(frontend): Jupyter tab requires page refresh to display content (#9614) 2025-07-08 18:30:58 +04:00
Hiep Le
4baf2a64c1 refactor(frontend): Show the git providers on the suggested tasks (#9608) 2025-07-08 18:25:09 +04:00
Hiep Le
2a833325e1 fix(frontend): The suggested tasks section only filters the tasks by the repository’s title. (#9606) 2025-07-08 18:24:30 +04:00
Hiep Le
aa2cacab44 fix(frontend): The terminal is still shown when connecting to an agent. (#9603) 2025-07-08 18:21:06 +04:00
tangwei12
ea07570f62 fix openhands cli loglevel (#9382)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-07-08 16:07:13 +02:00
Kenny Dizi
3f5a5005a2 Improve configuration for reasoning_effort (#9572) 2025-07-08 10:05:15 -04:00
mindflow-cn
7acee9e5da Allow workspace_mount_path to use relative paths (#9615)
Co-authored-by: jianchuanli <jianchuanli@langcode.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-07-08 21:47:28 +08:00
mamoodi
37cbeb735f Some documentation update (#9598)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-07-08 08:59:08 -04:00
Graham Neubig
c6c6c202f6 Fix CLI thought display order issue (#9417)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-07 23:33:57 +02:00
Tim O'Farrell
517a72fd0d Use the same event stream instance for conversations as sessions (#9545)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-07 14:37:17 -06:00
Xingyao Wang
7cfecb6e52 Increase success toast duration to 5 seconds with dynamic calculation (#9574)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-08 02:33:47 +08:00
Tim O'Farrell
8fe2e006ee Added run_in_loop method (#9586)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-07 11:01:17 -06:00
dependabot[bot]
6d62c341eb chore(deps): bump @heroui/react from 2.8.0-beta.11 to 2.8.0-beta.13 in /frontend in the version-all group (#9587)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 16:48:59 +00:00
Tim O'Farrell
229f35093d perf: make EventStore cur_id a lazy calculated property (#9544)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-07 09:58:46 -06:00
Rohit Malhotra
21a5e3eed5 Improve error logging in verify_repo_provider before AuthenticationError (#9530)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-07 10:02:46 -05:00
Hiep Le
97e3310dd5 fix(frontend): the GET microagents API is called multiple times, and the Available Microagents modal loads for an extended period if the conversation is connecting to an agent. (#9517) 2025-07-07 18:08:26 +04:00
Hiep Le
2053e72474 fix(frontend): Not able to scroll the chat input after pasting long content or clicking on a suggested action. (#9550) 2025-07-07 18:06:45 +04:00
dependabot[bot]
300f20368e chore(deps): bump the version-all group in /frontend with 4 updates (#9559)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 17:52:34 +04:00
Hiep Le
0bed046fcc fix(frontend): NaN is shown on the conversation card (#9581) 2025-07-07 17:51:56 +04:00
Hiep Le
0bf0dc9316 fix(frontend): [OpenHands Cloud] The Delete API Key modal does not display correctly if the API key name is too long. (#9556) 2025-07-07 17:50:11 +04:00
Hiep Le
0e8d9a8bb4 fix(frontend): [OpenHands Cloud] The API keys table does not display properly if the API key name is too long. (#9554) 2025-07-07 17:49:13 +04:00
Robert Brennan
9280bc34ad Enhance logging for org-level microagent loading to improve debugging (#9471)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-07 12:31:45 +02:00
Graham Neubig
b132348d22 Fix Jupyter tab not showing input commands (#9533)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-07 09:43:43 +02:00
TakumaNakao
1be77faf94 ADD gemini-2.5 to REASONING_EFFORT_SUPPORTED_MODELS (#9546) 2025-07-06 06:31:41 +00:00
Boxuan Li
a6301075ec Add core config to disable browser environment (#9570) 2025-07-06 08:20:58 +02:00
Boxuan Li
b98615bc1c Mark memory-profiler & jupyter_kernel_gateway dependency as non-optional (#9562) 2025-07-06 04:13:21 +02:00
Boxuan Li
29fdc701a3 Jupyter: remove poetry dependency (#9561) 2025-07-05 15:03:26 -07:00
Ryan H. Tran
8bc9207c24 Add instruction to use Arctic Inference (#9547) 2025-07-04 20:34:05 +07:00
Hiep Le
96008736a4 fix(frontend): [OpenHands Cloud] The input outline on the user setting page is not consistent. (#9552) 2025-07-04 13:23:03 +04:00
Robert Brennan
38d5db0547 Fix capitalization in 'Initializing agent...' status message (#9406)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-04 09:56:40 +02:00
Robert Brennan
8af1f1cac9 Add labels support to PR and MR creation tools (#9402)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-04 09:55:54 +02:00
Engel Nyst
ef502ccba8 Add CLI/vscode integration (#9085)
Co-authored-by: OpenHands-Gemini <openhands@all-hands.dev>
Co-authored-by: Claude 3.5 Sonnet <claude-3-5-sonnet@anthropic.com>
2025-07-03 22:42:06 +02:00
Tim O'Farrell
ece556c047 Fix issue where coro was not awaited (#9536) 2025-07-03 13:32:51 -06:00
Tim O'Farrell
55a09785ce Fix for issue where wrong method was called (#9532) 2025-07-03 11:46:48 -06:00
Mislav Lukach
2990c21d97 fix(ui): fix base components styling (#9528) 2025-07-03 21:21:18 +04:00
dependabot[bot]
14c8ea93c9 chore(deps): bump vite from 7.0.0 to 7.0.1 in /frontend in the version-all group (#9529)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 21:20:22 +04:00
Mislav Lukach
764077ef3d Feat/create UI dir (#9462)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-07-03 13:26:19 +00:00
Hiep Le
63ead2a638 fix(frontend): The "available microagents" modal does not show the latest agents after adding a new agent or updating the current agents (#9502) 2025-07-03 13:11:06 +00:00
Hiep Le
be0049c76e fix(frontend): Some strings are not included in the translation file. (#9524) 2025-07-03 12:55:13 +00:00
Hiep Le
bafd1596dd fix(frontend): The secret settings layout will be broken if the secret name is too long. (#9522) 2025-07-03 12:54:47 +00:00
Hiep Le
ce58ccab8a fix(frontend): Changing languages on the settings page does not work for some languages. (#9515) 2025-07-03 16:35:52 +04:00
sp.wack
b3c8b7c089 Fix WebSocket disconnection when uploading large files (#9504)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-03 16:28:30 +04:00
Engel Nyst
ac2947b7ff Fix /init on CLI Runtime (#9474)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-03 08:28:18 -04:00
mamoodi
91cd647f20 Add item to troubleshooting guide (#9490) 2025-07-02 16:31:26 -04:00
mamoodi
c521fb7a8f Release 0.48.0 (#9491) 2025-07-02 16:21:45 -04:00
Rohit Malhotra
f049411631 (Hotfix): Microagent won't load depending on version number format (#9508)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-02 18:06:36 +00:00
Tim O'Farrell
606ec59b33 Fix CLI confirmation input to handle invalid input properly (#9503)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-02 10:48:43 -06:00
Graham Neubig
d2fc5679ad Improve rate limit message to indicate automatic retry (#9281)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-02 12:27:35 -04:00
Hiep Le
7bfa05d38a refactor(frontend): Show branch name and git provider on the conversation cards (#9480)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-07-02 16:04:55 +00:00
dependabot[bot]
12a95fb548 chore(deps): bump the version-all group in /frontend with 7 updates (#9506)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 15:08:05 +00:00
llamantino
ae03c4eb80 chore: bump openhands-aci to 0.3.1 to fix ffmpeg warning (#9500) 2025-07-02 13:49:51 +00:00
mindflow-cn
8e486dfd6b Replace libtmux's deprecated methods in bash.py (#9463)
Co-authored-by: jianchuanli <jianchuanli@langcode.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-07-01 21:07:48 -04:00
Rohit Malhotra
48ee5659c9 Conditionally render 'Add GitHub repos' link based on provider (#9499)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-01 20:56:00 -04:00
Graham Neubig
b7613d7529 Fix feedback endpoint calls in OSS mode (#9476)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-01 16:31:05 -04:00
Graham Neubig
e05e627957 Add ArcticInference doc (#9492) 2025-07-01 14:15:13 -04:00
mamoodi
6da7e051be Make roadmap labels exempt from going stale (#9484) 2025-07-01 12:56:36 -04:00
dependabot[bot]
002e12a049 chore(deps): bump the version-all group in /frontend with 5 updates (#9486)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 20:09:02 +04:00
Graham Neubig
ed58858e03 Add setup.sh script execution to event stream (#9427)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-07-01 10:37:21 -04:00
Hiep Le
11ae4f96c2 fix(frontend): The "logout" action is still shown even if there is no associated account. (#9478) 2025-07-01 16:51:59 +04:00
Hiep Le
c2acf4e07e fix(frontend): Updated LLM settings are not applied to existing conversations. (#9460) 2025-06-30 16:52:59 +00:00
sp.wack
e9bdf761b7 hotfix(frontend): Fix action button cutoff (#9465) 2025-06-30 20:32:52 +04:00
Hiep Le
04b93069b4 feat(frontend): Stop conversation (#9458) 2025-06-30 20:31:37 +04:00
Hiep Le
ec03ce1ca0 feat(frontend): Tooltip for "suggested tasks" (#9447) 2025-06-30 14:46:39 +00:00
Hiep Le
46157a85d8 fix(frontend): Response issue - the content of the “Agent Tools & Metadata” modal is overflow. (#9449) 2025-06-30 14:44:04 +00:00
Hiep Le
a691e3148a fix(frontend): Responsive issue - the horizontal scrollbar is showing when resizing the browser window (#9446) 2025-06-30 18:40:17 +04:00
Hiep Le
4674e0b77a refactor(frontend): When users hover over the buttons, the pointer will not be displayed (#9442) 2025-06-30 13:54:29 +00:00
dependabot[bot]
d7d0329d25 chore(deps): bump node from 22.16.0-bookworm-slim to 24.2.0-bookworm-slim in /containers/app (#9040)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-30 17:40:14 +04:00
Graham Neubig
17853cd5bd Change default max_output_tokens to None and add comprehensive model tests (#9366)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-29 21:57:34 -04:00
Boxuan Li
c992b6d2a0 Fix CLI runtime not disabling jupyter plugin by default (#9452) 2025-06-29 17:04:16 -07:00
llamantino
34bf645d64 fix(cli): fix terminal input lag on Windows by start&stopping pause task (#9436) 2025-06-29 10:21:40 -07:00
Graham Neubig
1ae1c16b26 docs: Add repository support and missing options to headless mode documentation (#9311)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-29 01:05:52 +00:00
Boxuan Li
5099413729 Complete browsing unit tests (#9428) 2025-06-28 09:52:52 -07:00
AY
b06a3bdb7c Fixes #9394 - Improve CLI exit messaging to distinguish intentional exits and inter… (#9432) 2025-06-28 18:51:25 +02:00
Xingyao Wang
a7b234d1f6 feat(agent): Add TODO.md workflow for long-horizon tasks into a separate system prompt (#8896)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-28 09:08:13 -04: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
630 changed files with 53062 additions and 8459 deletions

1
.github/CODEOWNERS vendored
View File

@@ -3,6 +3,7 @@
# Frontend code owners
/frontend/ @rbren @amanape
/openhands-ui/ @amanape
# Evaluation code owners
/evaluation/ @xingyaoww @neubig

View File

@@ -9,8 +9,8 @@ on:
- main
pull_request:
paths:
- 'frontend/**'
- '.github/workflows/fe-unit-tests.yml'
- "frontend/**"
- ".github/workflows/fe-unit-tests.yml"
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
concurrency:
@@ -24,7 +24,7 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2204
strategy:
matrix:
node-version: [20, 22]
node-version: [22]
fail-fast: true
steps:
- name: Checkout
@@ -38,7 +38,7 @@ jobs:
run: npm ci
- name: Run TypeScript compilation
working-directory: ./frontend
run: npm run make-i18n && tsc
run: npm run build
- name: Run tests and collect coverage
working-directory: ./frontend
run: npm run test:coverage

View File

@@ -42,7 +42,6 @@ jobs:
json=$(jq -n -c '[
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
{ image: "ubuntu:24.04", tag: "ubuntu" }
]')
else
json=$(jq -n -c '[
@@ -56,12 +55,10 @@ jobs:
ghcr_build_app:
name: Build App Image
runs-on: blacksmith-4vcpu-ubuntu-2204
if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ext-v'))"
permissions:
contents: read
packages: write
outputs:
# Since this job uses outputs it cannot use matrix
hash_from_app_image: ${{ steps.get_hash_in_app_image.outputs.hash_from_app_image }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -87,24 +84,12 @@ jobs:
if: "!github.event.pull_request.head.repo.fork"
run: |
./containers/build.sh -i openhands -o ${{ env.REPO_OWNER }} --push
- name: Build app image
if: "github.event.pull_request.head.repo.fork"
run: |
./containers/build.sh -i openhands -o ${{ env.REPO_OWNER }} --load
- name: Get hash in App Image
id: get_hash_in_app_image
run: |
# Run the build script in the app image
docker run -e SANDBOX_USER_ID=0 -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/${{ env.REPO_OWNER }}/openhands:${{ env.RELEVANT_SHA }} /bin/bash -c "mkdir -p containers/runtime; python3 openhands/runtime/utils/runtime_build.py --base_image ${{ env.BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST }} --build_folder containers/runtime --force_rebuild" 2>&1 | tee docker-outputs.txt
# Get the hash from the build script
hash_from_app_image=$(cat docker-outputs.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1)
echo "hash_from_app_image=$hash_from_app_image" >> $GITHUB_OUTPUT
echo "Hash from app image: $hash_from_app_image"
# Builds the runtime Docker images
ghcr_build_runtime:
name: Build Image
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: blacksmith-8vcpu-ubuntu-2204
if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ext-v'))"
permissions:
contents: read
packages: write
@@ -130,22 +115,13 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
- name: Cache Poetry dependencies
uses: useblacksmith/cache@v5
with:
path: |
~/.cache/pypoetry
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
# This is the one that saves the cache, the others set 'lookup-only: true'
restore-keys: |
${{ runner.os }}-poetry-
- name: Install poetry via pipx
run: pipx install poetry
cache: poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies POETRY_GROUP=main INSTALL_PLAYWRIGHT=0
- name: Create source distribution and Dockerfile
@@ -190,61 +166,6 @@ jobs:
name: runtime-src-${{ matrix.base_image.tag }}
path: containers/runtime
verify_hash_equivalence_in_runtime_and_app:
name: Verify Hash Equivalence in Runtime and Docker images
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [ghcr_build_runtime, ghcr_build_app]
strategy:
fail-fast: false
matrix:
base_image: ['nikolaik']
env:
BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST: nikolaik/python-nodejs:python3.12-nodejs22
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Cache Poetry dependencies
uses: useblacksmith/cache@v5
with:
path: |
~/.cache/pypoetry
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
lookup-only: true
restore-keys: |
${{ runner.os }}-poetry-
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies POETRY_GROUP=main INSTALL_PLAYWRIGHT=0
- name: Get hash in App Image
run: |
echo "Hash from app image: ${{ needs.ghcr_build_app.outputs.hash_from_app_image }}"
echo "hash_from_app_image=${{ needs.ghcr_build_app.outputs.hash_from_app_image }}" >> $GITHUB_ENV
- name: Get hash using code (development mode)
run: |
mkdir -p containers/runtime
poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ env.BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST }} --build_folder containers/runtime --force_rebuild > output.txt 2>&1
hash_from_code=$(cat output.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1)
echo "hash_from_code=$hash_from_code" >> $GITHUB_ENV
- name: Compare hashes
run: |
echo "Hash from App Image: ${{ env.hash_from_app_image }}"
echo "Hash from Code: ${{ env.hash_from_code }}"
if [ "${{ env.hash_from_app_image }}" = "${{ env.hash_from_code }}" ]; then
echo "Hashes match!"
else
echo "Hashes do not match!"
exit 1
fi
# Run unit tests with the Docker runtime Docker images as root
test_runtime_root:
name: RT Unit Tests (Root)
@@ -276,25 +197,17 @@ jobs:
load: true
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
context: containers/runtime
- name: Cache Poetry dependencies
uses: useblacksmith/cache@v5
with:
path: |
~/.cache/pypoetry
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
lookup-only: true
restore-keys: |
${{ runner.os }}-poetry-
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
- name: Install poetry via pipx
run: pipx install poetry
cache: poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies INSTALL_PLAYWRIGHT=0
- name: Run docker runtime tests
shell: bash
run: |
# We install pytest-xdist in order to run tests across CPUs
poetry run pip install pytest-xdist
@@ -346,25 +259,17 @@ jobs:
load: true
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
context: containers/runtime
- name: Cache Poetry dependencies
uses: useblacksmith/cache@v5
with:
path: |
~/.cache/pypoetry
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
lookup-only: true
restore-keys: |
${{ runner.os }}-poetry-
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
- name: Install poetry via pipx
run: pipx install poetry
cache: poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies POETRY_GROUP=main,test,runtime INSTALL_PLAYWRIGHT=0
- name: Run runtime tests
shell: bash
run: |
# We install pytest-xdist in order to run tests across CPUs
poetry run pip install pytest-xdist
@@ -391,7 +296,7 @@ jobs:
name: All Runtime Tests Passed
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
needs: [test_runtime_root, test_runtime_oh]
steps:
- name: All tests passed
run: echo "All runtime tests have passed successfully!"
@@ -400,7 +305,7 @@ jobs:
name: All Runtime Tests Passed
if: ${{ cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
needs: [test_runtime_root, test_runtime_oh]
steps:
- name: Some tests failed
run: |
@@ -425,6 +330,7 @@ jobs:
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
SHORT_SHA: ${{ steps.short_sha.outputs.SHORT_SHA }}
shell: bash
run: |
echo "updating PR description"
DOCKER_RUN_COMMAND="docker run -it --rm \

View File

@@ -54,7 +54,7 @@ jobs:
Hi! I started running the integration tests on your PR. You will receive a comment with the results shortly.
- name: Install Python dependencies using Poetry
run: poetry install --with dev,test,runtime
run: poetry install --with dev,test,runtime,evaluation
- name: Configure config.toml for testing with Haiku
env:
@@ -179,8 +179,8 @@ jobs:
id: create_comment
uses: KeisukeYamashita/create-comment@v1
with:
# if triggered by PR, use PR number, otherwise use 5318 as fallback issue number for manual triggers
number: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || 5318 }}
# if triggered by PR, use PR number, otherwise use 9745 as fallback issue number for manual triggers
number: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || 9745 }}
unique: false
comment: |
Trigger by: ${{ github.event_name == 'pull_request' && format('Pull Request (integration-test label on PR #{0})', github.event.pull_request.number) || (github.event_name == 'workflow_dispatch' && format('Manual Trigger: {0}', github.event.inputs.reason)) || 'Nightly Scheduled Run' }}

View File

@@ -21,10 +21,10 @@ jobs:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Node.js 20
- name: Install Node.js 22
uses: useblacksmith/setup-node@v5
with:
node-version: 20
node-version: 22
- name: Install frontend dependencies
run: |
cd frontend
@@ -68,7 +68,7 @@ jobs:
uses: useblacksmith/setup-python@v6
with:
python-version: 3.12
cache: 'pip'
cache: "pip"
- name: Install pre-commit
run: pip install pre-commit==3.7.0
- name: Fix python lint issues

View File

@@ -7,7 +7,7 @@ name: Lint
on:
push:
branches:
- main
- main
pull_request:
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
@@ -22,10 +22,10 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- name: Install Node.js 20
- name: Install Node.js 22
uses: useblacksmith/setup-node@v5
with:
node-version: 20
node-version: 22
- name: Install dependencies
run: |
cd frontend
@@ -49,7 +49,7 @@ jobs:
uses: useblacksmith/setup-python@v6
with:
python-version: 3.12
cache: 'pip'
cache: "pip"
- name: Install pre-commit
run: pip install pre-commit==3.7.0
- name: Run pre-commit hooks

108
.github/workflows/npm-publish-ui.yml vendored Normal file
View File

@@ -0,0 +1,108 @@
name: Publish OpenHands UI Package
# * Always run on "main"
# * Run on PRs that have changes in the "openhands-ui" folder or this workflow
on:
push:
branches:
- main
paths:
- "openhands-ui/**"
- ".github/workflows/npm-publish-ui.yml"
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
concurrency:
group: npm-publish-ui
cancel-in-progress: false
jobs:
check-version:
name: Check if version has changed
runs-on: blacksmith-4vcpu-ubuntu-2204
defaults:
run:
shell: bash
outputs:
should-publish: ${{ steps.version-check.outputs.should-publish }}
current-version: ${{ steps.version-check.outputs.current-version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2 # Need previous commit to compare
- name: Check if version changed
id: version-check
run: |
# Get current version from package.json
CURRENT_VERSION=$(jq -r .version openhands-ui/package.json)
echo "current-version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
# Check if package.json version changed in this commit
if git diff HEAD~1 HEAD --name-only | grep -q "openhands-ui/package.json"; then
# Check if the version field specifically changed
if git diff HEAD~1 HEAD openhands-ui/package.json | grep -q '"version"'; then
echo "Version changed in package.json, will publish"
echo "should-publish=true" >> $GITHUB_OUTPUT
else
echo "package.json changed but version did not change, skipping publish"
echo "should-publish=false" >> $GITHUB_OUTPUT
fi
else
echo "package.json did not change, skipping publish"
echo "should-publish=false" >> $GITHUB_OUTPUT
fi
publish:
name: Publish to npm
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: check-version
if: needs.check-version.outputs.should-publish == 'true'
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: "openhands-ui/.bun-version"
- name: Install dependencies
working-directory: ./openhands-ui
run: bun install --frozen-lockfile
- name: Build package
working-directory: ./openhands-ui
run: bun run build
- name: Check if package already exists on npm
id: npm-check
working-directory: ./openhands-ui
run: |
PACKAGE_NAME=$(jq -r .name package.json)
VERSION="${{ needs.check-version.outputs.current-version }}"
# Check if this version already exists on npm
if npm view "$PACKAGE_NAME@$VERSION" version 2>/dev/null; then
echo "Version $VERSION already exists on npm, skipping publish"
echo "already-exists=true" >> $GITHUB_OUTPUT
else
echo "Version $VERSION does not exist on npm, proceeding with publish"
echo "already-exists=false" >> $GITHUB_OUTPUT
fi
- name: Setup npm authentication
if: steps.npm-check.outputs.already-exists == 'false'
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
- name: Publish to npm
if: steps.npm-check.outputs.already-exists == 'false'
working-directory: ./openhands-ui
run: |
# The prepublishOnly script will run automatically and build the package
npm publish
echo "✅ Successfully published @openhands/ui@${{ needs.check-version.outputs.current-version }} to npm"

View File

@@ -1,5 +1,5 @@
# Workflow that runs python unit tests
name: Run Python Unit Tests
# Workflow that runs python tests
name: Run Python Tests
# The jobs in this workflow are required, so they must run at all times
# * Always run on "main"
@@ -16,9 +16,9 @@ concurrency:
cancel-in-progress: true
jobs:
# Run python unit tests on Linux
# Run python tests on Linux
test-on-linux:
name: Python Unit Tests on Linux
name: Python Tests on Linux
runs-on: blacksmith-4vcpu-ubuntu-2204
env:
INSTALL_DOCKER: '0' # Set to '0' to skip Docker installation
@@ -51,6 +51,8 @@ jobs:
run: poetry run pytest --forked -n auto -svv ./tests/unit
- name: Run Runtime Tests with CLIRuntime
run: TEST_RUNTIME=cli poetry run pytest -svv tests/runtime/test_bash.py
- name: Run E2E Tests
run: poetry run pytest -svv tests/e2e
# Run specific Windows python tests
test-on-windows:

View File

@@ -1,56 +1,135 @@
# Run evaluation on a PR
# Run evaluation on a PR, after releases, or manually
name: Run Eval
# Runs when a PR is labeled with one of the "run-eval-" labels
# Runs when a PR is labeled with one of the "run-eval-" labels, after releases, or manually triggered
on:
pull_request:
types: [labeled]
release:
types: [published]
workflow_dispatch:
inputs:
branch:
description: 'Branch to evaluate'
required: true
default: 'main'
eval_instances:
description: 'Number of evaluation instances'
required: true
default: '50'
type: choice
options:
- '1'
- '2'
- '50'
- '100'
reason:
description: 'Reason for manual trigger'
required: false
default: ''
env:
# Environment variable for the master GitHub issue number where all evaluation results will be commented
# This should be set to the issue number where you want all evaluation results to be posted
MASTER_EVAL_ISSUE_NUMBER: ${{ vars.MASTER_EVAL_ISSUE_NUMBER || '0' }}
jobs:
trigger-job:
name: Trigger remote eval job
if: ${{ github.event.label.name == 'run-eval-1' || github.event.label.name == 'run-eval-2' || github.event.label.name == 'run-eval-50' || github.event.label.name == 'run-eval-100' }}
if: ${{ (github.event_name == 'pull_request' && (github.event.label.name == 'run-eval-1' || github.event.label.name == 'run-eval-2' || github.event.label.name == 'run-eval-50' || github.event.label.name == 'run-eval-100')) || github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout PR branch
- name: Checkout branch
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
ref: ${{ github.event_name == 'pull_request' && github.head_ref || (github.event_name == 'workflow_dispatch' && github.event.inputs.branch) || github.ref }}
- name: Trigger remote job
env:
PR_BRANCH: ${{ github.head_ref }}
- name: Set evaluation parameters
id: eval_params
run: |
REPO_URL="https://github.com/${{ github.repository }}"
echo "Repository URL: $REPO_URL"
echo "PR Branch: $PR_BRANCH"
if [[ "${{ github.event.label.name }}" == "run-eval-1" ]]; then
EVAL_INSTANCES="1"
elif [[ "${{ github.event.label.name }}" == "run-eval-2" ]]; then
EVAL_INSTANCES="2"
elif [[ "${{ github.event.label.name }}" == "run-eval-50" ]]; then
# Determine branch based on trigger type
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
EVAL_BRANCH="${{ github.head_ref }}"
echo "PR Branch: $EVAL_BRANCH"
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
EVAL_BRANCH="${{ github.event.inputs.branch }}"
echo "Manual Branch: $EVAL_BRANCH"
else
# For release events, use the tag name or main branch
EVAL_BRANCH="${{ github.ref_name }}"
echo "Release Branch/Tag: $EVAL_BRANCH"
fi
# Determine evaluation instances based on trigger type
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
if [[ "${{ github.event.label.name }}" == "run-eval-1" ]]; then
EVAL_INSTANCES="1"
elif [[ "${{ github.event.label.name }}" == "run-eval-2" ]]; then
EVAL_INSTANCES="2"
elif [[ "${{ github.event.label.name }}" == "run-eval-50" ]]; then
EVAL_INSTANCES="50"
elif [[ "${{ github.event.label.name }}" == "run-eval-100" ]]; then
EVAL_INSTANCES="100"
fi
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
EVAL_INSTANCES="${{ github.event.inputs.eval_instances }}"
else
# For release events, default to 50 instances
EVAL_INSTANCES="50"
elif [[ "${{ github.event.label.name }}" == "run-eval-100" ]]; then
EVAL_INSTANCES="100"
fi
echo "Evaluation instances: $EVAL_INSTANCES"
echo "repo_url=$REPO_URL" >> $GITHUB_OUTPUT
echo "eval_branch=$EVAL_BRANCH" >> $GITHUB_OUTPUT
echo "eval_instances=$EVAL_INSTANCES" >> $GITHUB_OUTPUT
- name: Trigger remote job
run: |
# Determine PR number for the remote evaluation system
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
PR_NUMBER="${{ github.event.pull_request.number }}"
else
# For non-PR triggers, use the master issue number as PR number
PR_NUMBER="${{ env.MASTER_EVAL_ISSUE_NUMBER }}"
fi
curl -X POST \
-H "Authorization: Bearer ${{ secrets.PAT_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
-d "{\"ref\": \"main\", \"inputs\": {\"github-repo\": \"${REPO_URL}\", \"github-branch\": \"${PR_BRANCH}\", \"pr-number\": \"${{ github.event.pull_request.number }}\", \"eval-instances\": \"${EVAL_INSTANCES}\"}}" \
-d "{\"ref\": \"main\", \"inputs\": {\"github-repo\": \"${{ steps.eval_params.outputs.repo_url }}\", \"github-branch\": \"${{ steps.eval_params.outputs.eval_branch }}\", \"pr-number\": \"${PR_NUMBER}\", \"eval-instances\": \"${{ steps.eval_params.outputs.eval_instances }}\"}}" \
https://api.github.com/repos/All-Hands-AI/evaluation/actions/workflows/create-branch.yml/dispatches
# Send Slack message
PR_URL="https://github.com/${{ github.repository }}/pull/${{ github.event.pull_request.number }}"
slack_text="PR $PR_URL has triggered evaluation on $EVAL_INSTANCES instances..."
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
TRIGGER_URL="https://github.com/${{ github.repository }}/pull/${{ github.event.pull_request.number }}"
slack_text="PR $TRIGGER_URL has triggered evaluation on ${{ steps.eval_params.outputs.eval_instances }} instances..."
elif [[ "${{ github.event_name }}" == "release" ]]; then
TRIGGER_URL="https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }}"
slack_text="Release $TRIGGER_URL has triggered evaluation on ${{ steps.eval_params.outputs.eval_instances }} instances..."
else
TRIGGER_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
slack_text="Manual trigger (${{ github.event.inputs.reason || 'No reason provided' }}) has triggered evaluation on ${{ steps.eval_params.outputs.eval_instances }} instances for branch ${{ steps.eval_params.outputs.eval_branch }}..."
fi
curl -X POST -H 'Content-type: application/json' --data '{"text":"'"$slack_text"'"}' \
https://hooks.slack.com/services/${{ secrets.SLACK_TOKEN }}
- name: Comment on PR
- name: Comment on issue/PR
uses: KeisukeYamashita/create-comment@v1
with:
# For PR triggers, comment on the PR. For other triggers, comment on the master issue
number: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || env.MASTER_EVAL_ISSUE_NUMBER }}
unique: false
comment: |
Running evaluation on the PR. Once eval is done, the results will be posted.
**Evaluation Triggered**
**Trigger:** ${{ github.event_name == 'pull_request' && format('Pull Request #{0}', github.event.pull_request.number) || (github.event_name == 'release' && 'Release') || format('Manual Trigger: {0}', github.event.inputs.reason || 'No reason provided') }}
**Branch:** ${{ steps.eval_params.outputs.eval_branch }}
**Instances:** ${{ steps.eval_params.outputs.eval_instances }}
**Commit:** ${{ github.sha }}
Running evaluation on the specified branch. Once eval is done, the results will be posted here.

View File

@@ -15,7 +15,7 @@ jobs:
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
days-before-stale: 30
exempt-issue-labels: 'tracked'
exempt-issue-labels: 'roadmap'
close-issue-message: 'This issue was closed because it has been stalled for over 30 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for over 30 days with no activity.'
days-before-close: 7

34
.github/workflows/ui-build.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: Run UI Component Build
# * Always run on "main"
# * Run on PRs that have changes in the "openhands-ui" folder or this workflow
on:
push:
branches:
- main
pull_request:
paths:
- 'openhands-ui/**'
- '.github/workflows/ui-build.yml'
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
ui-build:
name: Build openhands-ui
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version-file: "openhands-ui/.bun-version"
- name: Install dependencies
working-directory: ./openhands-ui
run: bun install --frozen-lockfile
- name: Build package
working-directory: ./openhands-ui
run: bun run build

View File

@@ -0,0 +1,156 @@
# Workflow that validates the VSCode extension builds correctly
name: VSCode Extension CI
# * Always run on "main"
# * Run on PRs that have changes in the VSCode extension folder or this workflow
# * Run on tags that start with "ext-v"
on:
push:
branches:
- main
tags:
- 'ext-v*'
pull_request:
paths:
- 'openhands/integrations/vscode/**'
- 'build_vscode.py'
- '.github/workflows/vscode-extension-build.yml'
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
# Validate VSCode extension builds correctly
validate-vscode-extension:
name: Validate VSCode Extension Build
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: useblacksmith/setup-node@v5
with:
node-version: '22'
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install VSCode extension dependencies
working-directory: ./openhands/integrations/vscode
run: npm ci
- name: Build VSCode extension via build_vscode.py
run: python build_vscode.py
env:
# Ensure we don't skip the build
SKIP_VSCODE_BUILD: ""
- name: Validate .vsix file
run: |
# Verify the .vsix was created and is valid
if [ -f "openhands/integrations/vscode/openhands-vscode-0.0.1.vsix" ]; then
echo "✅ VSCode extension built successfully"
ls -la openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
# Basic validation that the .vsix is a valid zip file
echo "🔍 Validating .vsix structure..."
file openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
unzip -t openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
echo "✅ VSCode extension validation passed"
else
echo "❌ VSCode extension build failed - .vsix not found"
exit 1
fi
- name: Upload VSCode extension artifact
uses: actions/upload-artifact@v4
with:
name: vscode-extension
path: openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
retention-days: 7
- name: Comment on PR with artifact link
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
// Get file size for display
const vsixPath = 'openhands/integrations/vscode/openhands-vscode-0.0.1.vsix';
const stats = fs.statSync(vsixPath);
const fileSizeKB = Math.round(stats.size / 1024);
const comment = `## 🔧 VSCode Extension Built Successfully!
The VSCode extension has been built and is ready for testing.
**📦 Download**: [openhands-vscode-0.0.1.vsix](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (${fileSizeKB} KB)
**🚀 To install**:
1. Download the artifact from the workflow run above
2. In VSCode: \`Ctrl+Shift+P\` → "Extensions: Install from VSIX..."
3. Select the downloaded \`.vsix\` file
**✅ Tested with**: Node.js 22
**🔍 Validation**: File structure and integrity verified
---
*Built from commit ${{ github.sha }}*`;
// Check if we already commented on this PR and delete it
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.login === 'github-actions[bot]' &&
comment.body.includes('VSCode Extension Built Successfully')
);
if (botComment) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
});
}
// Create a new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
release:
name: Create GitHub Release
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: validate-vscode-extension
if: startsWith(github.ref, 'refs/tags/ext-v')
steps:
- name: Download .vsix artifact
uses: actions/download-artifact@v4
with:
name: vscode-extension
path: ./
- name: Create Release
uses: ncipollo/release-action@v1.16.0
with:
artifacts: "*.vsix"
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
allowUpdates: true

2
.gitignore vendored
View File

@@ -182,6 +182,8 @@ cython_debug/
.roo/rules
.cline/rules
.windsurf/rules
.repomix
repomix-output.txt
# evaluation
evaluation/evaluation_outputs

View File

@@ -19,6 +19,7 @@ Before pushing any changes, you MUST ensure that any lint errors or simple test
* If you've made changes to the backend, you should run `pre-commit run --config ./dev_config/python/.pre-commit-config.yaml` (this will run on staged files).
* If you've made changes to the frontend, you should run `cd frontend && npm run lint:fix && npm run build ; cd ..`
* If you've made changes to the VSCode extension, you should run `cd openhands/integrations/vscode && npm run lint:fix && npm run compile ; cd ../../..`
The pre-commit hooks MUST pass successfully before pushing any changes to the repository. This is a mandatory requirement to maintain code quality and consistency.
@@ -29,6 +30,12 @@ then re-run the command to ensure it passes. Common issues include:
- Trailing whitespace
- Missing newlines at end of files
## Git Best Practices
- Prefer specific `git add <filename>` instead of `git add .` to avoid accidentally staging unintended files
- Be especially careful with `git reset --hard` after staging files, as it will remove accidentally staged files
- When remote has new changes, use `git fetch upstream && git rebase upstream/<branch>` on the same branch
## Repository Structure
Backend:
- Located in the `openhands` directory
@@ -60,6 +67,22 @@ Frontend:
- Mutation hooks should follow the pattern use[Action] (e.g., `useDeleteConversation`)
- Architecture rule: UI components → TanStack Query hooks → Data Access Layer (`frontend/src/api`) → API endpoints
VSCode Extension:
- Located in the `openhands/integrations/vscode` directory
- Setup: Run `npm install` in the extension directory
- Linting:
- Run linting with fixes: `npm run lint:fix`
- Check only: `npm run lint`
- Type checking: `npm run typecheck`
- Building:
- Compile TypeScript: `npm run compile`
- Package extension: `npm run package-vsix`
- Testing:
- Run tests: `npm run test`
- Development Best Practices:
- Use `vscode.window.createOutputChannel()` for debug logging instead of `showErrorMessage()` popups
- Pre-commit process runs both frontend and backend checks when committing extension changes
## Template for Github Pull Request
If you are starting a pull request (PR), please follow the template in `.github/pull_request_template.md`.
@@ -118,3 +141,65 @@ Your specialized knowledge and instructions here...
2. Add the setting to the backend:
- Add the setting to the `Settings` model in `openhands/storage/data_models/settings.py`
- Update any relevant backend code to apply the setting (e.g., in session creation)
### Adding New LLM Models
To add a new LLM model to OpenHands, you need to update multiple files across both frontend and backend:
#### Model Configuration Procedure:
1. **Frontend Model Arrays** (`frontend/src/utils/verified-models.ts`):
- Add the model to `VERIFIED_MODELS` array (main list of all verified models)
- Add to provider-specific arrays based on the model's provider:
- `VERIFIED_OPENAI_MODELS` for OpenAI models
- `VERIFIED_ANTHROPIC_MODELS` for Anthropic models
- `VERIFIED_MISTRAL_MODELS` for Mistral models
- `VERIFIED_OPENHANDS_MODELS` for models available through OpenHands provider
2. **Backend CLI Integration** (`openhands/cli/utils.py`):
- Add the model to the appropriate `VERIFIED_*_MODELS` arrays
- This ensures the model appears in CLI model selection
3. **Backend Model List** (`openhands/utils/llm.py`):
- **CRITICAL**: Add the model to the `openhands_models` list (lines 57-66) if using OpenHands provider
- This is required for the model to appear in the frontend model selector
- Format: `'openhands/model-name'` (e.g., `'openhands/o3'`)
4. **Backend LLM Configuration** (`openhands/llm/llm.py`):
- Add to feature-specific arrays based on model capabilities:
- `FUNCTION_CALLING_SUPPORTED_MODELS` if the model supports function calling
- `REASONING_EFFORT_SUPPORTED_MODELS` if the model supports reasoning effort parameters
- `CACHE_PROMPT_SUPPORTED_MODELS` if the model supports prompt caching
- `MODELS_WITHOUT_STOP_WORDS` if the model doesn't support stop words
5. **Validation**:
- Run backend linting: `pre-commit run --config ./dev_config/python/.pre-commit-config.yaml`
- Run frontend linting: `cd frontend && npm run lint:fix`
- Run frontend build: `cd frontend && npm run build`
#### Model Verification Arrays:
- **VERIFIED_MODELS**: Main array of all verified models shown in the UI
- **VERIFIED_OPENAI_MODELS**: OpenAI models (LiteLLM doesn't return provider prefix)
- **VERIFIED_ANTHROPIC_MODELS**: Anthropic models (LiteLLM doesn't return provider prefix)
- **VERIFIED_MISTRAL_MODELS**: Mistral models (LiteLLM doesn't return provider prefix)
- **VERIFIED_OPENHANDS_MODELS**: Models available through OpenHands managed provider
#### Model Feature Support Arrays:
- **FUNCTION_CALLING_SUPPORTED_MODELS**: Models that support structured function calling
- **REASONING_EFFORT_SUPPORTED_MODELS**: Models that support reasoning effort parameters (like o1, o3)
- **CACHE_PROMPT_SUPPORTED_MODELS**: Models that support prompt caching for efficiency
- **MODELS_WITHOUT_STOP_WORDS**: Models that don't support stop word parameters
#### Frontend Model Integration:
- Models are automatically available in the model selector UI once added to verified arrays
- The `extractModelAndProvider` utility automatically detects provider from model arrays
- Provider-specific models are grouped and prioritized in the UI selection
#### CLI Model Integration:
- Models appear in CLI provider selection based on the verified arrays
- The `organize_models_and_providers` function groups models by provider
- Default model selection prioritizes verified models for each provider

View File

@@ -1,56 +1,158 @@
#!/bin/bash
echo "Running OpenHands pre-commit hook..."
echo "This hook runs selective linting based on changed files."
# Store the exit code to return at the end
# This allows us to be additive to existing pre-commit hooks
EXIT_CODE=0
# Check if frontend directory has changed
frontend_changes=$(git diff --cached --name-only | grep "^frontend/")
if [ -n "$frontend_changes" ]; then
echo "Frontend changes detected. Running frontend checks..."
# Get the list of staged files
STAGED_FILES=$(git diff --cached --name-only)
# Check if frontend directory exists
if [ -d "frontend" ]; then
# Change to frontend directory
cd frontend || exit 1
# Check if any files match specific patterns
has_frontend_changes=false
has_backend_changes=false
has_vscode_changes=false
# Run lint:fix
echo "Running npm lint:fix..."
npm run lint:fix
# Check each file individually to avoid issues with grep
for file in $STAGED_FILES; do
if [[ $file == frontend/* ]]; then
has_frontend_changes=true
elif [[ $file == openhands/* || $file == evaluation/* || $file == tests/* ]]; then
has_backend_changes=true
# Check for VSCode extension changes (subset of backend changes)
if [[ $file == openhands/integrations/vscode/* ]]; then
has_vscode_changes=true
fi
fi
done
echo "Analyzing changes..."
echo "- Frontend changes: $has_frontend_changes"
echo "- Backend changes: $has_backend_changes"
echo "- VSCode extension changes: $has_vscode_changes"
# Run frontend linting if needed
if [ "$has_frontend_changes" = true ]; then
# Check if we're in a CI environment or if frontend dependencies are missing
if [ -n "$CI" ] || ! command -v react-router &> /dev/null || ! command -v vitest &> /dev/null; then
echo "Skipping frontend checks (CI environment or missing dependencies detected)."
echo "WARNING: Frontend files have changed but frontend checks are being skipped."
echo "Please run 'make lint-frontend' manually before submitting your PR."
else
echo "Running frontend linting..."
make lint-frontend
if [ $? -ne 0 ]; then
echo "Frontend linting failed. Please fix the issues before committing."
EXIT_CODE=1
else
echo "Frontend linting checks passed!"
fi
# Run build
echo "Running npm build..."
npm run build
if [ $? -ne 0 ]; then
echo "Frontend build failed. Please fix the issues before committing."
EXIT_CODE=1
fi
# Run additional frontend checks
if [ -d "frontend" ]; then
echo "Running additional frontend checks..."
cd frontend || exit 1
# Run tests
echo "Running npm test..."
npm test
if [ $? -ne 0 ]; then
echo "Frontend tests failed. Please fix the failing tests before committing."
EXIT_CODE=1
fi
# Run build
echo "Running npm build..."
npm run build
if [ $? -ne 0 ]; then
echo "Frontend build failed. Please fix the issues before committing."
EXIT_CODE=1
fi
# Return to the original directory
cd ..
# Run tests
echo "Running npm test..."
npm test
if [ $? -ne 0 ]; then
echo "Frontend tests failed. Please fix the failing tests before committing."
EXIT_CODE=1
fi
if [ $EXIT_CODE -eq 0 ]; then
echo "Frontend checks passed!"
cd ..
fi
else
echo "Frontend directory not found. Skipping frontend checks."
fi
else
echo "No frontend changes detected. Skipping frontend checks."
echo "Skipping frontend checks (no frontend changes detected)."
fi
# Run backend linting if needed
if [ "$has_backend_changes" = true ]; then
echo "Running backend linting..."
make lint-backend
if [ $? -ne 0 ]; then
echo "Backend linting failed. Please fix the issues before committing."
EXIT_CODE=1
else
echo "Backend linting checks passed!"
fi
else
echo "Skipping backend checks (no backend changes detected)."
fi
# Run VSCode extension checks if needed
if [ "$has_vscode_changes" = true ]; then
# Check if we're in a CI environment
if [ -n "$CI" ]; then
echo "Skipping VSCode extension checks (CI environment detected)."
echo "WARNING: VSCode extension files have changed but checks are being skipped."
echo "Please run VSCode extension checks manually before submitting your PR."
else
echo "Running VSCode extension checks..."
if [ -d "openhands/integrations/vscode" ]; then
cd openhands/integrations/vscode || exit 1
echo "Running npm lint:fix..."
npm run lint:fix
if [ $? -ne 0 ]; then
echo "VSCode extension linting failed. Please fix the issues before committing."
EXIT_CODE=1
else
echo "VSCode extension linting passed!"
fi
echo "Running npm typecheck..."
npm run typecheck
if [ $? -ne 0 ]; then
echo "VSCode extension type checking failed. Please fix the issues before committing."
EXIT_CODE=1
else
echo "VSCode extension type checking passed!"
fi
echo "Running npm compile..."
npm run compile
if [ $? -ne 0 ]; then
echo "VSCode extension compilation failed. Please fix the issues before committing."
EXIT_CODE=1
else
echo "VSCode extension compilation passed!"
fi
cd ../../..
fi
fi
else
echo "Skipping VSCode extension checks (no VSCode extension changes detected)."
fi
# If no specific code changes detected, run basic checks
if [ "$has_frontend_changes" = false ] && [ "$has_backend_changes" = false ]; then
echo "No specific code changes detected. Running basic checks..."
if [ -n "$STAGED_FILES" ]; then
# Run only basic pre-commit hooks for non-code files
poetry run pre-commit run --files $(echo "$STAGED_FILES" | tr '\n' ' ') --hook-stage commit --config ./dev_config/python/.pre-commit-config.yaml
if [ $? -ne 0 ]; then
echo "Basic checks failed. Please fix the issues before committing."
EXIT_CODE=1
else
echo "Basic checks passed!"
fi
else
echo "No files changed. Skipping basic checks."
fi
fi
# Run any existing pre-commit hooks that might have been installed by the user

View File

@@ -31,7 +31,7 @@ We're always looking to improve the look and feel of the application. If you've
for something that's bugging you, feel free to open up a PR that changes the [`./frontend`](./frontend) directory.
If you're looking to make a bigger change, add a new UI element, or significantly alter the style
of the application, please open an issue first, or better, join the #frontend channel in our Slack
of the application, please open an issue first, or better, join the #eng-ui-ux channel in our Slack
to gather consensus from our design team first.
#### Improving the agent

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

View File

@@ -174,7 +174,7 @@ install-python-dependencies:
fi
@echo "$(GREEN)Python dependencies installed successfully.$(RESET)"
install-frontend-dependencies:
install-frontend-dependencies: check-npm check-nodejs
@echo "$(YELLOW)Setting up frontend environment...$(RESET)"
@echo "$(YELLOW)Detect Node.js version...$(RESET)"
@cd frontend && node ./scripts/detect-node-version.js
@@ -182,17 +182,17 @@ install-frontend-dependencies:
@cd frontend && npm install
@echo "$(GREEN)Frontend dependencies installed successfully.$(RESET)"
install-pre-commit-hooks:
install-pre-commit-hooks: check-python check-poetry install-python-dependencies
@echo "$(YELLOW)Installing pre-commit hooks...$(RESET)"
@git config --unset-all core.hooksPath || true
@poetry run pre-commit install --config $(PRE_COMMIT_CONFIG_PATH)
@echo "$(GREEN)Pre-commit hooks installed successfully.$(RESET)"
lint-backend:
lint-backend: install-pre-commit-hooks
@echo "$(YELLOW)Running linters...$(RESET)"
@poetry run pre-commit run --all-files --show-diff-on-failure --config $(PRE_COMMIT_CONFIG_PATH)
lint-frontend:
lint-frontend: install-frontend-dependencies
@echo "$(YELLOW)Running linters for frontend...$(RESET)"
@cd frontend && npm run lint

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.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-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.46
docker.all-hands.dev/all-hands-ai/openhands:0.51
```
> **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.
@@ -117,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.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-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.46
docker.all-hands.dev/all-hands-ai/openhands:0.51
```
> **注意**: 如果您在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.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-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.46
docker.all-hands.dev/all-hands-ai/openhands:0.51
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。

114
build_vscode.py Normal file
View File

@@ -0,0 +1,114 @@
import os
import pathlib
import subprocess
# This script is intended to be run by Poetry during the build process.
# Define the expected name of the .vsix file based on the extension's package.json
# This should match the name and version in openhands-vscode/package.json
EXTENSION_NAME = 'openhands-vscode'
EXTENSION_VERSION = '0.0.1'
VSIX_FILENAME = f'{EXTENSION_NAME}-{EXTENSION_VERSION}.vsix'
# Paths
ROOT_DIR = pathlib.Path(__file__).parent.resolve()
VSCODE_EXTENSION_DIR = ROOT_DIR / 'openhands' / 'integrations' / 'vscode'
def check_node_version():
"""Check if Node.js version is sufficient for building the extension."""
try:
result = subprocess.run(
['node', '--version'], capture_output=True, text=True, check=True
)
version_str = result.stdout.strip()
# Extract major version number (e.g., "v12.22.9" -> 12)
major_version = int(version_str.lstrip('v').split('.')[0])
return major_version >= 18 # Align with frontend actual usage (18.20.1)
except (subprocess.CalledProcessError, FileNotFoundError, ValueError):
return False
def build_vscode_extension():
"""Builds the VS Code extension."""
vsix_path = VSCODE_EXTENSION_DIR / VSIX_FILENAME
# Check if VSCode extension build is disabled via environment variable
if os.environ.get('SKIP_VSCODE_BUILD', '').lower() in ('1', 'true', 'yes'):
print('--- Skipping VS Code extension build (SKIP_VSCODE_BUILD is set) ---')
if vsix_path.exists():
print(f'--- Using existing VS Code extension: {vsix_path} ---')
else:
print('--- No pre-built VS Code extension found ---')
return
# Check Node.js version - if insufficient, use pre-built extension as fallback
if not check_node_version():
print('--- Warning: Node.js version < 18 detected or Node.js not found ---')
print('--- Skipping VS Code extension build (requires Node.js >= 18) ---')
print('--- Using pre-built extension if available ---')
if not vsix_path.exists():
print('--- Warning: No pre-built VS Code extension found ---')
print('--- VS Code extension will not be available ---')
else:
print(f'--- Using pre-built VS Code extension: {vsix_path} ---')
return
print(f'--- Building VS Code extension in {VSCODE_EXTENSION_DIR} ---')
try:
# Ensure npm dependencies are installed
print('--- Running npm install for VS Code extension ---')
subprocess.run(
['npm', 'install'],
cwd=VSCODE_EXTENSION_DIR,
check=True,
shell=os.name == 'nt',
)
# Package the extension
print(f'--- Packaging VS Code extension ({VSIX_FILENAME}) ---')
subprocess.run(
['npm', 'run', 'package-vsix'],
cwd=VSCODE_EXTENSION_DIR,
check=True,
shell=os.name == 'nt',
)
# Verify the generated .vsix file exists
if not vsix_path.exists():
raise FileNotFoundError(
f'VS Code extension package not found after build: {vsix_path}'
)
print(f'--- VS Code extension built successfully: {vsix_path} ---')
except subprocess.CalledProcessError as e:
print(f'--- Warning: Failed to build VS Code extension: {e} ---')
print('--- Continuing without building extension ---')
if not vsix_path.exists():
print('--- Warning: No pre-built VS Code extension found ---')
print('--- VS Code extension will not be available ---')
def build(setup_kwargs):
"""
This function is called by Poetry during the build process.
`setup_kwargs` is a dictionary that will be passed to `setuptools.setup()`.
"""
print('--- Running custom Poetry build script (build_vscode.py) ---')
# Build the VS Code extension and place the .vsix file
build_vscode_extension()
# Poetry will handle including files based on pyproject.toml `include` patterns.
# Ensure openhands/integrations/vscode/*.vsix is included there.
print('--- Custom Poetry build script (build_vscode.py) finished ---')
if __name__ == '__main__':
print('Running build_vscode.py directly for testing VS Code extension packaging...')
build_vscode_extension()
print('Direct execution of build_vscode.py finished.')

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"
@@ -29,9 +18,6 @@
# Cache directory path
#cache_dir = "/tmp/cache"
# Reasoning effort for o1 models (low, medium, high, or not set)
#reasoning_effort = "medium"
# Debugging enabled
#debug = false
@@ -60,6 +46,9 @@
# Maximum file size for uploads, in megabytes
#file_uploads_max_file_size_mb = 0
# Enable the browser environment
#enable_browser = true
# Maximum budget per task, 0.0 means no limit
#max_budget_per_task = 0.0
@@ -127,6 +116,9 @@ api_key = ""
# API version
#api_version = ""
# Reasoning effort for OpenAI o-series models (low, medium, high, or not set)
#reasoning_effort = "medium"
# Cost per input token
#input_cost_per_token = 0.0
@@ -237,6 +229,7 @@ model = "gpt-4o"
[agent]
# Whether the browsing tool is enabled
# Note: when this is set to true, enable_browser in the core config must also be true
enable_browsing = true
# Whether the LLM draft editor is enabled
@@ -271,6 +264,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

View File

@@ -1,5 +1,5 @@
ARG OPENHANDS_BUILD_VERSION=dev
FROM node:22.16.0-bookworm-slim AS frontend-builder
FROM node:24.3.0-bookworm-slim AS frontend-builder
WORKDIR /app
@@ -45,6 +45,7 @@ ENV OPENHANDS_BUILD_VERSION=$OPENHANDS_BUILD_VERSION
ENV SANDBOX_USER_ID=0
ENV FILE_STORE=local
ENV FILE_STORE_PATH=/.openhands
ENV INIT_GIT_IN_EMPTY_WORKSPACE=1
RUN mkdir -p $FILE_STORE_PATH
RUN mkdir -p $WORKSPACE_BASE

View File

@@ -12,7 +12,7 @@ services:
- SANDBOX_API_HOSTNAME=host.docker.internal
- DOCKER_HOST_ADDR=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.46-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.51-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/|openhands-ui/)
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/)
- id: end-of-file-fixer
exclude: ^(docs/|modules/|python/|openhands-ui/)
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.46-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.51-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

@@ -34,6 +34,7 @@
{
"group": "Integrations",
"pages": [
"usage/cloud/bitbucket-installation",
"usage/cloud/github-installation",
"usage/cloud/gitlab-installation",
"usage/cloud/slack-installation"
@@ -66,7 +67,9 @@
"usage/llms/groq",
"usage/llms/local-llms",
"usage/llms/litellm-proxy",
"usage/llms/moonshot",
"usage/llms/openai-llms",
"usage/llms/openhands-llms",
"usage/llms/openrouter"
]
}
@@ -196,7 +199,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

@@ -1827,6 +1827,11 @@
"updated_at": {
"type": "string",
"format": "date-time"
},
"owner_type": {
"type": "string",
"enum": ["user", "organization"],
"nullable": true
}
}
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,48 @@
---
title: Bitbucket Integration
description: This guide walks you through the process of installing OpenHands Cloud for your Bitbucket repositories. Once
set up, it will allow OpenHands to work with your Bitbucket repository.
---
## Prerequisites
- Signed in to [OpenHands Cloud](https://app.all-hands.dev) with [a Bitbucket account](/usage/cloud/openhands-cloud).
## IP Whitelisting
If your Bitbucket Cloud instance has IP restrictions, you'll need to whitelist the following IP addresses to allow OpenHands to access your repositories:
### Core App IP
```
34.68.58.200
```
### Runtime IPs
```
34.10.175.217
34.136.162.246
34.45.0.142
34.28.69.126
35.224.240.213
34.70.174.52
34.42.4.87
35.222.133.153
34.29.175.97
34.60.55.59
```
## Adding Bitbucket Repository Access
Upon signing into OpenHands Cloud with a Bitbucket account, OpenHands will have access to your repositories.
## Working With Bitbucket Repos in Openhands Cloud
After signing in with a Bitbucket account, use the `select a repo` and `select a branch` dropdowns to select the
appropriate repository and branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
![Connect Repo](/static/img/connect-repo-no-github.png)
## Next Steps
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).
- [Use the Cloud API](/usage/cloud/cloud-api) to programmatically interact with OpenHands.

View File

@@ -9,8 +9,9 @@ description: The Cloud UI provides a web interface for interacting with OpenHand
The landing page is where you can:
- [Add GitHub repository access](/usage/cloud/github-installation#adding-github-repository-access) to OpenHands.
- [Select a GitHub repo](/usage/cloud/github-installation#working-with-github-repos-in-openhands-cloud) or
[a GitLab repo](/usage/cloud/gitlab-installation#working-with-gitlab-repos-in-openhands-cloud) to start working on.
- [Select a GitHub repo](/usage/cloud/github-installation#working-with-github-repos-in-openhands-cloud),
[a GitLab repo](/usage/cloud/gitlab-installation#working-with-gitlab-repos-in-openhands-cloud) or
[a Bitbucket repo](/usage/cloud/bitbucket-installation#working-with-bitbucket-repos-in-openhands-cloud) to start working on.
- See `Suggested Tasks` for repositories that OpenHands has access to.
- Launch an empty conversation using `Launch from Scratch`.

View File

@@ -51,8 +51,7 @@ Giving GitHub repository access to OpenHands also allows you to work on GitHub i
### Working with Issues
On your repository, label an issue with `openhands` or add a message starting with
`@openhands`. OpenHands will:
On your repository, label an issue with `openhands` or add a message starting with `@openhands`. OpenHands will:
1. Comment on the issue to let you know it is working on it.
- You can click on the link to track the progress on OpenHands Cloud.
2. Open a pull request if it determines that the issue has been successfully resolved.
@@ -65,6 +64,8 @@ To get OpenHands to work on pull requests, mention `@openhands` in the comments
- Request updates
- Get code explanations
**Important Note**: The `@openhands` mention functionality in pull requests only works if the pull request is both *to* and *from* a repository that you have added through the interface. This is because OpenHands needs appropriate permissions to access both repositories.
## Next Steps
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).

View File

@@ -1,7 +1,7 @@
---
title: GitLab Integration
description: This guide walks you through the process of installing OpenHands Cloud for your GitLab repositories. Once
set up, it will allow OpenHands to work with your GitLab repository.
set up, it will allow OpenHands to work with your GitLab repository through the Cloud UI or straight from GitLab!.
---
## Prerequisites
@@ -17,7 +17,7 @@ Upon signing into OpenHands Cloud with a GitLab account, OpenHands will have acc
After signing in with a Gitlab account, use the `select a repo` and `select a branch` dropdowns to select the
appropriate repository and branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
![Connect Repo](/static/img/connect-repo.png)
![Connect Repo](/static/img/connect-repo-no-github.png)
## Using Tokens with Reduced Scopes
@@ -25,6 +25,33 @@ OpenHands requests an API-scoped token during OAuth authentication. By default,
To restrict the agent's permissions, you can define a custom secret `GITLAB_TOKEN`, which will override the default token assigned to the agent.
While the high-permission API token is still requested and used for other components of the application (e.g. opening merge requests), the agent will not have access to it.
## Working on GitLab Issues and Merge Requests Using Openhands
<Note>
This feature works for personal projects and is available for group projects with a
[Premium or Ultimate tier subscription](https://docs.gitlab.com/user/project/integrations/webhooks/#group-webhooks).
A webhook is automatically installed within a few minutes after the owner/maintainer of the project or group logs into
OpenHands Cloud. If you decide to delete the webhook, then re-installing will require the support of All Hands AI but we are planning to improve this in a future release.
</Note>
Giving GitLab repository access to OpenHands also allows you to work on GitLab issues and merge requests directly.
### Working with Issues
On your repository, label an issue with `openhands` or add a message starting with `@openhands`. OpenHands will:
1. Comment on the issue to let you know it is working on it.
- You can click on the link to track the progress on OpenHands Cloud.
2. Open a merge request if it determines that the issue has been successfully resolved.
3. Comment on the issue with a summary of the performed tasks and a link to the PR.
### Working with Merge Requests
To get OpenHands to work on merge requests, mention `@openhands` in the comments to:
- Ask questions
- Request updates
- Get code explanations
## Next Steps
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).

View File

@@ -8,9 +8,9 @@ description: Getting started with OpenHands Cloud.
OpenHands Cloud is the hosted cloud version of All Hands AI's OpenHands. To get started with OpenHands Cloud,
visit [app.all-hands.dev](https://app.all-hands.dev).
You'll be prompted to connect with your GitHub or GitLab account:
You'll be prompted to connect with your GitHub, GitLab or Bitbucket account:
1. Click `Log in with GitHub` or `Log in with GitLab`.
1. Click `Log in with GitHub`, `Log in with GitLab` or `Log in with Bitbucket`.
2. Review the permissions requested by OpenHands and authorize the application.
- OpenHands will require certain permissions from your account. To read more about these permissions,
you can click the `Learn more` link on the authorization page.
@@ -22,5 +22,6 @@ Once you've connected your account, you can:
- [Install GitHub Integration](/usage/cloud/github-installation) to use OpenHands with your GitHub repositories.
- [Install GitLab Integration](/usage/cloud/gitlab-installation) to use OpenHands with your GitLab repositories.
- [Install Bitbucket Integration](/usage/cloud/bitbucket-installation) to use OpenHands with your Bitbucket repositories.
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).
- [Use the Cloud API](/usage/cloud/cloud-api) to programmatically interact with OpenHands.

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.
@@ -15,7 +24,7 @@ description: This guide walks you through installing the OpenHands Slack app.
**This step is for Slack admins/owners**
1. Make sure you have permissions to install Apps to your workspace.
2. Click the button below to install OpenHands Slack App <a target="_blank" href="https://slack.com/oauth/v2/authorize?client_id=7477886716822.8729519890534&scope=app_mentions:read,chat:write,users:read,channels:history,groups:history,mpim:history,im:history&user_scope=channels:history,groups:history,im:history,mpim:history"><img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcSet="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a>
2. Click the button below to install OpenHands Slack App <a target="_blank" href="https://slack.com/oauth/v2/authorize?client_id=7477886716822.8729519890534&scope=app_mentions:read,channels:history,chat:write,groups:history,im:history,mpim:history,users:read&user_scope="><img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcSet="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a>
3. In the top right corner, select the workspace to install the OpenHands Slack app.
4. Review permissions and click allow.

View File

@@ -8,26 +8,16 @@ description: This page outlines all available configuration options for OpenHand
In GUI Mode, any settings applied through the Settings UI will take precedence.
</Note>
## Location of the `config.toml` File
When running OpenHands in CLI, headless, or development mode, you can use a project-specific `config.toml` file for configuration, which must be
located in the same directory from which the command is run. Alternatively, you may use the `--config-file` option to
specify a different path to the `config.toml` file.
## 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

@@ -12,7 +12,8 @@ icon: question
[GitHub](/usage/cloud/github-installation), [GitLab](/usage/cloud/gitlab-installation),
and [Slack](/usage/cloud/slack-installation) integrations.
2. **Run on your own**: If you prefer to run it on your own hardware, follow our [Getting Started guide](/usage/local-setup).
3. **First steps**: Complete the [start building tutorial](/usage/getting-started) to learn the basics.
3. **First steps**: Read over the [start building guidelines](/usage/getting-started) and
[prompting best practices](/usage/prompting/prompting-best-practices) to learn the basics.
### Can I use OpenHands for production workloads?
@@ -88,7 +89,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
@@ -24,6 +33,45 @@ pip install openhands-ai
uvx --python 3.12 --from openhands-ai openhands
```
<AccordionGroup>
<Accordion title="Create shell aliases for easy access across environments">
Add the following to your shell configuration file (`.bashrc`, `.zshrc`, etc.):
```bash
# Add OpenHands aliases
alias openhands="uvx --python 3.12 --from openhands-ai openhands"
alias oh="uvx --python 3.12 --from openhands-ai openhands"
```
After adding these lines, reload your shell configuration with `source ~/.bashrc` or `source ~/.zshrc` (depending on your shell).
</Accordion>
<Accordion title="Install OpenHands in home directory without global installation">
You can install OpenHands in a virtual environment in your home directory using `uv`:
```bash
# Create a virtual environment in your home directory
cd ~
uv venv .openhands-venv --python 3.12
# Install OpenHands in the virtual environment
uv pip install -t ~/.openhands-venv/lib/python3.12/site-packages openhands-ai
# Add the bin directory to your PATH in your shell configuration file
echo 'export PATH="$PATH:$HOME/.openhands-venv/bin"' >> ~/.bashrc # or ~/.zshrc
# Reload your shell configuration
source ~/.bashrc # or source ~/.zshrc
```
</Accordion>
</AccordionGroup>
2. Launch an interactive OpenHands conversation from the command line:
```bash
openhands
@@ -55,7 +103,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -64,7 +112,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.46 \
docker.all-hands.dev/all-hands-ai/openhands:0.51 \
python -m openhands.cli.main --override-cli-mode true
```
@@ -75,7 +123,8 @@ docker run -it \
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 `-e SANDBOX_USER_ID=$(id -u)` is passed to the Docker command to ensure the sandbox user matches the host users
permissions. This prevents the agent from creating root-owned files in the mounted workspace.
The conversation history will be saved in `~/.openhands/sessions`.
@@ -104,6 +153,7 @@ You can use the following commands whenever the prompt (`>`) is displayed:
| `/new` | Start a new conversation |
| `/settings` | View and modify current LLM/agent settings |
| `/resume` | Resume the agent if paused |
| `/mcp` | Manage MCP server configuration and view connection errors |
#### Settings and Configuration
@@ -113,7 +163,7 @@ follow the prompts:
- **Basic settings**: Choose a model/provider and enter your API key.
- **Advanced settings**: Set custom endpoints, enable or disable confirmation mode, and configure memory condensation.
Settings can also be managed via the `config.toml` file.
Settings can also be managed via the `config.toml` file in the current directory or `~/.openhands/config.toml`.
#### Repository Initialization
@@ -125,6 +175,41 @@ project details and structure. Use this when onboarding the agent to a new codeb
You can pause the agent while it is running by pressing `Ctrl-P`. To continue the conversation after pausing, simply
type `/resume` at the prompt.
#### MCP Server Management
To configure Model Context Protocol (MCP) servers, you can refer to the documentation on [MCP servers](../mcp) and use the `/mcp` command in the CLI. This command provides an interactive interface for managing Model Context Protocol (MCP) servers:
- **List configured servers**: View all currently configured MCP servers (SSE, Stdio, and SHTTP)
- **Add new server**: Interactively add a new MCP server with guided prompts
- **Remove server**: Remove an existing MCP server from your configuration
- **View errors**: Display any connection errors that occurred during MCP server startup
This command modifies your `~/.openhands/config.toml` file and will prompt you to restart OpenHands for changes to take effect.
To enable the [Tavily MCP server](https://github.com/tavily-ai/tavily-mcp) search engine, you can set the `search_api_key` under the `[core]` section in the `~/.openhands/config.toml` file.
##### Example of the `config.toml` file with MCP server configuration:
```toml
[core]
search_api_key = "tvly-your-api-key-here"
[mcp]
stdio_servers = [
{name="fetch", command="uvx", args=["mcp-server-fetch"]},
]
sse_servers = [
# Basic SSE server with just a URL
"http://example.com:8080/sse",
]
shttp_servers = [
# Streamable HTTP server with API key authentication
{url="https://secure-example.com/mcp", api_key="your-api-key"}
]
```
## Tips and Troubleshooting
- Use `/help` at any time to see the list of available commands.

View File

@@ -25,7 +25,8 @@ You can use the Settings page at any time to:
- Setup the LLM provider and model for OpenHands.
- [Setup the search engine](/usage/search-engine-setup).
- [Configure MCP servers](/usage/mcp).
- [Connect to GitHub](/usage/how-to/gui-mode#github-setup) and [connect to GitLab](/usage/how-to/gui-mode#gitlab-setup).
- [Connect to GitHub](/usage/how-to/gui-mode#github-setup), [connect to GitLab](/usage/how-to/gui-mode#gitlab-setup)
and [connect to Bitbucket](/usage/how-to/gui-mode#bitbucket-setup).
- Set application settings like your preferred language, notifications and other preferences.
- [Manage custom secrets](/usage/common-settings#secrets-management).
@@ -122,17 +123,15 @@ OpenHands automatically exports a `GITLAB_TOKEN` to the shell environment if pro
</Accordion>
</AccordionGroup>
#### BitBucket Setup (Coming soon ...)
#### BitBucket Setup
<AccordionGroup>
<Accordion title="Setting Up a BitBucket Password">
1. **Generate an App Password**:
- On BitBucket, go to Personal Settings > App Password.
- Create a new password with the following scopes:
- `repository: read`
- `account`: `read`
- `repository: write`
- `pull requests: read`
- `pull requests: write`
- `issues: read`
- `issues: write`
- App passwords are non-expiring token. OpenHands will migrate to using API tokens in the future.
2. **Enter Token in OpenHands**:

View File

@@ -18,42 +18,79 @@ poetry run python -m openhands.core.main -t "write a bash script that prints hi"
You'll need to be sure to set your model, API key, and other settings via environment variables
[or the `config.toml` file](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml).
## With Docker
### Working with Repositories
To run OpenHands in Headless mode with Docker:
You can specify a repository for OpenHands to work with using `--selected-repo` or the `SANDBOX_SELECTED_REPO` environment variable:
1. Set the following environment variables in your terminal:
- `SANDBOX_VOLUMES` to specify the directory you want OpenHands to access ([See using SANDBOX_VOLUMES for more info](../runtimes/docker#using-sandbox_volumes))
- `LLM_MODEL` - the LLM model to use (e.g. `export LLM_MODEL="anthropic/claude-sonnet-4-20250514"`)
- `LLM_API_KEY` - your API key (e.g. `export LLM_API_KEY="sk_test_12345"`)
2. Run the following Docker command:
> **Note**: Currently, authentication tokens (GITHUB_TOKEN, GITLAB_TOKEN, or BITBUCKET_TOKEN) are required for all repository operations, including public repositories. This is a known limitation that may be addressed in future versions to allow tokenless access to public repositories.
```bash
# Using command-line argument
poetry run python -m openhands.core.main \
--selected-repo "owner/repo-name" \
-t "analyze the codebase and suggest improvements"
# Using environment variable
export SANDBOX_SELECTED_REPO="owner/repo-name"
poetry run python -m openhands.core.main -t "fix any linting issues"
# Authentication tokens are currently required for ALL repository operations (public and private)
# This includes GitHub, GitLab, and Bitbucket repositories
export GITHUB_TOKEN="your-token" # or GITLAB_TOKEN, BITBUCKET_TOKEN
poetry run python -m openhands.core.main \
--selected-repo "owner/repo-name" \
-t "review the security implementation"
# Using task files instead of inline task
echo "Review the README and suggest improvements" > task.txt
poetry run python -m openhands.core.main -f task.txt --selected-repo "owner/repo"
```
## With Docker
Set environment variables and run the Docker command:
```bash
# Set required environment variables
export SANDBOX_VOLUMES="/path/to/workspace" # See SANDBOX_VOLUMES docs for details
export LLM_MODEL="anthropic/claude-sonnet-4-20250514"
export LLM_API_KEY="your-api-key"
export SANDBOX_SELECTED_REPO="owner/repo-name" # Optional: requires GITHUB_TOKEN
export GITHUB_TOKEN="your-token" # Required for repository operations
# Run OpenHands
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
-e LLM_MODEL=$LLM_MODEL \
-e SANDBOX_SELECTED_REPO=$SANDBOX_SELECTED_REPO \
-e GITHUB_TOKEN=$GITHUB_TOKEN \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-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.46 \
docker.all-hands.dev/all-hands-ai/openhands:0.51 \
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.
> **Note**: If you used OpenHands before version 0.44, run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history.
The `-e SANDBOX_USER_ID=$(id -u)` is passed to the Docker command to ensure the sandbox user matches the host users
permissions. This prevents the agent from creating root-owned files in the mounted workspace.
## Advanced Headless Configurations
## Additional Options
To view all available configuration options for headless mode, run the Python command with the `--help` flag.
Common command-line options:
- `-d "/path/to/workspace"` - Set working directory
- `-f task.txt` - Load task from file
- `-i 50` - Set max iterations
- `-b 10.0` - Set budget limit (USD)
- `--no-auto-continue` - Interactive mode
### Additional Logs
Run `poetry run python -m openhands.core.main --help` for all options.
For the headless mode to log all the agent actions, in the terminal run: `export LOG_ALL_EVENTS=true`
Set `export LOG_ALL_EVENTS=true` to log all agent actions.

View File

@@ -10,7 +10,8 @@ This section is for users who want to connect OpenHands to different LLMs.
## Model Recommendations
Based on our evaluations of language models for coding tasks (using the SWE-bench dataset), we can provide some
recommendations for model selection. Our latest benchmarking results can be found in [this spreadsheet](https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0).
recommendations for model selection. Our latest benchmarking results can be found in
[this spreadsheet](https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0).
Based on these findings and community feedback, these are the latest models that have been verified to work reasonably well with OpenHands:
@@ -20,6 +21,7 @@ Based on these findings and community feedback, these are the latest models that
- [openai/o4-mini](https://openai.com/index/introducing-o3-and-o4-mini/)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
- [moonshot/kimi-k2-0711-preview](https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2)
If you have successfully run OpenHands with specific providers, we encourage you to open a PR to share your setup process
to help others using the same provider!
@@ -37,6 +39,12 @@ limits and monitor usage.
- [mistralai/devstral-small](https://www.all-hands.dev/blog/devstral-a-new-state-of-the-art-open-model-for-coding-agents) (20 May 2025) -- also available through [OpenRouter](https://openrouter.ai/mistralai/devstral-small:free)
- [all-hands/openhands-lm-32b-v0.1](https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model) (31 March 2025) -- also available through [OpenRouter](https://openrouter.ai/all-hands/openhands-lm-32b-v0.1)
### Known Issues
<Warning>
As of July 2025, there are known issues with Gemini 2.5 Pro conversations taking longer than normal with OpenHands. We are continuing to investigate.
</Warning>
<Note>
Most current local and open source models are not as powerful. When using such models, you may see long
wait times between messages, poor responses, or errors about malformed JSON. OpenHands can only be as powerful as the
@@ -70,17 +78,20 @@ We have a few guides for running OpenHands with specific model providers:
- [Groq](/usage/llms/groq)
- [Local LLMs with SGLang or vLLM](/usage/llms/local-llms)
- [LiteLLM Proxy](/usage/llms/litellm-proxy)
- [Moonshot AI](/usage/llms/moonshot)
- [OpenAI](/usage/llms/openai-llms)
- [OpenHands](/usage/llms/openhands-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
- **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).
For detailed information about model customization, see
[LLM Configuration Options](/usage/configuration-options#llm-configuration).
### API retries and rate limits

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.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-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.46
docker.all-hands.dev/all-hands-ai/openhands:0.51
```
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.46
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.51
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
@@ -175,6 +175,27 @@ vllm serve mistralai/Devstral-Small-2505 \
--enable-prefix-caching
```
If you are interested in further improved inference speed, you can also try Snowflake's version
of vLLM, [ArcticInference](https://www.snowflake.com/en/engineering-blog/fast-speculative-decoding-vllm-arctic/),
which can achieve up to 2x speedup in some cases.
1. Install the Arctic Inference library that automatically patches vLLM:
```bash
pip install git+https://github.com/snowflakedb/ArcticInference.git
```
2. Run the launch command with speculative decoding enabled:
```bash
vllm serve mistralai/Devstral-Small-2505 \
--host 0.0.0.0 --port 8000 \
--api-key mykey \
--tensor-parallel-size 2 \
--served-model-name Devstral-Small-2505 \
--speculative-config '{"method": "suffix"}'
```
### Run OpenHands (Alternative Backends)
#### Using Docker

View File

@@ -0,0 +1,25 @@
---
title: Moonshot AI
description: How to use Moonshot AI models with OpenHands
---
## Using Moonshot AI with OpenHands
[Moonshot AI](https://platform.moonshot.ai/) offers several powerful models, including Kimi-K2, which has been verified to work well with OpenHands.
### Setup
1. Sign up for an account at [Moonshot AI Platform](https://platform.moonshot.ai/)
2. Generate an API key from your account settings
3. Configure OpenHands to use Moonshot AI:
| Setting | Value |
| --- | --- |
| LLM Provider | `moonshot` |
| LLM Model | `kimi-k2-0711-preview` |
| API Key | Your Moonshot API key |
### Recommended Models
- `moonshot/kimi-k2-0711-preview` - Kimi-K2 is Moonshot's most powerful model with a 131K context window, function calling support, and web search capabilities.

View File

@@ -0,0 +1,35 @@
---
title: OpenHands
description: OpenHands LLM provider with access to state-of-the-art (SOTA) agentic coding models.
---
## Obtain Your OpenHands LLM API Key
1. [Log in to OpenHands Cloud](/usage/cloud/openhands-cloud).
2. Go to the Settings page and navigate to the `API Keys` tab.
3. Copy your `LLM API Key`.
![OpenHands LLM API Key](/static/img/openhands-llm-api-key.png)
## Configuration
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
- `LLM Provider` to `OpenHands`
- `LLM Model` to the model you will be using (e.g. claude-sonnet-4-20250514)
- `API Key` to your OpenHands LLM API key copied from above
## Using OpenHands LLM Provider in the CLI
1. [Run OpenHands CLI](/usage/how-to/cli-mode).
2. To select OpenHands as the LLM provider:
- If this is your first time running the CLI, choose `openhands` and then select the model that you would like to use.
- If you have previously run the CLI, run the `/settings` command and select to modify the `Basic` settings. Then
choose `openhands` and finally the model.
![OpenHands Provider in CLI](/static/img/openhands-provider-cli.png)
## Pricing
Pricing follows official API provider rates. [You can view model prices here.](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json)
For `qwen3-coder-480b`, we charge the cheapest FP8 rate available on openrouter: $0.4 per million input tokens and $1.6 per million output tokens.

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.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.51-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.46
docker.all-hands.dev/all-hands-ai/openhands:0.51
```
> **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

@@ -10,38 +10,83 @@ Model Context Protocol (MCP) is a mechanism that allows OpenHands to communicate
servers can provide additional functionality to the agent, such as specialized data processing, external API access,
or custom tools. MCP is based on the open standard defined at [modelcontextprotocol.io](https://modelcontextprotocol.io).
<Note>
MCP is currently not available on OpenHands Cloud. This feature is only available when running OpenHands locally.
</Note>
### How MCP Works
When OpenHands starts, it:
1. Reads the MCP configuration.
2. Connects to any configured SSE and SHTTP servers.
3. Starts any configured stdio servers.
4. Registers the tools provided by these servers with the agent.
The agent can then use these tools just like any built-in tool. When the agent calls an MCP tool:
1. OpenHands routes the call to the appropriate MCP server.
2. The server processes the request and returns a response.
3. OpenHands converts the response to an observation and presents it to the agent.
## Configuration
MCP configuration can be defined in:
* The OpenHands UI through the Settings under the `MCP` tab.
* The `config.toml` file under the `[mcp]` section if not using the UI.
### Configuration Example via config.toml
### Configuration Examples
#### Recommended: Using Proxy Servers (SSE/HTTP)
For stdio-based MCP servers, we recommend using MCP proxy tools like [`supergateway`](https://github.com/supercorp-ai/supergateway) instead of direct stdio connections.
[SuperGateway](https://github.com/supercorp-ai/supergateway) is a popular MCP proxy that converts stdio MCP servers to HTTP/SSE endpoints:
Start the proxy servers separately:
```bash
# Terminal 1: Filesystem server proxy
supergateway --stdio "npx @modelcontextprotocol/server-filesystem /" --port 8080
# Terminal 2: Fetch server proxy
supergateway --stdio "uvx mcp-server-fetch" --port 8081
```
Then configure OpenHands to use the HTTP endpoint:
```toml
[mcp]
# SSE Servers - External servers that communicate via Server-Sent Events
# SSE Servers - Recommended approach using proxy tools
sse_servers = [
# Basic SSE server with just a URL
"http://example.com:8080/mcp",
# SSE server with API key authentication
{url="https://secure-example.com/mcp", api_key="your-api-key"}
]
# SuperGateway proxy for fetch server
"http://localhost:8081/sse",
# Stdio Servers - Local processes that communicate via standard input/output
# External MCP service with authentication
{url="https://api.example.com/mcp/sse", api_key="your-api-key"}
]
```
#### Alternative: Direct Stdio Servers (Not Recommended for Production)
```toml
[mcp]
# Direct stdio servers - use only for development/testing
stdio_servers = [
# Basic stdio server
{name="fetch", command="uvx", args=["mcp-server-fetch"]},
# Stdio server with environment variables
{
name="data-processor",
command="python",
args=["-m", "my_mcp_server"],
name="filesystem",
command="npx",
args=["@modelcontextprotocol/server-filesystem", "/"],
env={
"DEBUG": "true",
"PORT": "8080"
"DEBUG": "true"
}
}
]
@@ -57,8 +102,26 @@ SSE servers are configured using either a string URL or an object with the follo
- Type: `str`
- Description: The URL of the SSE server
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication
### SHTTP Servers
SHTTP (Streamable HTTP) servers are configured using either a string URL or an object with the following properties:
- `url` (required)
- Type: `str`
- Description: The URL of the SHTTP server
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication
### Stdio Servers
**Note**: While stdio servers are supported, we recommend using MCP proxies (see above) for better reliability and performance.
Stdio servers are configured using an object with the following properties:
- `name` (required)
@@ -79,17 +142,55 @@ Stdio servers are configured using an object with the following properties:
- Default: `{}`
- Description: Environment variables to set for the server process
## How MCP Works
When OpenHands starts, it:
#### When to Use Direct Stdio
1. Reads the MCP configuration.
2. Connects to any configured SSE servers.
3. Starts any configured stdio servers.
4. Registers the tools provided by these servers with the agent.
Direct stdio connections may still be appropriate in these scenarios:
- **Development and testing**: Quick prototyping of MCP servers
- **Simple, single-use tools**: Tools that don't require high reliability or concurrent access
- **Local-only environments**: When you don't want to manage additional proxy processes
The agent can then use these tools just like any built-in tool. When the agent calls an MCP tool:
For production use, we recommend using proxy tools like SuperGateway.
1. OpenHands routes the call to the appropriate MCP server.
2. The server processes the request and returns a response.
3. OpenHands converts the response to an observation and presents it to the agent.
### Other Proxy Tools
Other options include:
- **Custom FastAPI/Express servers**: Build your own HTTP wrapper around stdio MCP servers
- **Docker-based proxies**: Containerized solutions for better isolation
- **Cloud-hosted MCP services**: Third-party services that provide MCP endpoints
### Troubleshooting MCP Connections
#### Common Issues with Stdio Servers
- **Process crashes**: Stdio processes may crash without proper error handling
- **Deadlocks**: Stdio communication can deadlock under high load
- **Resource leaks**: Zombie processes if not properly managed
- **Debugging difficulty**: Hard to inspect stdio communication
#### Benefits of Using Proxies
- **HTTP status codes**: Clear error reporting via standard HTTP responses
- **Request logging**: Easy to log and monitor HTTP requests
- **Load balancing**: Can distribute requests across multiple server instances
- **Health checks**: HTTP endpoints can provide health status
- **CORS support**: Better integration with web-based tools
## Transport Protocols
OpenHands supports three different MCP transport protocols:
### Server-Sent Events (SSE)
SSE is a legacy HTTP-based transport that uses Server-Sent Events for server-to-client communication and HTTP POST requests for client-to-server communication. This transport is suitable for basic streaming scenarios but has limitations in session management and connection resumability.
### Streamable HTTP (SHTTP)
SHTTP is the modern HTTP-based transport protocol that provides enhanced features over SSE:
- **Improved Session Management**: Supports stateful sessions with session IDs for maintaining context across requests
- **Connection Resumability**: Can resume broken connections and replay missed messages using event IDs
- **Bidirectional Communication**: Uses HTTP POST for client-to-server and optional SSE streams for server-to-client communication
- **Better Error Handling**: Enhanced error reporting and recovery mechanisms
SHTTP is the recommended transport for HTTP-based MCP servers as it provides better reliability and features compared to the legacy SSE transport.
### Standard Input/Output (stdio)
Stdio transport enables communication through standard input and output streams, making it ideal for local integrations and command-line tools. This transport is used for locally executed MCP servers that run as separate processes.

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,14 @@ 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.
## User Microagents When Running Openhands on Your Own
<Note>
This works with CLI, headless and development modes. It does not work out of the box when running OpenHands using the docker command.
</Note>
When running OpenHands on your own, you can place microagents in the `~/.openhands/microagents` folder on your local
system and OpenHands will always load it for all your conversations.

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

@@ -38,6 +38,21 @@ On initial prompt, an error is seen with `Permission Denied` or `PermissionError
* If mounting a local directory, ensure your `WORKSPACE_BASE` has the necessary permissions for the user running
OpenHands.
### Internal Server Error. Ports are not available
**Description**
When running on Windows, the error `Internal Server Error ("ports are not available: exposing port TCP
...: bind: An attempt was made to access a socket in a
way forbidden by its access permissions.")` is encountered.
**Resolution**
* Run the following command in PowerShell, as Administrator to reset the NAT service and release the ports:
```
Restart-Service -Name "winnat"
```
### Unable to access VS Code tab via local IP
**Description**

View File

@@ -101,13 +101,14 @@ The OpenHands evaluation harness supports a wide variety of benchmarks across [s
- SWE-Bench: [`evaluation/benchmarks/swe_bench`](./benchmarks/swe_bench)
- HumanEvalFix: [`evaluation/benchmarks/humanevalfix`](./benchmarks/humanevalfix)
- BIRD: [`evaluation/benchmarks/bird`](./benchmarks/bird)
- BioCoder: [`evaluation/benchmarks/ml_bench`](./benchmarks/ml_bench)
- BioCoder: [`evaluation/benchmarks/biocoder`](./benchmarks/biocoder)
- ML-Bench: [`evaluation/benchmarks/ml_bench`](./benchmarks/ml_bench)
- APIBench: [`evaluation/benchmarks/gorilla`](./benchmarks/gorilla/)
- ToolQA: [`evaluation/benchmarks/toolqa`](./benchmarks/toolqa/)
- AiderBench: [`evaluation/benchmarks/aider_bench`](./benchmarks/aider_bench/)
- Commit0: [`evaluation/benchmarks/commit0_bench`](./benchmarks/commit0_bench/)
- DiscoveryBench: [`evaluation/benchmarks/discoverybench`](./benchmarks/discoverybench/)
- TerminalBench: [`evaluation/benchmarks/terminal_bench`](./benchmarks/terminal_bench)
### Web Browsing

View File

@@ -117,6 +117,7 @@ def get_config(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace

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

@@ -41,6 +41,10 @@ default, it is set to 1.
- `language`, the language of your evaluating dataset.
- `dataset`, the absolute position of the dataset jsonl.
**Skipping errors on build**
For debugging purposes, you can set `export EVAL_SKIP_MAXIMUM_RETRIES_EXCEEDED=true` to continue evaluation even when instances reach maximum retries. After evaluation completes, check `maximum_retries_exceeded.jsonl` for a list of failed instances, fix those issues, and then run the evaluation again with `export EVAL_SKIP_MAXIMUM_RETRIES_EXCEEDED=false`.
The results will be generated in evaluation/evaluation_outputs/outputs/XXX/CodeActAgent/YYY/output.jsonl, you can refer to the [example](examples/output.jsonl).
## Runing evaluation

View File

@@ -17,6 +17,7 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
assert_and_raise,
check_maximum_retries_exceeded,
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
@@ -344,6 +345,7 @@ def get_config(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
@@ -843,3 +845,5 @@ if __name__ == '__main__':
timeout_seconds=120 * 60, # 2 hour PER instance should be more than enough
max_retries=5,
)
# Check if any instances reached maximum retries
check_maximum_retries_exceeded(metadata.eval_output_dir)

View File

@@ -38,6 +38,10 @@ Please follow instruction [here](../../README.md#setup) to setup your local deve
> - If your LLM config has temperature=0, we will automatically use temperature=0.1 for the 2nd and 3rd attempts
>
> To enable this iterative protocol, set `export ITERATIVE_EVAL_MODE=true`
>
> **Skipping errors on build**
>
> For debugging purposes, you can set `export EVAL_SKIP_MAXIMUM_RETRIES_EXCEEDED=true` to continue evaluation even when instances reach maximum retries. After evaluation completes, check `maximum_retries_exceeded.jsonl` for a list of failed instances, fix those issues, and then run the evaluation again with `export EVAL_SKIP_MAXIMUM_RETRIES_EXCEEDED=false`.
### Running Locally with Docker

View File

@@ -0,0 +1,45 @@
# **Localization Evaluation for SWE-Bench**
This folder implements localization evaluation at both file and function levels to complementing the assessment of agent inference on [SWE-Bench](https://www.swebench.com/).
## **1. Environment Setup**
- Python env: [Install python environment](../../../README.md#development-environment)
- LLM config: [Configure LLM config](../../../README.md#configure-openhands-and-your-llm)
## **2. Inference & Evaluation**
- Inference and evaluation follow the original `run_infer.sh` and `run_eval.sh` implementation
- You may refer to instructions at [README.md](../README.md) for running inference and evaluation on SWE-Bench
## **3. Localization Evaluation**
- Localization evaluation computes two-level localization accuracy, while also considers task success as an additional metric for overall evaluation:
- **File Localization Accuracy:** Accuracy of correctly localizing the target file
- **Function Localization Accuracy:** Accuracy of correctly localizing the target function
- **Resolve Rate** (will be auto-skipped if missing): Success rate of whether tasks are successfully resolved
- **File Localization Efficiency:** Average number of iterations taken to successfully localize the target file
- **Function Localization Efficiency:** Average number of iterations taken to successfully localize the target file
- **Task success efficiency:** Average number of iterations taken to resolve the task
- **Resource efficiency:** the API expenditure of the agent running inference on SWE-Bench instances
- Run localization evaluation
- Format:
```bash
./evaluation/benchmarks/swe_bench/scripts/eval_localization.sh [infer-dir] [split] [dataset] [max-infer-turn] [align-with-max]
```
- `infer-dir`: inference directory containing inference outputs
- `split`: SWE-Bench dataset split to use
- `dataset`: SWE-Bench dataset name
- `max-infer-turn`: the maximum number of iterations the agent took to run inference
- `align-with-max`: whether to align failure indices (e.g., incorrect localization, unresolved tasks) with `max_iter`
- Example:
```bash
# Example
./evaluation/benchmarks/swe_bench/scripts/eval_localization.sh \
--infer-dir ./evaluation/evaluation_outputs/outputs/princeton-nlp__SWE-bench_Verified-test/CodeActAgent/gpt_4o_100_N \
--split test \
--dataset princeton-nlp/SWE-bench_Verified \
--max-infer-turn 100 \
--align-with-max true
```
- Localization evaluation results will be automatically saved to `[infer-dir]/loc_eval`

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
assert_and_raise,
check_maximum_retries_exceeded,
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
@@ -109,9 +110,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata) -> MessageActio
template_name = 'swt.j2'
elif mode == 'swe':
if 'claude' in llm_model:
template_name = 'swe_claude.j2'
elif 'gemini' in llm_model:
template_name = 'swe_gemini.j2'
template_name = 'swe_default.j2'
elif 'gpt-4.1' in llm_model:
template_name = 'swe_gpt4.j2'
else:
@@ -227,6 +226,7 @@ def get_config(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
@@ -970,3 +970,5 @@ if __name__ == '__main__':
logger.info(
f'Done! Total {len(added_instance_ids)} instances added to {output_file}'
)
# Check if any instances reached maximum retries
check_maximum_retries_exceeded(metadata.eval_output_dir)

View File

@@ -203,6 +203,7 @@ def get_config(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace

View File

@@ -0,0 +1,227 @@
#!/usr/bin/env bash
set -eo pipefail
source "evaluation/utils/version_control.sh"
# Function to display usage information
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " --infer-dir DIR Directory containing model inference outputs"
echo " --split SPLIT SWE-Bench dataset split selection"
echo " --dataset DATASET Dataset name"
echo " --max-infer-turn NUM Max number of turns for coding agent"
echo " --align-with-max BOOL Align failed instance indices with max iteration (true/false)"
echo " -h, --help Display this help message"
echo ""
echo "Example:"
echo " $0 --infer-dir ./inference_outputs --split test --align-with-max false"
}
# Check if no arguments were provided
if [ $# -eq 0 ]; then
usage
exit 1
fi
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--infer-dir)
INFER_DIR="$2"
shift 2
;;
--split)
SPLIT="$2"
shift 2
;;
--dataset)
DATASET="$2"
shift 2
;;
--max-infer-turn)
MAX_TURN="$2"
shift 2
;;
--align-with-max)
ALIGN_WITH_MAX="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1"
usage
exit 1
;;
esac
done
# Check for required arguments (only INFER_DIR is required)
if [ -z "$INFER_DIR" ]; then
echo "Error: Missing required arguments (--infer-dir is required)"
usage
exit 1
fi
# Set defaults for optional arguments if not provided
if [ -z "$SPLIT" ]; then
SPLIT="test"
echo "Split not specified, using default: $SPLIT"
fi
if [ -z "$DATASET" ]; then
DATASET="princeton-nlp/SWE-bench_Verified"
echo "Dataset not specified, using default: $DATASET"
fi
if [ -z "$MAX_TURN" ]; then
MAX_TURN=20
echo "Max inference turn not specified, using default: $MAX_TURN"
fi
if [ -z "$ALIGN_WITH_MAX" ]; then
ALIGN_WITH_MAX="true"
echo "Align with max not specified, using default: $ALIGN_WITH_MAX"
fi
# Validate align-with-max value
if [ "$ALIGN_WITH_MAX" != "true" ] && [ "$ALIGN_WITH_MAX" != "false" ]; then
print_error "Invalid value for --align-with-max: $ALIGN_WITH_MAX. Must be 'true' or 'false'"
exit 1
fi
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_header() {
echo -e "${BLUE}[TASK]${NC} $1"
}
# Check if Python is available
print_header "Checking Python installation..."
if ! command -v python3 &> /dev/null; then
if ! command -v python &> /dev/null; then
print_error "Python is not installed or not in PATH"
exit 1
else
PYTHON_CMD="python"
print_status "Using python command"
fi
else
PYTHON_CMD="python3"
print_status "Using python3 command"
fi
# Check if the Python script exists
SCRIPT_NAME="./evaluation/benchmarks/swe_bench/loc_eval/loc_evaluator.py"
if [ ! -f "$SCRIPT_NAME" ]; then
print_error "Python script '$SCRIPT_NAME' not found in current directory"
print_warning "Make sure the Python script is in the same directory as this bash script"
exit 1
fi
# Check if required directories exist
print_header "Validating directories..."
if [ ! -d "$INFER_DIR" ]; then
print_error "Inference directory not found: $INFER_DIR"
exit 1
fi
# Evaluation outputs
EVAL_DIR="$INFER_DIR/eval_outputs"
# Display configuration
print_header "Starting Localization Evaluation with the following configuration:"
echo " Inference Directory: $INFER_DIR"
if [ -d "$EVAL_DIR" ]; then
echo " Evaluation Directory: $EVAL_DIR"
else
echo " Evaluation Directory: None (evaluation outputs doesn't exist)"
fi
echo " Output Directory: $INFER_DIR/loc_eval"
echo " Split: $SPLIT"
echo " Dataset: $DATASET"
echo " Max Turns: $MAX_TURN"
echo " Align with Max: $ALIGN_WITH_MAX"
echo " Python Command: $PYTHON_CMD"
echo ""
# Check Python dependencies (optional check)
print_header "Checking Python dependencies..."
$PYTHON_CMD -c "
import sys
required_modules = ['pandas', 'json', 'os', 'argparse', 'collections']
missing_modules = []
for module in required_modules:
try:
__import__(module)
except ImportError:
missing_modules.append(module)
if missing_modules:
print(f'Missing required modules: {missing_modules}')
sys.exit(1)
else:
print('All basic dependencies are available')
" || {
print_error "Some Python dependencies are missing"
print_warning "Please install required packages: pip install pandas"
exit 1
}
# Create log directory if doesn't exists
mkdir -p "$INFER_DIR/loc_eval"
# Set up logging
LOG_FILE="$INFER_DIR/loc_eval/loc_evaluation_$(date +%Y%m%d_%H%M%S).log"
print_status "Logging output to: $LOG_FILE"
# Build the command
CMD_ARGS="\"$SCRIPT_NAME\" \
--infer-dir \"$INFER_DIR\" \
--split \"$SPLIT\" \
--dataset \"$DATASET\" \
--max-infer-turn \"$MAX_TURN\" \
--align-with-max \"$ALIGN_WITH_MAX\""
# Run the Python script
print_header "Running localization evaluation..."
eval "$PYTHON_CMD $CMD_ARGS" 2>&1 | tee "$LOG_FILE"
# Check if the script ran successfully
if [ ${PIPESTATUS[0]} -eq 0 ]; then
print_status "Localization evaluation completed successfully!"
print_status "Results saved to: $INFER_DIR/loc_eval"
print_status "Log file: $LOG_FILE"
# Display summary if results exist
if [ -f "$INFER_DIR/loc_eval/loc_eval_results/loc_acc/overall_eval.json" ]; then
print_header "Evaluation Summary:"
cat "$INFER_DIR/loc_eval/loc_eval_results/loc_acc/overall_eval.json"
echo
fi
else
print_error "Localization evaluation failed!"
print_warning "Check the log file for details: $LOG_FILE"
exit 1
fi

View File

@@ -0,0 +1,31 @@
# Terminal-Bench Evaluation on OpenHands
Terminal-Bench has its own evaluation harness that is very different from OpenHands'. We
implemented [OpenHands agent](https://github.com/laude-institute/terminal-bench/tree/main/terminal_bench/agents/installed_agents/openhands) using OpenHands local runtime
inside terminal-bench framework. Hereby we introduce how to use the terminal-bench
harness to evaluate OpenHands.
## Installation
Terminal-bench ships a CLI tool to manage tasks and run evaluation.
Please follow official [Installation Doc](https://www.tbench.ai/docs/installation). You could also clone terminal-bench [source code](https://github.com/laude-institute/terminal-bench) and use `uv run tb` CLI.
## Evaluation
Please see [Terminal-Bench Leaderboard](https://www.tbench.ai/leaderboard) for the latest
instruction on benchmarking guidance. The dataset might evolve.
Sample command:
```bash
export LLM_BASE_URL=<optional base url>
export LLM_API_KEY=<llm key>
tb run \
--dataset-name terminal-bench-core \
--dataset-version 0.1.1 \
--agent openhands \
--model <model> \
--cleanup
```
You could run `tb --help` or `tb run --help` to learn more about their CLI.

View File

@@ -164,6 +164,7 @@ def get_config(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace

View File

@@ -25,7 +25,8 @@ class Test(BaseIntegrationTest):
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# git add
action = CmdRunAction(command='git add hello.py .vscode/')
cmd_str = 'git add hello.py'
action = CmdRunAction(command=cmd_str)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
@@ -40,15 +41,6 @@ class Test(BaseIntegrationTest):
reason=f'Failed to cat /workspace/hello.py: {obs.content}.',
)
# check if the file /workspace/.vscode/settings.json exists
action = CmdRunAction(command='cat /workspace/.vscode/settings.json')
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False,
reason=f'Failed to cat /workspace/.vscode/settings.json: {obs.content}.',
)
# check if the staging area is empty
action = CmdRunAction(command='git status')
obs = runtime.run_action(action)

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'
)
@@ -311,6 +311,76 @@ def assert_and_raise(condition: bool, msg: str):
raise EvalException(msg)
def log_skipped_maximum_retries_exceeded(instance, metadata, error, max_retries=5):
"""Log and skip the instance when maximum retries are exceeded.
Args:
instance: The instance that failed
metadata: The evaluation metadata
error: The error that occurred
max_retries: The maximum number of retries that were attempted
Returns:
EvalOutput with the error information
"""
from openhands.core.logger import openhands_logger as logger
# Log the error
logger.exception(error)
logger.error(
f'Maximum error retries reached for instance {instance.instance_id}. '
f'Check maximum_retries_exceeded.jsonl, fix the issue and run evaluation again. '
f'Skipping this instance and continuing with others.'
)
# Add the instance name to maximum_retries_exceeded.jsonl in the same folder as output.jsonl
if metadata and metadata.eval_output_dir:
retries_file_path = os.path.join(
metadata.eval_output_dir,
'maximum_retries_exceeded.jsonl',
)
try:
# Write the instance info as a JSON line
with open(retries_file_path, 'a') as f:
import json
# No need to get Docker image as we're not including it in the error entry
error_entry = {
'instance_id': instance.instance_id,
'error': str(error),
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
}
f.write(json.dumps(error_entry) + '\n')
logger.info(f'Added instance {instance.instance_id} to {retries_file_path}')
except Exception as write_error:
logger.error(
f'Failed to write to maximum_retries_exceeded.jsonl: {write_error}'
)
return EvalOutput(
instance_id=instance.instance_id,
test_result={},
error=f'Maximum retries ({max_retries}) reached: {str(error)}',
status='error',
)
def check_maximum_retries_exceeded(eval_output_dir):
"""Check if maximum_retries_exceeded.jsonl exists and output a message."""
from openhands.core.logger import openhands_logger as logger
retries_file_path = os.path.join(eval_output_dir, 'maximum_retries_exceeded.jsonl')
if os.path.exists(retries_file_path):
logger.info(
'ATTENTION: Some instances reached maximum error retries and were skipped.'
)
logger.info(f'These instances are listed in: {retries_file_path}')
logger.info(
'Fix these instances and run evaluation again with EVAL_SKIP_MAXIMUM_RETRIES_EXCEEDED=false'
)
def _process_instance_wrapper(
process_instance_func: Callable[[pd.Series, EvalMetadata, bool], EvalOutput],
instance: pd.Series,
@@ -363,11 +433,26 @@ def _process_instance_wrapper(
+ f'[Encountered after {max_retries} retries. Please check the logs and report the issue.]'
+ '-' * 10
)
# Raise an error after all retries & stop the evaluation
logger.exception(e)
raise RuntimeError(
f'Maximum error retries reached for instance {instance.instance_id}'
) from e
# Check if EVAL_SKIP_MAXIMUM_RETRIES_EXCEEDED is set to true
skip_errors = (
os.environ.get(
'EVAL_SKIP_MAXIMUM_RETRIES_EXCEEDED', 'false'
).lower()
== 'true'
)
if skip_errors:
# Use the dedicated function to log and skip maximum retries exceeded
return log_skipped_maximum_retries_exceeded(
instance, metadata, e, max_retries
)
else:
# Raise an error after all retries & stop the evaluation
logger.exception(e)
raise RuntimeError(
f'Maximum error retries reached for instance {instance.instance_id}'
) from e
msg = (
'-' * 10
+ '\n'
@@ -456,6 +541,10 @@ def run_evaluation(
output_fp.close()
logger.info('\nEvaluation finished.\n')
# Check if any instances reached maximum retries
if metadata and metadata.eval_output_dir:
check_maximum_retries_exceeded(metadata.eval_output_dir)
def reset_logger_for_multiprocessing(
logger: logging.Logger, instance_id: str, log_dir: str

View File

@@ -13,8 +13,9 @@
"plugin:react-hooks/recommended",
"plugin:@tanstack/query/recommended",
],
"plugins": ["prettier", "unused-imports"],
"plugins": ["prettier", "unused-imports", "i18next"],
"rules": {
"i18next/no-literal-string": "error",
"unused-imports/no-unused-imports": "error",
"prettier/prettier": ["error"],
// Resolves https://stackoverflow.com/questions/59265981/typescript-eslint-missing-file-extension-ts-import-extensions/59268871#59268871

View File

@@ -1,11 +1,11 @@
# Run frontend checks
echo "Running frontend checks..."
cd frontend
npm run check-unlocalized-strings
npm run lint
npm run check-translation-completeness
npx lint-staged
# Run backend pre-commit
echo "Running backend pre-commit..."
cd ..
pre-commit run --files openhands/**/* evaluation/**/* tests/**/* --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
poetry run pre-commit run --files openhands/**/* evaluation/**/* tests/**/* --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml

View File

@@ -10,9 +10,7 @@ describe("ChatMessage", () => {
expect(screen.getByText("Hello, World!")).toBeInTheDocument();
});
it.todo("should render an assistant message");
it.skip("should support code syntax highlighting", () => {
it("should support code syntax highlighting", () => {
const code = "```js\nconsole.log('Hello, World!')\n```";
render(<ChatMessage type="user" message={code} />);
@@ -46,8 +44,6 @@ describe("ChatMessage", () => {
);
});
it("should display an error toast if copying content to clipboard fails", async () => {});
it("should render a component passed as a prop", () => {
function Component() {
return <div data-testid="custom-component">Custom Component</div>;

View File

@@ -129,4 +129,159 @@ describe("ActionSuggestions", () => {
expect(createPRPrompt).toContain("meaningful branch name");
expect(createPRPrompt).not.toContain("SAME branch name");
});
it("should use correct provider name based on conversation git_provider, not user authenticated providers", async () => {
// Test case for GitHub repository
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
getConversationSpy.mockResolvedValue({
conversation_id: "test-github",
title: "GitHub Test",
selected_repository: "test-repo",
git_provider: "github",
selected_branch: "main",
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",
runtime_status: "STATUS$READY",
url: null,
session_api_key: null,
});
// Mock user having both GitHub and Bitbucket tokens
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: {
github: "github-token",
bitbucket: "bitbucket-token",
},
});
const onSuggestionsClick = vi.fn();
render(<ActionSuggestions onSuggestionsClick={onSuggestionsClick} />, {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
),
});
const buttons = await screen.findAllByTestId("suggestion");
const prButton = buttons.find((button) =>
button.textContent?.includes("Push & Create PR"),
);
expect(prButton).toBeInTheDocument();
if (prButton) {
prButton.click();
}
// The suggestion should mention GitHub, not Bitbucket
expect(onSuggestionsClick).toHaveBeenCalledWith(
expect.stringContaining("GitHub")
);
expect(onSuggestionsClick).not.toHaveBeenCalledWith(
expect.stringContaining("Bitbucket")
);
});
it("should use GitLab terminology when git_provider is gitlab", async () => {
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
getConversationSpy.mockResolvedValue({
conversation_id: "test-gitlab",
title: "GitLab Test",
selected_repository: "test-repo",
git_provider: "gitlab",
selected_branch: "main",
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",
runtime_status: "STATUS$READY",
url: null,
session_api_key: null,
});
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: {
gitlab: "gitlab-token",
},
});
const onSuggestionsClick = vi.fn();
render(<ActionSuggestions onSuggestionsClick={onSuggestionsClick} />, {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
),
});
const buttons = await screen.findAllByTestId("suggestion");
const prButton = buttons.find((button) =>
button.textContent?.includes("Push & Create PR"),
);
if (prButton) {
prButton.click();
}
// Should mention GitLab and "merge request" instead of "pull request"
expect(onSuggestionsClick).toHaveBeenCalledWith(
expect.stringContaining("GitLab")
);
expect(onSuggestionsClick).toHaveBeenCalledWith(
expect.stringContaining("merge request")
);
});
it("should use Bitbucket terminology when git_provider is bitbucket", async () => {
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
getConversationSpy.mockResolvedValue({
conversation_id: "test-bitbucket",
title: "Bitbucket Test",
selected_repository: "test-repo",
git_provider: "bitbucket",
selected_branch: "main",
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",
runtime_status: "STATUS$READY",
url: null,
session_api_key: null,
});
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: {
bitbucket: "bitbucket-token",
},
});
const onSuggestionsClick = vi.fn();
render(<ActionSuggestions onSuggestionsClick={onSuggestionsClick} />, {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
),
});
const buttons = await screen.findAllByTestId("suggestion");
const prButton = buttons.find((button) =>
button.textContent?.includes("Push & Create PR"),
);
if (prButton) {
prButton.click();
}
// Should mention Bitbucket
expect(onSuggestionsClick).toHaveBeenCalledWith(
expect.stringContaining("Bitbucket")
);
});
});

View File

@@ -28,7 +28,6 @@ describe("EventMessage", () => {
action: "finish" as const,
args: {
final_thought: "Task completed successfully",
task_completed: "success" as const,
outputs: {},
thought: "Task completed successfully",
},
@@ -114,7 +113,6 @@ describe("EventMessage", () => {
action: "finish" as const,
args: {
final_thought: "Task completed successfully",
task_completed: "success" as const,
outputs: {},
thought: "Task completed successfully",
},

View File

@@ -19,7 +19,13 @@ describe("AuthModal", () => {
});
it("should render the GitHub and GitLab buttons", () => {
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
render(
<AuthModal
githubAuthUrl="mock-url"
appMode="saas"
providersConfigured={["github", "gitlab"]}
/>,
);
const githubButton = screen.getByRole("button", {
name: "GITHUB$CONNECT_TO_GITHUB",
@@ -35,7 +41,13 @@ describe("AuthModal", () => {
it("should redirect to GitHub auth URL when GitHub button is clicked", async () => {
const user = userEvent.setup();
const mockUrl = "https://github.com/login/oauth/authorize";
render(<AuthModal githubAuthUrl={mockUrl} appMode="saas" />);
render(
<AuthModal
githubAuthUrl={mockUrl}
appMode="saas"
providersConfigured={["github"]}
/>,
);
const githubButton = screen.getByRole("button", {
name: "GITHUB$CONNECT_TO_GITHUB",
@@ -44,4 +56,63 @@ describe("AuthModal", () => {
expect(window.location.href).toBe(mockUrl);
});
it("should render Terms of Service and Privacy Policy text with correct links", () => {
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
// Find the terms of service section using data-testid
const termsSection = screen.getByTestId("auth-modal-terms-of-service");
expect(termsSection).toBeInTheDocument();
// Check that all text content is present in the paragraph
expect(termsSection).toHaveTextContent(
"AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR",
);
expect(termsSection).toHaveTextContent("COMMON$TERMS_OF_SERVICE");
expect(termsSection).toHaveTextContent("COMMON$AND");
expect(termsSection).toHaveTextContent("COMMON$PRIVACY_POLICY");
// Check Terms of Service link
const tosLink = screen.getByRole("link", {
name: "COMMON$TERMS_OF_SERVICE",
});
expect(tosLink).toBeInTheDocument();
expect(tosLink).toHaveAttribute("href", "https://www.all-hands.dev/tos");
expect(tosLink).toHaveAttribute("target", "_blank");
expect(tosLink).toHaveClass("underline", "hover:text-primary");
// Check Privacy Policy link
const privacyLink = screen.getByRole("link", {
name: "COMMON$PRIVACY_POLICY",
});
expect(privacyLink).toBeInTheDocument();
expect(privacyLink).toHaveAttribute(
"href",
"https://www.all-hands.dev/privacy",
);
expect(privacyLink).toHaveAttribute("target", "_blank");
expect(privacyLink).toHaveClass("underline", "hover:text-primary");
// Verify that both links are within the terms section
expect(termsSection).toContainElement(tosLink);
expect(termsSection).toContainElement(privacyLink);
});
it("should open Terms of Service link in new tab", () => {
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
const tosLink = screen.getByRole("link", {
name: "COMMON$TERMS_OF_SERVICE",
});
expect(tosLink).toHaveAttribute("target", "_blank");
});
it("should open Privacy Policy link in new tab", () => {
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
const privacyLink = screen.getByRole("link", {
name: "COMMON$PRIVACY_POLICY",
});
expect(privacyLink).toHaveAttribute("target", "_blank");
});
});

View File

@@ -0,0 +1,167 @@
import { render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { LaunchMicroagentModal } from "#/components/features/chat/microagent/launch-microagent-modal";
import { MemoryService } from "#/api/memory-service/memory-service.api";
import { FileService } from "#/api/file-service/file-service.api";
import { I18nKey } from "#/i18n/declaration";
vi.mock("react-router", async () => ({
useParams: vi.fn().mockReturnValue({
conversationId: "123",
}),
}));
// Mock the useHandleRuntimeActive hook
vi.mock("#/hooks/use-handle-runtime-active", () => ({
useHandleRuntimeActive: vi.fn().mockReturnValue({ runtimeActive: true }),
}));
// Mock the useMicroagentPrompt hook
vi.mock("#/hooks/query/use-microagent-prompt", () => ({
useMicroagentPrompt: vi.fn().mockReturnValue({
data: "Generated prompt",
isLoading: false
}),
}));
// Mock the useGetMicroagents hook
vi.mock("#/hooks/query/use-get-microagents", () => ({
useGetMicroagents: vi.fn().mockReturnValue({
data: ["file1", "file2"]
}),
}));
// Mock the useTranslation hook
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
[I18nKey.MICROAGENT$ADD_TO_MICROAGENT]: "Add to Microagent",
[I18nKey.MICROAGENT$WHAT_TO_REMEMBER]: "What would you like your microagent to remember?",
[I18nKey.MICROAGENT$WHERE_TO_PUT]: "Where should we put it?",
[I18nKey.MICROAGENT$ADD_TRIGGERS]: "Add triggers for the microagent",
[I18nKey.MICROAGENT$DESCRIBE_WHAT_TO_ADD]: "Describe what you want to add to the Microagent...",
[I18nKey.MICROAGENT$SELECT_FILE_OR_CUSTOM]: "Select a microagent file or enter a custom value",
[I18nKey.MICROAGENT$TYPE_TRIGGER_SPACE]: "Type a trigger and press Space to add it",
[I18nKey.MICROAGENT$LOADING_PROMPT]: "Loading prompt...",
[I18nKey.MICROAGENT$CANCEL]: "Cancel",
[I18nKey.MICROAGENT$LAUNCH]: "Launch"
};
return translations[key] || key;
},
i18n: {
changeLanguage: vi.fn(),
},
}),
Trans: ({ i18nKey }: { i18nKey: string }) => i18nKey,
}));
describe("LaunchMicroagentModal", () => {
const onCloseMock = vi.fn();
const onLaunchMock = vi.fn();
const eventId = 12;
const conversationId = "123";
const renderMicroagentModal = (
{ isLoading }: { isLoading: boolean } = { isLoading: false },
) =>
render(
<LaunchMicroagentModal
onClose={onCloseMock}
onLaunch={onLaunchMock}
eventId={eventId}
selectedRepo="some-repo"
isLoading={isLoading}
/>,
{
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
),
},
);
afterEach(() => {
vi.clearAllMocks();
});
it("should render the launch microagent modal", () => {
renderMicroagentModal();
expect(screen.getByTestId("launch-microagent-modal")).toBeInTheDocument();
});
it("should render the form fields", () => {
renderMicroagentModal();
// inputs
screen.getByTestId("query-input");
screen.getByTestId("target-input");
screen.getByTestId("trigger-input");
// action buttons
screen.getByRole("button", { name: "Launch" });
screen.getByRole("button", { name: "Cancel" });
});
it("should call onClose when pressing the cancel button", async () => {
renderMicroagentModal();
const cancelButton = screen.getByRole("button", { name: "Cancel" });
await userEvent.click(cancelButton);
expect(onCloseMock).toHaveBeenCalled();
});
it("should display the prompt from the hook", async () => {
renderMicroagentModal();
// Since we're mocking the hook, we just need to verify the UI shows the data
const descriptionInput = screen.getByTestId("query-input");
expect(descriptionInput).toHaveValue("Generated prompt");
});
it("should display the list of microagent files from the hook", async () => {
renderMicroagentModal();
// Since we're mocking the hook, we just need to verify the UI shows the data
const targetInput = screen.getByTestId("target-input");
expect(targetInput).toHaveValue("");
await userEvent.click(targetInput);
expect(screen.getByText("file1")).toBeInTheDocument();
expect(screen.getByText("file2")).toBeInTheDocument();
await userEvent.click(screen.getByText("file1"));
expect(targetInput).toHaveValue("file1");
});
it("should call onLaunch with the form data", async () => {
renderMicroagentModal();
const triggerInput = screen.getByTestId("trigger-input");
await userEvent.type(triggerInput, "trigger1 ");
await userEvent.type(triggerInput, "trigger2 ");
const targetInput = screen.getByTestId("target-input");
await userEvent.click(targetInput);
await userEvent.click(screen.getByText("file1"));
const launchButton = await screen.findByRole("button", { name: "Launch" });
await userEvent.click(launchButton);
expect(onLaunchMock).toHaveBeenCalledWith("Generated prompt", "file1", [
"trigger1",
"trigger2",
]);
});
it("should disable the launch button if isLoading is true", async () => {
renderMicroagentModal({ isLoading: true });
const launchButton = screen.getByRole("button", { name: "Launch" });
expect(launchButton).toBeDisabled();
});
});

View File

@@ -0,0 +1,107 @@
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Messages } from "#/components/features/chat/messages";
import {
AssistantMessageAction,
OpenHandsAction,
UserMessageAction,
} from "#/types/core/actions";
import { OpenHandsObservation } from "#/types/core/observations";
import OpenHands from "#/api/open-hands";
import { Conversation } from "#/api/open-hands.types";
vi.mock("react-router", () => ({
useParams: () => ({ conversationId: "123" }),
}));
let queryClient: QueryClient;
const renderMessages = ({
messages,
}: {
messages: (OpenHandsAction | OpenHandsObservation)[];
}) => {
const { rerender, ...rest } = render(
<Messages messages={messages} isAwaitingUserConfirmation={false} />,
{
wrapper: ({ children }) => (
<QueryClientProvider client={queryClient!}>
{children}
</QueryClientProvider>
),
},
);
const rerenderMessages = (
newMessages: (OpenHandsAction | OpenHandsObservation)[],
) => {
rerender(
<Messages messages={newMessages} isAwaitingUserConfirmation={false} />,
);
};
return { ...rest, rerender: rerenderMessages };
};
describe("Messages", () => {
beforeEach(() => {
queryClient = new QueryClient();
});
const assistantMessage: AssistantMessageAction = {
id: 0,
action: "message",
source: "agent",
message: "Hello, Assistant!",
timestamp: new Date().toISOString(),
args: {
image_urls: [],
file_urls: [],
thought: "",
wait_for_response: false,
},
};
const userMessage: UserMessageAction = {
id: 1,
action: "message",
source: "user",
message: "Hello, User!",
timestamp: new Date().toISOString(),
args: { content: "Hello, User!", image_urls: [], file_urls: [] },
};
it("should render", () => {
renderMessages({ messages: [userMessage, assistantMessage] });
expect(screen.getByText("Hello, User!")).toBeInTheDocument();
expect(screen.getByText("Hello, Assistant!")).toBeInTheDocument();
});
it("should render a launch to microagent action button on chat messages only if it is a user message", () => {
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
const mockConversation: Conversation = {
conversation_id: "123",
title: "Test Conversation",
status: "RUNNING",
runtime_status: "STATUS$READY",
created_at: new Date().toISOString(),
last_updated_at: new Date().toISOString(),
selected_branch: null,
selected_repository: null,
git_provider: "github",
session_api_key: null,
url: null,
};
getConversationSpy.mockResolvedValue(mockConversation);
renderMessages({
messages: [userMessage, assistantMessage],
});
expect(screen.getByText("Hello, User!")).toBeInTheDocument();
expect(screen.getByText("Hello, Assistant!")).toBeInTheDocument();
});
});

View File

@@ -16,8 +16,6 @@ import { ConversationCard } from "#/components/features/conversation-panel/conve
import { clickOnEditButton } from "./utils";
// We'll use the actual i18next implementation but override the translation function
import { I18nextProvider } from "react-i18next";
import i18n from "i18next";
// Mock the t function to return our custom translations
vi.mock("react-i18next", async () => {
@@ -27,9 +25,9 @@ vi.mock("react-i18next", async () => {
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
"CONVERSATION$CREATED": "Created",
"CONVERSATION$AGO": "ago",
"CONVERSATION$UPDATED": "Updated"
CONVERSATION$CREATED: "Created",
CONVERSATION$AGO: "ago",
CONVERSATION$UPDATED: "Updated",
};
return translations[key] || key;
},
@@ -82,7 +80,9 @@ describe("ConversationCard", () => {
expect(card).toHaveTextContent("ago");
// Use a regex to match the time part since it might have whitespace
const timeRegex = new RegExp(formatTimeDelta(new Date("2021-10-01T12:00:00Z")));
const timeRegex = new RegExp(
formatTimeDelta(new Date("2021-10-01T12:00:00Z")),
);
expect(card).toHaveTextContent(timeRegex);
});
@@ -108,7 +108,11 @@ describe("ConversationCard", () => {
onChangeTitle={onChangeTitle}
isActive
title="Conversation 1"
selectedRepository="org/selectedRepository"
selectedRepository={{
selected_repository: "org/selectedRepository",
selected_branch: "main",
git_provider: "github",
}}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
@@ -118,7 +122,8 @@ describe("ConversationCard", () => {
it("should toggle a context menu when clicking the ellipsis button", async () => {
const user = userEvent.setup();
renderWithProviders(
const onContextMenuToggle = vi.fn();
const { rerender } = renderWithProviders(
<ConversationCard
onDelete={onDelete}
onChangeTitle={onChangeTitle}
@@ -126,6 +131,8 @@ describe("ConversationCard", () => {
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
contextMenuOpen={false}
onContextMenuToggle={onContextMenuToggle}
/>,
);
@@ -134,15 +141,32 @@ describe("ConversationCard", () => {
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
expect(onContextMenuToggle).toHaveBeenCalledWith(true);
// Simulate context menu being opened by parent
rerender(
<ConversationCard
onDelete={onDelete}
onChangeTitle={onChangeTitle}
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
contextMenuOpen
onContextMenuToggle={onContextMenuToggle}
/>,
);
screen.getByTestId("context-menu");
await user.click(ellipsisButton);
expect(screen.queryByTestId("context-menu")).not.toBeInTheDocument();
expect(onContextMenuToggle).toHaveBeenCalledWith(false);
});
it("should call onDelete when the delete button is clicked", async () => {
const user = userEvent.setup();
const onContextMenuToggle = vi.fn();
renderWithProviders(
<ConversationCard
onDelete={onDelete}
@@ -151,18 +175,18 @@ describe("ConversationCard", () => {
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
contextMenuOpen
onContextMenuToggle={onContextMenuToggle}
/>,
);
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const menu = screen.getByTestId("context-menu");
const deleteButton = within(menu).getByTestId("delete-button");
await user.click(deleteButton);
expect(onDelete).toHaveBeenCalled();
expect(onContextMenuToggle).toHaveBeenCalledWith(false);
});
test("clicking the selectedRepository should not trigger the onClick handler", async () => {
@@ -173,7 +197,11 @@ describe("ConversationCard", () => {
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository="org/selectedRepository"
selectedRepository={{
selected_repository: "org/selectedRepository",
selected_branch: "main",
git_provider: "github",
}}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
@@ -188,7 +216,11 @@ describe("ConversationCard", () => {
test("conversation title should call onChangeTitle when changed and blurred", async () => {
const user = userEvent.setup();
renderWithProviders(
let menuOpen = true;
const onContextMenuToggle = vi.fn((isOpen: boolean) => {
menuOpen = isOpen;
});
const { rerender } = renderWithProviders(
<ConversationCard
onDelete={onDelete}
isActive
@@ -196,10 +228,27 @@ describe("ConversationCard", () => {
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
onChangeTitle={onChangeTitle}
contextMenuOpen={menuOpen}
onContextMenuToggle={onContextMenuToggle}
/>,
);
await clickOnEditButton(user);
// Re-render with updated state
rerender(
<ConversationCard
onDelete={onDelete}
isActive
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
onChangeTitle={onChangeTitle}
contextMenuOpen={menuOpen}
onContextMenuToggle={onContextMenuToggle}
/>,
);
const title = screen.getByTestId("conversation-card-title");
expect(title).toBeEnabled();
@@ -217,6 +266,7 @@ describe("ConversationCard", () => {
it("should reset title and not call onChangeTitle when the title is empty", async () => {
const user = userEvent.setup();
const onContextMenuToggle = vi.fn();
renderWithProviders(
<ConversationCard
onDelete={onDelete}
@@ -225,6 +275,8 @@ describe("ConversationCard", () => {
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
contextMenuOpen
onContextMenuToggle={onContextMenuToggle}
/>,
);
@@ -261,6 +313,7 @@ describe("ConversationCard", () => {
test("clicking the title should not trigger the onClick handler if edit mode", async () => {
const user = userEvent.setup();
const onContextMenuToggle = vi.fn();
renderWithProviders(
<ConversationCard
onDelete={onDelete}
@@ -269,6 +322,8 @@ describe("ConversationCard", () => {
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
contextMenuOpen
onContextMenuToggle={onContextMenuToggle}
/>,
);
@@ -282,6 +337,7 @@ describe("ConversationCard", () => {
test("clicking the delete button should not trigger the onClick handler", async () => {
const user = userEvent.setup();
const onContextMenuToggle = vi.fn();
renderWithProviders(
<ConversationCard
onDelete={onDelete}
@@ -290,12 +346,11 @@ describe("ConversationCard", () => {
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
contextMenuOpen
onContextMenuToggle={onContextMenuToggle}
/>,
);
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const menu = screen.getByTestId("context-menu");
const deleteButton = within(menu).getByTestId("delete-button");
@@ -305,7 +360,7 @@ describe("ConversationCard", () => {
});
it("should show display cost button only when showOptions is true", async () => {
const user = userEvent.setup();
const onContextMenuToggle = vi.fn();
const { rerender } = renderWithProviders(
<ConversationCard
onDelete={onDelete}
@@ -314,21 +369,17 @@ describe("ConversationCard", () => {
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
contextMenuOpen
onContextMenuToggle={onContextMenuToggle}
/>,
);
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
// Wait for context menu to appear
const menu = await screen.findByTestId("context-menu");
expect(
within(menu).queryByTestId("display-cost-button"),
).not.toBeInTheDocument();
// Close menu
await user.click(ellipsisButton);
rerender(
<ConversationCard
onDelete={onDelete}
@@ -338,12 +389,11 @@ describe("ConversationCard", () => {
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
contextMenuOpen
onContextMenuToggle={onContextMenuToggle}
/>,
);
// Open menu again
await user.click(ellipsisButton);
// Wait for context menu to appear and check for display cost button
const newMenu = await screen.findByTestId("context-menu");
within(newMenu).getByTestId("display-cost-button");
@@ -351,6 +401,7 @@ describe("ConversationCard", () => {
it("should show metrics modal when clicking the display cost button", async () => {
const user = userEvent.setup();
const onContextMenuToggle = vi.fn();
renderWithProviders(
<ConversationCard
onDelete={onDelete}
@@ -360,12 +411,11 @@ describe("ConversationCard", () => {
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
showOptions
contextMenuOpen
onContextMenuToggle={onContextMenuToggle}
/>,
);
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const menu = screen.getByTestId("context-menu");
const displayCostButton = within(menu).getByTestId("display-cost-button");
@@ -376,7 +426,7 @@ describe("ConversationCard", () => {
});
it("should not display the edit or delete options if the handler is not provided", async () => {
const user = userEvent.setup();
const onContextMenuToggle = vi.fn();
const { rerender } = renderWithProviders(
<ConversationCard
onClick={onClick}
@@ -384,19 +434,15 @@ describe("ConversationCard", () => {
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
contextMenuOpen
onContextMenuToggle={onContextMenuToggle}
/>,
);
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const menu = await screen.findByTestId("context-menu");
expect(within(menu).queryByTestId("edit-button")).toBeInTheDocument();
expect(within(menu).queryByTestId("delete-button")).not.toBeInTheDocument();
// toggle to hide the context menu
await user.click(ellipsisButton);
rerender(
<ConversationCard
onClick={onClick}
@@ -404,10 +450,11 @@ describe("ConversationCard", () => {
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
contextMenuOpen
onContextMenuToggle={onContextMenuToggle}
/>,
);
await user.click(ellipsisButton);
const newMenu = await screen.findByTestId("context-menu");
expect(
within(newMenu).queryByTestId("edit-button"),

View File

@@ -295,4 +295,521 @@ describe("ConversationPanel", () => {
const newCards = await screen.findAllByTestId("conversation-card");
expect(newCards).toHaveLength(3);
});
it("should cancel stopping a conversation", async () => {
const user = userEvent.setup();
// Create mock data with a RUNNING conversation
const mockRunningConversations: Conversation[] = [
{
conversation_id: "1",
title: "Running Conversation",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "RUNNING" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
{
conversation_id: "2",
title: "Starting Conversation",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-02T12:00:00Z",
created_at: "2021-10-02T12:00:00Z",
status: "STARTING" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
{
conversation_id: "3",
title: "Stopped Conversation",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-03T12:00:00Z",
created_at: "2021-10-03T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockResolvedValue(mockRunningConversations);
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
expect(cards).toHaveLength(3);
// Click ellipsis on the first card (RUNNING status)
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
// Stop button should be available for RUNNING conversation
const stopButton = screen.getByTestId("stop-button");
expect(stopButton).toBeInTheDocument();
// Click the stop button
await user.click(stopButton);
// Cancel the stopping action
const cancelButton = screen.getByRole("button", { name: /cancel/i });
await user.click(cancelButton);
expect(
screen.queryByRole("button", { name: /cancel/i }),
).not.toBeInTheDocument();
// Ensure the conversation status hasn't changed
const updatedCards = await screen.findAllByTestId("conversation-card");
expect(updatedCards).toHaveLength(3);
});
it("should stop a conversation", async () => {
const user = userEvent.setup();
const mockData: Conversation[] = [
{
conversation_id: "1",
title: "Running Conversation",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "RUNNING" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
{
conversation_id: "2",
title: "Starting Conversation",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-02T12:00:00Z",
created_at: "2021-10-02T12:00:00Z",
status: "STARTING" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockImplementation(async () => mockData);
const stopConversationSpy = vi.spyOn(OpenHands, "stopConversation");
stopConversationSpy.mockImplementation(async (id: string) => {
const conversation = mockData.find((conv) => conv.conversation_id === id);
if (conversation) {
conversation.status = "STOPPED";
return conversation;
}
return null;
});
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
expect(cards).toHaveLength(2);
// Click ellipsis on the first card (RUNNING status)
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const stopButton = screen.getByTestId("stop-button");
// Click the stop button
await user.click(stopButton);
// Confirm the stopping action
const confirmButton = screen.getByRole("button", { name: /confirm/i });
await user.click(confirmButton);
expect(
screen.queryByRole("button", { name: /confirm/i }),
).not.toBeInTheDocument();
// Verify the API was called
expect(stopConversationSpy).toHaveBeenCalledWith("1");
expect(stopConversationSpy).toHaveBeenCalledTimes(1);
});
it("should only show stop button for STARTING or RUNNING conversations", async () => {
const user = userEvent.setup();
const mockMixedStatusConversations: Conversation[] = [
{
conversation_id: "1",
title: "Running Conversation",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "RUNNING" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
{
conversation_id: "2",
title: "Starting Conversation",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-02T12:00:00Z",
created_at: "2021-10-02T12:00:00Z",
status: "STARTING" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
{
conversation_id: "3",
title: "Stopped Conversation",
selected_repository: null,
git_provider: null,
selected_branch: null,
last_updated_at: "2021-10-03T12:00:00Z",
created_at: "2021-10-03T12:00:00Z",
status: "STOPPED" as const,
runtime_status: null,
url: null,
session_api_key: null,
},
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockResolvedValue(mockMixedStatusConversations);
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
expect(cards).toHaveLength(3);
// Test RUNNING conversation - should show stop button
const runningEllipsisButton = within(cards[0]).getByTestId(
"ellipsis-button",
);
await user.click(runningEllipsisButton);
expect(screen.getByTestId("stop-button")).toBeInTheDocument();
// Click outside to close the menu
await user.click(document.body);
// Test STARTING conversation - should show stop button
const startingEllipsisButton = within(cards[1]).getByTestId(
"ellipsis-button",
);
await user.click(startingEllipsisButton);
expect(screen.getByTestId("stop-button")).toBeInTheDocument();
// Click outside to close the menu
await user.click(document.body);
// Test STOPPED conversation - should NOT show stop button
const stoppedEllipsisButton = within(cards[2]).getByTestId(
"ellipsis-button",
);
await user.click(stoppedEllipsisButton);
expect(screen.queryByTestId("stop-button")).not.toBeInTheDocument();
});
it("should show edit button in context menu", async () => {
const user = userEvent.setup();
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
expect(cards).toHaveLength(3);
// Click ellipsis to open context menu
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
// Edit button should be visible
const editButton = screen.getByTestId("edit-button");
expect(editButton).toBeInTheDocument();
expect(editButton).toHaveTextContent("BUTTON$EDIT_TITLE");
});
it("should enter edit mode when edit button is clicked", async () => {
const user = userEvent.setup();
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Click ellipsis to open context menu
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
// Click edit button
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Should find input field instead of title text
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
expect(titleInput).toBeInTheDocument();
expect(titleInput.tagName).toBe("INPUT");
expect(titleInput).toHaveValue("Conversation 1");
expect(titleInput).toHaveFocus();
});
it("should successfully update conversation title", async () => {
const user = userEvent.setup();
// Mock the updateConversation API call
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockResolvedValue(true);
// Mock the toast function
const mockToast = vi.fn();
vi.mock("#/utils/custom-toast-handlers", () => ({
displaySuccessToast: mockToast,
}));
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Edit the title
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.clear(titleInput);
await user.type(titleInput, "Updated Title");
// Blur the input to save
await user.tab();
// Verify API call was made with correct parameters
expect(updateConversationSpy).toHaveBeenCalledWith("1", {
title: "Updated Title",
});
});
it("should save title when Enter key is pressed", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Edit the title and press Enter
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.clear(titleInput);
await user.type(titleInput, "Title Updated via Enter");
await user.keyboard("{Enter}");
// Verify API call was made
expect(updateConversationSpy).toHaveBeenCalledWith("1", {
title: "Title Updated via Enter",
});
});
it("should trim whitespace from title", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Edit the title with extra whitespace
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.clear(titleInput);
await user.type(titleInput, " Trimmed Title ");
await user.tab();
// Verify API call was made with trimmed title
expect(updateConversationSpy).toHaveBeenCalledWith("1", {
title: "Trimmed Title",
});
// Verify input shows trimmed value
expect(titleInput).toHaveValue("Trimmed Title");
});
it("should revert to original title when empty", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Clear the title completely
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.clear(titleInput);
await user.tab();
// Verify API was not called
expect(updateConversationSpy).not.toHaveBeenCalled();
// Verify input reverted to original value
expect(titleInput).toHaveValue("Conversation 1");
});
it("should handle API error when updating title", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockRejectedValue(new Error("API Error"));
vi.mock("#/utils/custom-toast-handlers", () => ({
displayErrorToast: vi.fn(),
}));
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Edit the title
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.clear(titleInput);
await user.type(titleInput, "Failed Update");
await user.tab();
// Verify API call was made
expect(updateConversationSpy).toHaveBeenCalledWith("1", {
title: "Failed Update",
});
// Wait for error handling
await waitFor(() => {
expect(updateConversationSpy).toHaveBeenCalled();
});
});
it("should close context menu when edit button is clicked", async () => {
const user = userEvent.setup();
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Click ellipsis to open context menu
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
// Verify context menu is open
const contextMenu = screen.getByTestId("context-menu");
expect(contextMenu).toBeInTheDocument();
// Click edit button
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Verify context menu is closed
expect(screen.queryByTestId("context-menu")).not.toBeInTheDocument();
});
it("should not call API when title is unchanged", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Don't change the title, just blur
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.tab();
// Verify API was called with the same title (since handleConversationTitleChange will always be called)
expect(updateConversationSpy).toHaveBeenCalledWith("1", {
title: "Conversation 1",
});
});
it("should handle special characters in title", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Edit the title with special characters
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.clear(titleInput);
await user.type(titleInput, "Special @#$%^&*()_+ Characters");
await user.tab();
// Verify API call was made with special characters
expect(updateConversationSpy).toHaveBeenCalledWith("1", {
title: "Special @#$%^&*()_+ Characters",
});
});
});

View File

@@ -17,12 +17,12 @@ vi.mock("react-i18next", async () => {
t: (key: string) => {
// Return a mock translation for the test
const translations: Record<string, string> = {
"HOME$LETS_START_BUILDING": "Let's start building",
"HOME$LAUNCH_FROM_SCRATCH": "Launch from Scratch",
"HOME$LOADING": "Loading...",
"HOME$OPENHANDS_DESCRIPTION": "OpenHands is an AI software engineer",
"HOME$NOT_SURE_HOW_TO_START": "Not sure how to start?",
"HOME$READ_THIS": "Read this"
HOME$LETS_START_BUILDING: "Let's start building",
HOME$LAUNCH_FROM_SCRATCH: "Launch from Scratch",
HOME$LOADING: "Loading...",
HOME$OPENHANDS_DESCRIPTION: "OpenHands is an AI software engineer",
HOME$NOT_SURE_HOW_TO_START: "Not sure how to start?",
HOME$READ_THIS: "Read this",
};
return translations[key] || key;
},
@@ -69,7 +69,7 @@ describe("HomeHeader", () => {
undefined,
undefined,
undefined,
[],
undefined,
undefined,
undefined,
undefined,

View File

@@ -119,16 +119,46 @@ describe("RepoConnector", () => {
expect(launchButton).toBeEnabled();
});
it("should render the 'add git(hub|lab) repos' links if saas mode", async () => {
it("should render the 'add github repos' link if saas mode and github provider is set", async () => {
const getConfiSpy = vi.spyOn(OpenHands, "getConfig");
// @ts-expect-error - only return the APP_MODE
getConfiSpy.mockResolvedValue({
APP_MODE: "saas",
});
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: {
github: "some-token",
gitlab: null,
},
});
renderRepoConnector();
await screen.findByText("Add GitHub repos");
await screen.findByText("HOME$ADD_GITHUB_REPOS");
});
it("should not render the 'add github repos' link if github provider is not set", async () => {
const getConfiSpy = vi.spyOn(OpenHands, "getConfig");
// @ts-expect-error - only return the APP_MODE
getConfiSpy.mockResolvedValue({
APP_MODE: "saas",
});
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: {
gitlab: "some-token",
github: null,
},
});
renderRepoConnector();
expect(screen.queryByText("HOME$ADD_GITHUB_REPOS")).not.toBeInTheDocument();
});
it("should not render the 'add git(hub|lab) repos' links if oss mode", async () => {
@@ -176,8 +206,8 @@ describe("RepoConnector", () => {
"rbren/polaris",
"github",
undefined,
[],
undefined,
"main",
undefined,
undefined,
);

View File

@@ -66,6 +66,11 @@ vi.mock("#/hooks/use-debounce", () => ({
useDebounce: (value: string) => value,
}));
vi.mock("react-router", async (importActual) => ({
...(await importActual()),
useNavigate: vi.fn(),
}));
const mockOnRepoSelection = vi.fn();
const renderForm = () =>
render(<RepositorySelectionForm onRepoSelection={mockOnRepoSelection} />, {
@@ -252,8 +257,6 @@ describe("RepositorySelectionForm", () => {
expect(searchedRepo).toBeInTheDocument();
await userEvent.click(searchedRepo);
expect(mockOnRepoSelection).toHaveBeenCalledWith(
MOCK_SEARCH_REPOS[0].full_name,
);
expect(mockOnRepoSelection).toHaveBeenCalledWith(MOCK_SEARCH_REPOS[0]);
});
});

View File

@@ -88,9 +88,15 @@ describe("TaskCard", () => {
MOCK_RESPOSITORIES[0].full_name,
MOCK_RESPOSITORIES[0].git_provider,
undefined,
[],
{
git_provider: "github",
issue_number: 123,
repo: "repo1",
task_type: "MERGE_CONFLICTS",
title: "Task 1",
},
undefined,
undefined,
MOCK_TASK_1,
undefined,
);
});
@@ -105,4 +111,29 @@ describe("TaskCard", () => {
expect(launchButton).toHaveTextContent(/Loading/i);
expect(launchButton).toBeDisabled();
});
it("should navigate to the conversation page after creating a conversation", async () => {
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
createConversationSpy.mockResolvedValue({
conversation_id: "test-conversation-id",
title: "Test Conversation",
selected_repository: "repo1",
selected_branch: "main",
git_provider: "github",
last_updated_at: "2023-01-01T00:00:00Z",
created_at: "2023-01-01T00:00:00Z",
status: "RUNNING",
runtime_status: "STATUS$READY",
url: null,
session_api_key: null
});
renderTaskCard();
const launchButton = screen.getByTestId("task-launch-button");
await userEvent.click(launchButton);
// Wait for navigation to the conversation page
await screen.findByTestId("conversation-screen");
});
});

View File

@@ -1,4 +1,4 @@
import { render, screen, waitFor } from "@testing-library/react";
import { render, screen, waitFor, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Provider } from "react-redux";
@@ -7,6 +7,21 @@ import { setupStore } from "test-utils";
import { TaskSuggestions } from "#/components/features/home/tasks/task-suggestions";
import { SuggestionsService } from "#/api/suggestions-service/suggestions-service.api";
import { MOCK_TASKS } from "#/mocks/task-suggestions-handlers";
import userEvent from "@testing-library/user-event";
// Mock the translation function
vi.mock("react-i18next", async () => {
const actual = await vi.importActual("react-i18next");
return {
...(actual as object),
useTranslation: () => ({
t: (key: string) => key,
i18n: {
changeLanguage: () => new Promise(() => {}),
},
}),
};
});
const renderTaskSuggestions = () => {
const RouterStub = createRoutesStub([
@@ -53,7 +68,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 () => {
@@ -93,4 +108,26 @@ describe("TaskSuggestions", () => {
expect(screen.queryByTestId("task-group-skeleton")).not.toBeInTheDocument();
});
it("should render the tooltip button", () => {
renderTaskSuggestions();
const tooltipButton = screen.getByTestId("task-suggestions-info");
expect(tooltipButton).toBeInTheDocument();
});
it("should have the correct aria-label", () => {
renderTaskSuggestions();
const tooltipButton = screen.getByTestId("task-suggestions-info");
expect(tooltipButton).toHaveAttribute(
"aria-label",
"TASKS$TASK_SUGGESTIONS_INFO",
);
});
it("should render the info icon", () => {
renderTaskSuggestions();
const tooltipButton = screen.getByTestId("task-suggestions-info");
const icon = tooltipButton.querySelector("svg");
expect(icon).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,52 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { MaintenanceBanner } from "#/components/features/maintenance/maintenance-banner";
// Mock react-i18next
vi.mock("react-i18next", async () => {
const actual = await vi.importActual<typeof import("react-i18next")>("react-i18next");
return {
...actual,
useTranslation: () => ({
t: (key: string, options?: { time?: string }) => {
const translations: Record<string, string> = {
"MAINTENANCE$SCHEDULED_MESSAGE": `Scheduled maintenance will begin at ${options?.time || "{{time}}"}`,
};
return translations[key] || key;
},
}),
};
});
describe("MaintenanceBanner", () => {
it("renders maintenance banner with formatted time", () => {
const startTime = "2024-01-15T10:00:00-05:00"; // EST timestamp
const { container } = render(<MaintenanceBanner startTime={startTime} />);
// Check if the banner is rendered
expect(screen.getByText(/Scheduled maintenance will begin at/)).toBeInTheDocument();
// Check if the warning icon (SVG) is present
const svgIcon = container.querySelector('svg');
expect(svgIcon).toBeInTheDocument();
});
it("handles invalid date gracefully", () => {
const invalidTime = "invalid-date";
render(<MaintenanceBanner startTime={invalidTime} />);
// Should still render the banner with the original string
expect(screen.getByText(/Scheduled maintenance will begin at invalid-date/)).toBeInTheDocument();
});
it("formats ISO date string correctly", () => {
const isoTime = "2024-01-15T15:30:00.000Z";
render(<MaintenanceBanner startTime={isoTime} />);
// Should render the banner (exact time format will depend on user's timezone)
expect(screen.getByText(/Scheduled maintenance will begin at/)).toBeInTheDocument();
});
});

View File

@@ -73,4 +73,73 @@ describe("TrajectoryActions", () => {
expect(onExportTrajectory).toHaveBeenCalled();
});
describe("SaaS mode", () => {
it("should only render export button when isSaasMode is true", () => {
renderWithProviders(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
onExportTrajectory={onExportTrajectory}
isSaasMode={true}
/>,
);
const actions = screen.getByTestId("feedback-actions");
// Should not render feedback buttons in SaaS mode
expect(within(actions).queryByTestId("positive-feedback")).toBeNull();
expect(within(actions).queryByTestId("negative-feedback")).toBeNull();
// Should still render export button
within(actions).getByTestId("export-trajectory");
});
it("should render all buttons when isSaasMode is false", () => {
renderWithProviders(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
onExportTrajectory={onExportTrajectory}
isSaasMode={false}
/>,
);
const actions = screen.getByTestId("feedback-actions");
within(actions).getByTestId("positive-feedback");
within(actions).getByTestId("negative-feedback");
within(actions).getByTestId("export-trajectory");
});
it("should render all buttons when isSaasMode is undefined (default behavior)", () => {
renderWithProviders(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
onExportTrajectory={onExportTrajectory}
/>,
);
const actions = screen.getByTestId("feedback-actions");
within(actions).getByTestId("positive-feedback");
within(actions).getByTestId("negative-feedback");
within(actions).getByTestId("export-trajectory");
});
it("should call onExportTrajectory when export button is clicked in SaaS mode", async () => {
renderWithProviders(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
onExportTrajectory={onExportTrajectory}
isSaasMode={true}
/>,
);
const exportButton = screen.getByTestId("export-trajectory");
await user.click(exportButton);
expect(onExportTrajectory).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,84 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderWithProviders } from "test-utils";
import { MicroagentsModal } from "#/components/features/conversation-panel/microagents-modal";
import OpenHands from "#/api/open-hands";
import { AgentState } from "#/types/agent-state";
vi.mock("react-redux", async () => {
const actual = await vi.importActual("react-redux");
return {
...actual,
useDispatch: () => vi.fn(),
useSelector: () => ({
agent: {
curAgentState: AgentState.AWAITING_USER_INPUT,
},
}),
};
});
describe("MicroagentsModal - Refresh Button", () => {
const mockOnClose = vi.fn();
const conversationId = "test-conversation-id";
const defaultProps = {
onClose: mockOnClose,
conversationId,
};
const mockMicroagents = [
{
name: "Test Agent 1",
type: "repo" as const,
triggers: ["test", "example"],
content: "This is test content for agent 1",
},
{
name: "Test Agent 2",
type: "knowledge" as const,
triggers: ["help", "support"],
content: "This is test content for agent 2",
},
];
beforeEach(() => {
// Reset all mocks before each test
vi.clearAllMocks();
// Setup default mock for getUserConversations
vi.spyOn(OpenHands, "getMicroagents").mockResolvedValue({
microagents: mockMicroagents,
});
});
afterEach(() => {
vi.clearAllMocks();
});
describe("Refresh Button Rendering", () => {
it("should render the refresh button with correct text and test ID", () => {
renderWithProviders(<MicroagentsModal {...defaultProps} />);
const refreshButton = screen.getByTestId("refresh-microagents");
expect(refreshButton).toBeInTheDocument();
expect(refreshButton).toHaveTextContent("BUTTON$REFRESH");
});
});
describe("Refresh Button Functionality", () => {
it("should call refetch when refresh button is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(<MicroagentsModal {...defaultProps} />);
const refreshSpy = vi.spyOn(OpenHands, "getMicroagents");
const refreshButton = screen.getByTestId("refresh-microagents");
await user.click(refreshButton);
expect(refreshSpy).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -0,0 +1,62 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { BadgeInput } from "#/components/shared/inputs/badge-input";
describe("BadgeInput", () => {
it("should render the values", () => {
const onChangeMock = vi.fn();
render(<BadgeInput value={["test", "test2"]} onChange={onChangeMock} />);
expect(screen.getByText("test")).toBeInTheDocument();
expect(screen.getByText("test2")).toBeInTheDocument();
});
it("should render the input's as a badge on space", async () => {
const onChangeMock = vi.fn();
render(<BadgeInput value={["badge1"]} onChange={onChangeMock} />);
const input = screen.getByTestId("badge-input");
expect(input).toHaveValue("");
await userEvent.type(input, "test");
await userEvent.type(input, " ");
expect(onChangeMock).toHaveBeenCalledWith(["badge1", "test"]);
expect(input).toHaveValue("");
});
it("should remove the badge on backspace", async () => {
const onChangeMock = vi.fn();
render(<BadgeInput value={["badge1", "badge2"]} onChange={onChangeMock} />);
const input = screen.getByTestId("badge-input");
expect(input).toHaveValue("");
await userEvent.type(input, "{backspace}");
expect(onChangeMock).toHaveBeenCalledWith(["badge1"]);
expect(input).toHaveValue("");
});
it("should remove the badge on click", async () => {
const onChangeMock = vi.fn();
render(<BadgeInput value={["badge1"]} onChange={onChangeMock} />);
const removeButton = screen.getByTestId("remove-button");
await userEvent.click(removeButton);
expect(onChangeMock).toHaveBeenCalledWith([]);
});
it("should not create empty badges", async () => {
const onChangeMock = vi.fn();
render(<BadgeInput value={[]} onChange={onChangeMock} />);
const input = screen.getByTestId("badge-input");
expect(input).toHaveValue("");
await userEvent.type(input, " ");
expect(onChangeMock).not.toHaveBeenCalled();
});
});

View File

@@ -21,7 +21,12 @@ describe("UserActions", () => {
});
it("should toggle the user menu when the user avatar is clicked", async () => {
render(<UserActions onLogout={onLogoutMock} />);
render(
<UserActions
onLogout={onLogoutMock}
user={{ avatar_url: "https://example.com/avatar.png" }}
/>,
);
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
@@ -57,15 +62,102 @@ describe("UserActions", () => {
).not.toBeInTheDocument();
});
test("logout button is always enabled", async () => {
it("should NOT show context menu when user is undefined and avatar is clicked", async () => {
render(<UserActions onLogout={onLogoutMock} />);
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
await user.click(logoutOption);
// Context menu should NOT appear because user is undefined
expect(
screen.queryByTestId("account-settings-context-menu"),
).not.toBeInTheDocument();
});
expect(onLogoutMock).toHaveBeenCalledOnce();
it("should show context menu even when user has no avatar_url", async () => {
render(<UserActions onLogout={onLogoutMock} user={{ avatar_url: "" }} />);
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
// Context menu SHOULD appear because user object exists (even with empty avatar_url)
expect(
screen.getByTestId("account-settings-context-menu"),
).toBeInTheDocument();
});
it("should NOT be able to access logout when no user is provided", async () => {
render(<UserActions onLogout={onLogoutMock} />);
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
// Logout option should not be accessible because context menu doesn't appear
expect(
screen.queryByText("ACCOUNT_SETTINGS$LOGOUT"),
).not.toBeInTheDocument();
expect(onLogoutMock).not.toHaveBeenCalled();
});
it("should handle user prop changing from undefined to defined", () => {
const { rerender } = render(<UserActions onLogout={onLogoutMock} />);
// Initially no user - context menu shouldn't work
expect(
screen.queryByTestId("account-settings-context-menu"),
).not.toBeInTheDocument();
// Add user prop
rerender(
<UserActions
onLogout={onLogoutMock}
user={{ avatar_url: "https://example.com/avatar.png" }}
/>,
);
// Component should still render correctly
expect(screen.getByTestId("user-actions")).toBeInTheDocument();
expect(screen.getByTestId("user-avatar")).toBeInTheDocument();
});
it("should handle user prop changing from defined to undefined", async () => {
const { rerender } = render(
<UserActions
onLogout={onLogoutMock}
user={{ avatar_url: "https://example.com/avatar.png" }}
/>,
);
// Click to open menu
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
expect(
screen.getByTestId("account-settings-context-menu"),
).toBeInTheDocument();
// Remove user prop - menu should disappear
rerender(<UserActions onLogout={onLogoutMock} />);
expect(
screen.queryByTestId("account-settings-context-menu"),
).not.toBeInTheDocument();
});
it("should work with loading state and user provided", async () => {
render(
<UserActions
onLogout={onLogoutMock}
user={{ avatar_url: "https://example.com/avatar.png" }}
isLoading={true}
/>,
);
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
// Context menu should still appear even when loading
expect(
screen.getByTestId("account-settings-context-menu"),
).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,140 @@
import { renderHook, waitFor } from "@testing-library/react";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { useFeedbackExists } from "#/hooks/query/use-feedback-exists";
// Mock the useConfig hook
vi.mock("#/hooks/query/use-config", () => ({
useConfig: vi.fn(),
}));
// Mock the useConversationId hook
vi.mock("#/hooks/use-conversation-id", () => ({
useConversationId: () => ({ conversationId: "test-conversation-id" }),
}));
describe("useFeedbackExists", () => {
let queryClient: QueryClient;
const mockCheckFeedbackExists = vi.spyOn(OpenHands, "checkFeedbackExists");
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
mockCheckFeedbackExists.mockClear();
});
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
it("should not call API when APP_MODE is not saas", async () => {
const { useConfig } = await import("#/hooks/query/use-config");
vi.mocked(useConfig).mockReturnValue({
data: { APP_MODE: "oss" },
isLoading: false,
error: null,
} as ReturnType<typeof useConfig>);
const { result } = renderHook(() => useFeedbackExists(123), {
wrapper,
});
// Wait for any potential async operations
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
// Verify that the API was not called
expect(mockCheckFeedbackExists).not.toHaveBeenCalled();
// Verify that the query is disabled
expect(result.current.data).toBeUndefined();
});
it("should call API when APP_MODE is saas", async () => {
const { useConfig } = await import("#/hooks/query/use-config");
vi.mocked(useConfig).mockReturnValue({
data: { APP_MODE: "saas" },
isLoading: false,
error: null,
} as ReturnType<typeof useConfig>);
mockCheckFeedbackExists.mockResolvedValue({
exists: true,
rating: 5,
reason: "Great job!",
});
const { result } = renderHook(() => useFeedbackExists(123), {
wrapper,
});
// Wait for the query to complete
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
// Verify that the API was called
expect(mockCheckFeedbackExists).toHaveBeenCalledWith(
"test-conversation-id",
123,
);
// Verify that the data is returned
expect(result.current.data).toEqual({
exists: true,
rating: 5,
reason: "Great job!",
});
});
it("should not call API when eventId is not provided", async () => {
const { useConfig } = await import("#/hooks/query/use-config");
vi.mocked(useConfig).mockReturnValue({
data: { APP_MODE: "saas" },
isLoading: false,
error: null,
} as ReturnType<typeof useConfig>);
const { result } = renderHook(() => useFeedbackExists(undefined), {
wrapper,
});
// Wait for any potential async operations
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
// Verify that the API was not called
expect(mockCheckFeedbackExists).not.toHaveBeenCalled();
// Verify that the query is disabled
expect(result.current.data).toBeUndefined();
});
it("should not call API when config is not loaded yet", async () => {
const { useConfig } = await import("#/hooks/query/use-config");
vi.mocked(useConfig).mockReturnValue({
data: undefined,
isLoading: true,
error: null,
} as ReturnType<typeof useConfig>);
const { result } = renderHook(() => useFeedbackExists(123), {
wrapper,
});
// Wait for any potential async operations
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
// Verify that the API was not called
expect(mockCheckFeedbackExists).not.toHaveBeenCalled();
// Verify that the query is disabled
expect(result.current.data).toBeUndefined();
});
});

View File

@@ -0,0 +1,105 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { MicroagentStatusIndicator } from "#/components/features/chat/microagent/microagent-status-indicator";
import { MicroagentStatus } from "#/types/microagent-status";
// Mock the translation hook
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
describe("MicroagentStatusIndicator", () => {
it("should show 'View your PR' when status is completed and PR URL is provided", () => {
render(
<MicroagentStatusIndicator
status={MicroagentStatus.COMPLETED}
conversationId="test-conversation"
prUrl="https://github.com/owner/repo/pull/123"
/>,
);
const link = screen.getByRole("link", { name: "MICROAGENT$VIEW_YOUR_PR" });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute(
"href",
"https://github.com/owner/repo/pull/123",
);
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
});
it("should show default completed message when status is completed but no PR URL", () => {
render(
<MicroagentStatusIndicator
status={MicroagentStatus.COMPLETED}
conversationId="test-conversation"
/>,
);
const link = screen.getByRole("link", {
name: "MICROAGENT$STATUS_COMPLETED",
});
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", "/conversations/test-conversation");
});
it("should show creating status without PR URL", () => {
render(
<MicroagentStatusIndicator
status={MicroagentStatus.CREATING}
conversationId="test-conversation"
/>,
);
expect(screen.getByText("MICROAGENT$STATUS_CREATING")).toBeInTheDocument();
});
it("should show error status", () => {
render(
<MicroagentStatusIndicator
status={MicroagentStatus.ERROR}
conversationId="test-conversation"
/>,
);
expect(screen.getByText("MICROAGENT$STATUS_ERROR")).toBeInTheDocument();
});
it("should prioritize PR URL over conversation link when both are provided", () => {
render(
<MicroagentStatusIndicator
status={MicroagentStatus.COMPLETED}
conversationId="test-conversation"
prUrl="https://github.com/owner/repo/pull/123"
/>,
);
const link = screen.getByRole("link", { name: "MICROAGENT$VIEW_YOUR_PR" });
expect(link).toHaveAttribute(
"href",
"https://github.com/owner/repo/pull/123",
);
// Should not link to conversation when PR URL is available
expect(link).not.toHaveAttribute(
"href",
"/conversations/test-conversation",
);
});
it("should work with GitLab MR URLs", () => {
render(
<MicroagentStatusIndicator
status={MicroagentStatus.COMPLETED}
prUrl="https://gitlab.com/owner/repo/-/merge_requests/456"
/>,
);
const link = screen.getByRole("link", { name: "MICROAGENT$VIEW_YOUR_PR" });
expect(link).toHaveAttribute(
"href",
"https://gitlab.com/owner/repo/-/merge_requests/456",
);
});
});

View File

@@ -0,0 +1,142 @@
import { describe, it, expect } from "vitest";
import {
extractPRUrls,
containsPRUrl,
getFirstPRUrl,
} from "#/utils/parse-pr-url";
describe("parse-pr-url", () => {
describe("extractPRUrls", () => {
it("should extract GitHub PR URLs", () => {
const text = "Check out this PR: https://github.com/owner/repo/pull/123";
const urls = extractPRUrls(text);
expect(urls).toEqual(["https://github.com/owner/repo/pull/123"]);
});
it("should extract GitLab MR URLs", () => {
const text =
"Merge request: https://gitlab.com/owner/repo/-/merge_requests/456";
const urls = extractPRUrls(text);
expect(urls).toEqual([
"https://gitlab.com/owner/repo/-/merge_requests/456",
]);
});
it("should extract Bitbucket PR URLs", () => {
const text =
"PR link: https://bitbucket.org/owner/repo/pull-requests/789";
const urls = extractPRUrls(text);
expect(urls).toEqual([
"https://bitbucket.org/owner/repo/pull-requests/789",
]);
});
it("should extract Azure DevOps PR URLs", () => {
const text =
"Azure PR: https://dev.azure.com/org/project/_git/repo/pullrequest/101";
const urls = extractPRUrls(text);
expect(urls).toEqual([
"https://dev.azure.com/org/project/_git/repo/pullrequest/101",
]);
});
it("should extract multiple PR URLs", () => {
const text = `
GitHub: https://github.com/owner/repo/pull/123
GitLab: https://gitlab.com/owner/repo/-/merge_requests/456
`;
const urls = extractPRUrls(text);
expect(urls).toHaveLength(2);
expect(urls).toContain("https://github.com/owner/repo/pull/123");
expect(urls).toContain(
"https://gitlab.com/owner/repo/-/merge_requests/456",
);
});
it("should handle self-hosted GitLab URLs", () => {
const text =
"Self-hosted: https://gitlab.example.com/owner/repo/-/merge_requests/123";
const urls = extractPRUrls(text);
expect(urls).toEqual([
"https://gitlab.example.com/owner/repo/-/merge_requests/123",
]);
});
it("should return empty array when no PR URLs found", () => {
const text = "This is just regular text with no PR URLs";
const urls = extractPRUrls(text);
expect(urls).toEqual([]);
});
it("should handle URLs with HTTP instead of HTTPS", () => {
const text = "HTTP PR: http://github.com/owner/repo/pull/123";
const urls = extractPRUrls(text);
expect(urls).toEqual(["http://github.com/owner/repo/pull/123"]);
});
it("should remove duplicate URLs", () => {
const text = `
Same PR mentioned twice:
https://github.com/owner/repo/pull/123
https://github.com/owner/repo/pull/123
`;
const urls = extractPRUrls(text);
expect(urls).toEqual(["https://github.com/owner/repo/pull/123"]);
});
});
describe("containsPRUrl", () => {
it("should return true when PR URL is present", () => {
const text = "Check out this PR: https://github.com/owner/repo/pull/123";
expect(containsPRUrl(text)).toBe(true);
});
it("should return false when no PR URL is present", () => {
const text = "This is just regular text";
expect(containsPRUrl(text)).toBe(false);
});
});
describe("getFirstPRUrl", () => {
it("should return the first PR URL found", () => {
const text = `
First: https://github.com/owner/repo/pull/123
Second: https://gitlab.com/owner/repo/-/merge_requests/456
`;
const url = getFirstPRUrl(text);
expect(url).toBe("https://github.com/owner/repo/pull/123");
});
it("should return null when no PR URL is found", () => {
const text = "This is just regular text";
const url = getFirstPRUrl(text);
expect(url).toBeNull();
});
});
describe("real-world scenarios", () => {
it("should handle typical microagent finish messages", () => {
const text = `
I have successfully created a pull request with the requested changes.
You can view the PR here: https://github.com/All-Hands-AI/OpenHands/pull/1234
The changes include:
- Updated the component
- Added tests
- Fixed the issue
`;
const url = getFirstPRUrl(text);
expect(url).toBe("https://github.com/All-Hands-AI/OpenHands/pull/1234");
});
it("should handle messages with PR URLs in the middle", () => {
const text = `
Task completed successfully! I've created a pull request at
https://github.com/owner/repo/pull/567 with all the requested changes.
Please review when you have a chance.
`;
const url = getFirstPRUrl(text);
expect(url).toBe("https://github.com/owner/repo/pull/567");
});
});
});

View File

@@ -90,7 +90,7 @@ describe("HomeScreen", () => {
const mainContainer = screen
.getByTestId("home-screen")
.querySelector("main");
expect(mainContainer).toHaveClass("flex", "flex-col", "md:flex-row");
expect(mainContainer).toHaveClass("flex", "flex-col", "lg:flex-row");
});
it("should filter the suggested tasks based on the selected repository", async () => {
@@ -222,10 +222,12 @@ describe("HomeScreen", () => {
// All other buttons should be disabled when the header button is clicked
await userEvent.click(headerLaunchButton);
expect(headerLaunchButton).toBeDisabled();
expect(repoLaunchButton).toBeDisabled();
tasksLaunchButtonsAfter.forEach((button) => {
expect(button).toBeDisabled();
await waitFor(() => {
expect(headerLaunchButton).toBeDisabled();
expect(repoLaunchButton).toBeDisabled();
tasksLaunchButtonsAfter.forEach((button) => {
expect(button).toBeDisabled();
});
});
});
@@ -240,10 +242,12 @@ describe("HomeScreen", () => {
// All other buttons should be disabled when the repo button is clicked
await userEvent.click(repoLaunchButton);
expect(headerLaunchButton).toBeDisabled();
expect(repoLaunchButton).toBeDisabled();
tasksLaunchButtonsAfter.forEach((button) => {
expect(button).toBeDisabled();
await waitFor(() => {
expect(headerLaunchButton).toBeDisabled();
expect(repoLaunchButton).toBeDisabled();
tasksLaunchButtonsAfter.forEach((button) => {
expect(button).toBeDisabled();
});
});
});
@@ -258,10 +262,12 @@ describe("HomeScreen", () => {
// All other buttons should be disabled when the task button is clicked
await userEvent.click(tasksLaunchButtons[0]);
expect(headerLaunchButton).toBeDisabled();
expect(repoLaunchButton).toBeDisabled();
tasksLaunchButtonsAfter.forEach((button) => {
expect(button).toBeDisabled();
await waitFor(() => {
expect(headerLaunchButton).toBeDisabled();
expect(repoLaunchButton).toBeDisabled();
tasksLaunchButtonsAfter.forEach((button) => {
expect(button).toBeDisabled();
});
});
});
});

View File

@@ -366,17 +366,17 @@ describe("Form submission", () => {
renderLlmSettingsScreen();
await screen.findByTestId("llm-settings-screen");
screen.getByTestId("llm-settings-form-advanced");
await screen.findByTestId("llm-settings-form-advanced");
const submitButton = screen.getByTestId("submit-button");
const submitButton = await screen.findByTestId("submit-button");
expect(submitButton).toBeDisabled();
const model = screen.getByTestId("llm-custom-model-input");
const baseUrl = screen.getByTestId("base-url-input");
const apiKey = screen.getByTestId("llm-api-key-input");
const agent = screen.getByTestId("agent-input");
const confirmation = screen.getByTestId("enable-confirmation-mode-switch");
const condensor = screen.getByTestId("enable-memory-condenser-switch");
const model = await screen.findByTestId("llm-custom-model-input");
const baseUrl = await screen.findByTestId("base-url-input");
const apiKey = await screen.findByTestId("llm-api-key-input");
const agent = await screen.findByTestId("agent-input");
const confirmation = await screen.findByTestId("enable-confirmation-mode-switch");
const condensor = await screen.findByTestId("enable-memory-condenser-switch");
// enter custom model
await userEvent.type(model, "-mini");
@@ -449,7 +449,7 @@ describe("Form submission", () => {
expect(submitButton).toBeDisabled();
// select security analyzer
const securityAnalyzer = screen.getByTestId("security-analyzer-input");
const securityAnalyzer = await screen.findByTestId("security-analyzer-input");
await userEvent.click(securityAnalyzer);
const securityAnalyzerOption = screen.getByText("mock-invariant");
await userEvent.click(securityAnalyzerOption);

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

@@ -1,8 +1,8 @@
import { render, screen, within } from "@testing-library/react";
import { createRoutesStub } from "react-router";
import { describe, expect, it, vi } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import SettingsScreen from "#/routes/settings";
import { QueryClientProvider } from "@tanstack/react-query";
import SettingsScreen, { clientLoader } from "#/routes/settings";
import OpenHands from "#/api/open-hands";
// Mock the i18next hook
@@ -31,16 +31,27 @@ vi.mock("react-i18next", async () => {
});
describe("Settings Screen", () => {
const { handleLogoutMock } = vi.hoisted(() => ({
const { handleLogoutMock, mockQueryClient } = vi.hoisted(() => ({
handleLogoutMock: vi.fn(),
mockQueryClient: (() => {
const { QueryClient } = require("@tanstack/react-query");
return new QueryClient();
})(),
}));
vi.mock("#/hooks/use-app-logout", () => ({
useAppLogout: vi.fn().mockReturnValue({ handleLogout: handleLogoutMock }),
}));
vi.mock("#/query-client-config", () => ({
queryClient: mockQueryClient,
}));
const RouterStub = createRoutesStub([
{
Component: SettingsScreen,
// @ts-expect-error - custom loader
clientLoader,
path: "/settings",
children: [
{
@@ -56,8 +67,8 @@ describe("Settings Screen", () => {
path: "/settings/app",
},
{
Component: () => <div data-testid="credits-settings-screen" />,
path: "/settings/credits",
Component: () => <div data-testid="billing-settings-screen" />,
path: "/settings/billing",
},
{
Component: () => <div data-testid="api-keys-settings-screen" />,
@@ -67,26 +78,27 @@ describe("Settings Screen", () => {
},
]);
const renderSettingsScreen = (path = "/settings") => {
const queryClient = new QueryClient();
return render(<RouterStub initialEntries={[path]} />, {
const renderSettingsScreen = (path = "/settings") =>
render(<RouterStub initialEntries={[path]} />, {
wrapper: ({ children }) => (
<QueryClientProvider client={queryClient}>
<QueryClientProvider client={mockQueryClient}>
{children}
</QueryClientProvider>
),
});
};
it("should render the navbar", async () => {
const sectionsToInclude = ["llm", "integrations", "application", "secrets"];
const sectionsToExclude = ["api keys", "credits"];
const sectionsToExclude = ["api keys", "credits", "billing"];
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
// @ts-expect-error - only return app mode
getConfigSpy.mockResolvedValue({
APP_MODE: "oss",
});
// Clear any existing query data
mockQueryClient.clear();
renderSettingsScreen();
const navbar = await screen.findByTestId("settings-navbar");
@@ -102,6 +114,8 @@ describe("Settings Screen", () => {
});
expect(sectionElement).not.toBeInTheDocument();
});
getConfigSpy.mockRestore();
});
it("should render the saas navbar", async () => {
@@ -113,12 +127,15 @@ describe("Settings Screen", () => {
const sectionsToInclude = [
"integrations",
"application",
"credits",
"credits", // The nav item shows "credits" text but routes to /billing
"secrets",
"api keys",
];
const sectionsToExclude = ["llm"];
// Clear any existing query data
mockQueryClient.clear();
renderSettingsScreen();
const navbar = await screen.findByTestId("settings-navbar");
@@ -134,30 +151,44 @@ describe("Settings Screen", () => {
});
expect(sectionElement).not.toBeInTheDocument();
});
getConfigSpy.mockRestore();
});
it("should not be able to access oss-restricted routes in oss", async () => {
it("should not be able to access saas-only routes in oss mode", async () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
// @ts-expect-error - only return app mode
getConfigSpy.mockResolvedValue({
APP_MODE: "oss",
});
const { rerender } = renderSettingsScreen("/settings/credits");
// Clear any existing query data
mockQueryClient.clear();
// In OSS mode, accessing restricted routes should redirect to /settings
// Since createRoutesStub doesn't handle clientLoader redirects properly,
// we test that the correct navbar is shown (OSS navbar) and that
// the restricted route components are not rendered when accessing /settings
renderSettingsScreen("/settings");
// Verify we're in OSS mode by checking the navbar
const navbar = await screen.findByTestId("settings-navbar");
expect(within(navbar).getByText("LLM")).toBeInTheDocument();
expect(
screen.queryByTestId("credits-settings-screen"),
within(navbar).queryByText("credits", { exact: false }),
).not.toBeInTheDocument();
rerender(<RouterStub initialEntries={["/settings/api-keys"]} />);
expect(
screen.queryByTestId("api-keys-settings-screen"),
).not.toBeInTheDocument();
rerender(<RouterStub initialEntries={["/settings/billing"]} />);
// Verify the LLM settings screen is shown
expect(screen.getByTestId("llm-settings-screen")).toBeInTheDocument();
expect(
screen.queryByTestId("billing-settings-screen"),
).not.toBeInTheDocument();
rerender(<RouterStub initialEntries={["/settings"]} />);
expect(
screen.queryByTestId("api-keys-settings-screen"),
).not.toBeInTheDocument();
getConfigSpy.mockRestore();
});
it.todo("should not be able to access saas-restricted routes in saas");
it.todo("should not be able to access oss-only routes in saas mode");
});

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