Compare commits

...

115 Commits

Author SHA1 Message Date
Chuck Butkus 75a4033bef Update dockerfile 2025-08-27 01:42:18 -04:00
Chuck Butkus 2f9a4f96ae Fix dockerfile 2025-08-26 19:18:00 -04:00
chuckbutkus ac152da39e Merge branch 'main' into non-root-user 2025-08-26 14:25:39 -04:00
Tim O'Farrell 4a4f213f57 Remove unused translation keys from translation.json (#10631)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-26 11:59:48 -06:00
Chuck Butkus 77210bc678 Revert "Fix failing Docker-dependent tests in test_runtime_build.py"
This reverts commit 39049b173a.
2025-08-26 13:08:38 -04:00
Chuck Butkus 425b4f77a9 Merge branch 'main' into non-root-user 2025-08-26 12:57:00 -04:00
Tim O'Farrell f9099fe6db Refactor conversation status (#10590)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-26 08:06:26 -06:00
Xingyao Wang 8f46a0a7a3 Add gpt-5-mini-2025-08-07 as verified model & supported in OpenHands provider (#10628)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-26 13:15:16 +00:00
dependabot[bot] 55d204ae1b chore(deps): bump the version-all group in /frontend with 21 updates (#10614)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-26 06:09:20 +00:00
baii 4d7cd228da Fix(backend): correctly forward AWS Bedrock aws_access_key_id / aws_secret_access_key / aws_region_name to litellm (#9663)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-08-25 22:07:28 +00:00
Tim O'Farrell a3f92df4b3 Fix for issue where exceptions are swallowed (#10602) 2025-08-25 15:50:15 -06:00
Engel Nyst e41f8f5215 feat(settings): configurable condenser max history size (FE+BE) (#10591) 2025-08-25 22:50:52 +02:00
Jamie Chicago 6448f5a681 docs: Add Ubuntu installation steps for Windows WSL setup (#10485)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-25 20:43:04 +00:00
Graham Neubig 5fcc648d5f Add E2E test for multi-conversation resume functionality (Issue #10384) (#10390)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-25 15:15:54 -04:00
danieljbruntz c9d96038c1 feat: Add OPENHANDS_FORCE_VISION env var to override vision capability detection (#10255)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-08-25 19:05:58 +00:00
Calvin Smith 408af4e012 fix: Extend cases where truncation triggers (#10607)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-08-25 12:59:41 -06:00
mamoodi d9ac2faff6 Add backlog label to exemption (#10598) 2025-08-25 14:57:35 -04:00
Rohit Malhotra 64383a66e2 docs: Update Project Management integration docs (#10161)
Co-authored-by: OpenHands <openhands@all-hands.dev>
2025-08-25 14:51:06 -04:00
chuckbutkus 7fbcb29499 Allow for path based runtimes in the SAAS environment (#10518)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-25 14:31:07 -04:00
hereisok e7aae1495c perf: remove the sleep before runtime initialization (#10033)
Signed-off-by: hereisok <hereisok@angai.wk@gmail.com>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-08-25 13:56:57 -04:00
Hiep Le d33f27d141 refactor(frontend): separate the microagents and conversations (microagent management) (#10596)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-26 00:50:25 +07:00
Hiep Le d08851859b refactor(frontend): update helper text (microagent management) (#10595)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-26 00:49:11 +07:00
Hiep Le 7f4d311294 fix: subscription logic by polling for available runtime (microagent management, memory UI) (#10519)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-25 20:44:00 +04:00
dependabot[bot] 049f058ed1 chore(deps-dev): bump eslint-plugin-unused-imports from 4.1.4 to 4.2.0 in /frontend in the eslint group (#10511)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-25 18:03:22 +04:00
Tim O'Farrell bb6cf5a816 Refactor authentication error handling with global FastAPI exception handler (#10403)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-25 07:54:30 -06:00
Engel Nyst d9bc5824a0 docs: add shell guidance to avoid set -e variants in this environment (#10579)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-24 13:39:37 +08:00
Xingyao Wang fd5b5075d6 Simplify CLI markdown rendering; remove python-markdown deps; update tests (#10538)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-24 01:23:06 +08:00
Hiep Le f5cd7b256d feat(frontend): Implement LLM risk analyzer UI (#10569)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: llamantino <213239228+llamantino@users.noreply.github.com>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ryan H. Tran <descience.thh10@gmail.com>
Co-authored-by: Neeraj Panwar <49247372+npneeraj@users.noreply.github.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: Insop <1240382+insop@users.noreply.github.com>
Co-authored-by: test <test@test.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Zhonghao Jiang <zhonghao.J@outlook.com>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-08-23 02:08:45 +07:00
Neeraj Panwar df86fd275d Fixes bug 9682 (#9692)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-08-22 16:51:53 +00:00
Xingyao Wang d22a2e39e7 feat(agent): add security-related items in system prompt to defense against data exfiltration (#10477)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-22 15:53:13 +00:00
Xingyao Wang ca424ec15d [agent] Add LLM risk analyzer (#9349)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: llamantino <213239228+llamantino@users.noreply.github.com>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ryan H. Tran <descience.thh10@gmail.com>
Co-authored-by: Neeraj Panwar <49247372+npneeraj@users.noreply.github.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: Insop <1240382+insop@users.noreply.github.com>
Co-authored-by: test <test@test.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Zhonghao Jiang <zhonghao.J@outlook.com>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-08-22 14:02:36 +00:00
Xingyao Wang 4507a25b85 Evaluation: redirect sessions to repo-local .eval_sessions via helper; apply across entrypoints; add tests (#10540)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-22 13:34:02 +00:00
llamantino d9cf5b7302 ci: add GitHub Action to post welcome message on good first issues (#9707)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-08-22 09:09:45 -04:00
Xingyao Wang 2a86e32263 fix(CI): Pin @modelcontextprotocol/server-filesystem to version 2025.8.18 (#10561)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-22 05:00:11 +08:00
Engel Nyst b311ae6e15 fix: normalize malformed <parameter> tags (Qwen3) (#10539) 2025-08-21 19:03:20 +02:00
Ryan H. Tran adb773789a Upgrade aci to 0.3.2: clamp view_range end to file length and emit warning instead of error (#10502) 2025-08-21 23:01:54 +07:00
Engel Nyst 91d3d1d20a Fix: expose aggregated LLM metrics in State for evaluation scripts (#10537)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-21 17:43:09 +02:00
llamantino e9e2c98946 fix(tests): increase hard timeout in test_bash_server to avoid timeout on Windows (#9930) 2025-08-21 17:12:42 +02:00
Engel Nyst 7861c1ddf7 fix(anthropic): disable extended thinking for Opus 4.1 (#10532)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-21 00:13:15 +02:00
Engel Nyst 5ce5469bfa docs: update OpenAPI specification to include all current endpoints (#10412)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-20 21:58:35 +02:00
Xingyao Wang 4a3f5dd9b4 fix(runtime): correctly set session_api_key for local runtime (#10506) 2025-08-21 03:51:19 +08:00
Joe O'Connor bc8b995dd3 Add additional networks (#9566)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-08-20 18:52:31 +00:00
chuckbutkus 07c4742496 Add useful tools jq and gettext to image (#10531) 2025-08-20 18:27:09 +00:00
mamoodi b5887f8a9d Fix CLI docs command (#10520) 2025-08-20 14:53:15 +00:00
mamoodi 0166df6575 Release 0.54.0 (#10465) 2025-08-20 10:29:15 -04:00
Ryan H. Tran e03a1f4e37 Move TASKS.md to session-specific directory in ~/.openhands (#10493)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-20 22:26:55 +08:00
sp.wack c763f0e368 chroe(vscode): Refresh vscode integration lockfile (#9965)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-08-20 15:33:11 +02:00
Engel Nyst bb0e24d23b Centralize model feature checks (#10414)
Co-authored-by: OpenHands-GPT-5 <openhands@all-hands.dev>
2025-08-19 20:30:07 +00:00
sp.wack aa6b454772 fix: Enhance GitHub repository search to include user organizations (#10324)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-19 15:56:15 +00:00
sp.wack 0297b3da18 Fix conversation ID validation to return 400 instead of 500 for long IDs (#10496) 2025-08-19 18:03:05 +04:00
Hiep Le 476954f3a4 refactor(frontend): update the styling for the microagent management page. (#10494) 2025-08-19 19:50:42 +07:00
dependabot[bot] f296d7bde5 chore(deps): bump abatilo/actions-poetry from 3 to 4 (#10487)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 13:58:39 +02:00
Zacharias Fisches f866b3f8ea Update modal runtime for modal>=1.0 (#10479)
Co-authored-by: Ryan H. Tran <descience.thh10@gmail.com>
2025-08-19 10:33:03 +00:00
Zacharias Fisches 36d31b74f7 fix jinja / dockerfile syntax by removing newlines (#10476)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-08-19 02:50:41 +00:00
Engel Nyst 634a7691a2 tests: reorganize unit tests into subdirectories mirroring source modules (#10484)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-19 01:11:07 +02:00
chuckbutkus a84670e6bf Merge branch 'main' into non-root-user 2025-08-18 17:39:39 -04:00
Xingyao Wang 81ba4399fa fix(frontend): fix MCP tab in frontend unit tests (#10481)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-18 21:25:09 +00:00
Rohit Malhotra 875036d920 (Hotfix): Fix logs and filestore init for llm registry (#10470) 2025-08-18 20:57:08 +00:00
Xingyao Wang 39333dd5de feat: enable MCP in SaaS (#10480) 2025-08-18 20:40:42 +00:00
Rohit Malhotra 3660933d59 refactor: replace 'convo' naming with 'conversation' (#10473)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-18 15:10:32 -04:00
Xingyao Wang baf2cc5c7e Pin OpenAI Python SDK to 1.99.9 to avoid LiteLLM import breakage (BerriAI/litellm#13711) (#10471)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-08-18 18:45:34 +00:00
chuckbutkus fc3b3f733f Merge branch 'main' into non-root-user 2025-08-15 17:28:04 -04:00
chuckbutkus 150f56f252 Merge branch 'main' into non-root-user 2025-08-13 12:39:17 -04:00
chuckbutkus 05171f08fb Merge branch 'main' into non-root-user 2025-08-13 00:28:53 -04:00
chuckbutkus 64a160cf03 Merge branch 'main' into non-root-user 2025-08-12 23:12:11 -04:00
chuckbutkus 8b1f38e52e Merge branch 'main' into non-root-user 2025-08-12 15:44:52 -04:00
Chuck Butkus 868f434f97 Fix logging 2025-08-12 12:49:30 -04:00
Chuck Butkus 210f7fc653 Lint fix 2025-08-12 12:40:03 -04:00
Chuck Butkus f9512cd234 Move logging to debug 2025-08-12 12:31:57 -04:00
chuckbutkus ff8e659905 Merge branch 'main' into non-root-user 2025-08-12 12:17:54 -04:00
chuckbutkus c32610a440 Merge branch 'main' into non-root-user 2025-08-11 20:40:40 -04:00
Chuck Butkus 26abb81a86 Fix merge 2025-08-11 16:51:56 -04:00
Chuck Butkus 23121301eb Revert "Fix circular import by moving refine_prompt to platform_utils"
This reverts commit e075873962.
2025-08-11 16:50:18 -04:00
chuckbutkus 97cd7eb0a2 Merge branch 'main' into non-root-user 2025-08-11 16:42:35 -04:00
Chuck Butkus 12e8183336 Revert "test"
This reverts commit b69b01cc15.
2025-08-11 16:42:18 -04:00
openhands e075873962 Fix circular import by moving refine_prompt to platform_utils
- Created new openhands/utils/platform_utils.py module for platform-specific utilities
- Moved refine_prompt function from openhands/agenthub/codeact_agent/tools/bash.py to platform_utils
- Updated imports in openhands/utils/prompt.py and bash.py to use the new location
- Resolves circular import: utils.prompt -> agenthub -> codeact_agent -> memory.conversation_memory -> utils.prompt

This fixes the ImportError when starting pods:
'cannot import name ConversationInstructions from partially initialized module openhands.utils.prompt'
2025-08-11 06:13:28 +00:00
Chuck Butkus ae1288a3e8 revert 2025-08-11 02:04:31 -04:00
Chuck Butkus 3a2327a879 Fix circular reference 2025-08-11 01:05:03 -04:00
Chuck Butkus d1f40ffab1 test 2025-08-11 00:46:18 -04:00
Chuck Butkus 31241f2490 Change shell to bash 2025-08-11 00:24:53 -04:00
Chuck Butkus 08ffd53636 revert 2025-08-11 00:11:02 -04:00
openhands c88173c1df Fix Docker installation for Ubuntu 24.04 (noble) by using jammy repository 2025-08-11 04:05:02 +00:00
Chuck Butkus 5f8f4fd42c Another try 2025-08-10 23:57:45 -04:00
Chuck Butkus 310fa75535 Revert "Fix Dockerfile.j2"
This reverts commit a0dfbe39bf.
2025-08-10 23:56:46 -04:00
Chuck Butkus a0dfbe39bf Fix Dockerfile.j2 2025-08-10 23:52:30 -04:00
Chuck Butkus b69b01cc15 test 2025-08-10 23:33:07 -04:00
chuckbutkus acf8539a3f Merge branch 'main' into non-root-user 2025-08-10 23:19:14 -04:00
Chuck Butkus 08c6686026 Update Dockerfile 2025-08-10 22:52:39 -04:00
openhands 691f42cf77 Fix test assertions for COPY command with --chown flag
The Dockerfile generation now includes --chown=openhands:openhands in COPY commands,
but the test assertions were still expecting the old format without the chown flag.

Updated test assertions in:
- test_generate_dockerfile_build_from_scratch
- test_generate_dockerfile_build_from_lock
- test_generate_dockerfile_build_from_versioned

This fixes the CI test failures that were occurring due to the mismatch between
expected and actual Dockerfile content.
2025-08-10 05:47:04 +00:00
openhands 39049b173a Fix failing Docker-dependent tests in test_runtime_build.py 2025-08-10 05:31:00 +00:00
chuckbutkus 1b911ea5ba Merge branch 'main' into non-root-user 2025-08-10 01:04:53 -04:00
Chuck Butkus 971fcd7296 Lint fixes 2025-08-10 00:58:01 -04:00
Chuck Butkus a43778bf24 test 2025-08-10 00:10:35 -04:00
Chuck Butkus 67278772ad More logging 2025-08-09 22:47:49 -04:00
Chuck Butkus 33ec22ff91 More logging 2025-08-09 21:54:30 -04:00
Chuck Butkus c8dd141997 Logging 2025-08-09 15:18:45 -04:00
Chuck Butkus e2a56c9302 Fix su 2025-08-08 00:23:25 -04:00
Chuck Butkus 1d53587cbd Fix path 2025-08-07 23:51:58 -04:00
Chuck Butkus 880a798152 Update 2025-08-07 23:11:22 -04:00
chuckbutkus a356b09f30 Merge branch 'main' into non-root-user 2025-08-07 22:34:55 -04:00
Chuck Butkus 01e91cebe4 Update 2025-08-07 22:30:09 -04:00
Chuck Butkus 93cbba4e06 Another try 2025-08-07 22:05:15 -04:00
Chuck Butkus df45db5fa1 Try again 2025-08-07 21:16:58 -04:00
Chuck Butkus 36c628014f Update 2025-08-07 17:11:20 -04:00
chuckbutkus ff38146cb6 Merge branch 'main' into non-root-user 2025-08-07 15:58:29 -04:00
openhands 7512ffc16a Update Dockerfile.j2 to create openhands user and group with proper ownership 2025-08-07 16:27:22 +00:00
Chuck Butkus aa545c29f1 Revert "Try build for non-root"
This reverts commit 54cdc5c744.
2025-08-07 12:05:53 -04:00
Chuck Butkus 54cdc5c744 Try build for non-root 2025-08-07 00:46:01 -04:00
Chuck Butkus a5afc1ff7a Another fix 2025-08-06 23:50:35 -04:00
Chuck Butkus ecf23d3e74 Missed one 2025-08-06 23:48:27 -04:00
Chuck Butkus c2e080a340 Change linux group name 2025-08-06 23:42:13 -04:00
chuckbutkus 4b2ca6ca71 Merge branch 'main' into update-group-id 2025-08-06 16:58:35 -04:00
Chuck Butkus 42d1a54670 Add debug logging 2025-08-05 12:05:26 -04:00
Chuck Butkus 25261673a1 Fix runtime image creation to create user before using it. 2025-08-05 10:25:57 -04:00
Chuck Butkus d025d225ad Update user and group creation in Dockerfile 2025-08-05 09:16:08 -04:00
327 changed files with 10572 additions and 5090 deletions
+2 -1
View File
@@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v4
- name: Install poetry via pipx
uses: abatilo/actions-poetry@v3
uses: abatilo/actions-poetry@v4
with:
poetry-version: 2.1.3
@@ -187,6 +187,7 @@ jobs:
test_settings.py::test_github_token_configuration \
test_conversation.py::test_conversation_start \
test_browsing_catchphrase.py::test_browsing_catchphrase \
test_multi_conversation_resume.py::test_multi_conversation_resume \
-v --no-header --capture=no --timeout=900
- name: Upload test results
+1 -1
View File
@@ -73,7 +73,7 @@ jobs:
- name: Install Python dependencies using Poetry
run: poetry install --with dev,test,runtime
- name: Run Windows unit tests
run: poetry run pytest -svv tests/unit/test_windows_bash.py
run: poetry run pytest -svv tests/unit/runtime/utils/test_windows_bash.py
env:
PYTHONPATH: ".;$env:PYTHONPATH"
DEBUG: "1"
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
stale-issue-message: 'This issue is stale because it has been open for 40 days with no activity. Remove the stale label or leave a comment, otherwise it will be closed in 10 days.'
stale-pr-message: 'This PR is stale because it has been open for 40 days with no activity. Remove the stale label or leave a comment, otherwise it will be closed in 10 days.'
days-before-stale: 40
exempt-issue-labels: 'roadmap'
exempt-issue-labels: roadmap,backlog
close-issue-message: 'This issue was automatically closed due to 50 days of inactivity. We do this to help keep the issues somewhat manageable and focus on active issues.'
close-pr-message: 'This PR was closed because it had no activity for 50 days. If you feel this was closed in error, and you would like to continue the PR, please resubmit or let us know.'
days-before-close: 10
@@ -0,0 +1,50 @@
name: Welcome Good First Issue
on:
issues:
types: [labeled]
permissions:
issues: write
jobs:
comment-on-good-first-issue:
if: github.event.label.name == 'good first issue'
runs-on: ubuntu-latest
steps:
- name: Check if welcome comment already exists
id: check_comment
uses: actions/github-script@v7
with:
result-encoding: string
script: |
const issueNumber = context.issue.number;
const comments = await github.rest.issues.listComments({
...context.repo,
issue_number: issueNumber
});
const alreadyCommented = comments.data.some(
(comment) =>
comment.body.includes('<!-- auto-comment:good-first-issue -->')
);
return alreadyCommented ? 'true' : 'false';
- name: Leave welcome comment
if: steps.check_comment.outputs.result == 'false'
uses: actions/github-script@v7
with:
script: |
const repoUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}`;
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: "🙌 **Hey there, future contributor!** 🙌\n\n" +
"This issue has been labeled as **good first issue**, which means it's a great place to get started with the OpenHands project.\n\n" +
"If you're interested in working on it, feel free to! No need to ask for permission.\n\n" +
"Be sure to check out our [development setup guide](" + repoUrl + "/blob/main/Development.md) to get your environment set up, and follow our [contribution guidelines](" + repoUrl + "/blob/main/CONTRIBUTING.md) when you're ready to submit a fix.\n\n" +
"🙌 Happy hacking! 🙌\n\n" +
"<!-- auto-comment:good-first-issue -->"
});
+2
View File
@@ -257,3 +257,5 @@ containers/runtime/code
# test results
test-results
.sessions
.eval_sessions
+1 -1
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.53-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.54-nikolaik`
## Develop inside Docker container
+3 -3
View File
@@ -79,17 +79,17 @@ You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)
You can also run OpenHands directly with Docker:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-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.53
docker.all-hands.dev/all-hands-ai/openhands:0.54
```
</details>
+3 -3
View File
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-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.53
docker.all-hands.dev/all-hands-ai/openhands:0.54
```
> **注意**: 如果您在0.44版本之前使用过OpenHands,您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
+3 -3
View File
@@ -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.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-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.53
docker.all-hands.dev/all-hands-ai/openhands:0.54
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
+3 -2
View File
@@ -363,10 +363,11 @@ classpath = "my_package.my_module.MyCustomAgent"
#confirmation_mode = false
# The security analyzer to use (For Headless / CLI only - In Web this is overridden by Session Init)
#security_analyzer = ""
# Available options: 'llm' (default), 'invariant'
#security_analyzer = "llm"
# Whether to enable security analyzer
#enable_security_analyzer = false
#enable_security_analyzer = true
#################################### Condenser #################################
# Condensers control how conversation history is managed and compressed when
+14 -14
View File
@@ -21,7 +21,7 @@ ENV POETRY_NO_INTERACTION=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
RUN apt-get update -y \
&& apt-get install -y curl make git build-essential \
&& apt-get install -y curl make git build-essential jq gettext \
&& python3 -m pip install poetry --break-system-packages
COPY pyproject.toml poetry.lock ./
@@ -58,34 +58,34 @@ RUN sed -i 's/^UID_MIN.*/UID_MIN 499/' /etc/login.defs
# Default is 60000, but we've seen up to 200000
RUN sed -i 's/^UID_MAX.*/UID_MAX 1000000/' /etc/login.defs
RUN groupadd --gid $OPENHANDS_USER_ID app
RUN groupadd --gid $OPENHANDS_USER_ID openhands
RUN useradd -l -m -u $OPENHANDS_USER_ID --gid $OPENHANDS_USER_ID -s /bin/bash openhands && \
usermod -aG app openhands && \
usermod -aG openhands openhands && \
usermod -aG sudo openhands && \
echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
RUN chown -R openhands:app /app && chmod -R 770 /app
RUN sudo chown -R openhands:app $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_BASE
RUN chown -R openhands:openhands /app && chmod -R 770 /app
RUN sudo chown -R openhands:openhands $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_BASE
USER openhands
ENV VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH" \
PYTHONPATH='/app'
COPY --chown=openhands:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY --chown=openhands:openhands --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY --chown=openhands:app --chmod=770 ./microagents ./microagents
COPY --chown=openhands:app --chmod=770 ./openhands ./openhands
COPY --chown=openhands:app --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
COPY --chown=openhands:app pyproject.toml poetry.lock README.md MANIFEST.in LICENSE ./
COPY --chown=openhands:openhands --chmod=770 ./microagents ./microagents
COPY --chown=openhands:openhands --chmod=770 ./openhands ./openhands
COPY --chown=openhands:openhands --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
COPY --chown=openhands:openhands pyproject.toml poetry.lock README.md MANIFEST.in LICENSE ./
# This is run as "openhands" user, and will create __pycache__ with openhands:openhands ownership
RUN python openhands/core/download.py # No-op to download assets
# Add this line to set group ownership of all files/directories not already in "app" group
# openhands:openhands -> openhands:app
RUN find /app \! -group app -exec chgrp app {} +
# openhands:openhands -> openhands:openhands
RUN find /app \! -group openhands -exec chgrp openhands {} +
COPY --chown=openhands:app --chmod=770 --from=frontend-builder /app/build ./frontend/build
COPY --chown=openhands:app --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh
COPY --chown=openhands:openhands --chmod=770 --from=frontend-builder /app/build ./frontend/build
COPY --chown=openhands:openhands --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh
USER root
+1 -1
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.53-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.54-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+1 -1
View File
@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.54-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:
+3295 -1449
View File
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
---
title: Jira Data Center Integration (Beta)
title: Jira Data Center Integration (Coming soon...)
description: Complete guide for setting up Jira Data Center integration with OpenHands Cloud, including service account creation, personal access token generation, webhook configuration, and workspace integration setup.
---
@@ -1,5 +1,5 @@
---
title: Jira Cloud Integration
title: Jira Cloud Integration (Coming soon...)
description: Complete guide for setting up Jira Cloud integration with OpenHands Cloud, including service account creation, API token generation, webhook configuration, and workspace integration setup.
---
@@ -1,5 +1,5 @@
---
title: Linear Integration
title: Linear Integration (Coming soon...)
description: Complete guide for setting up Linear integration with OpenHands Cloud, including service account creation, API key generation, webhook configuration, and workspace integration setup.
---
@@ -1,5 +1,5 @@
---
title: Project Management Tool Integrations
title: Project Management Tool Integrations (Coming soon...)
description: Overview of OpenHands Cloud integrations with project management platforms including Jira Cloud, Jira Data Center, and Linear. Learn about setup requirements, usage methods, and troubleshooting.
---
@@ -18,9 +18,9 @@ Integration requires two levels of setup:
2. **Workspace Integration** - Self-service configuration through the OpenHands Cloud UI to link your OpenHands account to the target workspace
### Platform-Specific Setup Guides:
- [Jira Cloud Integration](./jira-integration.md)
- [Jira Data Center Integration](./jira-dc-integration.md)
- [Linear Integration](./linear-integration.md)
- [Jira Cloud Integration (Coming soon...)](./jira-integration.md)
- [Jira Data Center Integration (Coming soon...)](./jira-dc-integration.md)
- [Linear Integration (Coming soon...)](./linear-integration.md)
## Usage
+52
View File
@@ -0,0 +1,52 @@
# Confirmation Mode and Security Analyzers
OpenHands provides a security framework to help protect users from potentially risky actions through **Confirmation Mode** and **Security Analyzers**. This system analyzes agent actions and prompts users for confirmation when high-risk operations are detected.
## Overview
The security system consists of two main components:
1. **Confirmation Mode**: When enabled, the agent will pause and ask for user confirmation before executing actions that are flagged as high-risk by the security analyzer.
2. **Security Analyzers**: These are modules that evaluate the risk level of agent actions and determine whether user confirmation is required.
## Configuration
### CLI
In CLI mode, confirmation is enabled by default. You will have an option to uses the LLM Analyzer and will automatically confirm LOW and MEDIUM risk actions, only prompting for HIGH risk actions.
## Security Analyzers
OpenHands includes multiple analyzers:
- **No Analyzer**: Do not use any security analyzer. The agent will prompt you to confirm *EVERY* action.
- **LLM Risk Analyzer** (default): Uses the same LLM as the agent to assess action risk levels
- **Invariant Analyzer**: Uses Invariant Labs' policy engine to evaluate action traces against security policies
### LLM Risk Analyzer
The default analyzer that leverages the agent's LLM to evaluate the security risk of each action. It considers the action type, parameters, and context to assign risk levels.
### Invariant Analyzer
An advanced analyzer that:
- Collects conversation events and parses them into a trace
- Checks the trace against an Invariant policy to classify risk (low, medium, high)
- Manages an Invariant server container automatically if needed
- Supports optional browsing-alignment and harmful-content checks
## How It Works
1. **Action Analysis**: When the agent wants to perform an action, the selected security analyzer evaluates its risk level.
2. **Risk Assessment**: The analyzer returns one of three risk levels:
- **LOW**: Action proceeds without confirmation
- **MEDIUM**: Action proceeds without confirmation (may be configurable in future)
- **HIGH**: Action is paused, and user confirmation is requested
3. **User Confirmation**: For high-risk actions, a confirmation dialog appears with:
- Description of the action
- Risk assessment explanation
- Options to approve or deny action
4. **Action Execution**: Based on user response:
- **Approve**: Action proceeds as planned
- **Deny**: Action is cancelled
+3 -3
View File
@@ -119,7 +119,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.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -128,8 +128,8 @@ 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.53 \
python -m openhands.cli.main --override-cli-mode true
docker.all-hands.dev/all-hands-ai/openhands:0.54 \
python -m openhands.cli.entry --override-cli-mode true
```
<Note>
+2 -2
View File
@@ -61,7 +61,7 @@ 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.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -73,7 +73,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.53 \
docker.all-hands.dev/all-hands-ai/openhands:0.54 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+4 -4
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.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-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.53
docker.all-hands.dev/all-hands-ai/openhands:0.54
```
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.53
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.54
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
+11 -4
View File
@@ -45,6 +45,13 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
1. [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
2. Run `wsl --version` in powershell and confirm `Default Version: 2`.
**Ubuntu (Linux Distribution)**
1. Install Ubuntu: `wsl --install -d Ubuntu` in PowerShell as Administrator.
2. Restart computer when prompted.
3. Open Ubuntu from Start menu to complete setup.
4. Verify installation: `wsl --list` should show Ubuntu.
**Docker Desktop**
1. [Install Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install).
@@ -53,7 +60,7 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
- Resources > WSL Integration: `Enable integration with my default WSL distro` is enabled.
<Note>
The docker command below to start the app must be run inside the WSL terminal.
The docker command below to start the app must be run inside the WSL terminal. Use `wsl -d Ubuntu` in PowerShell or search "Ubuntu" in the Start menu to access the Ubuntu terminal.
</Note>
**Alternative: Windows without WSL**
@@ -109,17 +116,17 @@ Note that you'll still need `uv` installed for the default MCP servers to work p
<Accordion title="Docker Command (Click to expand)">
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-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.53
docker.all-hands.dev/all-hands-ai/openhands:0.54
```
</Accordion>
+25
View File
@@ -130,3 +130,28 @@ docker run # ... \
<Note>
**Docker Desktop Required**: Network isolation features, including custom networks and `host.docker.internal` routing, require Docker Desktop. Docker Engine alone does not support these features on localhost across custom networks. If you're using Docker Engine without Docker Desktop, network isolation may not work as expected.
</Note>
### Sidecar Containers
If you want to run sidecar containers to the sandbox 'runner' containers without exposing the sandbox containers to the host network, you can use the `SANDBOX_ADDITIONAL_NETWORKS` environment variable to specify additional Docker network names that should be added to the sandbox containers.
```bash
docker network create openhands-sccache
docker run -d \
--hostname openhandsredis \
--network openhands-sccache \
redis
docker run # ...
-e SANDBOX_ADDITIONAL_NETWORKS='["openhands-sccache"]' \
# ...
```
Then all sandbox instances will have to access a shared redis instance at `openhandsredis:6379`.
#### Docker Compose gotcha
Note that Docker Compose adds a prefix (a scope) by default to created networks, which is not taken into account by the additional networks config. Therefore when using docker compose you have to either:
- specify a network name via the `name` field to remove the scoping (https://docs.docker.com/reference/compose-file/networks/#name)
- or provide the scope within the given config (e.g. `SANDBOX_ADDITIONAL_NETWORKS: '["myscope_openhands-sccache"]'` where `myscope` is the docker-compose assigned prefix).
+10 -12
View File
@@ -9,7 +9,8 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -60,18 +61,15 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
# Create config with EDA-specific container image
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
# Override the container image for EDA
config.sandbox.base_container_image = 'python:3.12-bookworm'
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.enable_prompt_extensions = False
@@ -146,7 +144,7 @@ def process_instance(
logger.info(f'Final message: {final_message} | Ground truth: {instance["text"]}')
test_result = game.reward()
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
+8 -14
View File
@@ -17,7 +17,8 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -40,19 +41,12 @@ from openhands.utils.async_utils import call_async_from_sync
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-slim'
# Create config with agent_bench-specific container image
config = get_openhands_config_for_eval(metadata=metadata)
# Override the container image for agent_bench
config.sandbox.base_container_image = 'python:3.12-slim'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.enable_prompt_extensions = False
@@ -273,7 +267,7 @@ def process_instance(
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Save the output
output = EvalOutput(
@@ -17,6 +17,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -49,15 +51,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.11-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
sandbox_config=sandbox_config,
runtime=os.environ.get('RUNTIME', 'docker'),
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -246,7 +243,7 @@ def process_instance(
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Save the output
output = EvalOutput(
+6 -9
View File
@@ -15,6 +15,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -60,15 +62,10 @@ def get_config(
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = BIOCODER_BENCH_CONTAINER_IMAGE
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -294,7 +291,7 @@ def process_instance(
raise ValueError('State should not be None.')
test_result = complete_runtime(runtime, instance)
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
+6 -9
View File
@@ -18,6 +18,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -74,15 +76,10 @@ def get_config(
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -422,7 +419,7 @@ def process_instance(
# You can simply get the LAST `MessageAction` from the returned `state.history` and parse it for evaluation.
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
@@ -11,6 +11,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -39,14 +41,8 @@ def get_config(
)
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
workspace_base=None,
workspace_mount_path=None,
config = get_openhands_config_for_eval(
metadata=metadata, runtime='docker', sandbox_config=sandbox_config
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -88,7 +84,7 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
+7 -10
View File
@@ -16,6 +16,8 @@ from evaluation.utils.shared import (
assert_and_raise,
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -113,16 +115,11 @@ def get_config(
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = base_container_image
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
enable_browser=RUN_WITH_BROWSING,
config = get_openhands_config_for_eval(
metadata=metadata,
sandbox_config=sandbox_config,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
enable_browser=RUN_WITH_BROWSING,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -480,7 +477,7 @@ def process_instance(
# NOTE: this is NO LONGER the event stream, but an agent history that includes delegate agent's events
histories = [event_to_dict(event) for event in state.history]
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Save the output
output = EvalOutput(
@@ -17,6 +17,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -64,15 +66,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -294,7 +291,7 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
test_result = complete_runtime(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
+6 -9
View File
@@ -22,6 +22,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -59,15 +61,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'nikolaik/python-nodejs:python3.12-nodejs22'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
sandbox_config=sandbox_config,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
if metadata.agent_config:
@@ -269,7 +266,7 @@ Here is the task:
'model_answer': model_answer,
'ground_truth': instance['Final answer'],
}
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
+6 -9
View File
@@ -12,6 +12,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -42,15 +44,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -108,7 +105,7 @@ def process_instance(
# attempt to parse model_answer
ast_eval_fn = instance['ast_eval']
correct, hallucination = ast_eval_fn(instance_id, model_answer_raw)
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
logger.info(
f'Final message: {model_answer_raw} | Correctness: {correct} | Hallucination: {hallucination}'
)
+6 -9
View File
@@ -30,6 +30,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -63,15 +65,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -292,7 +289,7 @@ Ok now its time to start solving the question. Good luck!
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Save the output
output = EvalOutput(
@@ -23,6 +23,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -84,15 +86,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -248,7 +245,7 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
test_result = complete_runtime(runtime, instance)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
@@ -16,6 +16,7 @@ import ruamel.yaml
from evaluation.utils.shared import (
EvalMetadata,
get_default_sandbox_config_for_eval,
get_openhands_config_for_eval,
make_metadata,
)
from openhands.core.config import (
@@ -37,15 +38,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -22,6 +22,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -47,15 +49,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -335,7 +332,7 @@ Be thorough in your exploration, testing, and reasoning. It's fine if your think
)
)
assert state is not None
metrics = state.metrics.get() if state.metrics else {}
metrics = get_metrics(state)
test_result = complete_runtime(runtime, instance)
@@ -10,6 +10,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -51,15 +53,10 @@ def get_config(
'$OH_INTERPRETER_PATH -m pip install scitools-pyke'
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -247,7 +244,7 @@ def process_instance(
)
test_result['final_message'] = final_message
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
+6 -9
View File
@@ -13,6 +13,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -57,15 +59,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'xingyaoww/od-eval-miniwob:v1.0'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime=os.environ.get('RUNTIME', 'docker'),
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -174,7 +171,7 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Instruction is the first message from the USER
instruction = ''
+6 -9
View File
@@ -15,6 +15,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -109,15 +111,10 @@ def get_config(
f'$OH_INTERPRETER_PATH -m pip install {" ".join(MINT_DEPENDENCIES)}'
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -205,7 +202,7 @@ def process_instance(
task_state = state.extra_data['task_state']
logger.info('Task state: ' + str(task_state.to_dict()))
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
+6 -9
View File
@@ -26,6 +26,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -79,15 +81,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'public.ecr.aws/i5g0m1f6/ml-bench'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -250,7 +247,7 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
)
)
assert state is not None
metrics = state.metrics.get() if state.metrics else {}
metrics = get_metrics(state)
test_result = complete_runtime(runtime)
@@ -23,6 +23,7 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
get_default_sandbox_config_for_eval,
get_openhands_config_for_eval,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
@@ -87,13 +88,9 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> OpenHandsConfig:
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
run_as_openhands=False,
config = get_openhands_config_for_eval(
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
return config
@@ -21,6 +21,7 @@ from evaluation.utils.shared import (
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
@@ -341,16 +342,11 @@ def get_config(
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
config = get_openhands_config_for_eval(
metadata=metadata,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -31,6 +31,7 @@ from evaluation.utils.shared import (
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
@@ -174,15 +175,10 @@ def get_config(
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(
@@ -12,6 +12,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -63,16 +65,10 @@ def get_config(
sandbox_config.base_container_image = (
'docker.io/xingyaoww/openhands-eval-scienceagentbench'
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime=os.environ.get('RUNTIME', 'docker'),
max_budget_per_task=4,
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -218,7 +214,7 @@ If the program uses some packages that are incompatible, please figure out alter
# You can simply get the LAST `MessageAction` from the returned `state.history` and parse it for evaluation.
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
@@ -19,6 +19,7 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
get_default_sandbox_config_for_eval,
get_openhands_config_for_eval,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
@@ -83,13 +84,9 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> OpenHandsConfig:
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
run_as_openhands=False,
config = get_openhands_config_for_eval(
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
return config
+4 -8
View File
@@ -32,6 +32,7 @@ from evaluation.utils.shared import (
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
@@ -227,16 +228,11 @@ def get_config(
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
config = get_openhands_config_for_eval(
metadata=metadata,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(
@@ -21,6 +21,7 @@ from evaluation.utils.shared import (
EvalException,
EvalMetadata,
EvalOutput,
get_metrics,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -179,7 +180,7 @@ def process_instance(
raise ValueError('State should not be None.')
histories = [event_to_dict(event) for event in state.history]
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Save the output
instruction = message_action.content
@@ -20,6 +20,7 @@ from evaluation.utils.shared import (
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
@@ -199,16 +200,11 @@ def get_config(
'REPO_PATH': f'/workspace/{workspace_dir_name}/',
}
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
config = get_openhands_config_for_eval(
metadata=metadata,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
+15 -13
View File
@@ -37,6 +37,7 @@ from evaluation.benchmarks.testgeneval.utils import load_testgeneval_dataset
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
get_openhands_config_for_eval,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
@@ -58,20 +59,21 @@ def get_config(instance: pd.Series) -> OpenHandsConfig:
f'Invalid container image for instance {instance["instance_id_swebench"]}.'
)
logger.info(f'Using instance container image: {base_container_image}.')
return OpenHandsConfig(
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'eventstream'),
sandbox=SandboxConfig(
base_container_image=base_container_image,
use_host_network=False,
timeout=1800,
api_key=os.environ.get('ALLHANDS_API_KEY'),
remote_runtime_api_url=os.environ.get(
'SANDBOX_REMOTE_RUNTIME_API_URL', 'http://localhost:8000'
),
# Create custom sandbox config for testgeneval with specific requirements
sandbox_config = SandboxConfig(
base_container_image=base_container_image,
use_host_network=False,
timeout=1800, # Longer timeout than default (300)
api_key=os.environ.get('ALLHANDS_API_KEY'),
remote_runtime_api_url=os.environ.get(
'SANDBOX_REMOTE_RUNTIME_API_URL', 'http://localhost:8000'
),
workspace_base=None,
workspace_mount_path=None,
)
return get_openhands_config_for_eval(
sandbox_config=sandbox_config,
runtime=os.environ.get('RUNTIME', 'docker'), # Different default runtime
)
+20 -22
View File
@@ -25,6 +25,7 @@ from evaluation.utils.shared import (
assert_and_raise,
codeact_user_response,
get_metrics,
get_openhands_config_for_eval,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
@@ -126,29 +127,26 @@ def get_config(
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
runtime=os.environ.get('RUNTIME', 'eventstream'),
sandbox=SandboxConfig(
base_container_image=base_container_image,
enable_auto_lint=True,
use_host_network=False,
# large enough timeout, since some testcases take very long to run
timeout=300,
# Add platform to the sandbox config to solve issue 4401
platform='linux/amd64',
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_api_url=os.environ.get(
'SANDBOX_REMOTE_RUNTIME_API_URL', 'http://localhost:8000'
),
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
sandbox_config = SandboxConfig(
base_container_image=base_container_image,
enable_auto_lint=True,
use_host_network=False,
# large enough timeout, since some testcases take very long to run
timeout=300,
# Add platform to the sandbox config to solve issue 4401
platform='linux/amd64',
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_api_url=os.environ.get(
'SANDBOX_REMOTE_RUNTIME_API_URL', 'http://localhost:8000'
),
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
)
config = get_openhands_config_for_eval(
metadata=metadata,
sandbox_config=sandbox_config,
runtime=os.environ.get('RUNTIME', 'docker'),
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -12,7 +12,10 @@ import tempfile
import yaml
from browsing import pre_login
from evaluation.utils.shared import get_default_sandbox_config_for_eval
from evaluation.utils.shared import (
get_default_sandbox_config_for_eval,
get_openhands_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
LLMConfig,
@@ -42,19 +45,17 @@ def get_config(
sandbox_config.enable_auto_lint = True
# If the web services are running on the host machine, this must be set to True
sandbox_config.use_host_network = True
config = OpenHandsConfig(
run_as_openhands=False,
max_budget_per_task=4,
config = get_openhands_config_for_eval(
max_iterations=100,
save_trajectory_path=os.path.join(
mount_path_on_host, f'traj_{task_short_name}.json'
),
sandbox=sandbox_config,
# we mount trajectories path so that trajectories, generated by OpenHands
# controller, can be accessible to the evaluator file in the runtime container
sandbox_config=sandbox_config,
workspace_mount_path=mount_path_on_host,
workspace_mount_path_in_sandbox='/outputs',
)
config.save_trajectory_path = os.path.join(
mount_path_on_host, f'traj_{task_short_name}.json'
)
config.max_budget_per_task = 4
config.set_llm_config(llm_config)
if agent_config:
config.set_agent_config(agent_config)
+6 -9
View File
@@ -11,6 +11,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -43,15 +45,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -134,7 +131,7 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
correct = eval_answer(str(model_answer_raw), str(answer))
logger.info(f'Final message: {model_answer_raw} | Correctness: {correct}')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
@@ -20,6 +20,7 @@ from evaluation.utils.shared import (
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
@@ -160,16 +161,11 @@ def get_config(
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
config = get_openhands_config_for_eval(
metadata=metadata,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -12,6 +12,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -72,16 +74,10 @@ def get_config(
'VWA_WIKIPEDIA': f'{base_url}:8888',
'VWA_HOMEPAGE': f'{base_url}:4399',
}
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
attach_to_existing=True,
sandbox_config=sandbox_config,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -179,7 +175,7 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Instruction obtained from the first message from the USER
instruction = ''
+6 -9
View File
@@ -12,6 +12,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -64,15 +66,10 @@ def get_config(
'MAP': f'{base_url}:3000',
'HOMEPAGE': f'{base_url}:4399',
}
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -163,7 +160,7 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Instruction is the first message from the USER
instruction = ''
+7 -11
View File
@@ -9,6 +9,8 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -44,18 +46,12 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.platform = 'linux/amd64'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime=os.environ.get('RUNTIME', 'docker'),
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
# debug
debug=True,
sandbox_config=sandbox_config,
)
config.debug = True
config.set_llm_config(
update_llm_config_for_completions_logging(
metadata.llm_config, metadata.eval_output_dir, instance_id
@@ -135,7 +131,7 @@ def process_instance(
assert len(histories) > 0, 'History should not be empty'
test_result: TestResult = test_class.verify_result(runtime, histories)
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
finally:
runtime.close()
+93 -2
View File
@@ -668,8 +668,23 @@ def is_fatal_runtime_error(error: str | None) -> bool:
def get_metrics(state: State) -> dict[str, Any]:
"""Extract metrics from the state."""
metrics = state.metrics.get() if state.metrics else {}
"""Extract metrics for evaluations.
Prefer ConversationStats (source of truth) and fall back to state.metrics for
backward compatibility.
"""
metrics: dict[str, Any]
try:
if getattr(state, 'conversation_stats', None):
combined = state.conversation_stats.get_combined_metrics()
metrics = combined.get()
elif getattr(state, 'metrics', None):
metrics = state.metrics.get()
else:
metrics = {}
except Exception:
metrics = state.metrics.get() if getattr(state, 'metrics', None) else {}
metrics['condenser'] = get_condensation_metadata(state)
return metrics
@@ -688,3 +703,79 @@ def get_default_sandbox_config_for_eval() -> SandboxConfig:
remote_runtime_enable_retries=True,
remote_runtime_class='sysbox',
)
def get_openhands_config_for_eval(
metadata: EvalMetadata | None = None,
sandbox_config: SandboxConfig | None = None,
runtime: str | None = None,
max_iterations: int | None = None,
default_agent: str | None = None,
enable_browser: bool = False,
workspace_base: str | None = None,
workspace_mount_path: str | None = None,
):
"""Create an OpenHandsConfig with common patterns used across evaluation scripts.
This function provides a standardized way to create OpenHands configurations
for evaluation runs, with sensible defaults that match the patterns used in
most run_infer.py scripts. Individual evaluation scripts can override specific
attributes as needed.
Args:
metadata: EvalMetadata containing agent class, max iterations, etc.
sandbox_config: Custom sandbox config. If None, uses get_default_sandbox_config_for_eval()
runtime: Runtime type. If None, uses environment RUNTIME or 'docker'
max_iterations: Max iterations for the agent. If None, uses metadata.max_iterations
default_agent: Agent class name. If None, uses metadata.agent_class
enable_browser: Whether to enable browser functionality
workspace_base: Workspace base path. Defaults to None
workspace_mount_path: Workspace mount path. Defaults to None
Returns:
OpenHandsConfig: Configured for evaluation with eval-specific overrides applied
"""
# Defer import to avoid circular imports at module load time
from openhands.core.config.openhands_config import (
OpenHandsConfig as _OHConfig, # type: ignore
)
# Use provided sandbox config or get default
if sandbox_config is None:
sandbox_config = get_default_sandbox_config_for_eval()
# Extract values from metadata if provided
if metadata is not None:
if max_iterations is None:
max_iterations = metadata.max_iterations
if default_agent is None:
default_agent = metadata.agent_class
# Use environment runtime or default
if runtime is None:
runtime = os.environ.get('RUNTIME', 'docker')
# Provide sensible defaults if still None
if default_agent is None:
default_agent = 'CodeActAgent'
if max_iterations is None:
max_iterations = 50
# Always use repo-local .eval_sessions directory (absolute path)
eval_store = os.path.abspath(os.path.join(os.getcwd(), '.eval_sessions'))
# Create the base config with evaluation-specific overrides
config = _OHConfig(
default_agent=default_agent,
run_as_openhands=False,
runtime=runtime,
max_iterations=max_iterations,
enable_browser=enable_browser,
sandbox=sandbox_config,
workspace_base=workspace_base,
workspace_mount_path=workspace_mount_path,
file_store='local',
file_store_path=eval_store,
)
return config
@@ -232,13 +232,16 @@ describe("RepositorySelectionForm", () => {
renderForm();
const dropdown = await screen.findByTestId("repo-dropdown");
const input = dropdown.querySelector('input[type="text"]') as HTMLInputElement;
const input = dropdown.querySelector(
'input[type="text"]',
) as HTMLInputElement;
expect(input).toBeInTheDocument();
await userEvent.type(input, "https://github.com/kubernetes/kubernetes");
expect(searchGitReposSpy).toHaveBeenLastCalledWith(
"kubernetes/kubernetes",
3,
"github",
);
});
@@ -268,13 +271,16 @@ describe("RepositorySelectionForm", () => {
renderForm();
const dropdown = await screen.findByTestId("repo-dropdown");
const input = dropdown.querySelector('input[type="text"]') as HTMLInputElement;
const input = dropdown.querySelector(
'input[type="text"]',
) as HTMLInputElement;
expect(input).toBeInTheDocument();
await userEvent.type(input, "https://github.com/kubernetes/kubernetes");
expect(searchGitReposSpy).toHaveBeenLastCalledWith(
"kubernetes/kubernetes",
3,
"github",
);
});
});
+44 -22
View File
@@ -79,6 +79,35 @@ describe("Content", () => {
expect(screen.getByTestId("set-indicator")).toBeInTheDocument();
});
});
it("should conditionally show security analyzer based on confirmation mode", async () => {
renderLlmSettingsScreen();
await screen.findByTestId("llm-settings-screen");
const confirmation = screen.getByTestId("enable-confirmation-mode-switch");
// Initially confirmation mode is false, so security analyzer should not be visible
expect(confirmation).not.toBeChecked();
expect(
screen.queryByTestId("security-analyzer-input"),
).not.toBeInTheDocument();
// Enable confirmation mode
await userEvent.click(confirmation);
expect(confirmation).toBeChecked();
// Security analyzer should now be visible
screen.getByTestId("security-analyzer-input");
// Disable confirmation mode again
await userEvent.click(confirmation);
expect(confirmation).not.toBeChecked();
// Security analyzer should be hidden again
expect(
screen.queryByTestId("security-analyzer-input"),
).not.toBeInTheDocument();
});
});
describe("Advanced form", () => {
@@ -107,7 +136,6 @@ describe("Content", () => {
within(advancedForm).getByTestId("llm-api-key-input");
within(advancedForm).getByTestId("llm-api-key-help-anchor-advanced");
within(advancedForm).getByTestId("agent-input");
within(advancedForm).getByTestId("enable-confirmation-mode-switch");
within(advancedForm).getByTestId("enable-memory-condenser-switch");
await userEvent.click(advancedSwitch);
@@ -130,9 +158,6 @@ describe("Content", () => {
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");
expect(model).toHaveValue("openhands/claude-sonnet-4-20250514");
@@ -140,15 +165,7 @@ describe("Content", () => {
expect(apiKey).toHaveValue("");
expect(apiKey).toHaveProperty("placeholder", "");
expect(agent).toHaveValue("CodeActAgent");
expect(confirmation).not.toBeChecked();
expect(condensor).toBeChecked();
// check that security analyzer is present
expect(
screen.queryByTestId("security-analyzer-input"),
).not.toBeInTheDocument();
await userEvent.click(confirmation);
screen.getByTestId("security-analyzer-input");
});
it("should render the advanced form if existings settings are advanced", async () => {
@@ -177,7 +194,7 @@ describe("Content", () => {
agent: "CoActAgent",
confirmation_mode: true,
enable_default_condenser: false,
security_analyzer: "mock-invariant",
security_analyzer: "none",
});
renderLlmSettingsScreen();
@@ -203,7 +220,7 @@ describe("Content", () => {
expect(agent).toHaveValue("CoActAgent");
expect(confirmation).toBeChecked();
expect(condensor).not.toBeChecked();
expect(securityAnalyzer).toHaveValue("mock-invariant");
expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_NONE");
});
});
});
@@ -293,7 +310,7 @@ describe("Form submission", () => {
// select security analyzer
const securityAnalyzer = screen.getByTestId("security-analyzer-input");
await userEvent.click(securityAnalyzer);
const securityAnalyzerOption = screen.getByText("mock-invariant");
const securityAnalyzerOption = screen.getByText("SETTINGS$SECURITY_ANALYZER_NONE");
await userEvent.click(securityAnalyzerOption);
const submitButton = screen.getByTestId("submit-button");
@@ -306,7 +323,7 @@ describe("Form submission", () => {
agent: "CoActAgent",
confirmation_mode: true,
enable_default_condenser: false,
security_analyzer: "mock-invariant",
security_analyzer: null,
}),
);
});
@@ -375,9 +392,11 @@ describe("Form submission", () => {
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");
// Confirmation mode switch is now in basic settings, always visible
const confirmation = await screen.findByTestId("enable-confirmation-mode-switch");
// enter custom model
await userEvent.type(model, "-mini");
expect(model).toHaveValue("openai/gpt-4o-mini");
@@ -451,14 +470,17 @@ describe("Form submission", () => {
// select security analyzer
const securityAnalyzer = await screen.findByTestId("security-analyzer-input");
await userEvent.click(securityAnalyzer);
const securityAnalyzerOption = screen.getByText("mock-invariant");
const securityAnalyzerOption = screen.getByText("SETTINGS$SECURITY_ANALYZER_NONE");
await userEvent.click(securityAnalyzerOption);
expect(securityAnalyzer).toHaveValue("mock-invariant");
expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_NONE");
expect(submitButton).not.toBeDisabled();
await userEvent.clear(securityAnalyzer);
expect(securityAnalyzer).toHaveValue("");
// revert back to original value
await userEvent.click(securityAnalyzer);
const originalSecurityAnalyzerOption = screen.getByText("SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT");
await userEvent.click(originalSecurityAnalyzerOption);
expect(securityAnalyzer).toHaveValue("SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT");
expect(submitButton).toBeDisabled();
});
@@ -552,7 +574,7 @@ describe("Form submission", () => {
expect.objectContaining({
llm_model: "openhands/claude-sonnet-4-20250514",
llm_base_url: "",
confirmation_mode: false,
confirmation_mode: true, // Confirmation mode is now a basic setting, should be preserved
}),
);
});
@@ -107,9 +107,7 @@ describe("Content", () => {
expect(screen.queryByTestId("add-secret-button")).not.toBeInTheDocument(),
);
const button = await screen.findByTestId("connect-git-button");
await userEvent.click(button);
screen.getByTestId("git-settings-screen");
expect(button).toHaveAttribute("href", "/settings/integrations");
});
it("should render an empty table when there are no existing secrets", async () => {
+1 -1
View File
@@ -136,7 +136,7 @@ describe("Settings Screen", () => {
"secrets",
"api keys",
];
const sectionsToExclude = ["llm", "mcp"];
const sectionsToExclude = ["llm"];
renderSettingsScreen();
@@ -29,23 +29,5 @@ describe("hasAdvancedSettingsSet", () => {
}),
).toBe(true);
});
test("CONFIRMATION_MODE is true", () => {
expect(
hasAdvancedSettingsSet({
...DEFAULT_SETTINGS,
CONFIRMATION_MODE: true,
}),
).toBe(true);
});
test("SECURITY_ANALYZER is set", () => {
expect(
hasAdvancedSettingsSet({
...DEFAULT_SETTINGS,
SECURITY_ANALYZER: "test",
}),
).toBe(true);
});
});
});
+170 -278
View File
File diff suppressed because it is too large Load Diff
+23 -23
View File
@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.53.0",
"version": "0.54.0",
"private": true,
"type": "module",
"engines": {
@@ -11,17 +11,17 @@
"@heroui/use-infinite-scroll": "^2.2.10",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.8.0",
"@react-router/serve": "^7.8.0",
"@react-router/node": "^7.8.2",
"@react-router/serve": "^7.8.2",
"@react-types/shared": "^3.31.0",
"@reduxjs/toolkit": "^2.8.2",
"@stripe/react-stripe-js": "^3.9.0",
"@stripe/react-stripe-js": "^3.9.1",
"@stripe/stripe-js": "^7.8.0",
"@tailwindcss/postcss": "^4.1.12",
"@tailwindcss/vite": "^4.1.12",
"@tanstack/react-query": "^5.85.3",
"@tanstack/react-query": "^5.85.5",
"@uidotdev/usehooks": "^2.4.1",
"@vitejs/plugin-react": "^5.0.0",
"@vitejs/plugin-react": "^5.0.1",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.11.0",
@@ -29,32 +29,32 @@
"date-fns": "^4.1.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.23.12",
"i18next": "^25.3.6",
"i18next": "^25.4.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.29",
"jose": "^6.0.12",
"lucide-react": "^0.539.0",
"isbot": "^5.1.30",
"jose": "^6.0.13",
"lucide-react": "^0.541.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.260.1",
"posthog-js": "^1.260.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.5.1",
"react-i18next": "^15.6.1",
"react-hot-toast": "^2.6.0",
"react-i18next": "^15.7.2",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.8.0",
"react-router": "^7.8.2",
"react-select": "^5.10.2",
"react-syntax-highlighter": "^15.6.1",
"react-syntax-highlighter": "^15.6.5",
"react-textarea-autosize": "^8.5.9",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"vite": "^7.1.1",
"vite": "^7.1.3",
"web-vitals": "^5.1.0",
"ws": "^8.18.2"
},
@@ -88,16 +88,16 @@
"@babel/traverse": "^7.28.3",
"@babel/types": "^7.28.2",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.54.2",
"@react-router/dev": "^7.8.0",
"@playwright/test": "^1.55.0",
"@react-router/dev": "^7.8.2",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.83.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.7.0",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.2.0",
"@types/react": "^19.1.9",
"@types/node": "^24.3.0",
"@types/react": "^19.1.11",
"@types/react-dom": "^19.1.7",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
@@ -117,7 +117,7 @@
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.1.4",
"eslint-plugin-unused-imports": "^4.2.0",
"husky": "^9.1.7",
"jsdom": "^26.1.0",
"lint-staged": "^16.1.4",
@@ -126,7 +126,7 @@
"stripe": "^18.4.0",
"tailwindcss": "^4.1.8",
"typescript": "^5.9.2",
"vite-plugin-svgr": "^4.2.0",
"vite-plugin-svgr": "^4.5.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.2"
},
@@ -1,4 +1,5 @@
import { useMemo } from "react";
import { StylesConfig } from "react-select";
import { Provider } from "../../types/settings";
import { ReactSelectDropdown, SelectOption } from "./react-select-dropdown";
@@ -11,6 +12,8 @@ export interface GitProviderDropdownProps {
disabled?: boolean;
isLoading?: boolean;
onChange?: (provider: Provider | null) => void;
classNamePrefix?: string;
styles?: StylesConfig<SelectOption, false>;
}
export function GitProviderDropdown({
@@ -22,6 +25,8 @@ export function GitProviderDropdown({
disabled = false,
isLoading = false,
onChange,
classNamePrefix,
styles,
}: GitProviderDropdownProps) {
const options: SelectOption[] = useMemo(
() =>
@@ -53,6 +58,8 @@ export function GitProviderDropdown({
isSearchable={false}
isLoading={isLoading}
onChange={handleChange}
classNamePrefix={classNamePrefix}
styles={styles}
/>
);
}
@@ -1,7 +1,9 @@
import { useCallback, useMemo, useRef } from "react";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Provider } from "../../types/settings";
import { useGitRepositories } from "../../hooks/query/use-git-repositories";
import { useSearchRepositories } from "../../hooks/query/use-search-repositories";
import { useDebounce } from "../../hooks/use-debounce";
import OpenHands from "../../api/open-hands";
import { GitRepository } from "../../types/git";
import {
@@ -19,10 +21,6 @@ export interface GitRepositoryDropdownProps {
onChange?: (repository?: GitRepository) => void;
}
interface SearchCache {
[key: string]: GitRepository[];
}
export function GitRepositoryDropdown({
provider,
value,
@@ -33,6 +31,20 @@ export function GitRepositoryDropdown({
onChange,
}: GitRepositoryDropdownProps) {
const { t } = useTranslation();
const [searchInput, setSearchInput] = useState("");
const debouncedSearchInput = useDebounce(searchInput, 300);
// Process search input to handle URLs
const processedSearchInput = useMemo(() => {
if (debouncedSearchInput.startsWith("https://")) {
const match = debouncedSearchInput.match(
/https:\/\/[^/]+\/([^/]+\/[^/]+)/,
);
return match ? match[1] : debouncedSearchInput;
}
return debouncedSearchInput;
}, [debouncedSearchInput]);
const {
data,
fetchNextPage,
@@ -45,6 +57,10 @@ export function GitRepositoryDropdown({
enabled: !disabled,
});
// Search query for processed input (handles URLs)
const { data: searchData, isLoading: isSearchLoading } =
useSearchRepositories(processedSearchInput, provider);
const allOptions: AsyncSelectOption[] = useMemo(
() =>
data?.pages
@@ -58,75 +74,83 @@ export function GitRepositoryDropdown({
[data],
);
// Keep track of search results
const searchCache = useRef<SearchCache>({});
const searchOptions: AsyncSelectOption[] = useMemo(
() =>
searchData
? searchData.map((repo) => ({
value: repo.id,
label: repo.full_name,
}))
: [],
[searchData],
);
const selectedOption = useMemo(() => {
// First check in loaded pages
const option = allOptions.find((opt) => opt.value === value);
if (option) return option;
// If not found, check in search cache
const repo = Object.values(searchCache.current)
.flat()
.find((r) => r.id === value);
if (repo) {
return {
value: repo.id,
label: repo.full_name,
};
}
// If not found, check in search results
const searchOption = searchOptions.find((opt) => opt.value === value);
if (searchOption) return searchOption;
return null;
}, [allOptions, value]);
}, [allOptions, searchOptions, value]);
const loadOptions = useCallback(
async (inputValue: string): Promise<AsyncSelectOption[]> => {
// Update search input to trigger debounced search
setSearchInput(inputValue);
// If empty input, show all loaded options
if (!inputValue.trim()) {
return allOptions;
}
// If it looks like a URL, extract the repo name and search
// For very short inputs, do local filtering
if (inputValue.length < 2) {
return allOptions.filter((option) =>
option.label.toLowerCase().includes(inputValue.toLowerCase()),
);
}
// Handle URL inputs by performing direct search
if (inputValue.startsWith("https://")) {
const match = inputValue.match(/https:\/\/[^/]+\/([^/]+\/[^/]+)/);
if (match) {
const repoName = match[1];
const searchResults = await OpenHands.searchGitRepositories(
repoName,
3,
);
// Cache the search results
searchCache.current[repoName] = searchResults;
return searchResults.map((repo) => ({
value: repo.id,
label: repo.full_name,
}));
try {
// Perform direct search for URL-based inputs
const repositories = await OpenHands.searchGitRepositories(
repoName,
3,
provider,
);
return repositories.map((repo) => ({
value: repo.full_name,
label: repo.full_name,
data: repo,
}));
} catch (error) {
// Fall back to local filtering if search fails
return allOptions.filter((option) =>
option.label.toLowerCase().includes(repoName.toLowerCase()),
);
}
}
}
// For any other input, search via API
if (inputValue.length >= 2) {
// Only search if at least 2 characters
const searchResults = await OpenHands.searchGitRepositories(
inputValue,
10,
);
// Cache the search results
searchCache.current[inputValue] = searchResults;
return searchResults.map((repo) => ({
value: repo.id,
label: repo.full_name,
}));
// For regular text inputs, use hook-based search results if available
if (searchOptions.length > 0 && processedSearchInput === inputValue) {
return searchOptions;
}
// For very short inputs, do local filtering
// Fallback to local filtering while search is loading
return allOptions.filter((option) =>
option.label.toLowerCase().includes(inputValue.toLowerCase()),
);
},
[allOptions],
[allOptions, searchOptions, processedSearchInput, provider],
);
const handleChange = (option: AsyncSelectOption | null) => {
@@ -142,9 +166,7 @@ export function GitRepositoryDropdown({
// If not found, check in search results
if (!repo) {
repo = Object.values(searchCache.current)
.flat()
.find((r) => r.id === option.value);
repo = searchData?.find((r) => r.id === option.value);
}
onChange?.(repo);
@@ -167,7 +189,7 @@ export function GitRepositoryDropdown({
errorMessage={errorMessage}
disabled={disabled}
isClearable={false}
isLoading={isLoading || isLoading || isFetchingNextPage}
isLoading={isLoading || isFetchingNextPage || isSearchLoading}
cacheOptions
defaultOptions={allOptions}
onChange={handleChange}
@@ -1,5 +1,5 @@
import { useMemo } from "react";
import Select from "react-select";
import Select, { StylesConfig } from "react-select";
import { cn } from "#/utils/utils";
import { SelectOptionBase, getCustomStyles } from "./react-select-styles";
@@ -17,6 +17,8 @@ export interface ReactSelectDropdownProps {
isSearchable?: boolean;
isLoading?: boolean;
onChange?: (option: SelectOption | null) => void;
classNamePrefix?: string;
styles?: StylesConfig<SelectOption, false>;
}
export function ReactSelectDropdown({
@@ -31,6 +33,8 @@ export function ReactSelectDropdown({
isSearchable = true,
isLoading = false,
onChange,
classNamePrefix,
styles,
}: ReactSelectDropdownProps) {
const customStyles = useMemo(() => getCustomStyles<SelectOption>(), []);
@@ -46,8 +50,9 @@ export function ReactSelectDropdown({
isSearchable={isSearchable}
isLoading={isLoading}
onChange={onChange}
styles={customStyles}
styles={styles || customStyles}
className="w-full"
classNamePrefix={classNamePrefix}
/>
{errorMessage && (
<p className="text-red-500 text-sm mt-1">{errorMessage}</p>
@@ -90,3 +90,26 @@ export const getCustomStyles = <T extends SelectOptionBase>(): StylesConfig<
color: "#B7BDC2", // tertiary-light
}),
});
export const getGitProviderMicroagentManagementCustomStyles = <
T extends SelectOptionBase,
>(): StylesConfig<T, false> => ({
...getCustomStyles<T>(),
control: (provided, state) => ({
...provided,
backgroundColor: state.isDisabled ? "#363636" : "#454545", // darker tertiary when disabled
border: "1px solid #717888",
borderRadius: "0.125rem",
minHeight: "2.5rem",
padding: "0 0.5rem",
boxShadow: "none",
opacity: state.isDisabled ? 0.6 : 1,
cursor: state.isDisabled ? "not-allowed" : "pointer",
"&:hover": {
borderColor: "#717888",
},
"& .git-provider-dropdown__value-container": {
padding: "2px 0",
},
}),
});
@@ -24,6 +24,17 @@ import { AgentState } from "#/types/agent-state";
import { getFirstPRUrl } from "#/utils/parse-pr-url";
import MemoryIcon from "#/icons/memory_icon.svg?react";
const isErrorEvent = (evt: unknown): evt is { error: true; message: string } =>
typeof evt === "object" &&
evt !== null &&
"error" in evt &&
evt.error === true;
const isAgentStatusError = (evt: unknown): boolean =>
isOpenHandsEvent(evt) &&
isAgentStateChangeObservation(evt) &&
evt.extras.agent_state === AgentState.ERROR;
interface MessagesProps {
messages: (OpenHandsAction | OpenHandsObservation)[];
isAwaitingUserConfirmation: boolean;
@@ -31,8 +42,11 @@ interface MessagesProps {
export const Messages: React.FC<MessagesProps> = React.memo(
({ messages, isAwaitingUserConfirmation }) => {
const { createConversationAndSubscribe, isPending } =
useCreateConversationAndSubscribeMultiple();
const {
createConversationAndSubscribe,
isPending,
unsubscribeFromConversation,
} = useCreateConversationAndSubscribeMultiple();
const { getOptimisticUserMessage } = useOptimisticUserMessage();
const { conversationId } = useConversationId();
const { data: conversation } = useUserConversation(conversationId);
@@ -93,20 +107,6 @@ export const Messages: React.FC<MessagesProps> = React.memo(
const handleMicroagentEvent = React.useCallback(
(socketEvent: unknown, microagentConversationId: string) => {
// Handle error events
const isErrorEvent = (
evt: unknown,
): evt is { error: true; message: string } =>
typeof evt === "object" &&
evt !== null &&
"error" in evt &&
evt.error === true;
const isAgentStatusError = (evt: unknown): boolean =>
isOpenHandsEvent(evt) &&
isAgentStateChangeObservation(evt) &&
evt.extras.agent_state === AgentState.ERROR;
if (isErrorEvent(socketEvent) || isAgentStatusError(socketEvent)) {
setMicroagentStatuses((prev) =>
prev.map((statusEntry) =>
@@ -119,7 +119,11 @@ export const Messages: React.FC<MessagesProps> = React.memo(
isOpenHandsEvent(socketEvent) &&
isAgentStateChangeObservation(socketEvent)
) {
if (socketEvent.extras.agent_state === AgentState.FINISHED) {
// Handle completion states
if (
socketEvent.extras.agent_state === AgentState.FINISHED ||
socketEvent.extras.agent_state === AgentState.AWAITING_USER_INPUT
) {
setMicroagentStatuses((prev) =>
prev.map((statusEntry) =>
statusEntry.conversationId === microagentConversationId
@@ -127,6 +131,8 @@ export const Messages: React.FC<MessagesProps> = React.memo(
: statusEntry,
),
);
unsubscribeFromConversation(microagentConversationId);
}
} else if (
isOpenHandsEvent(socketEvent) &&
@@ -147,9 +153,27 @@ export const Messages: React.FC<MessagesProps> = React.memo(
),
);
}
unsubscribeFromConversation(microagentConversationId);
} else {
// For any other event, transition from WAITING to CREATING if still waiting
setMicroagentStatuses((prev) => {
const currentStatus = prev.find(
(entry) => entry.conversationId === microagentConversationId,
)?.status;
if (currentStatus === MicroagentStatus.WAITING) {
return prev.map((statusEntry) =>
statusEntry.conversationId === microagentConversationId
? { ...statusEntry, status: MicroagentStatus.CREATING }
: statusEntry,
);
}
return prev; // No change needed
});
}
},
[setMicroagentStatuses],
[setMicroagentStatuses, unsubscribeFromConversation],
);
const handleLaunchMicroagent = (
@@ -178,13 +202,13 @@ export const Messages: React.FC<MessagesProps> = React.memo(
},
onSuccessCallback: (newConversationId: string) => {
setShowLaunchMicroagentModal(false);
// Update status with conversation ID
// Update status with conversation ID - start with WAITING
setMicroagentStatuses((prev) => [
...prev.filter((status) => status.eventId !== selectedEventId),
{
eventId: selectedEventId,
conversationId: newConversationId,
status: MicroagentStatus.CREATING,
status: MicroagentStatus.WAITING,
},
]);
},
@@ -19,6 +19,8 @@ export function MicroagentStatusIndicator({
const getStatusText = () => {
switch (status) {
case MicroagentStatus.WAITING:
return t("MICROAGENT$STATUS_WAITING");
case MicroagentStatus.CREATING:
return t("MICROAGENT$STATUS_CREATING");
case MicroagentStatus.COMPLETED:
@@ -35,6 +37,8 @@ export function MicroagentStatusIndicator({
const getStatusIcon = () => {
switch (status) {
case MicroagentStatus.WAITING:
return <Spinner size="sm" />;
case MicroagentStatus.CREATING:
return <Spinner size="sm" />;
case MicroagentStatus.COMPLETED:
@@ -10,6 +10,11 @@ interface ConversationCreatedToastProps {
onClose: () => void;
}
interface ConversationStartingToastProps {
conversationId: string;
onClose: () => void;
}
function ConversationCreatedToast({
conversationId,
onClose,
@@ -37,6 +42,33 @@ function ConversationCreatedToast({
);
}
function ConversationStartingToast({
conversationId,
onClose,
}: ConversationStartingToastProps) {
const { t } = useTranslation();
return (
<div className="flex items-start gap-2">
<Spinner size="sm" />
<div>
{t("MICROAGENT$CONVERSATION_STARTING")}
<br />
<a
href={`/conversations/${conversationId}`}
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{t("MICROAGENT$VIEW_CONVERSATION")}
</a>
</div>
<button type="button" onClick={onClose}>
<CloseIcon />
</button>
</div>
);
}
interface ConversationFinishedToastProps {
conversationId: string;
onClose: () => void;
@@ -78,10 +110,18 @@ function ConversationErroredToast({
errorMessage,
onClose,
}: ConversationErroredToastProps) {
const { t } = useTranslation();
// Check if the error message is a translation key
const displayMessage =
errorMessage === "MICROAGENT$UNKNOWN_ERROR"
? t(errorMessage)
: errorMessage;
return (
<div className="flex items-start gap-2">
<SuccessIndicator status="error" />
<div>{errorMessage}</div>
<div>{displayMessage}</div>
<button type="button" onClick={onClose}>
<CloseIcon />
</button>
@@ -136,3 +176,18 @@ export const renderConversationErroredToast = (
duration: 5000,
},
);
export const renderConversationStartingToast = (conversationId: string) =>
toast(
(toastInstance) => (
<ConversationStartingToast
conversationId={conversationId}
onClose={() => toast.dismiss(toastInstance.id)}
/>
),
{
...TOAST_OPTIONS,
id: `starting-${conversationId}`,
duration: 10000, // Show for 10 seconds or until dismissed
},
);
@@ -7,11 +7,10 @@ import { ConversationCard } from "../conversation-panel/conversation-card";
import { Provider } from "#/types/settings";
interface ControlsProps {
setSecurityOpen: (isOpen: boolean) => void;
showSecurityLock: boolean;
}
export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
export function Controls({ showSecurityLock }: ControlsProps) {
const { data: conversation } = useActiveConversation();
const [contextMenuOpen, setContextMenuOpen] = React.useState(false);
@@ -21,9 +20,7 @@ export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
<AgentControlBar />
<AgentStatusBar />
{showSecurityLock && (
<SecurityLock onClick={() => setSecurityOpen(true)} />
)}
{showSecurityLock && <SecurityLock />}
</div>
<ConversationCard
@@ -1,17 +1,28 @@
import { IoLockClosed } from "react-icons/io5";
import { Tooltip } from "@heroui/react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { I18nKey } from "#/i18n/declaration";
interface SecurityLockProps {
onClick: () => void;
}
export function SecurityLock() {
const { t } = useTranslation();
export function SecurityLock({ onClick }: SecurityLockProps) {
return (
<div
className="cursor-pointer hover:opacity-80 transition-all"
style={{ marginRight: "8px" }}
onClick={onClick}
<Tooltip
content={
<div className="max-w-xs p-2">
{t(I18nKey.SETTINGS$CONFIRMATION_MODE_LOCK_TOOLTIP)}
</div>
}
placement="top"
>
<IoLockClosed size={20} />
</div>
<Link
to="/settings"
className="mr-2 cursor-pointer hover:opacity-80 transition-all"
aria-label={t(I18nKey.SETTINGS$TITLE)}
>
<IoLockClosed size={20} />
</Link>
</Tooltip>
);
}
@@ -23,9 +23,9 @@ export function ConfirmStopModal({
<ModalBackdrop>
<ModalBody className="items-start border border-tertiary">
<div className="flex flex-col gap-2">
<BaseModalTitle title={t(I18nKey.CONVERSATION$CONFIRM_STOP)} />
<BaseModalTitle title={t(I18nKey.CONVERSATION$CONFIRM_PAUSE)} />
<BaseModalDescription
description={t(I18nKey.CONVERSATION$STOP_WARNING)}
description={t(I18nKey.CONVERSATION$PAUSE_WARNING)}
/>
</div>
<div
@@ -129,7 +129,7 @@ export function ConversationCardContextMenu({
{onStop && (
<ContextMenuListItem testId="stop-button" onClick={onStop}>
<ContextMenuIconText icon={Power} text={t(I18nKey.BUTTON$STOP)} />
<ContextMenuIconText icon={Power} text={t(I18nKey.BUTTON$PAUSE)} />
</ContextMenuListItem>
)}
@@ -1,4 +1,6 @@
import { ConversationStatus } from "#/types/conversation-status";
import ArchivedIcon from "./state-indicators/archived.svg?react";
import ErrorIcon from "./state-indicators/error.svg?react";
import RunningIcon from "./state-indicators/running.svg?react";
import StartingIcon from "./state-indicators/starting.svg?react";
import StoppedIcon from "./state-indicators/stopped.svg?react";
@@ -9,6 +11,8 @@ const CONVERSATION_STATUS_INDICATORS: Record<ConversationStatus, SVGIcon> = {
STOPPED: StoppedIcon,
RUNNING: RunningIcon,
STARTING: StartingIcon,
ARCHIVED: ArchivedIcon,
ERROR: ErrorIcon,
};
interface ConversationStateIndicatorProps {
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#A7A9AC"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17 7h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1 0 1.43-.98 2.63-2.31 2.98l1.46 1.46C20.88 15.61 22 13.95 22 12c0-2.76-2.24-5-5-5zm-1 4h-2.19l2 2H16zM2 4.27l3.11 3.11C3.29 8.12 2 9.91 2 12c0 2.76 2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1 0-1.59 1.21-2.9 2.76-3.07L8.73 11H8v2h2.73L13 15.27V17h1.73l4.01 4L20 19.74 3.27 3 2 4.27z"/><path d="M0 24V0" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 512 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#e7000b"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>

After

Width:  |  Height:  |  Size: 254 B

@@ -17,7 +17,7 @@ export function MicroagentManagementAccordionTitle({
<TooltipButton
tooltip={repository.full_name}
ariaLabel={repository.full_name}
className="text-white text-base font-normal bg-transparent p-0 min-w-0 h-auto cursor-pointer truncate max-w-[232px]"
className="text-white text-base font-normal bg-transparent p-0 min-w-0 h-auto cursor-pointer truncate max-w-[194px] translate-y-[-1px]"
testId="repository-name-tooltip"
placement="bottom"
>
@@ -7,8 +7,6 @@ import {
} from "#/state/microagent-management-slice";
import { RootState } from "#/store";
import { GitRepository } from "#/types/git";
import PlusIcon from "#/icons/plus.svg?react";
import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
interface MicroagentManagementAddMicroagentButtonProps {
repository: GitRepository;
@@ -25,23 +23,22 @@ export function MicroagentManagementAddMicroagentButton({
const dispatch = useDispatch();
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
dispatch(setAddMicroagentModalVisible(!addMicroagentModalVisible));
dispatch(setSelectedRepository(repository));
};
return (
<div onClick={handleClick}>
<TooltipButton
tooltip={t(I18nKey.COMMON$ADD_MICROAGENT)}
ariaLabel={t(I18nKey.COMMON$ADD_MICROAGENT)}
className="p-0 min-w-0 h-6 w-6 flex items-center justify-center bg-transparent cursor-pointer"
testId="add-microagent-button"
placement="bottom"
>
<PlusIcon width={22} height={22} />
</TooltipButton>
</div>
<button
type="button"
onClick={handleClick}
className="translate-y-[-1px]"
data-testid="add-microagent-button"
>
<span className="text-sm font-normal leading-5 text-[#8480FF] cursor-pointer hover:text-[#6C63FF] transition-colors duration-200">
{t(I18nKey.COMMON$ADD_MICROAGENT)}
</span>
</button>
);
}
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { MicroagentManagementSidebar } from "./microagent-management-sidebar";
import { MicroagentManagementMain } from "./microagent-management-main";
@@ -25,6 +26,13 @@ import { GitRepository } from "#/types/git";
import { queryClient } from "#/query-client-config";
import { Provider } from "#/types/settings";
import { MicroagentManagementLearnThisRepoModal } from "./microagent-management-learn-this-repo-modal";
import {
displaySuccessToast,
displayErrorToast,
} from "#/utils/custom-toast-handlers";
import { getFirstPRUrl } from "#/utils/parse-pr-url";
import { I18nKey } from "#/i18n/declaration";
import { useUserProviders } from "#/hooks/use-user-providers";
// Handle error events
const isErrorEvent = (evt: unknown): evt is { error: true; message: string } =>
@@ -58,16 +66,10 @@ const getConversationInstructions = (
gitProvider: Provider,
) => `Create a microagent for the repository ${repositoryName} by following the steps below:
- Step 1: Create a markdown file inside the .openhands/microagents folder with the name of the microagent (The microagent must be created in the .openhands/microagents folder and should be able to perform the described task when triggered).
- This is the instructions about what the microagent should do: ${formData.query}
${
- Step 1: Create a markdown file inside the .openhands/microagents folder with the name of the microagent (The microagent must be created in the .openhands/microagents folder and should be able to perform the described task when triggered). This is the instructions about what the microagent should do: ${formData.query}. ${
formData.triggers && formData.triggers.length > 0
? `
- This is the triggers of the microagent: ${formData.triggers.join(", ")}
`
: "- Please be noted that the microagent doesn't have any triggers."
? `This is the triggers of the microagent: ${formData.triggers.join(", ")}`
: "Please be noted that the microagent doesn't have any triggers."
}
- Step 2: Create a new branch for the repository ${repositoryName}, must avoid duplicated branches.
@@ -84,16 +86,10 @@ const getUpdateConversationInstructions = (
) => `Update the microagent for the repository ${repositoryName} by following the steps below:
- Step 1: Update the microagent. This is the path of the microagent: ${formData.microagentPath} (The updated microagent must be in the .openhands/microagents folder and should be able to perform the described task when triggered).
- This is the updated instructions about what the microagent should do: ${formData.query}
${
- Step 1: Update the microagent. This is the path of the microagent: ${formData.microagentPath} (The updated microagent must be in the .openhands/microagents folder and should be able to perform the described task when triggered). This is the updated instructions about what the microagent should do: ${formData.query}. ${
formData.triggers && formData.triggers.length > 0
? `
- This is the triggers of the microagent: ${formData.triggers.join(", ")}
`
: "- Please be noted that the microagent doesn't have any triggers."
? `This is the triggers of the microagent: ${formData.triggers.join(", ")}`
: "Please be noted that the microagent doesn't have any triggers."
}
- Step 2: Create a new branch for the repository ${repositoryName}, must avoid duplicated branches.
@@ -112,6 +108,10 @@ export function MicroagentManagementContent() {
learnThisRepoModalVisible,
} = useSelector((state: RootState) => state.microagentManagement);
const { providers } = useUserProviders();
const { t } = useTranslation();
const dispatch = useDispatch();
const { createConversationAndSubscribe, isPending } =
@@ -159,6 +159,33 @@ export function MicroagentManagementContent() {
? (selectedRepository as GitRepository).full_name
: "";
// Check if agent is running and ready to work
if (
isOpenHandsEvent(socketEvent) &&
isAgentStateChangeObservation(socketEvent) &&
socketEvent.extras.agent_state === AgentState.RUNNING
) {
displaySuccessToast(
t(I18nKey.MICROAGENT_MANAGEMENT$OPENING_PR_TO_CREATE_MICROAGENT),
);
}
// Check if agent has finished and we have a PR
if (isOpenHandsEvent(socketEvent) && isFinishAction(socketEvent)) {
const prUrl = getFirstPRUrl(socketEvent.args.final_thought || "");
if (!prUrl) {
// Agent finished but no PR found
displaySuccessToast(t(I18nKey.MICROAGENT_MANAGEMENT$PR_NOT_CREATED));
}
}
// Handle error events
if (isErrorEvent(socketEvent) || isAgentStatusError(socketEvent)) {
displayErrorToast(
t(I18nKey.MICROAGENT_MANAGEMENT$ERROR_CREATING_MICROAGENT),
);
}
if (shouldInvalidateConversationsList(socketEvent)) {
invalidateConversationsList(repositoryName);
}
@@ -289,11 +316,18 @@ export function MicroagentManagementContent() {
</>
);
const providersAreSet = providers.length > 0;
if (width < 1024) {
return (
<div className="w-full h-full flex flex-col gap-6">
<div className="w-full rounded-lg border border-[#525252] bg-[#24272E] max-h-[494px] min-h-[494px]">
<MicroagentManagementSidebar isSmallerScreen />
{providersAreSet && (
<MicroagentManagementSidebar
isSmallerScreen
providers={providers}
/>
)}
</div>
<div className="w-full rounded-lg border border-[#525252] bg-[#24272E] flex-1 min-h-[494px]">
<MicroagentManagementMain />
@@ -305,7 +339,7 @@ export function MicroagentManagementContent() {
return (
<div className="w-full h-full flex rounded-lg border border-[#525252] bg-[#24272E] overflow-hidden">
<MicroagentManagementSidebar />
{providersAreSet && <MicroagentManagementSidebar providers={providers} />}
<div className="flex-1">
<MicroagentManagementMain />
</div>
@@ -59,8 +59,10 @@ export function MicroagentManagementMicroagentCard({
if (runtimeStatus === "STATUS$ERROR") {
return t(I18nKey.MICROAGENT$STATUS_ERROR);
}
if (conversationStatus === "RUNNING" && runtimeStatus === "STATUS$READY") {
return t(I18nKey.MICROAGENT$STATUS_OPENING_PR);
if (conversationStatus === "RUNNING") {
return runtimeStatus === "STATUS$READY"
? t(I18nKey.MICROAGENT$STATUS_OPENING_PR)
: t(I18nKey.COMMON$STARTING);
}
return "";
}, [conversationStatus, runtimeStatus, t, hasPr]);
@@ -1,3 +1,4 @@
import { useTranslation } from "react-i18next";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Spinner } from "@heroui/react";
@@ -8,6 +9,8 @@ import { useSearchConversations } from "#/hooks/query/use-search-conversations";
import { GitRepository } from "#/types/git";
import { RootState } from "#/store";
import { setSelectedMicroagentItem } from "#/state/microagent-management-slice";
import { cn } from "#/utils/utils";
import { I18nKey } from "#/i18n/declaration";
interface MicroagentManagementRepoMicroagentsProps {
repository: GitRepository;
@@ -22,6 +25,8 @@ export function MicroagentManagementRepoMicroagents({
const dispatch = useDispatch();
const { t } = useTranslation();
const { full_name: repositoryName } = repository;
// Extract owner and repo from repositoryName (format: "owner/repo")
@@ -65,6 +70,18 @@ export function MicroagentManagementRepoMicroagents({
}
}, [conversations]);
useEffect(
() => () => {
dispatch(
setSelectedMicroagentItem({
microagent: null,
conversation: null,
}),
);
},
[],
);
// Show loading only when both queries are loading
const isLoading = isLoadingMicroagents || isLoadingConversations;
@@ -82,7 +99,7 @@ export function MicroagentManagementRepoMicroagents({
// If there's an error with microagents, show the learn this repo component
if (isError) {
return (
<div className="pb-4">
<div>
<MicroagentManagementLearnThisRepo repository={repository} />
</div>
);
@@ -91,34 +108,47 @@ export function MicroagentManagementRepoMicroagents({
const numberOfMicroagents = microagents?.length || 0;
const numberOfConversations = conversations?.length || 0;
const totalItems = numberOfMicroagents + numberOfConversations;
const hasMicroagents = numberOfMicroagents > 0;
const hasConversations = numberOfConversations > 0;
return (
<div className="pb-4">
<div>
{totalItems === 0 && (
<MicroagentManagementLearnThisRepo repository={repository} />
)}
{/* Render microagents */}
{numberOfMicroagents > 0 &&
microagents?.map((microagent) => (
<div key={microagent.name} className="pb-4 last:pb-0">
<MicroagentManagementMicroagentCard
microagent={microagent}
repository={repository}
/>
</div>
))}
{hasMicroagents && (
<div className="flex flex-col">
<span className="text-md text-white font-medium leading-5 mb-4">
{t(I18nKey.MICROAGENT_MANAGEMENT$EXISTING_MICROAGENTS)}
</span>
{microagents?.map((microagent) => (
<div key={microagent.name} className="pb-4 last:pb-0">
<MicroagentManagementMicroagentCard
microagent={microagent}
repository={repository}
/>
</div>
))}
</div>
)}
{/* Render conversations */}
{numberOfConversations > 0 &&
conversations?.map((conversation) => (
<div key={conversation.conversation_id} className="pb-4 last:pb-0">
<MicroagentManagementMicroagentCard
conversation={conversation}
repository={repository}
/>
</div>
))}
{hasConversations && (
<div className={cn("flex flex-col", hasMicroagents && "mt-4")}>
<span className="text-md text-white font-medium leading-5 mb-4">
{t(I18nKey.MICROAGENT_MANAGEMENT$OPEN_MICROAGENT_PULL_REQUESTS)}
</span>
{conversations?.map((conversation) => (
<div key={conversation.conversation_id} className="pb-4 last:pb-0">
<MicroagentManagementMicroagentCard
conversation={conversation}
repository={repository}
/>
</div>
))}
</div>
)}
</div>
);
}
@@ -1,15 +1,12 @@
import { useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Accordion, AccordionItem } from "@heroui/react";
import { MicroagentManagementRepoMicroagents } from "./microagent-management-repo-microagents";
import { GitRepository } from "#/types/git";
import { cn } from "#/utils/utils";
import { TabType } from "#/types/microagent-management";
import { MicroagentManagementNoRepositories } from "./microagent-management-no-repositories";
import { I18nKey } from "#/i18n/declaration";
import { DOCUMENTATION_URL } from "#/utils/constants";
import { MicroagentManagementAccordionTitle } from "./microagent-management-accordion-title";
import { sanitizeQuery } from "#/utils/sanitize-query";
type MicroagentManagementRepositoriesProps = {
repositories: GitRepository[];
@@ -21,23 +18,9 @@ export function MicroagentManagementRepositories({
tabType,
}: MicroagentManagementRepositoriesProps) {
const { t } = useTranslation();
const [searchQuery, setSearchQuery] = useState("");
const numberOfRepoMicroagents = repositories.length;
// Filter repositories based on search query
const filteredRepositories = useMemo(() => {
if (!searchQuery.trim()) {
return repositories;
}
const sanitizedQuery = sanitizeQuery(searchQuery);
return repositories.filter((repository) => {
const sanitizedRepoName = sanitizeQuery(repository.full_name);
return sanitizedRepoName.includes(sanitizedQuery);
});
}, [repositories, searchQuery]);
if (numberOfRepoMicroagents === 0) {
if (tabType === "personal") {
return (
@@ -73,36 +56,19 @@ export function MicroagentManagementRepositories({
return (
<div className="flex flex-col gap-4 w-full">
{/* Search Input */}
<div className="flex flex-col gap-2 w-full">
<label htmlFor="repository-search" className="sr-only">
{t(I18nKey.COMMON$SEARCH_REPOSITORIES)}
</label>
<input
id="repository-search"
name="repository-search"
type="text"
placeholder={`${t(I18nKey.COMMON$SEARCH_REPOSITORIES)}...`}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className={cn(
"bg-tertiary border border-[#717888] bg-[#454545] w-full rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
)}
/>
</div>
{/* Repositories Accordion */}
<Accordion
variant="splitted"
className="w-full px-0 gap-3"
itemClasses={{
base: "shadow-none bg-transparent border border-[#ffffff40] rounded-[6px] cursor-pointer",
trigger: "cursor-pointer gap-1",
base: "shadow-none bg-transparent cursor-pointer px-0",
trigger: "cursor-pointer gap-2 py-3",
indicator:
"flex items-center justify-center p-0.5 pr-[3px] text-white hover:bg-[#454545] rounded transition-colors duration-200 rotate-180",
}}
selectionMode="multiple"
>
{filteredRepositories.map((repository) => (
{repositories.map((repository) => (
<AccordionItem
key={repository.id}
aria-label={repository.full_name}
@@ -1,59 +1,109 @@
import { useEffect } from "react";
import { useEffect, useState, useMemo } from "react";
import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
import { Spinner } from "@heroui/react";
import { MicroagentManagementSidebarHeader } from "./microagent-management-sidebar-header";
import { MicroagentManagementSidebarTabs } from "./microagent-management-sidebar-tabs";
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
import { useUserProviders } from "#/hooks/use-user-providers";
import { useGitRepositories } from "#/hooks/query/use-git-repositories";
import { GitProviderDropdown } from "#/components/common/git-provider-dropdown";
import {
setPersonalRepositories,
setOrganizationRepositories,
setRepositories,
} from "#/state/microagent-management-slice";
import { GitRepository } from "#/types/git";
import { Provider } from "#/types/settings";
import { cn } from "#/utils/utils";
import { sanitizeQuery } from "#/utils/sanitize-query";
import { I18nKey } from "#/i18n/declaration";
import { getGitProviderMicroagentManagementCustomStyles } from "#/components/common/react-select-styles";
interface MicroagentManagementSidebarProps {
isSmallerScreen?: boolean;
providers: Provider[];
}
export function MicroagentManagementSidebar({
isSmallerScreen = false,
providers,
}: MicroagentManagementSidebarProps) {
const [selectedProvider, setSelectedProvider] = useState<Provider | null>(
providers.length > 0 ? providers[0] : null,
);
const [searchQuery, setSearchQuery] = useState("");
const dispatch = useDispatch();
const { t } = useTranslation();
const { providers } = useUserProviders();
const selectedProvider = providers.length > 0 ? providers[0] : null;
const { data: repositories, isLoading } =
useUserRepositories(selectedProvider);
const { data: repositories, isLoading } = useGitRepositories({
provider: selectedProvider,
pageSize: 200,
enabled: !!selectedProvider,
});
// Auto-select provider if there's only one
useEffect(() => {
if (providers.length > 0 && !selectedProvider) {
setSelectedProvider(providers[0]);
}
}, [providers, selectedProvider]);
const handleProviderChange = (provider: Provider | null) => {
setSelectedProvider(provider);
setSearchQuery("");
};
// Filter repositories based on search query
const filteredRepositories = useMemo(() => {
if (!repositories?.pages) return null;
// Flatten all pages to get all repositories
const allRepositories = repositories.pages.flatMap((page) => page.data);
if (!searchQuery.trim()) {
return allRepositories;
}
const sanitizedQuery = sanitizeQuery(searchQuery);
return allRepositories.filter((repository: GitRepository) => {
const sanitizedRepoName = sanitizeQuery(repository.full_name);
return sanitizedRepoName.includes(sanitizedQuery);
});
}, [repositories, searchQuery, selectedProvider]);
useEffect(() => {
if (repositories?.pages) {
const personalRepos: GitRepository[] = [];
const organizationRepos: GitRepository[] = [];
const otherRepos: GitRepository[] = [];
// Flatten all pages to get all repositories
const allRepositories = repositories.pages.flatMap((page) => page.data);
allRepositories.forEach((repo: GitRepository) => {
const hasOpenHandsSuffix = repo.full_name.endsWith("/.openhands");
if (repo.owner_type === "user" && hasOpenHandsSuffix) {
personalRepos.push(repo);
} else if (repo.owner_type === "organization" && hasOpenHandsSuffix) {
organizationRepos.push(repo);
} else {
otherRepos.push(repo);
}
});
dispatch(setPersonalRepositories(personalRepos));
dispatch(setOrganizationRepositories(organizationRepos));
dispatch(setRepositories(otherRepos));
if (!filteredRepositories?.length) {
dispatch(setPersonalRepositories([]));
dispatch(setOrganizationRepositories([]));
dispatch(setRepositories([]));
return;
}
}, [repositories, dispatch]);
const personalRepos: GitRepository[] = [];
const organizationRepos: GitRepository[] = [];
const otherRepos: GitRepository[] = [];
filteredRepositories.forEach((repo: GitRepository) => {
const hasOpenHandsSuffix =
selectedProvider === "gitlab"
? repo.full_name.endsWith("/openhands-config")
: repo.full_name.endsWith("/.openhands");
if (repo.owner_type === "user" && hasOpenHandsSuffix) {
personalRepos.push(repo);
} else if (repo.owner_type === "organization" && hasOpenHandsSuffix) {
organizationRepos.push(repo);
} else {
otherRepos.push(repo);
}
});
dispatch(setPersonalRepositories(personalRepos));
dispatch(setOrganizationRepositories(organizationRepos));
dispatch(setRepositories(otherRepos));
}, [filteredRepositories, selectedProvider, dispatch]);
return (
<div
@@ -63,6 +113,41 @@ export function MicroagentManagementSidebar({
)}
>
<MicroagentManagementSidebarHeader />
{/* Provider Selection */}
{providers.length > 1 && (
<div className="mt-6">
<GitProviderDropdown
providers={providers}
value={selectedProvider}
placeholder="Select Provider"
onChange={handleProviderChange}
className="w-full"
classNamePrefix="git-provider-dropdown"
styles={getGitProviderMicroagentManagementCustomStyles()}
/>
</div>
)}
{/* Search Input */}
<div className="flex flex-col gap-2 w-full mt-6">
<label htmlFor="repository-search" className="sr-only">
{t(I18nKey.COMMON$SEARCH_REPOSITORIES)}
</label>
<input
id="repository-search"
name="repository-search"
type="text"
placeholder={`${t(I18nKey.COMMON$SEARCH_REPOSITORIES)}...`}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className={cn(
"bg-tertiary border border-[#717888] bg-[#454545] w-full rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed h-10 box-shadow-none outline-none",
)}
/>
</div>
{isLoading ? (
<div className="flex flex-col items-center justify-center gap-4 flex-1">
<Spinner size="sm" />
@@ -1,8 +1,7 @@
import { Tooltip } from "@heroui/react";
import { useTranslation } from "react-i18next";
import ConfirmIcon from "#/assets/confirm";
import RejectIcon from "#/assets/reject";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
interface ActionTooltipProps {
type: "confirm" | "reject";
@@ -12,25 +11,35 @@ interface ActionTooltipProps {
export function ActionTooltip({ type, onClick }: ActionTooltipProps) {
const { t } = useTranslation();
const content =
type === "confirm"
? t(I18nKey.CHAT_INTERFACE$USER_CONFIRMED)
: t(I18nKey.CHAT_INTERFACE$USER_REJECTED);
const isConfirm = type === "confirm";
const ariaLabel = isConfirm
? t(I18nKey.ACTION$CONFIRM)
: t(I18nKey.ACTION$REJECT);
const content = isConfirm
? t(I18nKey.CHAT_INTERFACE$USER_CONFIRMED)
: t(I18nKey.CHAT_INTERFACE$USER_REJECTED);
const buttonLabel = isConfirm
? `${t(I18nKey.CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE)} ⌘↩`
: `${t(I18nKey.BUTTON$CANCEL)} ⇧⌘⌫`;
return (
<Tooltip content={content} closeDelay={100}>
<button
data-testid={`action-${type}-button`}
type="button"
aria-label={
aria-label={ariaLabel}
className={cn(
"rounded px-2 h-6.5 text-sm font-medium leading-5 cursor-pointer hover:opacity-80",
type === "confirm"
? t(I18nKey.ACTION$CONFIRM)
: t(I18nKey.ACTION$REJECT)
}
className="bg-tertiary rounded-full p-1 hover:bg-base-secondary"
? "bg-tertiary text-white"
: "bg-white text-[#0D0F11]",
)}
onClick={onClick}
>
{type === "confirm" ? <ConfirmIcon /> : <RejectIcon />}
{buttonLabel}
</button>
</Tooltip>
);
@@ -1,31 +1,120 @@
import { useDispatch, useSelector } from "react-redux";
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { AgentState } from "#/types/agent-state";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
import { useWsClient } from "#/context/ws-client-provider";
import { ActionTooltip } from "../action-tooltip";
import { isOpenHandsAction } from "#/types/core/guards";
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
import { RiskAlert } from "#/components/shared/risk-alert";
import WarningIcon from "#/icons/u-warning.svg?react";
import { RootState } from "#/store";
import { addSubmittedEventId } from "#/state/event-message-slice";
export function ConfirmationButtons() {
const { t } = useTranslation();
const { send } = useWsClient();
const submittedEventIds = useSelector(
(state: RootState) => state.eventMessage.submittedEventIds,
);
const handleStateChange = (state: AgentState) => {
const event = generateAgentStateChangeEvent(state);
send(event);
};
const dispatch = useDispatch();
const { t } = useTranslation();
const { send, parsedEvents } = useWsClient();
// Find the most recent action awaiting confirmation
const awaitingAction = parsedEvents
.slice()
.reverse()
.find((ev) => {
if (!isOpenHandsAction(ev) || ev.source !== "agent") return false;
const args = ev.args as Record<string, unknown>;
return args?.confirmation_state === "awaiting_confirmation";
});
const handleStateChange = useCallback(
(state: AgentState) => {
if (!awaitingAction) {
return;
}
dispatch(addSubmittedEventId(awaitingAction.id));
send(generateAgentStateChangeEvent(state));
},
[send],
);
// Handle keyboard shortcuts
useEffect(() => {
if (!awaitingAction) {
return undefined;
}
const handleCancelShortcut = (event: KeyboardEvent) => {
if (event.shiftKey && event.metaKey && event.key === "Backspace") {
event.preventDefault();
handleStateChange(AgentState.USER_REJECTED);
}
};
const handleContinueShortcut = (event: KeyboardEvent) => {
if (event.metaKey && event.key === "Enter") {
event.preventDefault();
handleStateChange(AgentState.USER_CONFIRMED);
}
};
const handleKeyDown = (event: KeyboardEvent) => {
// Cancel: Shift+Cmd+Backspace (⇧⌘⌫)
handleCancelShortcut(event);
// Continue: Cmd+Enter (⌘↩)
handleContinueShortcut(event);
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [awaitingAction, handleStateChange]);
if (!awaitingAction || submittedEventIds.includes(awaitingAction.id)) {
return null;
}
const { args } = awaitingAction as { args: Record<string, unknown> };
const risk = args?.security_risk;
const isHighRisk =
typeof risk === "string"
? risk.toLowerCase() === "high"
: Number(risk) === ActionSecurityRisk.HIGH;
return (
<div className="flex justify-between items-center pt-4">
<p>{t(I18nKey.CHAT_INTERFACE$USER_ASK_CONFIRMATION)}</p>
<div className="flex items-center gap-3">
<ActionTooltip
type="confirm"
onClick={() => handleStateChange(AgentState.USER_CONFIRMED)}
/>
<ActionTooltip
type="reject"
onClick={() => handleStateChange(AgentState.USER_REJECTED)}
<div className="flex flex-col gap-2 pt-4">
{isHighRisk && (
<RiskAlert
content={t(I18nKey.CHAT_INTERFACE$HIGH_RISK_WARNING)}
icon={<WarningIcon width={16} height={16} color="#fff" />}
severity="high"
title={t(I18nKey.COMMON$HIGH_RISK)}
/>
)}
<div className="flex justify-between items-center">
<p className="text-sm font-normal text-white">
{t(I18nKey.CHAT_INTERFACE$USER_ASK_CONFIRMATION)}
</p>
<div className="flex items-center gap-3">
<ActionTooltip
type="reject"
onClick={() => handleStateChange(AgentState.USER_REJECTED)}
/>
<ActionTooltip
type="confirm"
onClick={() => handleStateChange(AgentState.USER_CONFIRMED)}
/>
</div>
</div>
</div>
);
@@ -23,7 +23,7 @@ export function ModalBackdrop({ children, onClose }: ModalBackdropProps) {
<div className="fixed inset-0 flex items-center justify-center z-20">
<div
onClick={handleClick}
className="fixed inset-0 bg-black bg-opacity-80"
className="fixed inset-0 bg-black opacity-60"
/>
<div className="relative">{children}</div>
</div>
@@ -93,14 +93,14 @@ function SecurityInvariant() {
(risk: ActionSecurityRisk) => {
switch (risk) {
case ActionSecurityRisk.LOW:
return t(I18nKey.SECURITY_ANALYZER$LOW_RISK);
return t(I18nKey.SECURITY$LOW_RISK);
case ActionSecurityRisk.MEDIUM:
return t(I18nKey.SECURITY_ANALYZER$MEDIUM_RISK);
return t(I18nKey.SECURITY$MEDIUM_RISK);
case ActionSecurityRisk.HIGH:
return t(I18nKey.SECURITY_ANALYZER$HIGH_RISK);
return t(I18nKey.SECURITY$HIGH_RISK);
case ActionSecurityRisk.UNKNOWN:
default:
return t(I18nKey.SECURITY_ANALYZER$UNKNOWN_RISK);
return t(I18nKey.SECURITY$UNKNOWN_RISK);
}
},
[t],
@@ -0,0 +1,36 @@
import { ReactNode } from "react";
import { cn } from "#/utils/utils";
interface RiskAlertProps {
className?: string;
content: ReactNode;
icon?: ReactNode;
severity: "high" | "medium" | "low";
title: string;
}
export function RiskAlert({
className,
content,
icon,
severity,
title,
}: RiskAlertProps) {
// Currently, we are only supporting the high risk alert. If we use want to support other risk levels, we can add them here and use cva to create different variants of this component.
if (severity === "high") {
return (
<div
className={cn(
"flex items-center gap-3.5 bg-[#4A0709] border border-[#FF0006] text-red-400 rounded-xl px-3.5 h-13 text-sm text-white",
className,
)}
>
{icon && <span className="">{icon}</span>}
<span className="font-bold">{title}</span>
<span className="font-normal">{content}</span>
</div>
);
}
return null;
}
@@ -33,6 +33,7 @@ interface ConversationSubscriptionsContextType {
sessionApiKey: string | null;
providersSet: ("github" | "gitlab" | "bitbucket" | "enterprise_sso")[];
baseUrl: string;
socketPath?: string;
onEvent?: (event: unknown, conversationId: string) => void;
}) => void;
unsubscribeFromConversation: (conversationId: string) => void;
@@ -95,10 +96,10 @@ export function ConversationSubscriptionsProvider({
[],
);
const unsubscribeFromConversation = useCallback(
(conversationId: string) => {
// Get a local reference to the socket data to avoid race conditions
const socketData = conversationSockets[conversationId];
const unsubscribeFromConversation = useCallback((conversationId: string) => {
// Use functional update to access current socket data and perform cleanup
setConversationSockets((prev) => {
const socketData = prev[conversationId];
if (socketData) {
const { socket } = socketData;
@@ -112,24 +113,23 @@ export function ConversationSubscriptionsProvider({
socket.disconnect();
}
// Update state to remove the socket
setConversationSockets((prev) => {
const newSockets = { ...prev };
delete newSockets[conversationId];
return newSockets;
});
// Remove from active IDs
setActiveConversationIds((prev) =>
prev.filter((id) => id !== conversationId),
);
// Clean up event handler reference
delete eventHandlersRef.current[conversationId];
// Remove the socket from state
const newSockets = { ...prev };
delete newSockets[conversationId];
return newSockets;
}
},
[conversationSockets],
);
return prev; // No change if socket not found
});
// Remove from active IDs
setActiveConversationIds((prev) =>
prev.filter((id) => id !== conversationId),
);
}, []);
const subscribeToConversation = useCallback(
(options: {
@@ -137,10 +137,17 @@ export function ConversationSubscriptionsProvider({
sessionApiKey: string | null;
providersSet: ("github" | "gitlab" | "bitbucket" | "enterprise_sso")[];
baseUrl: string;
socketPath?: string;
onEvent?: (event: unknown, conversationId: string) => void;
}) => {
const { conversationId, sessionApiKey, providersSet, baseUrl, onEvent } =
options;
const {
conversationId,
sessionApiKey,
providersSet,
baseUrl,
socketPath,
onEvent,
} = options;
// If already subscribed, don't create a new subscription
if (conversationSockets[conversationId]) {
@@ -173,9 +180,7 @@ export function ConversationSubscriptionsProvider({
if (isErrorEvent(event) || isAgentStatusError(event)) {
renderConversationErroredToast(
conversationId,
isErrorEvent(event)
? event.message
: "Unknown error, please try again",
isErrorEvent(event) ? event.message : "MICROAGENT$UNKNOWN_ERROR",
);
} else if (isStatusUpdate(event)) {
if (event.type === "info" && event.id === "STATUS$STARTING_RUNTIME") {
@@ -199,6 +204,7 @@ export function ConversationSubscriptionsProvider({
// Create socket connection
const socket = io(baseUrl, {
transports: ["websocket"],
path: socketPath ?? "/socket.io",
query: {
conversation_id: conversationId,
session_api_key: sessionApiKey,
+12 -3
View File
@@ -317,15 +317,24 @@ export function WsClientProvider({
session_api_key: conversation.session_api_key, // Have to set here because socketio doesn't support custom headers. :(
};
let baseUrl = null;
let baseUrl: string | null = null;
let socketPath: string;
if (conversation.url && !conversation.url.startsWith("/")) {
baseUrl = new URL(conversation.url).host;
const u = new URL(conversation.url);
baseUrl = u.host;
const pathBeforeApi = u.pathname.split("/api/conversations")[0] || "/";
// Socket.IO server default path is /socket.io; prefix with pathBeforeApi for path mode
socketPath = `${pathBeforeApi.replace(/\/$/, "")}/socket.io`;
} else {
baseUrl = import.meta.env.VITE_BACKEND_BASE_URL || window?.location.host;
baseUrl =
(import.meta.env.VITE_BACKEND_BASE_URL as string | undefined) ||
window?.location.host;
socketPath = "/socket.io";
}
sio = io(baseUrl, {
transports: ["websocket"],
path: socketPath,
query,
});
@@ -19,6 +19,8 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
: settings.llm_api_key?.trim() || undefined,
remote_runtime_resource_factor: settings.REMOTE_RUNTIME_RESOURCE_FACTOR,
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
condenser_max_size:
settings.CONDENSER_MAX_SIZE ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
user_consents_to_analytics: settings.user_consents_to_analytics,
provider_tokens_set: settings.PROVIDER_TOKENS_SET,
+2
View File
@@ -22,6 +22,8 @@ const getSettingsQueryFn = async (): Promise<Settings> => {
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
PROVIDER_TOKENS_SET: apiSettings.provider_tokens_set,
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
CONDENSER_MAX_SIZE:
apiSettings.condenser_max_size ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
ENABLE_PROACTIVE_CONVERSATION_STARTERS:
apiSettings.enable_proactive_conversation_starters,
@@ -1,14 +1,27 @@
import React from "react";
import { useQueries, type Query } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { AxiosError } from "axios";
import { useCreateConversation } from "./mutation/use-create-conversation";
import { useUserProviders } from "./use-user-providers";
import { useConversationSubscriptions } from "#/context/conversation-subscriptions-provider";
import { Provider } from "#/types/settings";
import { CreateMicroagent } from "#/api/open-hands.types";
import { CreateMicroagent, Conversation } from "#/api/open-hands.types";
import OpenHands from "#/api/open-hands";
import { renderConversationStartingToast } from "#/components/features/chat/microagent/microagent-status-toast";
interface ConversationData {
conversationId: string;
sessionApiKey: string | null;
baseUrl: string;
socketPath: string;
onEventCallback?: (event: unknown, conversationId: string) => void;
}
/**
* Custom hook to create a conversation and subscribe to it, supporting multiple subscriptions.
* This extends the functionality of useCreateConversationAndSubscribe to allow subscribing to
* multiple conversations simultaneously.
* This version waits for conversation status to be "RUNNING" before establishing WebSocket connection.
* Shows immediate toast feedback and polls conversation status until ready.
*/
export const useCreateConversationAndSubscribeMultiple = () => {
const { mutate: createConversation, isPending } = useCreateConversation();
@@ -20,6 +33,88 @@ export const useCreateConversationAndSubscribeMultiple = () => {
activeConversationIds,
} = useConversationSubscriptions();
// Store conversation data immediately after creation
const [createdConversations, setCreatedConversations] = React.useState<
Record<string, ConversationData>
>({});
// Get conversation IDs that need polling
const conversationIdsToWatch = Object.keys(createdConversations);
// Poll each conversation until it's ready
const conversationQueries = useQueries({
queries: conversationIdsToWatch.map((conversationId) => ({
queryKey: ["conversation-ready-poll", conversationId],
queryFn: () => OpenHands.getConversation(conversationId),
enabled: !!conversationId,
refetchInterval: (query: Query<Conversation | null, AxiosError>) => {
const status = query.state.data?.status;
if (status === "STARTING") {
return 3000; // Poll every 3 seconds while STARTING
}
return false; // Stop polling once not STARTING
},
retry: false,
})),
});
// Extract stable values from queries for dependency array
const queryStatuses = conversationQueries.map((query) => query.data?.status);
const queryDataExists = conversationQueries.map((query) => !!query.data);
// Effect to handle subscription when conversations are ready
React.useEffect(() => {
conversationQueries.forEach((query, index) => {
const conversationId = conversationIdsToWatch[index];
const conversationData = createdConversations[conversationId];
if (!query.data || !conversationData) return;
const { status, url, session_api_key: sessionApiKey } = query.data;
let { baseUrl } = conversationData;
if (url && !url.startsWith("/")) {
baseUrl = new URL(url).host;
}
if (status === "RUNNING") {
// Conversation is ready - subscribe to WebSocket
subscribeToConversation({
conversationId,
sessionApiKey,
providersSet: providers,
baseUrl,
socketPath: conversationData.socketPath,
onEvent: conversationData.onEventCallback,
});
// Remove from created conversations (cleanup)
setCreatedConversations((prev) => {
const newCreated = { ...prev };
delete newCreated[conversationId];
return newCreated;
});
} else if (status === "STOPPED") {
// Dismiss the starting toast
toast.dismiss(`starting-${conversationId}`);
// Remove from created conversations (cleanup)
setCreatedConversations((prev) => {
const newCreated = { ...prev };
delete newCreated[conversationId];
return newCreated;
});
}
});
}, [
queryStatuses,
queryDataExists,
conversationIdsToWatch,
createdConversations,
subscribeToConversation,
providers,
]);
const createConversationAndSubscribe = React.useCallback(
({
query,
@@ -49,33 +144,46 @@ export const useCreateConversationAndSubscribeMultiple = () => {
},
{
onSuccess: (data) => {
// Show immediate toast to let user know something is happening
renderConversationStartingToast(data.conversation_id);
// Call the success callback immediately
if (onSuccessCallback) {
onSuccessCallback(data.conversation_id);
}
// Only handle immediate post-creation tasks here
let baseUrl = "";
let socketPath: string;
if (data?.url && !data.url.startsWith("/")) {
baseUrl = new URL(data.url).host;
const u = new URL(data.url);
baseUrl = u.host;
const pathBeforeApi =
u.pathname.split("/api/conversations")[0] || "/";
socketPath = `${pathBeforeApi.replace(/\/$/, "")}/socket.io`;
} else {
baseUrl =
(import.meta.env.VITE_BACKEND_BASE_URL as string | undefined) ||
window?.location.host;
socketPath = "/socket.io";
}
// Subscribe to the conversation
subscribeToConversation({
conversationId: data.conversation_id,
sessionApiKey: data.session_api_key,
providersSet: providers,
baseUrl,
onEvent: onEventCallback,
});
// Call the success callback if provided
if (onSuccessCallback) {
onSuccessCallback(data.conversation_id);
}
// Store conversation data for polling and eventual subscription
setCreatedConversations((prev) => ({
...prev,
[data.conversation_id]: {
conversationId: data.conversation_id,
sessionApiKey: data.session_api_key,
baseUrl,
socketPath,
onEventCallback,
},
}));
},
},
);
},
[createConversation, subscribeToConversation, providers],
[createConversation],
);
return {
+24 -6
View File
@@ -97,6 +97,8 @@ export enum I18nKey {
SETTINGS$BASE_URL = "SETTINGS$BASE_URL",
SETTINGS$AGENT = "SETTINGS$AGENT",
SETTINGS$ENABLE_MEMORY_CONDENSATION = "SETTINGS$ENABLE_MEMORY_CONDENSATION",
SETTINGS$CONDENSER_MAX_SIZE = "SETTINGS$CONDENSER_MAX_SIZE",
SETTINGS$CONDENSER_MAX_SIZE_TOOLTIP = "SETTINGS$CONDENSER_MAX_SIZE_TOOLTIP",
SETTINGS$LANGUAGE = "SETTINGS$LANGUAGE",
ACTION$PUSH_TO_BRANCH = "ACTION$PUSH_TO_BRANCH",
ACTION$PUSH_CREATE_PR = "ACTION$PUSH_CREATE_PR",
@@ -326,6 +328,7 @@ export enum I18nKey {
USER$ACCOUNT_SETTINGS = "USER$ACCOUNT_SETTINGS",
JUPYTER$OUTPUT_LABEL = "JUPYTER$OUTPUT_LABEL",
BUTTON$STOP = "BUTTON$STOP",
BUTTON$PAUSE = "BUTTON$PAUSE",
BUTTON$EDIT_TITLE = "BUTTON$EDIT_TITLE",
BUTTON$DOWNLOAD_VIA_VSCODE = "BUTTON$DOWNLOAD_VIA_VSCODE",
BUTTON$DISPLAY_COST = "BUTTON$DISPLAY_COST",
@@ -337,6 +340,8 @@ export enum I18nKey {
LANDING$RECENT_CONVERSATION = "LANDING$RECENT_CONVERSATION",
CONVERSATION$CONFIRM_DELETE = "CONVERSATION$CONFIRM_DELETE",
CONVERSATION$CONFIRM_STOP = "CONVERSATION$CONFIRM_STOP",
CONVERSATION$CONFIRM_PAUSE = "CONVERSATION$CONFIRM_PAUSE",
CONVERSATION$PAUSE_WARNING = "CONVERSATION$PAUSE_WARNING",
CONVERSATION$STOP_WARNING = "CONVERSATION$STOP_WARNING",
CONVERSATION$METRICS_INFO = "CONVERSATION$METRICS_INFO",
CONVERSATION$CREATED = "CONVERSATION$CREATED",
@@ -357,6 +362,7 @@ export enum I18nKey {
CHAT_INTERFACE$INPUT_PLACEHOLDER = "CHAT_INTERFACE$INPUT_PLACEHOLDER",
CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE = "CHAT_INTERFACE$INPUT_CONTINUE_MESSAGE",
CHAT_INTERFACE$USER_ASK_CONFIRMATION = "CHAT_INTERFACE$USER_ASK_CONFIRMATION",
CHAT_INTERFACE$HIGH_RISK_WARNING = "CHAT_INTERFACE$HIGH_RISK_WARNING",
CHAT_INTERFACE$USER_CONFIRMED = "CHAT_INTERFACE$USER_CONFIRMED",
CHAT_INTERFACE$USER_REJECTED = "CHAT_INTERFACE$USER_REJECTED",
CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT = "CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT",
@@ -371,10 +377,6 @@ export enum I18nKey {
CHAT_INTERFACE$MESSAGE_ARIA_LABEL = "CHAT_INTERFACE$MESSAGE_ARIA_LABEL",
CHAT_INTERFACE$CHAT_CONVERSATION = "CHAT_INTERFACE$CHAT_CONVERSATION",
CHAT_INTERFACE$UNKNOWN_SENDER = "CHAT_INTERFACE$UNKNOWN_SENDER",
SECURITY_ANALYZER$UNKNOWN_RISK = "SECURITY_ANALYZER$UNKNOWN_RISK",
SECURITY_ANALYZER$LOW_RISK = "SECURITY_ANALYZER$LOW_RISK",
SECURITY_ANALYZER$MEDIUM_RISK = "SECURITY_ANALYZER$MEDIUM_RISK",
SECURITY_ANALYZER$HIGH_RISK = "SECURITY_ANALYZER$HIGH_RISK",
SETTINGS$MODEL_TOOLTIP = "SETTINGS$MODEL_TOOLTIP",
SETTINGS$AGENT_TOOLTIP = "SETTINGS$AGENT_TOOLTIP",
SETTINGS$LANGUAGE_TOOLTIP = "SETTINGS$LANGUAGE_TOOLTIP",
@@ -385,9 +387,12 @@ export enum I18nKey {
SETTINGS$REFRESH_LLM_API_KEY = "SETTINGS$REFRESH_LLM_API_KEY",
SETTINGS$CONFIRMATION_MODE = "SETTINGS$CONFIRMATION_MODE",
SETTINGS$CONFIRMATION_MODE_TOOLTIP = "SETTINGS$CONFIRMATION_MODE_TOOLTIP",
SETTINGS$CONFIRMATION_MODE_LOCK_TOOLTIP = "SETTINGS$CONFIRMATION_MODE_LOCK_TOOLTIP",
SETTINGS$AGENT_SELECT_ENABLED = "SETTINGS$AGENT_SELECT_ENABLED",
SETTINGS$SECURITY_ANALYZER = "SETTINGS$SECURITY_ANALYZER",
SETTINGS$SECURITY_ANALYZER_PLACEHOLDER = "SETTINGS$SECURITY_ANALYZER_PLACEHOLDER",
SETTINGS$SECURITY_ANALYZER_TOOLTIP = "SETTINGS$SECURITY_ANALYZER_TOOLTIP",
SETTINGS$SECURITY_ANALYZER_DESCRIPTION = "SETTINGS$SECURITY_ANALYZER_DESCRIPTION",
SETTINGS$DONT_KNOW_API_KEY = "SETTINGS$DONT_KNOW_API_KEY",
SETTINGS$CLICK_FOR_INSTRUCTIONS = "SETTINGS$CLICK_FOR_INSTRUCTIONS",
SETTINGS$SAVED = "SETTINGS$SAVED",
@@ -781,8 +786,6 @@ export enum I18nKey {
PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR = "PROJECT_MANAGEMENT$SVC_ACC_EMAIL_VALIDATION_ERROR",
PROJECT_MANAGEMENT$SVC_ACC_API_KEY_VALIDATION_ERROR = "PROJECT_MANAGEMENT$SVC_ACC_API_KEY_VALIDATION_ERROR",
MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT = "MICROAGENT_MANAGEMENT$ERROR_LOADING_MICROAGENT_CONTENT",
SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT = "SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT",
SETTINGS$MCP_ERROR_URL_DUPLICATE = "SETTINGS$MCP_ERROR_URL_DUPLICATE",
SETTINGS$MCP_SERVER_TYPE_SSE = "SETTINGS$MCP_SERVER_TYPE_SSE",
SETTINGS$MCP_SERVER_TYPE_STDIO = "SETTINGS$MCP_SERVER_TYPE_STDIO",
SETTINGS$MCP_SERVER_TYPE_SHTTP = "SETTINGS$MCP_SERVER_TYPE_SHTTP",
@@ -794,6 +797,8 @@ export enum I18nKey {
SETTINGS$MCP_ERROR_NAME_DUPLICATE = "SETTINGS$MCP_ERROR_NAME_DUPLICATE",
SETTINGS$MCP_ERROR_COMMAND_REQUIRED = "SETTINGS$MCP_ERROR_COMMAND_REQUIRED",
SETTINGS$MCP_ERROR_COMMAND_NO_SPACES = "SETTINGS$MCP_ERROR_COMMAND_NO_SPACES",
SETTINGS$MCP_ERROR_URL_DUPLICATE = "SETTINGS$MCP_ERROR_URL_DUPLICATE",
SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT = "SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT",
SETTINGS$MCP_SERVER_TYPE = "SETTINGS$MCP_SERVER_TYPE",
SETTINGS$MCP_API_KEY_PLACEHOLDER = "SETTINGS$MCP_API_KEY_PLACEHOLDER",
SETTINGS$MCP_COMMAND_ARGUMENTS = "SETTINGS$MCP_COMMAND_ARGUMENTS",
@@ -810,4 +815,17 @@ export enum I18nKey {
PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION = "PROJECT_MANAGEMENT$CONFIGURE_MODAL_DESCRIPTION",
PROJECT_MANAGEMENT$IMPORTANT_WORKSPACE_INTEGRATION = "PROJECT_MANAGEMENT$IMPORTANT_WORKSPACE_INTEGRATION",
SETTINGS = "SETTINGS",
MICROAGENT_MANAGEMENT$OPENING_PR_TO_CREATE_MICROAGENT = "MICROAGENT_MANAGEMENT$OPENING_PR_TO_CREATE_MICROAGENT",
MICROAGENT_MANAGEMENT$PR_READY_FOR_REVIEW = "MICROAGENT_MANAGEMENT$PR_READY_FOR_REVIEW",
MICROAGENT_MANAGEMENT$PR_NOT_CREATED = "MICROAGENT_MANAGEMENT$PR_NOT_CREATED",
MICROAGENT_MANAGEMENT$ERROR_CREATING_MICROAGENT = "MICROAGENT_MANAGEMENT$ERROR_CREATING_MICROAGENT",
MICROAGENT$STATUS_WAITING = "MICROAGENT$STATUS_WAITING",
MICROAGENT$UNKNOWN_ERROR = "MICROAGENT$UNKNOWN_ERROR",
MICROAGENT$CONVERSATION_STARTING = "MICROAGENT$CONVERSATION_STARTING",
MICROAGENT_MANAGEMENT$EXISTING_MICROAGENTS = "MICROAGENT_MANAGEMENT$EXISTING_MICROAGENTS",
MICROAGENT_MANAGEMENT$OPEN_MICROAGENT_PULL_REQUESTS = "MICROAGENT_MANAGEMENT$OPEN_MICROAGENT_PULL_REQUESTS",
SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT = "SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT",
SETTINGS$SECURITY_ANALYZER_NONE = "SETTINGS$SECURITY_ANALYZER_NONE",
SETTINGS$SECURITY_ANALYZER_INVARIANT = "SETTINGS$SECURITY_ANALYZER_INVARIANT",
COMMON$HIGH_RISK = "COMMON$HIGH_RISK",
}
+473 -265
View File
@@ -432,68 +432,68 @@
"uk": "Повторний вхід до OpenHands..."
},
"SECURITY$LOW_RISK": {
"en": "Low Risk",
"ja": "リスク",
"zh-CN": "风险",
"zh-TW": "風險",
"ko-KR": "낮은 위험",
"no": "Lav risiko",
"it": "Rischio basso",
"pt": "Baixo risco",
"es": "Riesgo bajo",
"ar": "مخاطر منخفضة",
"fr": "Risque faible",
"tr": "Düşük risk",
"de": "Geringes Risiko",
"uk": "Низький ризик"
"en": "Risk: Low",
"ja": "リスク: 低",
"zh-CN": "风险: 低",
"zh-TW": "風險: 低",
"ko-KR": "위험: 낮음",
"no": "Risiko: Lav",
"it": "Rischio: Basso",
"pt": "Risco: Baixo",
"es": "Riesgo: Bajo",
"ar": "المخاطر: منخفضة",
"fr": "Risque : Faible",
"tr": "Risk: Düşük",
"de": "Risiko: Gering",
"uk": "Ризик: Низький"
},
"SECURITY$MEDIUM_RISK": {
"en": "Medium Risk",
"ja": "リスク",
"zh-CN": "中等风险",
"zh-TW": "中等風險",
"ko-KR": "중간 위험",
"no": "Middels risiko",
"it": "Rischio medio",
"pt": "Risco médio",
"es": "Riesgo medio",
"ar": "مخاطر متوسطة",
"fr": "Risque moyen",
"tr": "Orta risk",
"de": "Mittleres Risiko",
"uk": "Середній ризик"
"en": "Risk: Medium",
"ja": "リスク: 中",
"zh-CN": "风险: 中等",
"zh-TW": "風險: 中等",
"ko-KR": "위험: 중간",
"no": "Risiko: Middels",
"it": "Rischio: Medio",
"pt": "Risco: Médio",
"es": "Riesgo: Medio",
"ar": "المخاطر: متوسطة",
"fr": "Risque : Moyen",
"tr": "Risk: Orta",
"de": "Risiko: Mittel",
"uk": "Ризик: Середній"
},
"SECURITY$HIGH_RISK": {
"en": "High Risk",
"ja": "リスク",
"zh-CN": "风险",
"zh-TW": "風險",
"ko-KR": "높은 위험",
"no": "Høy risiko",
"it": "Rischio alto",
"pt": "Alto risco",
"es": "Riesgo alto",
"ar": "مخاطر عالية",
"fr": "Risque élevé",
"tr": "Yüksek risk",
"de": "Hohes Risiko",
"uk": "Високий ризик"
"en": "Risk: High",
"ja": "リスク: 高",
"zh-CN": "风险: 高",
"zh-TW": "風險: 高",
"ko-KR": "위험: 높음",
"no": "Risiko: Høy",
"it": "Rischio: Alto",
"pt": "Risco: Alto",
"es": "Riesgo: Alto",
"ar": "المخاطر: عالية",
"fr": "Risque : Élevé",
"tr": "Risk: Yüksek",
"de": "Risiko: Hoch",
"uk": "Ризик: Високий"
},
"SECURITY$UNKNOWN_RISK": {
"en": "Unknown Risk",
"ja": "不明なリスク",
"zh-CN": "未知风险",
"zh-TW": "未知風險",
"ko-KR": "알 수 없는 위험",
"no": "Ukjent risiko",
"it": "Rischio sconosciuto",
"pt": "Risco desconhecido",
"es": "Riesgo desconocido",
"ar": "مخاطر غير معروفة",
"fr": "Risque inconnu",
"tr": "Bilinmeyen risk",
"de": "Unbekanntes Risiko",
"uk": "Невідомий ризик"
"en": "Risk: Unknown",
"ja": "リスク: 不明",
"zh-CN": "风险: 未知",
"zh-TW": "風險: 未知",
"ko-KR": "위험: 알 수 없",
"no": "Risiko: Ukjent",
"it": "Rischio: Sconosciuto",
"pt": "Risco: Desconhecido",
"es": "Riesgo: Desconocido",
"ar": "المخاطر: غير معروفة",
"fr": "Risque : Inconnu",
"tr": "Risk: Bilinmeyen",
"de": "Risiko: Unbekannt",
"uk": "Ризик: Невідомий"
},
"FINISH$TASK_COMPLETED_SUCCESSFULLY": {
"en": "I believe that the task was **completed successfully**.",
@@ -1551,6 +1551,38 @@
"de": "Speicherkondensation aktivieren",
"uk": "Увімкнути конденсацію пам'яті"
},
"SETTINGS$CONDENSER_MAX_SIZE": {
"en": "Memory condenser max history size",
"ja": "メモリ凝縮の最大履歴サイズ",
"zh-CN": "内存凝缩最大历史大小",
"zh-TW": "記憶體凝縮最大歷史大小",
"ko-KR": "메모리 응축 최대 기록 크기",
"no": "Maks historikkstørrelse for minnekondenser",
"it": "Dimensione massima cronologia condensatore di memoria",
"pt": "Tamanho máximo do histórico do condensador de memória",
"es": "Tamaño máximo del historial del condensador de memoria",
"ar": "الحد الأقصى لحجم سجل مكثف الذاكرة",
"fr": "Taille maximale de l'historique du condenseur de mémoire",
"tr": "Bellek yoğunlaştırıcı maksimum geçmiş boyutu",
"de": "Maximale Verlaufgröße des Speicherkondensators",
"uk": "Максимальний розмір історії конденсатора пам'яті"
},
"SETTINGS$CONDENSER_MAX_SIZE_TOOLTIP": {
"en": "After this many events, the condenser will summarize history. Minimum 10.",
"ja": "このイベント数を超えると、凝縮器が履歴を要約します。最小 10。",
"zh-CN": "达到此事件数量后,凝缩器将汇总历史。最小 10。",
"zh-TW": "超過此事件數後,凝縮器會摘要歷史。最小 10。",
"ko-KR": "이 이벤트 수 이후 응축기가 기록을 요약합니다. 최소 10.",
"no": "Etter så mange hendelser vil kondenseren oppsummere historikken. Minimum 10.",
"it": "Dopo questo numero di eventi, il condensatore riassumerà la cronologia. Minimo 10.",
"pt": "Após esse número de eventos, o condensador irá resumir o histórico. Mínimo 10.",
"es": "Después de este número de eventos, el condensador resumirá el historial. Mínimo 10.",
"ar": "بعد هذا العدد من الأحداث، سيقوم المكثف بتلخيص السجل. الحد الأدنى 10.",
"fr": "Après ce nombre d'événements, le condenseur résumera l'historique. Minimum 10.",
"tr": "Bu kadar olaydan sonra yoğunlaştırıcı geçmişi özetler. En az 10.",
"de": "Nach so vielen Ereignissen fasst der Kondensator die Historie zusammen. Minimum 10.",
"uk": "Після цієї кількості подій конденсатор узагальнить історію. Мінімум 10."
},
"SETTINGS$LANGUAGE": {
"en": "Language",
"ja": "言語",
@@ -2063,22 +2095,6 @@
"de": "Git-Anbieter",
"uk": "Git-провайдер"
},
"ACCOUNT_SETTINGS$TITLE": {
"en": "Account Settings",
"ja": "アカウント設定",
"zh-CN": "账户设置",
"zh-TW": "帳戶設定",
"ko-KR": "계정 설정",
"no": "Kontoinnstillinger",
"it": "Impostazioni account",
"pt": "Configurações da conta",
"es": "Configuración de la cuenta",
"ar": "إعدادات الحساب",
"fr": "Paramètres du compte",
"tr": "Hesap ayarları",
"de": "Kontoeinstellungen",
"uk": "Налаштування облікового запису"
},
"WORKSPACE$TERMINAL_TAB_LABEL": {
"en": "Terminal",
"zh-CN": "终端",
@@ -2432,20 +2448,20 @@
"uk": "Git налаштування"
},
"SETTINGS$GIT_SETTINGS_DESCRIPTION": {
"en": "Configure Git integration settings",
"ja": "Git統合設定を構成する",
"zh-CN": "配置Git集成设置",
"zh-TW": "配置Git整合設定",
"ko-KR": "Git 통합 설정 구성",
"de": "Git-Integrationseinstellungen konfigurieren",
"no": "Konfigurer Git-integrasjonsinnstillinger",
"it": "Configura le impostazioni di integrazione Git",
"pt": "Configure as configurações de integração Git",
"es": "Configure los ajustes de integración Git",
"ar": "تكوين إعدادات تكامل Git",
"fr": "Configurer les paramètres d'intégration Git",
"tr": "Git entegrasyon ayarlarını yapılandırın",
"uk": "Налаштуйте параметри інтеграції Git"
"en": "Configure the username and email that OpenHands uses to commit changes.",
"ja": "OpenHandsがコミットに使用するユーザー名とメールを設定します。",
"zh-CN": "配置OpenHands用于提交更改的用户名和电子邮件。",
"zh-TW": "配置OpenHands用於提交更改的用戶名和電子郵件。",
"ko-KR": "OpenHands가 변경 사항을 커밋할 때 사용하는 사용자 이름과 이메일을 구성합니다.",
"de": "Konfigurieren Sie den Benutzernamen und die E-Mail, die OpenHands zum Committen von Änderungen verwendet.",
"no": "Konfigurer brukernavnet og e-posten som OpenHands bruker for å committe endringer.",
"it": "Configura il nome utente e l'email che OpenHands utilizza per committare le modifiche.",
"pt": "Configure o nome de usuário e o email que o OpenHands usa para fazer commits de alterações.",
"es": "Configure el nombre de usuario y el correo electrónico que OpenHands utiliza para confirmar cambios.",
"ar": "قم بتكوين اسم المستخدم والبريد الإلكتروني الذي يستخدمه OpenHands لارتكاب التغييرات.",
"fr": "Configurez le nom d'utilisateur et l'email qu'OpenHands utilise pour valider les modifications.",
"tr": "OpenHands'ın değişiklikleri commit etmek için kullandığı kullanıcı adını ve e-postayı yapılandırın.",
"uk": "Налаштуйте ім'я користувача та електронну пошту, які OpenHands використовує для фіксації змін."
},
"SETTINGS$SOUND_NOTIFICATIONS": {
"en": "Sound Notifications",
@@ -2520,11 +2536,11 @@
"de": "Lösbarkeitsanalyse aktivieren",
"no": "Aktiver løsningsanalyse",
"it": "Abilita analisi di risolvibilità",
"pt": "Ativar análise de resolubilidade",
"es": "Habilitar análisis de resolubilidad",
"pt": "Ativar análise de solucionabilidade",
"es": "Habilitar análisis de solvencia",
"ar": "تمكين تحليل القابلية للحل",
"fr": "Activer l'analyse de solvabilité",
"tr": "Çözülebilirlik analizini etkinleştir",
"fr": "Activer l'analyse de solvabilité",
"tr": "Çözürlük Analizini Etkinleştir",
"uk": "Увімкнути аналіз розв'язності"
},
"SETTINGS$SEARCH_API_KEY": {
@@ -5215,6 +5231,22 @@
"tr": "Durdur",
"uk": "Стоп"
},
"BUTTON$PAUSE": {
"en": "Pause",
"ja": "一時停止",
"zh-CN": "暂停",
"zh-TW": "暫停",
"ko-KR": "일시정지",
"fr": "Mettre en pause",
"es": "Pausar",
"de": "Pausieren",
"it": "Pausa",
"pt": "Pausar",
"ar": "إيقاف مؤقت",
"no": "Pause",
"tr": "Duraklat",
"uk": "Призупинити"
},
"BUTTON$EDIT_TITLE": {
"en": "Edit Title",
"ja": "タイトルを編集",
@@ -5391,8 +5423,40 @@
"de": "Stopp bestätigen",
"uk": "Підтвердити зупинку"
},
"CONVERSATION$CONFIRM_PAUSE": {
"en": "Confirm Pause",
"ja": "一時停止の確認",
"zh-CN": "确认暂停",
"zh-TW": "確認暫停",
"ko-KR": "일시정지 확인",
"no": "Bekreft pause",
"it": "Conferma pausa",
"pt": "Confirmar pausa",
"es": "Confirmar pausa",
"ar": "تأكيد الإيقاف المؤقت",
"fr": "Confirmer la mise en pause",
"tr": "Duraklatmayı Onayla",
"de": "Pause bestätigen",
"uk": "Підтвердити призупинення"
},
"CONVERSATION$PAUSE_WARNING": {
"en": "Are you sure you want to pause this conversation?",
"ja": "この会話を一時停止してもよろしいですか?",
"zh-CN": "您确定要暂停此对话吗?",
"zh-TW": "您確定要暫停此對話嗎?",
"ko-KR": "이 대화를 일시정지하시겠습니까?",
"no": "Er du sikker på at du vil pause denne samtalen?",
"it": "Sei sicuro di voler mettere in pausa questa conversazione?",
"pt": "Tem certeza de que deseja pausar esta conversa?",
"es": "¿Está seguro de que desea pausar esta conversación?",
"ar": "هل أنت متأكد أنك تريد إيقاف هذه المحادثة مؤقتًا؟",
"fr": "Êtes-vous sûr de vouloir mettre cette conversation en pause ?",
"tr": "Bu konuşmayı duraklatmak istediğinizden emin misiniz?",
"de": "Sind Sie sicher, dass Sie dieses Gespräch pausieren möchten?",
"uk": "Ви впевнені, що хочете призупинити цю розмову?"
},
"CONVERSATION$STOP_WARNING": {
"en": "Are you sure you want to stop this conversation?",
"en": "Are you sure you want to pause this conversation?",
"ja": "この会話を停止してもよろしいですか?",
"zh-CN": "您确定要停止此对话吗?",
"zh-TW": "您確定要停止此對話嗎?",
@@ -5711,6 +5775,22 @@
"ja": "このアクションを実行してもよろしいですか?",
"uk": "Ви хочете продовжити цю дію?"
},
"CHAT_INTERFACE$HIGH_RISK_WARNING": {
"en": "Review carefully before proceeding.",
"zh-CN": "在继续之前请仔细检查。",
"de": "Überprüfen Sie sorgfältig, bevor Sie fortfahren.",
"zh-TW": "在繼續之前請仔細檢查。",
"ko-KR": "계속하기 전에 신중히 검토하세요.",
"no": "Gå nøye gjennom før du fortsetter.",
"it": "Esamina attentamente prima di procedere.",
"pt": "Revise cuidadosamente antes de prosseguir.",
"es": "Revise cuidadosamente antes de continuar.",
"ar": "يرجى المراجعة بعناية قبل المتابعة.",
"fr": "Examinez attentivement avant de continuer.",
"tr": "Devam etmeden önce dikkatlice gözden geçirin.",
"ja": "続行する前に慎重に確認してください。",
"uk": "Уважно перевірте перед продовженням."
},
"CHAT_INTERFACE$USER_CONFIRMED": {
"en": "Confirm the requested action",
"de": "Bestätigen Sie die angeforderte Aktion",
@@ -5935,70 +6015,6 @@
"ja": "不明な送信者",
"uk": "Невідомий"
},
"SECURITY_ANALYZER$UNKNOWN_RISK": {
"en": "Unknown Risk",
"de": "Unbekanntes Risiko",
"zh-CN": "未知风险",
"ko-KR": "알 수 없는 위험",
"no": "Ukjent risiko",
"zh-TW": "未知風險",
"it": "Rischio sconosciuto",
"pt": "Risco desconhecido",
"es": "Riesgo desconocido",
"ar": "مخاطر غير معروفة",
"fr": "Risque inconnu",
"tr": "Bilinmeyen risk",
"ja": "不明なリスク",
"uk": "Невідомий ризик"
},
"SECURITY_ANALYZER$LOW_RISK": {
"en": "Low Risk",
"de": "Niedriges Risiko",
"zh-CN": "低风险",
"ko-KR": "낮은 위험",
"no": "Lav risiko",
"zh-TW": "低風險",
"it": "Rischio basso",
"pt": "Baixo risco",
"es": "Riesgo bajo",
"ar": "مخاطر منخفضة",
"fr": "Risque faible",
"tr": "Düşük risk",
"ja": "低リスク",
"uk": "Низький ризик"
},
"SECURITY_ANALYZER$MEDIUM_RISK": {
"en": "Medium Risk",
"de": "Mittleres Risiko",
"zh-CN": "中等风险",
"ko-KR": "중간 위험",
"no": "Middels risiko",
"zh-TW": "中等風險",
"it": "Rischio medio",
"pt": "Risco médio",
"es": "Riesgo medio",
"ar": "مخاطر متوسطة",
"fr": "Risque moyen",
"tr": "Orta risk",
"ja": "中リスク",
"uk": "Середній ризик"
},
"SECURITY_ANALYZER$HIGH_RISK": {
"en": "High Risk",
"de": "Hohes Risiko",
"zh-CN": "高风险",
"ko-KR": "높은 위험",
"no": "Høy risiko",
"zh-TW": "高風險",
"it": "Rischio elevato",
"pt": "Alto risco",
"es": "Riesgo alto",
"ar": "مخاطر عالية",
"fr": "Risque élevé",
"tr": "Yüksek risk",
"ja": "高リスク",
"uk": "Високий ризик"
},
"SETTINGS$MODEL_TOOLTIP": {
"en": "Select the language model to use.",
"zh-CN": "选择要使用的语言模型",
@@ -6159,6 +6175,22 @@
"ja": "エージェントのアクションを実行前に確認",
"uk": "Очікує підтвердження користувача перед виконанням коду."
},
"SETTINGS$CONFIRMATION_MODE_LOCK_TOOLTIP": {
"en": "The agent is in confirmation mode. It will prompt the user to confirm certain actions when security analyzer policy detected a high-risk action. Click this icon to go to settings tab for more information.",
"de": "Der Agent befindet sich im Bestätigungsmodus. Er wird den Benutzer auffordern, bestimmte Aktionen zu bestätigen, wenn die Sicherheitsanalysator-Richtlinie eine risikoreiche Aktion erkannt hat. Weitere Informationen finden Sie auf der Registerkarte Einstellungen.",
"zh-CN": "代理处于确认模式。当安全分析器策略检测到高风险操作时,它会提示用户确认某些操作。查看设置选项卡了解更多信息。",
"zh-TW": "代理處於確認模式。當安全分析器策略檢測到高風險操作時,它會提示使用者確認某些操作。查看設定選項卡了解更多資訊。",
"ko-KR": "에이전트가 확인 모드에 있습니다. 보안 분석기 정책이 고위험 작업을 감지하면 사용자에게 특정 작업을 확인하도록 요청합니다. 자세한 내용은 설정 탭을 확인하세요.",
"no": "Agenten er i bekreftelsesmodus. Den vil be brukeren om å bekrefte visse handlinger når sikkerhetsanalysatorpolitikken oppdager en høyrisiko-handling. Sjekk innstillingsfanen for mer informasjon.",
"it": "L'agente è in modalità di conferma. Chiederà all'utente di confermare certe azioni quando la politica dell'analizzatore di sicurezza rileva un'azione ad alto rischio. Controlla la scheda impostazioni per maggiori informazioni.",
"pt": "O agente está no modo de confirmação. Ele solicitará ao usuário que confirme certas ações quando a política do analisador de segurança detectar uma ação de alto risco. Verifique a aba de configurações para mais informações.",
"es": "El agente está en modo de confirmación. Solicitará al usuario que confirme ciertas acciones cuando la política del analizador de seguridad detecte una acción de alto riesgo. Consulte la pestaña de configuración para obtener más información.",
"ar": "الوكيل في وضع التأكيد. سيطلب من المستخدم تأكيد إجراءات معينة عندما تكتشف سياسة محلل الأمان إجراءً عالي المخاطر. تحقق من علامة تبويب الإعدادات للحصول على مزيد من المعلومات.",
"fr": "L'agent est en mode de confirmation. Il demandera à l'utilisateur de confirmer certaines actions lorsque la politique de l'analyseur de sécurité détecte une action à haut risque. Consultez l'onglet paramètres pour plus d'informations.",
"tr": "Ajan onay modunda. Güvenlik analizörü politikası yüksek riskli bir eylem tespit ettiğinde kullanıcıdan belirli eylemleri onaylamasını isteyecek. Daha fazla bilgi için ayarlar sekmesini kontrol edin.",
"ja": "エージェントは確認モードです。セキュリティアナライザーポリシーが高リスクアクションを検出した場合、特定のアクションの確認をユーザーに求めます。詳細については設定タブを確認してください。",
"uk": "Агент знаходиться в режимі підтвердження. Він попросить користувача підтвердити певні дії, коли політика аналізатора безпеки виявить дію високого ризику. Перевірте вкладку налаштувань для отримання додаткової інформації."
},
"SETTINGS$AGENT_SELECT_ENABLED": {
"en": "Enable Agent Selection - Advanced Users",
"zh-CN": "启用智能体选择 - 高级用户",
@@ -6207,6 +6239,38 @@
"ja": "セキュリティアナライザーを選択…",
"uk": "Виберіть аналізатор безпеки…"
},
"SETTINGS$SECURITY_ANALYZER_TOOLTIP": {
"en": "When enabled, the agent will pause and ask for confirmation when it tries to execute high-risk actions",
"de": "Wenn aktiviert, pausiert der Agent und fragt nach Bestätigung, wenn er versucht, risikoreiche Aktionen auszuführen",
"zh-CN": "启用后,代理在尝试执行高风险操作时会暂停并要求确认",
"zh-TW": "啟用後,代理在嘗試執行高風險操作時會暫停並要求確認",
"ko-KR": "활성화되면 에이전트가 고위험 작업을 실행하려고 할 때 일시 중지하고 확인을 요청합니다",
"no": "Når aktivert, vil agenten pause og be om bekreftelse når den prøver å utføre høyrisiko-handlinger",
"it": "Quando abilitato, l'agente si fermerà e chiederà conferma quando tenta di eseguire azioni ad alto rischio",
"pt": "Quando ativado, o agente pausará e pedirá confirmação quando tentar executar ações de alto risco",
"es": "Cuando está habilitado, el agente se pausará y pedirá confirmación cuando trate de ejecutar acciones de alto riesgo",
"ar": "عند التمكين، سيتوقف الوكيل ويطلب التأكيد عندما يحاول تنفيذ إجراءات عالية المخاطر",
"fr": "Lorsqu'il est activé, l'agent se mettra en pause et demandera confirmation lorsqu'il tentera d'exécuter des actions à haut risque",
"tr": "Etkinleştirildiğinde, ajan yüksek riskli eylemleri gerçekleştirmeye çalıştığında duraklar ve onay ister",
"ja": "有効にすると、エージェントは高リスクなアクションを実行しようとする際に一時停止し、確認を求めます",
"uk": "Коли увімкнено, агент зупиниться і попросить підтвердження, коли спробує виконати дії високого ризику"
},
"SETTINGS$SECURITY_ANALYZER_DESCRIPTION": {
"en": "The security analyzer will be used in conjunction with confirmation mode. By default, it utilizes LLM-predicted action risk to determine whether to prompt the user for confirmation. If the risk is HIGH, it will prompt the user for confirmation by default.",
"de": "Der Sicherheitsanalysator wird in Verbindung mit dem Bestätigungsmodus verwendet. Standardmäßig nutzt er LLM-vorhergesagtes Aktionsrisiko, um zu bestimmen, ob der Benutzer zur Bestätigung aufgefordert werden soll. Wenn das Risiko HOCH ist, wird er standardmäßig zur Bestätigung auffordern.",
"zh-CN": "安全分析器将与确认模式结合使用。默认情况下,它利用LLM预测的操作风险来确定是否提示用户确认。如果风险为高,它将默认提示用户确认。",
"zh-TW": "安全分析器將與確認模式結合使用。預設情況下,它利用LLM預測的操作風險來確定是否提示用戶確認。如果風險為高,它將預設提示用戶確認。",
"ko-KR": "보안 분석기는 확인 모드와 함께 사용됩니다. 기본적으로 LLM이 예측한 작업 위험을 활용하여 사용자에게 확인을 요청할지 결정합니다. 위험이 높으면 기본적으로 사용자에게 확인을 요청합니다.",
"no": "Sikkerhetsanalysatoren vil bli brukt i forbindelse med bekreftelsesmodus. Som standard bruker den LLM-forutsagt handlingsrisiko for å bestemme om brukeren skal bli bedt om bekreftelse. Hvis risikoen er HØY, vil den be om bekreftelse som standard.",
"it": "L'analizzatore di sicurezza verrà utilizzato insieme alla modalità di conferma. Per impostazione predefinita, utilizza il rischio di azione previsto dall'LLM per determinare se richiedere conferma all'utente. Se il rischio è ALTO, richiederà conferma per impostazione predefinita.",
"pt": "O analisador de segurança será usado em conjunto com o modo de confirmação. Por padrão, utiliza o risco de ação previsto pelo LLM para determinar se deve solicitar confirmação ao usuário. Se o risco for ALTO, solicitará confirmação por padrão.",
"es": "El analizador de seguridad se utilizará junto con el modo de confirmación. Por defecto, utiliza el riesgo de acción predicho por LLM para determinar si solicitar confirmación al usuario. Si el riesgo es ALTO, solicitará confirmación por defecto.",
"ar": "سيتم استخدام محلل الأمان بالتزامن مع وضع التأكيد. افتراضياً، يستخدم مخاطر الإجراء المتوقعة من LLM لتحديد ما إذا كان يجب مطالبة المستخدم بالتأكيد. إذا كان الخطر عالياً، فسيطالب بالتأكيد افتراضياً.",
"fr": "L'analyseur de sécurité sera utilisé en conjonction avec le mode de confirmation. Par défaut, il utilise le risque d'action prédit par LLM pour déterminer s'il faut demander confirmation à l'utilisateur. Si le risque est ÉLEVÉ, il demandera confirmation par défaut.",
"tr": "Güvenlik analizörü onay modu ile birlikte kullanılacaktır. Varsayılan olarak, kullanıcıdan onay istenip istenmeyeceğini belirlemek için LLM tarafından tahmin edilen eylem riskini kullanır. Risk YÜKSEK ise, varsayılan olarak kullanıcıdan onay isteyecektir.",
"ja": "セキュリティアナライザーは確認モードと組み合わせて使用されます。デフォルトでは、LLMが予測したアクションリスクを利用して、ユーザーに確認を求めるかどうかを決定します。リスクが高い場合、デフォルトでユーザーに確認を求めます。",
"uk": "Аналізатор безпеки буде використовуватися разом з режимом підтвердження. За замовчуванням він використовує передбачений LLM ризик дії для визначення, чи потрібно запитувати підтвердження у користувача. Якщо ризик ВИСОКИЙ, він запитуватиме підтвердження за замовчуванням."
},
"SETTINGS$DONT_KNOW_API_KEY": {
"en": "Don't know your API key?",
"ja": "APIキーがわかりませんか?",
@@ -7583,22 +7647,6 @@
"tr": "Ajan görevine devam et",
"uk": "Відновити завдання агента"
},
"ACTION_BUTTON$PAUSE": {
"en": "Pause the current task",
"zh-CN": "暂停",
"zh-TW": "暫停",
"ko-KR": "일시정지",
"ja": "一時停止",
"no": "Sett gjeldende oppgave på pause",
"ar": "إيقاف المهمة الحالية مؤقتاً",
"de": "Aktuelle Aufgabe pausieren",
"fr": "Mettre en pause la tâche actuelle",
"it": "Metti in pausa il compito corrente",
"pt": "Pausar a tarefa atual",
"es": "Pausar la tarea actual",
"tr": "Mevcut görevi duraklat",
"uk": "Призупинити поточне завдання"
},
"BROWSER$SCREENSHOT_ALT": {
"en": "Browser Screenshot",
"zh-CN": "截图",
@@ -8207,22 +8255,6 @@
"tr": "Kullanıcı avatarı yer tutucusu",
"uk": "заповнювач аватара користувача"
},
"ACCOUNT_SETTINGS$SETTINGS": {
"en": "Account Settings",
"ja": "アカウント設定",
"zh-CN": "账户设置",
"zh-TW": "帳戶設定",
"ko-KR": "계정 설정",
"no": "Kontoinnstillinger",
"it": "Impostazioni account",
"pt": "Configurações da conta",
"es": "Configuración de la cuenta",
"ar": "إعدادات الحساب",
"fr": "Paramètres du compte",
"tr": "Hesap ayarları",
"de": "Kontoeinstellungen",
"uk": "Налаштування облікового запису"
},
"ACCOUNT_SETTINGS$LOGOUT": {
"en": "Logout",
"ja": "ログアウト",
@@ -9167,38 +9199,6 @@
"tr": "Bekleme listesine katıl",
"uk": "Приєднатися до списку очікування"
},
"ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS": {
"en": "Additional Settings",
"ja": "追加設定",
"zh-CN": "附加设置",
"zh-TW": "附加設定",
"ko-KR": "추가 설정",
"de": "Zusätzliche Einstellungen",
"no": "Ytterligere innstillinger",
"it": "Impostazioni aggiuntive",
"pt": "Configurações adicionais",
"es": "Configuraciones adicionales",
"ar": "إعدادات إضافية",
"fr": "Paramètres supplémentaires",
"tr": "Ek Ayarlar",
"uk": "Додаткові налаштування"
},
"ACCOUNT_SETTINGS$DISCONNECT_FROM_GITHUB": {
"en": "Disconnect from GitHub",
"ja": "GitHubから切断",
"zh-CN": "断开与GitHub的连接",
"zh-TW": "中斷與GitHub的連接",
"ko-KR": "GitHub 연결 해제",
"de": "Von GitHub trennen",
"no": "Koble fra GitHub",
"it": "Disconnetti da GitHub",
"pt": "Desconectar do GitHub",
"es": "Desconectar de GitHub",
"ar": "قطع الاتصال من GitHub",
"fr": "Se déconnecter de GitHub",
"tr": "GitHub'dan bağlantıyı kes",
"uk": "Відключитися від GitHub"
},
"CONVERSATION$DELETE_WARNING": {
"en": "Are you sure you want to delete this conversation? This action cannot be undone.",
"ja": "この会話を削除してもよろしいですか?この操作は元に戻せません。",
@@ -11536,20 +11536,20 @@
"uk": "Визначте тригери для мікроагента"
},
"MICROAGENT_MANAGEMENT$HELP_TEXT_DESCRIBING_VALID_TRIGGERS": {
"en": "Help text describing valid triggers.",
"ja": "有効なトリガーについて説明するヘルプテキスト。",
"zh-CN": "描述有效触发器的帮助文本。",
"zh-TW": "描述有效觸發條件的說明文字。",
"ko-KR": "유효한 트리거를 설명하는 도움말 텍스트입니다.",
"no": "Hjelpetekst som beskriver gyldige utløsere.",
"it": "Testo di aiuto che descrive i trigger validi.",
"pt": "Texto de ajuda descrevendo gatilhos válidos.",
"es": "Texto de ayuda que describe desencadenantes válidos.",
"ar": "نص المساعدة الذي يصف المشغلات الصالحة.",
"fr": "Texte d'aide décrivant les déclencheurs valides.",
"tr": "Geçerli tetikleyicileri açıklayan yardım metni.",
"de": "Hilfetext, der gültige Auslöser beschreibt.",
"uk": "Текст довідки, що описує дійсні тригери."
"en": "Enter a keyword that OpenHands will use to trigger this microagent (Optional).",
"ja": "OpenHandsがこのマイクロエージェントを起動するために使用するキーワードを入力してください(任意)。",
"zh-CN": "输入OpenHands将用于触发此微代理的关键字(可选)。",
"zh-TW": "輸入OpenHands將用於觸發此微代理的關鍵字(可選)。",
"ko-KR": "OpenHands가 이 마이크로에이전트를 트리거하는 데 사용할 키워드를 입력하세요(선택 사항).",
"no": "Skriv inn et nøkkelord som OpenHands vil bruke for å utløse denne mikroagenten (valgfritt).",
"it": "Inserisci una parola chiave che OpenHands userà per attivare questo microagent (opzionale).",
"pt": "Digite uma palavra-chave que o OpenHands usará para acionar este microagente (Opcional).",
"es": "Introduce una palabra clave que OpenHands usará para activar este microagente (Opcional).",
"ar": "أدخل كلمة مفتاحية سيستخدمها OpenHands لتشغيل هذا الوكيل الصغير (اختياري).",
"fr": "Entrez un mot-clé qu'OpenHands utilisera pour déclencher ce microagent (facultatif).",
"tr": "OpenHands'ın bu mikro ajanı tetiklemek için kullanacağı bir anahtar kelime girin (İsteğe bağlı).",
"de": "Geben Sie ein Schlüsselwort ein, das OpenHands verwendet, um diesen Microagenten auszulösen (optional).",
"uk": "Введіть ключове слово, яке OpenHands використовуватиме для запуску цього мікроагента (необов'язково)."
},
"COMMON$FOR_EXAMPLE": {
"en": "For example",
@@ -12495,38 +12495,6 @@
"de": "Fehler beim Laden des Microagent-Inhalts.",
"uk": "Помилка під час завантаження вмісту мікроагента."
},
"SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT": {
"en": "Environment variables must follow KEY=value format",
"ja": "Environment variables must follow KEY=value format",
"zh-CN": "Environment variables must follow KEY=value format",
"zh-TW": "Environment variables must follow KEY=value format",
"ko-KR": "Environment variables must follow KEY=value format",
"no": "Environment variables must follow KEY=value format",
"it": "Environment variables must follow KEY=value format",
"pt": "Environment variables must follow KEY=value format",
"es": "Environment variables must follow KEY=value format",
"ar": "Environment variables must follow KEY=value format",
"fr": "Environment variables must follow KEY=value format",
"tr": "Environment variables must follow KEY=value format",
"de": "Environment variables must follow KEY=value format",
"uk": "Environment variables must follow KEY=value format"
},
"SETTINGS$MCP_ERROR_URL_DUPLICATE": {
"en": "A server with this URL already exists for the selected type",
"ja": "A server with this URL already exists for the selected type",
"zh-CN": "A server with this URL already exists for the selected type",
"zh-TW": "A server with this URL already exists for the selected type",
"ko-KR": "A server with this URL already exists for the selected type",
"no": "A server with this URL already exists for the selected type",
"it": "A server with this URL already exists for the selected type",
"pt": "A server with this URL already exists for the selected type",
"es": "A server with this URL already exists for the selected type",
"ar": "A server with this URL already exists for the selected type",
"fr": "A server with this URL already exists for the selected type",
"tr": "A server with this URL already exists for the selected type",
"de": "A server with this URL already exists for the selected type",
"uk": "A server with this URL already exists for the selected type"
},
"SETTINGS$MCP_SERVER_TYPE_SSE": {
"en": "SSE",
"ja": "SSE",
@@ -12703,6 +12671,38 @@
"de": "Befehl darf keine Leerzeichen enthalten",
"uk": "Команда не може містити пробіли"
},
"SETTINGS$MCP_ERROR_URL_DUPLICATE": {
"en": "A server with this URL already exists for the selected type",
"ja": "A server with this URL already exists for the selected type",
"zh-CN": "A server with this URL already exists for the selected type",
"zh-TW": "A server with this URL already exists for the selected type",
"ko-KR": "A server with this URL already exists for the selected type",
"no": "A server with this URL already exists for the selected type",
"it": "A server with this URL already exists for the selected type",
"pt": "A server with this URL already exists for the selected type",
"es": "A server with this URL already exists for the selected type",
"ar": "A server with this URL already exists for the selected type",
"fr": "A server with this URL already exists for the selected type",
"tr": "A server with this URL already exists for the selected type",
"de": "A server with this URL already exists for the selected type",
"uk": "A server with this URL already exists for the selected type"
},
"SETTINGS$MCP_ERROR_ENV_INVALID_FORMAT": {
"en": "Environment variables must follow KEY=value format",
"ja": "Environment variables must follow KEY=value format",
"zh-CN": "Environment variables must follow KEY=value format",
"zh-TW": "Environment variables must follow KEY=value format",
"ko-KR": "Environment variables must follow KEY=value format",
"no": "Environment variables must follow KEY=value format",
"it": "Environment variables must follow KEY=value format",
"pt": "Environment variables must follow KEY=value format",
"es": "Environment variables must follow KEY=value format",
"ar": "Environment variables must follow KEY=value format",
"fr": "Environment variables must follow KEY=value format",
"tr": "Environment variables must follow KEY=value format",
"de": "Environment variables must follow KEY=value format",
"uk": "Environment variables must follow KEY=value format"
},
"SETTINGS$MCP_SERVER_TYPE": {
"en": "Server Type",
"ja": "サーバータイプ",
@@ -12958,5 +12958,213 @@
"tr": "A server with this URL already exists for the selected type",
"de": "A server with this URL already exists for the selected type",
"uk": "A server with this URL already exists for the selected type"
},
"MICROAGENT_MANAGEMENT$OPENING_PR_TO_CREATE_MICROAGENT": {
"en": "Opening a PR to create the microagent for you...",
"ja": "マイクロエージェントを作成するためのプルリクエストを作成しています...",
"zh-CN": "正在为您创建微代理的拉取请求...",
"zh-TW": "正在為您建立微代理的拉取請求...",
"ko-KR": "마이크로에이전트를 생성하기 위한 PR을 열고 있습니다...",
"no": "Åpner en PR for å opprette mikroagenten for deg...",
"it": "Apertura di una PR per creare il microagente per te...",
"pt": "Abrindo um PR para criar o microagente para você...",
"es": "Abriendo un PR para crear el microagente para ti...",
"ar": "يتم فتح طلب سحب لإنشاء الوكيل الدقيق من أجلك...",
"fr": "Ouverture d'une PR pour créer le microagent pour vous...",
"tr": "Sizin için mikro ajanı oluşturmak üzere bir PR açılıyor...",
"de": "Es wird ein PR geöffnet, um den Microagent für Sie zu erstellen...",
"uk": "Відкривається PR для створення мікроагента для вас..."
},
"MICROAGENT_MANAGEMENT$PR_READY_FOR_REVIEW": {
"en": "PR is ready for review! The microagent has been created successfully.",
"ja": "PRのレビューが可能です!マイクロエージェントが正常に作成されました。",
"zh-CN": "PR已准备好审核!微代理已成功创建。",
"zh-TW": "PR 已準備好審查!微代理已成功建立。",
"ko-KR": "PR이 검토를 위해 준비되었습니다! 마이크로에이전트가 성공적으로 생성되었습니다.",
"no": "PR er klar for gjennomgang! Mikroagenten har blitt opprettet.",
"it": "La PR è pronta per la revisione! Il microagente è stato creato con successo.",
"pt": "PR pronto para revisão! O microagente foi criado com sucesso.",
"es": "¡La PR está lista para revisión! El microagente se ha creado correctamente.",
"ar": "طلب السحب جاهز للمراجعة! تم إنشاء الوكيل الدقيق بنجاح.",
"fr": "La PR est prête pour révision ! Le microagent a été créé avec succès.",
"tr": "PR incelemeye hazır! Mikro ajan başarıyla oluşturuldu.",
"de": "PR ist bereit zur Überprüfung! Der Microagent wurde erfolgreich erstellt.",
"uk": "PR готовий до перегляду! Мікроагента успішно створено."
},
"MICROAGENT_MANAGEMENT$PR_NOT_CREATED": {
"en": "The agent has finished its task but was unable to create a PR.",
"ja": "エージェントはタスクを完了しましたが、PRを作成できませんでした。",
"zh-CN": "代理已完成任务,但无法创建 PR。",
"zh-TW": "代理已完成任務,但無法建立 PR。",
"ko-KR": "에이전트가 작업을 완료했지만 PR을 생성할 수 없었습니다.",
"no": "Agenten har fullført oppgaven, men klarte ikke å opprette en PR.",
"it": "L'agente ha terminato il suo compito ma non è riuscito a creare una PR.",
"pt": "O agente concluiu sua tarefa, mas não conseguiu criar um PR.",
"es": "El agente ha terminado su tarea pero no pudo crear un PR.",
"ar": "أكمل الوكيل مهمته لكنه لم يتمكن من إنشاء طلب سحب (PR).",
"fr": "L'agent a terminé sa tâche mais n'a pas pu créer de PR.",
"tr": "Ajan görevini tamamladı ancak bir PR oluşturamadı.",
"de": "Der Agent hat seine Aufgabe abgeschlossen, konnte aber keinen PR erstellen.",
"uk": "Агент завершив завдання, але не зміг створити PR."
},
"MICROAGENT_MANAGEMENT$ERROR_CREATING_MICROAGENT": {
"en": "Something went wrong. Try initiating the microagent again.",
"ja": "問題が発生しました。もう一度マイクロエージェントを開始してください。",
"zh-CN": "出现了问题。请重试启动微代理。",
"zh-TW": "發生錯誤。請再次嘗試啟動微代理。",
"ko-KR": "문제가 발생했습니다. 마이크로에이전트를 다시 시작해 보세요.",
"no": "Noe gikk galt. Prøv å starte mikroagenten på nytt.",
"it": "Qualcosa è andato storto. Prova a iniziare di nuovo il microagente.",
"pt": "Algo deu errado. Tente iniciar o microagente novamente.",
"es": "Algo salió mal. Intenta iniciar el microagente de nuevo.",
"ar": "حدث خطأ ما. حاول بدء تشغيل الوكيل الدقيق مرة أخرى.",
"fr": "Une erreur s'est produite. Essayez de relancer le microagent.",
"tr": "Bir şeyler ters gitti. Mikro ajanı tekrar başlatmayı deneyin.",
"de": "Etwas ist schiefgelaufen. Versuchen Sie, den Microagenten erneut zu starten.",
"uk": "Щось пішло не так. Спробуйте ініціювати мікроагента ще раз."
},
"MICROAGENT$STATUS_WAITING": {
"en": "Waiting for runtime to start...",
"ja": "ランタイムの開始を待機中...",
"zh-CN": "等待运行时启动...",
"zh-TW": "等待運行時啟動...",
"ko-KR": "런타임 시작을 기다리는 중...",
"no": "Venter på at runtime skal starte...",
"it": "In attesa dell'avvio del runtime...",
"pt": "Aguardando o runtime iniciar...",
"es": "Esperando que inicie el runtime...",
"ar": "في انتظار بدء وقت التشغيل...",
"fr": "En attente du démarrage du runtime...",
"tr": "Çalışma zamanının başlaması bekleniyor...",
"de": "Warten auf den Start der Laufzeit...",
"uk": "Очікування запуску середовища виконання..."
},
"MICROAGENT$UNKNOWN_ERROR": {
"en": "Unknown error, please try again",
"ja": "不明なエラーです。もう一度お試しください",
"zh-CN": "未知错误,请重试",
"zh-TW": "未知錯誤,請重試",
"ko-KR": "알 수 없는 오류입니다. 다시 시도해 주세요",
"no": "Ukjent feil, vennligst prøv igjen",
"it": "Errore sconosciuto, riprova",
"pt": "Erro desconhecido, tente novamente",
"es": "Error desconocido, inténtalo de nuevo",
"ar": "خطأ غير معروف، يرجى المحاولة مرة أخرى",
"fr": "Erreur inconnue, veuillez réessayer",
"tr": "Bilinmeyen hata, lütfen tekrar deneyin",
"de": "Unbekannter Fehler, bitte versuchen Sie es erneut",
"uk": "Невідома помилка, спробуйте ще раз"
},
"MICROAGENT$CONVERSATION_STARTING": {
"en": "Starting conversation...",
"ja": "会話を開始しています...",
"zh-CN": "正在开始对话...",
"zh-TW": "正在開始對話...",
"ko-KR": "대화를 시작하는 중...",
"no": "Starter samtale...",
"it": "Avvio conversazione...",
"pt": "Iniciando conversa...",
"es": "Iniciando conversación...",
"ar": "بدء المحادثة...",
"fr": "Démarrage de la conversation...",
"tr": "Konuşma başlatılıyor...",
"de": "Gespräch wird gestartet...",
"uk": "Розпочинається розмова..."
},
"MICROAGENT_MANAGEMENT$EXISTING_MICROAGENTS": {
"en": "Existing Microagents",
"ja": "既存のマイクロエージェント",
"zh-CN": "现有微代理",
"zh-TW": "現有微代理",
"ko-KR": "기존 마이크로에이전트",
"no": "Eksisterende mikroagenter",
"it": "Microagent esistenti",
"pt": "Microagentes existentes",
"es": "Microagentes existentes",
"ar": "الوكلاء الدقيقون الحاليون",
"fr": "Microagents existants",
"tr": "Mevcut Mikroajanlar",
"de": "Vorhandene Mikroagenten",
"uk": "Існуючі мікроагенти"
},
"MICROAGENT_MANAGEMENT$OPEN_MICROAGENT_PULL_REQUESTS": {
"en": "Open Microagent Pull Requests",
"ja": "未解決のマイクロエージェントのプルリクエスト",
"zh-CN": "未合并的微代理拉取请求",
"zh-TW": "未合併的微代理拉取請求",
"ko-KR": "오픈된 마이크로에이전트 풀 리퀘스트",
"no": "Åpne mikroagent-pull requests",
"it": "Pull request di microagent aperte",
"pt": "Pull requests de microagentes abertas",
"es": "Pull requests de microagentes abiertas",
"ar": "طلبات السحب المفتوحة للوكلاء الدقيقين",
"fr": "Pull requests de microagents ouvertes",
"tr": "Açık Mikroajan Pull İstekleri",
"de": "Offene Microagent-Pull-Requests",
"uk": "Відкриті pull-запити мікроагентів"
},
"SETTINGS$SECURITY_ANALYZER_LLM_DEFAULT": {
"en": "LLM Analyzer (Default)",
"ja": "LLMアナライザー(デフォルト)",
"zh-CN": "LLM 分析器(默认)",
"zh-TW": "LLM 分析器(預設)",
"ko-KR": "LLM 분석기(기본)",
"no": "LLM-analysator (standard)",
"it": "Analizzatore LLM (Predefinito)",
"pt": "Analisador LLM (Padrão)",
"es": "Analizador LLM (Predeterminado)",
"ar": "محلل LLM (افتراضي)",
"fr": "Analyseur LLM (Par défaut)",
"tr": "LLM Analizörü (Varsayılan)",
"de": "LLM-Analysator (Standard)",
"uk": "Аналізатор LLM (За замовчуванням)"
},
"SETTINGS$SECURITY_ANALYZER_NONE": {
"en": "None (Ask for every command)",
"ja": "なし(すべてのコマンドで確認)",
"zh-CN": "无(每条命令都询问)",
"zh-TW": "無(每個指令都詢問)",
"ko-KR": "없음(모든 명령마다 확인)",
"no": "Ingen (Spør for hver kommando)",
"it": "Nessuno (Chiedi per ogni comando)",
"pt": "Nenhum (Perguntar para cada comando)",
"es": "Ninguno (Preguntar para cada comando)",
"ar": "لا شيء (اسأل عن كل أمر)",
"fr": "Aucun (Demander pour chaque commande)",
"tr": "Yok (Her komutta sor)",
"de": "Keine (Bei jedem Befehl nachfragen)",
"uk": "Немає (Запитувати для кожної команди)"
},
"SETTINGS$SECURITY_ANALYZER_INVARIANT": {
"en": "Invariant Rule-based Analyzer",
"ja": "不変ルールベース分析器",
"zh-CN": "Invariant 规则分析器",
"zh-TW": "Invariant 規則式分析器",
"ko-KR": "Invariant 규칙 기반 분석기",
"no": "Invariant regelbasert analysator",
"it": "Analizzatore basato su regole Invariant",
"pt": "Analisador baseado em regras Invariant",
"es": "Analizador basado en reglas Invariant",
"ar": "محلل قائم على القواعد Invariant",
"fr": "Analyseur à base de règles Invariant",
"tr": "Invariant Kural Tabanlı Analizör",
"de": "Invariant regelbasierter Analysator",
"uk": "Аналізатор на основі правил Invariant"
},
"COMMON$HIGH_RISK": {
"en": "High Risk",
"ja": "高リスク",
"zh-CN": "高风险",
"zh-TW": "高風險",
"ko-KR": "고위험",
"no": "Høy risiko",
"it": "Alto rischio",
"pt": "Alto risco",
"es": "Alto riesgo",
"ar": "مخاطر عالية",
"fr": "Risque élevé",
"tr": "Yüksek Risk",
"de": "Hohes Risiko",
"uk": "Високий ризик"
}
}

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