Compare commits

...

35 Commits

Author SHA1 Message Date
openhands 9d8985fac9 Modify split_bash_commands to filter out comment-only commands
- Changed split_bash_commands to only return non-comment commands
- This eliminates the need to filter comment-only commands in calling code
- Preserves backward compatibility for comment-only input (returns as single command)
- Handles mixed commands by filtering out standalone comment lines
- Commands with inline comments are preserved as-is
- All existing tests continue to pass
- Fixed formatting in related test files
2025-08-12 16:49:38 +00:00
openhands c5cf95d351 Fix bash comment handling issue #10243 2025-08-12 16:30:21 +00:00
openhands f19c491728 Add failing tests for bash comment handling issue #10243 2025-08-12 16:24:38 +00:00
jpelletier1 e1559651b8 Unhide Git Settings feature and add explanatory text (#10256)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-12 14:18:15 +00:00
Ibragim Badertdinov 19a6b6b618 feat(eval): Support evaluation on SWE-rebench (#10251) 2025-08-12 14:05:43 +00:00
Xingyao Wang 2b7e44819f chore(agent_prompt): Add EXTERNAL_SERVICES section to system prompt template (#10244)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-12 21:53:53 +08:00
Xingyao Wang 0699a0ce7c fix: copy microagents file into runtime image (#10245)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-08-12 12:42:42 +00:00
Insop 1d0d88d491 Readability improvement & remove duplicated and unused prompts (#10241) 2025-08-12 12:42:17 +08:00
Tim O'Farrell 6f21b6700a Fix for issues where callbacks are not batched (#10235) 2025-08-11 15:44:48 -06:00
Tim O'Farrell af49b615b1 Add BatchedWebHookFileStore for batching webhook updates (#10119)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-11 12:51:08 -06:00
Tim O'Farrell 4651edd5b3 Fix circular import by moving refine_prompt to dedicated module (#10223)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-11 12:17:18 -06:00
olyashok d7f72fec9c OverlayFS support for docker runtimes (#10222) 2025-08-11 18:11:08 +00:00
mamoodi 09011c91f8 Remove rbren from UI changes reviewers (#10230) 2025-08-11 13:32:29 -04:00
Xingyao Wang e56fabfc5e feat(cli): Add markdown schema visualization in CLI (#10193)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-11 15:47:38 +00:00
Xingyao Wang 56f752557c Implement auto-pagination for conversation list with infinite scroll (#10129)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-08-11 15:03:29 +00:00
Calvin Smith 5f2ad7fbb0 Solvability setting switch (#9727)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-08-11 08:57:47 -06:00
Ryan H. Tran 758e30c9a8 Remove SecretStr conversion in GAIA eval (#10204) 2025-08-11 21:30:18 +08:00
dependabot[bot] 28017f232e chore(deps): bump the version-all group across 1 directory with 9 updates (#10168)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-11 14:51:36 +04:00
Tim O'Farrell 3302c31c60 Removed Hack that is no longer required (#10195) 2025-08-10 12:13:19 -06:00
Xingyao Wang 116ba199d1 feat(agent): stop using short tool description for gpt-5 (#10184) 2025-08-09 17:56:52 -04:00
Boxuan Li 803bdced9c Fix Windows prompt refinement: ensure 'bash' is replaced with 'powershell' in all prompts (#10179)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-08 20:28:36 -07:00
Xingyao Wang 3eecac2003 docs: Add GPT-5 model recommendation and fix pricing display issue (#10177)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-08 19:19:59 +00:00
mamoodi c02e09fc2d Hide Git Settings section from Application settings (#10176)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-08 19:06:40 +00:00
Tim O'Farrell 18f8661770 feat: add mcp_shttp_servers override to conversation initialization (#10171)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-08 18:05:44 +00:00
Xingyao Wang 04ff4a025b feat(cli): Use CLI to launch OpenHands UI server via Docker (#9783)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-09 02:04:07 +08:00
mamoodi 81ef363658 Increase stale bot inactivity time and better messaging (#10167)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-08-08 16:41:15 +00:00
Xingyao Wang 1474c5bc1c Support gpt-5-2025-08-07 and add it to OpenHands provider (#10172)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-08 16:05:51 +00:00
sp.wack 9b0a5da839 Use EventStore directly in remember prompt; merge client services (#10143)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-08 18:03:03 +04:00
Graham Neubig 7ab2ad2c1b Fix authentication setup issues in unit tests (#10118)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-07 22:12:21 -04:00
Graham Neubig 8416a019cb Fix unit test failures by prioritizing current directory in PYTHONPATH (#10105)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-07 22:12:02 -04:00
Engel Nyst 73a7c7786d Load previous conversation by id (CLI) (#10156) 2025-08-07 23:09:20 +02:00
aeft 11d12c5a01 fix: prevent CLI argument parser defaults from overriding config file values (#10140) 2025-08-08 04:48:04 +08:00
Xingyao Wang c4f303a07b chore(eval): Remove eval_infer_remote.sh script and related references (#10157)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-07 20:46:59 +00:00
Kenny Dizi 3a629cdf08 Add support model claude-opus-4-1-20250805 (#10120) 2025-08-07 18:48:34 +00:00
sp.wack 6ea33b657d chore(frontend): Remove some dead code (#10121) 2025-08-08 02:40:35 +08:00
125 changed files with 2962 additions and 1102 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
# See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Frontend code owners
/frontend/ @rbren @amanape
/frontend/ @amanape
/openhands-ui/ @amanape
# Evaluation code owners
+36 -14
View File
@@ -1,33 +1,53 @@
#!/bin/bash
set -euxo pipefail
# This script updates the PR description with commands to run the PR locally
# It adds both Docker and uvx commands
# Get the branch name for the PR
BRANCH_NAME=$(gh pr view $PR_NUMBER --json headRefName --jq .headRefName)
BRANCH_NAME=$(gh pr view "$PR_NUMBER" --json headRefName --jq .headRefName)
# Define the Docker command
DOCKER_RUN_COMMAND="docker run -it --rm \
-p 3000:3000 \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:$SHORT_SHA-nikolaik \
--name openhands-app-$SHORT_SHA \
docker.all-hands.dev/all-hands-ai/openhands:$SHORT_SHA"
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:${SHORT_SHA}-nikolaik \
--name openhands-app-${SHORT_SHA} \
docker.all-hands.dev/all-hands-ai/openhands:${SHORT_SHA}"
# Define the uvx command
UVX_RUN_COMMAND="uvx --python 3.12 --from git+https://github.com/All-Hands-AI/OpenHands@$BRANCH_NAME openhands"
UVX_RUN_COMMAND="uvx --python 3.12 --from git+https://github.com/All-Hands-AI/OpenHands@${BRANCH_NAME} openhands"
# Get the current PR body
PR_BODY=$(gh pr view $PR_NUMBER --json body --jq .body)
PR_BODY=$(gh pr view "$PR_NUMBER" --json body --jq .body)
# Prepare the new PR body with both commands
if echo "$PR_BODY" | grep -q "To run this PR locally, use the following command:"; then
# For existing PR descriptions, replace the command section
NEW_PR_BODY=$(echo "$PR_BODY" | sed "s|To run this PR locally, use the following command:.*\`\`\`|To run this PR locally, use the following command:\n\nGUI with Docker:\n\`\`\`\n$DOCKER_RUN_COMMAND\n\`\`\`\n\nCLI with uvx:\n\`\`\`\n$UVX_RUN_COMMAND\n\`\`\`|s")
# For existing PR descriptions, use a more robust approach
# Split the PR body at the "To run this PR locally" section and replace everything after it
BEFORE_SECTION=$(echo "$PR_BODY" | sed '/To run this PR locally, use the following command:/,$d')
NEW_PR_BODY=$(cat <<EOF
${BEFORE_SECTION}
To run this PR locally, use the following command:
GUI with Docker:
\`\`\`
${DOCKER_RUN_COMMAND}
\`\`\`
CLI with uvx:
\`\`\`
${UVX_RUN_COMMAND}
\`\`\`
EOF
)
else
# For new PR descriptions
NEW_PR_BODY="${PR_BODY}
# For new PR descriptions: use heredoc safely without indentation
NEW_PR_BODY=$(cat <<EOF
$PR_BODY
---
@@ -35,15 +55,17 @@ To run this PR locally, use the following command:
GUI with Docker:
\`\`\`
$DOCKER_RUN_COMMAND
${DOCKER_RUN_COMMAND}
\`\`\`
CLI with uvx:
\`\`\`
$UVX_RUN_COMMAND
\`\`\`"
${UVX_RUN_COMMAND}
\`\`\`
EOF
)
fi
# Update the PR description
echo "Updating PR description with Docker and uvx commands"
gh pr edit $PR_NUMBER --body "$NEW_PR_BODY"
gh pr edit "$PR_NUMBER" --body "$NEW_PR_BODY"
+5 -3
View File
@@ -48,11 +48,11 @@ jobs:
- name: Build Environment
run: make build
- name: Run Unit Tests
run: poetry run pytest --forked -n auto -svv ./tests/unit
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -svv ./tests/unit
- name: Run Runtime Tests with CLIRuntime
run: TEST_RUNTIME=cli poetry run pytest -svv tests/runtime/test_bash.py
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -svv tests/runtime/test_bash.py
- name: Run E2E Tests
run: poetry run pytest -svv tests/e2e
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest -svv tests/e2e
# Run specific Windows python tests
test-on-windows:
@@ -77,9 +77,11 @@ jobs:
- name: Run Windows unit tests
run: poetry run pytest -svv tests/unit/test_windows_bash.py
env:
PYTHONPATH: ".;$env:PYTHONPATH"
DEBUG: "1"
- name: Run Windows runtime tests with LocalRuntime
run: $env:TEST_RUNTIME="local"; poetry run pytest -svv tests/runtime/test_bash.py
env:
PYTHONPATH: ".;$env:PYTHONPATH"
TEST_RUNTIME: local
DEBUG: "1"
+6 -6
View File
@@ -12,11 +12,11 @@ jobs:
steps:
- uses: actions/stale@v9
with:
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
days-before-stale: 30
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'
close-issue-message: 'This issue was closed because it has been stalled for over 30 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for over 30 days with no activity.'
days-before-close: 7
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
operations-per-run: 150
+1 -1
View File
@@ -40,7 +40,7 @@ repos:
hooks:
- id: mypy
additional_dependencies:
[types-requests, types-setuptools, types-pyyaml, types-toml, types-docker, pydantic, lxml]
[types-requests, types-setuptools, types-pyyaml, types-toml, types-docker, types-Markdown, pydantic, lxml]
# To see gaps add `--html-report mypy-report/`
entry: mypy --config-file dev_config/python/mypy.ini openhands/
always_run: true
+61
View File
@@ -7,6 +7,67 @@ description: High level overview of the Graphical User Interface (GUI) in OpenHa
- [OpenHands is running](/usage/local-setup)
## Launching the GUI Server
### Using the CLI Command
You can launch the OpenHands GUI server directly from the command line using the `serve` command:
<Callout type="info">
**Prerequisites**: You need to have the [OpenHands CLI installed](/usage/how-to/cli-mode) first, OR have `uv` installed and run `uvx --python 3.12 --from openhands-ai openhands serve`. Otherwise, you'll need to use Docker directly (see the [Docker section](#using-docker-directly) below).
</Callout>
```bash
openhands serve
```
This command will:
- Check that Docker is installed and running
- Pull the required Docker images
- Launch the OpenHands GUI server at http://localhost:3000
- Use the same configuration directory (`~/.openhands`) as the CLI mode
#### Mounting Your Current Directory
To mount your current working directory into the GUI server container, use the `--mount-cwd` flag:
```bash
openhands serve --mount-cwd
```
This is useful when you want to work on files in your current directory through the GUI. The directory will be mounted at `/workspace` inside the container.
#### Using GPU Support
If you have NVIDIA GPUs and want to make them available to the OpenHands container, use the `--gpu` flag:
```bash
openhands serve --gpu
```
This will enable GPU support via nvidia-docker, mounting all available GPUs into the container. You can combine this with other flags:
```bash
openhands serve --gpu --mount-cwd
```
**Prerequisites for GPU support:**
- NVIDIA GPU drivers must be installed on your host system
- [NVIDIA Container Toolkit (nvidia-docker2)](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) must be installed and configured
#### Requirements
Before using the `openhands serve` command, ensure that:
- Docker is installed and running on your system
- You have internet access to pull the required Docker images
- Port 3000 is available on your system
The CLI will automatically check these requirements and provide helpful error messages if anything is missing.
### Using Docker Directly
Alternatively, you can run the GUI server using Docker directly. See the [local setup guide](/usage/local-setup) for detailed Docker instructions.
## Overview
### Initial Setup
+1 -1
View File
@@ -18,7 +18,7 @@ Based on these findings and community feedback, these are the latest models that
### Cloud / API-Based Models
- [anthropic/claude-sonnet-4-20250514](https://www.anthropic.com/api) (recommended)
- [openai/o4-mini](https://openai.com/index/introducing-o3-and-o4-mini/)
- [openai/gpt-5-2025-08-07](https://openai.com/api/) (recommended)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
- [moonshot/kimi-k2-0711-preview](https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2)
+1 -1
View File
@@ -32,4 +32,4 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr
Pricing follows official API provider rates. [You can view model prices here.](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json)
For `qwen3-coder-480b`, we charge the cheapest FP8 rate available on openrouter: $0.4 per million input tokens and $1.6 per million output tokens.
For `qwen3-coder-480b`, we charge the cheapest FP8 rate available on openrouter: \$0.4 per million input tokens and \$1.6 per million output tokens.
+24
View File
@@ -66,6 +66,30 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
### Start the App
#### Option 1: Using the CLI Launcher (Recommended)
If you have Python 3.12+ installed, you can use the CLI launcher for a simpler experience:
```bash
# Install OpenHands
pip install openhands-ai
# Launch the GUI server
openhands serve
# Or with GPU support (requires nvidia-docker)
openhands serve --gpu
# Or with current directory mounted
openhands serve --mount-cwd
```
Or using `uvx --python 3.12 --from openhands-ai openhands serve` if you have [uv](https://docs.astral.sh/uv/) installed.
This will automatically handle Docker requirements checking, image pulling, and launching the GUI server. The `--gpu` flag enables GPU support via nvidia-docker, and `--mount-cwd` mounts your current directory into the container.
#### Option 2: Using Docker Directly
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.51-nikolaik
+2 -2
View File
@@ -18,8 +18,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -172,7 +172,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--answerer_model', '-a', default='gpt-3.5-turbo', help='answerer model'
)
+2 -2
View File
@@ -26,8 +26,8 @@ from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -525,7 +525,7 @@ def commit0_setup(dataset: pd.DataFrame, repo_split: str) -> pd.DataFrame:
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,
+3 -5
View File
@@ -10,7 +10,6 @@ import huggingface_hub
import pandas as pd
from datasets import load_dataset
from PIL import Image
from pydantic import SecretStr
from evaluation.benchmarks.gaia.scorer import question_scorer
from evaluation.benchmarks.gaia.utils import (
@@ -31,8 +30,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
load_from_toml,
)
from openhands.core.config.utils import get_agent_config_arg
@@ -80,8 +79,7 @@ def get_config(
config_copy = copy.deepcopy(config)
load_from_toml(config_copy)
if config_copy.search_api_key:
config.search_api_key = SecretStr(config_copy.search_api_key)
config.search_api_key = config_copy.search_api_key
return config
@@ -294,7 +292,7 @@ Here is the task:
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--level',
type=str,
+2 -2
View File
@@ -20,8 +20,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -134,7 +134,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--hubs',
type=str,
+2 -2
View File
@@ -38,8 +38,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -312,7 +312,7 @@ Ok now its time to start solving the question. Good luck!
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
# data split must be one of 'gpqa_main', 'gqpa_diamond', 'gpqa_experts', 'gpqa_extended'
parser.add_argument(
'--data-split',
@@ -21,7 +21,7 @@ from evaluation.utils.shared import (
from openhands.core.config import (
LLMConfig,
OpenHandsConfig,
get_parser,
get_evaluation_parser,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
@@ -167,7 +167,7 @@ def process_predictions(predictions_path: str):
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'-s',
'--eval-split',
@@ -30,8 +30,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
@@ -358,7 +358,7 @@ Be thorough in your exploration, testing, and reasoning. It's fine if your think
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'-s',
'--eval-split',
@@ -18,8 +18,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -267,7 +267,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,
+2 -2
View File
@@ -23,8 +23,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -229,7 +229,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
SUBSETS = [
# Eurus subset: https://arxiv.org/abs/2404.02078
@@ -4,7 +4,11 @@ import pprint
import tqdm
from openhands.core.config import get_llm_config_arg, get_parser, load_openhands_config
from openhands.core.config import (
get_evaluation_parser,
get_llm_config_arg,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
from openhands.llm.llm import LLM
@@ -111,7 +115,7 @@ def classify_error(llm: LLM, failed_case: dict) -> str:
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--json_file_path',
type=str,
+2 -2
View File
@@ -34,8 +34,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
load_openhands_config,
)
from openhands.core.logger import openhands_logger as logger
@@ -273,7 +273,7 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'-s',
'--eval-split',
@@ -30,7 +30,7 @@ from evaluation.utils.shared import (
from openhands.core.config import (
LLMConfig,
OpenHandsConfig,
get_parser,
get_evaluation_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime
@@ -323,7 +323,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--input-file',
type=str,
@@ -32,8 +32,8 @@ from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -772,7 +772,7 @@ def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
if __name__ == '__main__':
# pdb.set_trace()
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,
@@ -21,8 +21,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -239,7 +239,7 @@ If the program uses some packages that are incompatible, please figure out alter
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--use-knowledge',
type=str,
+2 -17
View File
@@ -2,6 +2,8 @@
This folder contains the evaluation harness that we built on top of the original [SWE-Bench benchmark](https://www.swebench.com/) ([paper](https://arxiv.org/abs/2310.06770)).
**UPDATE (8/12/2025): We now support running SWE-rebench evaluation (see the paper [here](https://arxiv.org/abs/2505.20411))! For how to run it, checkout [this README](./SWE-rebench.md).**
**UPDATE (6/15/2025): We now support running SWE-bench-Live evaluation (see the paper [here](https://arxiv.org/abs/2505.23419))! For how to run it, checkout [this README](./SWE-bench-Live.md).**
**UPDATE (5/26/2025): We now support running interactive SWE-Bench evaluation (see the paper [here](https://arxiv.org/abs/2502.13069))! For how to run it, checkout [this README](./SWE-Interact.md).**
@@ -183,24 +185,7 @@ The final results will be saved to `evaluation/evaluation_outputs/outputs/swe_be
- `report.json`: a JSON file that contains keys like `"resolved_ids"` pointing to instance IDs that are resolved by the agent.
- `logs/`: a directory of test logs
### Run evaluation with `RemoteRuntime`
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details), it allows you to run rollout in parallel in the cloud, so you don't need a powerful machine to run evaluation.
Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply if you want to try this out!
```bash
./evaluation/benchmarks/swe_bench/scripts/eval_infer_remote.sh [output.jsonl filepath] [num_workers]
# Example - This evaluates patches generated by CodeActAgent on Llama-3.1-70B-Instruct-Turbo on "princeton-nlp/SWE-bench_Lite"'s test set, with 16 number of workers running in parallel
ALLHANDS_API_KEY="YOUR-API-KEY" RUNTIME=remote SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.eval.all-hands.dev" EVAL_DOCKER_IMAGE_PREFIX="us-central1-docker.pkg.dev/evaluation-092424/swe-bench-images" \
evaluation/benchmarks/swe_bench/scripts/eval_infer_remote.sh evaluation/evaluation_outputs/outputs/swe-bench-lite/CodeActAgent/Llama-3.1-70B-Instruct-Turbo_maxiter_100_N_v1.9-no-hint/output.jsonl 16 "princeton-nlp/SWE-bench_Lite" "test"
```
To clean-up all existing runtimes that you've already started, run:
```bash
ALLHANDS_API_KEY="YOUR-API-KEY" ./evaluation/utils/scripts/cleanup_remote_runtime.sh
```
## SWT-Bench Evaluation
@@ -0,0 +1,84 @@
# SWE-rebench
<p align="center">
<a href="https://arxiv.org/abs/2505.20411">📃 Paper</a>
<a href="https://huggingface.co/datasets/nebius/SWE-rebench">🤗 HuggingFace</a>
<a href="https://swe-rebench.com/leaderboard">📊 Leaderboard</a>
</p>
SWE-rebench is a large-scale dataset for verifiable software engineering tasks.
It comes in **two datasets**:
* **[`nebius/SWE-rebench-leaderboard`](https://huggingface.co/datasets/nebius/SWE-rebench-leaderboard)** updatable benchmark used for [leaderboard evaluation](https://swe-rebench.com/leaderboard).
* **[`nebius/SWE-rebench`](https://huggingface.co/datasets/nebius/SWE-rebench)** full dataset with **21,302 tasks**, suitable for training or large-scale offline evaluation.
This document explains how to run OpenHands on SWE-rebench, using the leaderboard split as the main example.
To run on the full dataset, simply replace the dataset name.
## Setting Up
Set up your development environment and configure your LLM provider by following the [SWE-bench README](README.md) in this directory.
## Running Inference
Use the existing SWE-bench inference script, changing the dataset to `nebius/SWE-rebench-leaderboard` and selecting the split (`test` for leaderboard submission):
```bash
./evaluation/benchmarks/swe_bench/scripts/run_infer.sh \
llm.your_llm HEAD CodeActAgent 30 50 1 nebius/SWE-rebench-leaderboard test
```
Arguments:
* `llm.your_llm` your model configuration key
* `HEAD` commit reference for reproducibility
* `CodeActAgent` agent type
* `10` number of examples to evaluate
* `50` maximum iterations per task (increase if needed)
* `1` number of workers
* `nebius/SWE-rebench-leaderboard` Hugging Face dataset name
* `test` dataset split
**Tip:** To run on the **full 21k dataset**, replace `nebius/SWE-rebench-leaderboard` with `nebius/SWE-rebench`.
## Evaluating Results
After inference completes, evaluate using the [SWE-bench-fork evaluation harness](https://github.com/SWE-rebench/SWE-bench-fork).
1. Convert the OpenHands output to SWE-bench evaluation format:
```bash
python evaluation/benchmarks/swe_bench/scripts/live/convert.py \
--output_jsonl path/to/evaluation/output.jsonl > preds.jsonl
```
2. Clone the SWE-bench-fork repo (https://github.com/SWE-rebench/SWE-bench-fork) and follow its README to install dependencies.
3. Run the evaluation using the fork:
```bash
python -m swebench.harness.run_evaluation \
--dataset_name nebius/SWE-rebench-leaderboard \
--split test \
--predictions_path preds.jsonl \
--max_workers 10 \
--run_id openhands
```
## Citation
```bibtex
@article{badertdinov2025swerebench,
title={SWE-rebench: An Automated Pipeline for Task Collection and Decontaminated Evaluation of Software Engineering Agents},
author={Badertdinov, Ibragim and Golubev, Alexander and Nekrashevich, Maksim and Shevtsov, Anton and Karasik, Simon and Andriushchenko, Andrei and Trofimova, Maria and Litvintseva, Daria and Yangel, Boris},
journal={arXiv preprint arXiv:2505.20411},
year={2025}
}
```
@@ -26,7 +26,7 @@ from evaluation.utils.shared import (
from openhands.core.config import (
LLMConfig,
OpenHandsConfig,
get_parser,
get_evaluation_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime
@@ -353,7 +353,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--input-file',
type=str,
@@ -1,65 +0,0 @@
<uploaded_files>
/workspace/{{ workspace_dir_name }}
</uploaded_files>
I've uploaded a python code repository in the directory {{ workspace_dir_name }}. Consider the following issue description:
<issue_description>
{{ instance.problem_statement }}
</issue_description>
Can you help me implement the necessary changes to the repository so that the requirements specified in the <issue_description> are met?
I've already taken care of all changes to any of the test files described in the <issue_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!
Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.
Your task is to make the minimal changes to non-test files in the /workspace/{{ workspace_dir_name }} directory to ensure the <issue_description> is satisfied.
Follow these phases to resolve the issue:
Phase 1. READING: read the problem and reword it in clearer terms
1.1 If there are code or config snippets. Express in words any best practices or conventions in them.
1.2 Hightlight message errors, method names, variables, file names, stack traces, and technical details.
1.3 Explain the problem in clear terms.
1.4 Enumerate the steps to reproduce the problem.
1.5 Hightlight any best practices to take into account when testing and fixing the issue
Phase 2. RUNNING: install and run the tests on the repository
2.1 Follow the readme
2.2 Install the environment and anything needed
2.2 Iterate and figure out how to run the tests
Phase 3. EXPLORATION: find the files that are related to the problem and possible solutions
3.1 Use `grep` to search for relevant methods, classes, keywords and error messages.
3.2 Identify all files related to the problem statement.
3.3 Propose the methods and files to fix the issue and explain why.
3.4 From the possible file locations, select the most likely location to fix the issue.
Phase 4. TEST CREATION: before implementing any fix, create a script to reproduce and verify the issue.
4.1 Look at existing test files in the repository to understand the test format/structure.
4.2 Create a minimal reproduction script that reproduces the located issue.
4.3 Run the reproduction script to confirm you are reproducing the issue.
4.4 Adjust the reproduction script as necessary.
Phase 5. FIX ANALYSIS: state clearly the problem and how to fix it
5.1 State clearly what the problem is.
5.2 State clearly where the problem is located.
5.3 State clearly how the test reproduces the issue.
5.4 State clearly the best practices to take into account in the fix.
5.5 State clearly how to fix the problem.
Phase 6. FIX IMPLEMENTATION: Edit the source code to implement your chosen solution.
6.1 Make minimal, focused changes to fix the issue.
Phase 7. VERIFICATION: Test your implementation thoroughly.
7.1 Run your reproduction script to verify the fix works.
7.2 Add edge cases to your test script to ensure comprehensive coverage.
7.3 Run existing tests related to the modified code to ensure you haven't broken anything.
8. FINAL REVIEW: Carefully re-read the problem description and compare your changes with the base commit {{ instance.base_commit }}.
8.1 Ensure you've fully addressed all requirements.
8.2 Run any tests in the repository related to:
8.2.1 The issue you are fixing
8.2.2 The files you modified
8.2.3 The functions you changed
8.3 If any tests fail, revise your implementation until all tests pass
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
@@ -1,45 +0,0 @@
# Task: Fix Issue in Python Repository
## Repository Context
You are provided with a Python code repository that contains an issue requiring your attention. The repository is located in a sandboxed environment, and you have access to the codebase to implement the necessary changes.
The code repository is located at: `/workspace/{{ workspace_dir_name }}`
(This path is provided for context; use file system tools to confirm paths before access).
## Goal
Your goal is to fix the issue described in the **Issue Description** section below. Implement the necessary changes to **non-test files only** within the repository, ensuring that **all relevant tests pass** after your changes.
## Key Requirements & Constraints
1. **Understand the problem** very well: it is a bug report, and you know humans don't always write good descriptions. Explore the codebase to understand the related code and the problem in depth. It is possible that the solution needs to be a bit more extensive than just the stated text. Don't exagerate though: don't do unrelated refactoring, but also don't interpret the description too strictly.
2. **Focus on the issues:** Implement the fix focusing on non-test files related to the issue.
2. **Environment Ready:** The Python environment is pre-configured with all dependencies. Do not install packages.
3. **Mandatory Testing Procedure:**
* **Create Test to Reproduce the Issue:** *Before* implementing any fix, you MUST create a *new test* (separate from existing tests) that specifically reproduces the issue.
* Take existing tests as example to understand the testing format/structure.
* Enhance this test with edge cases.
* Run this test to confirm reproduction.
* **Verify Fix:** After implementing the fix, run your test again to verify the issue is resolved.
* **Identify ALL Relevant Tests:** You MUST perform a **dedicated search and analysis** to identify **all** existing unit tests potentially affected by your changes. This includes:
* Tests in the same module/directory as the changed files (e.g., `tests/` subdirectories).
* Tests explicitly importing or using the modified code/classes/functions.
* Tests mentioned in the issue description or related documentation.
* Tests covering functionalities that *depend on* the modified code (analyze callers/dependencies if necessary).
**If you cannot confidently identify a specific subset, you MUST identify and plan to run the entire test suite for the modified application or module(s). State your identified test scope clearly.**
* **Run Identified Relevant Tests:** You MUST execute the **complete set** of relevant existing unit tests you identified in the previous step. Ensure you are running the *correct and comprehensive set* of tests. You MUST NOT modify these existing tests.
* **Final Check & Verification:** Before finishing, ensure **all** identified relevant existing tests pass. **Explicitly confirm that you have considered potential omissions in your test selection and believe the executed tests comprehensively cover the impact of your changes.** Failing to identify and run the *complete* relevant set constitutes a failure. If any identified tests fail, revise your fix. Passing all relevant tests is the primary measure of success.
4. **Defensive Programming:** Actively practice defensive programming: anticipate and handle potential edge cases, unexpected inputs, and different ways the affected code might be called **to ensure the fix works reliably and allows relevant tests to pass.** Analyze the potential impact on other parts of the codebase.
5. **Final Review:** Compare your solution against the original issue and the base commit ({{ instance.base_commit }}) to ensure completeness and test passage.
## General Workflow Guidance
* Prioritize understanding the problem, exploring the code, planning your fix, implementing it carefully using the required diff format, and **thoroughly testing** according to the **Mandatory Testing Procedure**.
* Consider trade-offs between different solutions. The goal is a **robust change that makes the relevant tests pass.** Quality, correctness, and reliability are key.
* Actively practice defensive programming: anticipate and handle potential edge cases, unexpected inputs, and different ways the affected code might be called **to ensure the fix works reliably and allows relevant tests to pass.** Analyze the potential impact on other parts of the codebase.
* IMPORTANT: Your solution will be tested by additional hidden tests, so do not assume the task is complete just because visible tests pass! Refine the solution until you are confident that it is robust and comprehensive according to the **Defensive Programming** requirement.
## Final Note
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
## Issue Description
{{ instance.problem_statement }}
+9 -5
View File
@@ -43,8 +43,8 @@ from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import get_condenser_config_arg
@@ -80,6 +80,8 @@ def set_dataset_type(dataset_name: str) -> str:
DATASET_TYPE = 'SWE-Gym'
elif 'swe-bench-live' in name_lower:
DATASET_TYPE = 'SWE-bench-Live'
elif 'swe-rebench' in name_lower:
DATASET_TYPE = 'SWE-rebench'
elif 'multimodal' in name_lower:
DATASET_TYPE = 'Multimodal'
else:
@@ -109,9 +111,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata) -> MessageActio
if mode.startswith('swt'):
template_name = 'swt.j2'
elif mode == 'swe':
if 'claude' in llm_model:
template_name = 'swe_default.j2'
elif 'gpt-4.1' in llm_model:
if 'gpt-4.1' in llm_model:
template_name = 'swe_gpt4.j2'
else:
template_name = (
@@ -180,6 +180,8 @@ def get_instance_docker_image(
docker_image_prefix = 'docker.io/starryzhang/'
elif DATASET_TYPE == 'SWE-bench':
docker_image_prefix = 'docker.io/swebench/'
elif DATASET_TYPE == 'SWE-rebench':
docker_image_prefix = 'docker.io/swerebench/'
repo, name = instance_id.split('__')
image_name = f'{docker_image_prefix.rstrip("/")}/sweb.eval.x86_64.{repo}_1776_{name}:latest'.lower()
logger.debug(f'Using official SWE-Bench image: {image_name}')
@@ -320,6 +322,8 @@ def initialize_runtime(
# inject the instance swe entry
if DATASET_TYPE == 'SWE-bench-Live':
entry_script_path = 'instance_swe_entry_live.sh'
elif DATASET_TYPE == 'SWE-rebench':
entry_script_path = 'instance_swe_entry_rebench.sh'
else:
entry_script_path = 'instance_swe_entry.sh'
runtime.copy_to(
@@ -732,7 +736,7 @@ def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,
@@ -28,8 +28,8 @@ from evaluation.utils.shared import (
)
from openhands.controller.state.state import State
from openhands.core.config import (
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import get_condenser_config_arg
@@ -201,7 +201,7 @@ def process_instance(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,
@@ -31,8 +31,8 @@ from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -644,7 +644,7 @@ SWEGYM_EXCLUDE_IDS = [
]
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,
@@ -1,46 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
INPUT_FILE=$1
NUM_WORKERS=$2
DATASET=$3
SPLIT=$4
if [ -z "$INPUT_FILE" ]; then
echo "INPUT_FILE not specified (should be a path to a jsonl file)"
exit 1
fi
if [ -z "$DATASET" ]; then
echo "DATASET not specified, use default princeton-nlp/SWE-bench_Lite"
DATASET="princeton-nlp/SWE-bench_Lite"
fi
if [ -z "$SPLIT" ]; then
echo "SPLIT not specified, use default test"
SPLIT="test"
fi
if [ -z "$NUM_WORKERS" ]; then
echo "NUM_WORKERS not specified, use default 1"
NUM_WORKERS=1
fi
echo "... Evaluating on $INPUT_FILE ..."
COMMAND="poetry run python evaluation/benchmarks/swe_bench/eval_infer.py \
--eval-num-workers $NUM_WORKERS \
--input-file $INPUT_FILE \
--dataset $DATASET \
--split $SPLIT"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
fi
# Run the command
eval $COMMAND
# update the output with evaluation results
poetry run python evaluation/benchmarks/swe_bench/scripts/eval/update_output_with_eval.py $INPUT_FILE
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
source ~/.bashrc
SWEUTIL_DIR=/swe_util
# FIXME: Cannot read SWE_INSTANCE_ID from the environment variable
# SWE_INSTANCE_ID=django__django-11099
if [ -z "$SWE_INSTANCE_ID" ]; then
echo "Error: SWE_INSTANCE_ID is not set." >&2
exit 1
fi
# Read the swe-bench-test-lite.json file and extract the required item based on instance_id
item=$(jq --arg INSTANCE_ID "$SWE_INSTANCE_ID" '.[] | select(.instance_id == $INSTANCE_ID)' $SWEUTIL_DIR/eval_data/instances/swe-bench-instance.json)
if [[ -z "$item" ]]; then
echo "No item found for the provided instance ID."
exit 1
fi
WORKSPACE_NAME=$(echo "$item" | jq -r '(.repo | tostring) + "__" + (.version | tostring) | gsub("/"; "__")')
echo "WORKSPACE_NAME: $WORKSPACE_NAME"
# Clear the workspace
if [ -d /workspace ]; then
rm -rf /workspace/*
else
mkdir /workspace
fi
# Copy repo to workspace
if [ -d /workspace/$WORKSPACE_NAME ]; then
rm -rf /workspace/$WORKSPACE_NAME
fi
mkdir -p /workspace
cp -r /testbed /workspace/$WORKSPACE_NAME
# Activate instance-specific environment
if [ -d /opt/miniconda3 ]; then
. /opt/miniconda3/etc/profile.d/conda.sh
conda activate testbed
fi
export PATH=/opt/conda/envs/testbed/bin:$PATH
+1 -2
View File
@@ -5,8 +5,7 @@ pynguin_ids = ['pydata__xarray-6548-16541', 'pydata__xarray-7003-16557', 'pydata
ids = ['pydata__xarray-3114-16452', 'pydata__xarray-3151-16453', 'pydata__xarray-3156-16454', 'pydata__xarray-3239-16456', 'pydata__xarray-3239-16457', 'pydata__xarray-3239-16458', 'pydata__xarray-3302-16459', 'pydata__xarray-3364-16461', 'pydata__xarray-3677-16471', 'pydata__xarray-3905-16478', 'pydata__xarray-4182-16484', 'pydata__xarray-4248-16486', 'pydata__xarray-4339-16487', 'pydata__xarray-4419-16488', 'pydata__xarray-4629-16492', 'pydata__xarray-4750-16496', 'pydata__xarray-4802-16505', 'pydata__xarray-4966-16515', 'pydata__xarray-4994-16516', 'pydata__xarray-5033-16517', 'pydata__xarray-5126-16518', 'pydata__xarray-5126-16519', 'pydata__xarray-5131-16520', 'pydata__xarray-5365-16529', 'pydata__xarray-5455-16530', 'pydata__xarray-5662-16532', 'pydata__xarray-5731-16534', 'pydata__xarray-6135-16535', 'pydata__xarray-6135-16536', 'pydata__xarray-6386-16537', 'pydata__xarray-6394-16538', 'pydata__xarray-6400-16539', 'pydata__xarray-6461-16540', 'pydata__xarray-6548-16541', 'pydata__xarray-6599-16543', 'pydata__xarray-6601-16544', 'pydata__xarray-6882-16548', 'pydata__xarray-6889-16549', 'pydata__xarray-7003-16557', 'pydata__xarray-7147-16571', 'pydata__xarray-7150-16572', 'pydata__xarray-7203-16577', 'pydata__xarray-7229-16578', 'pydata__xarray-7393-16581', 'pydata__xarray-7400-16582']
Command eval (our approach):
poetry run ./evaluation/benchmarks/testgeneval/scripts/eval_infer_remote.sh evaluation/evaluation_outputs/outputs/kjain14__testgeneval-test/CodeActAgent/gpt-4o_maxiter_25_N_v0.20.0-no-hint-run_1/output.jsonl 10 kjain14/testgeneval test true
Command run (our approach):
./evaluation/benchmarks/testgeneval/scripts/run_infer.sh llm.eval_gpt HEAD CodeActAgent -1 25 10 kjain14/testgeneval test 1 ../TestGenEval/results/testgeneval/preds/gpt-4o-2024-08-06__testgeneval__0.2__test.jsonl
@@ -41,7 +41,7 @@ from evaluation.utils.shared import (
reset_logger_for_multiprocessing,
run_evaluation,
)
from openhands.core.config import OpenHandsConfig, SandboxConfig, get_parser
from openhands.core.config import OpenHandsConfig, SandboxConfig, get_evaluation_parser
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime
from openhands.events.action import CmdRunAction
@@ -484,7 +484,7 @@ def count_and_log_fields(evaluated_predictions, fields, key):
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--input-file', type=str, required=True, help='Path to input predictions file'
)
@@ -37,8 +37,8 @@ from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
SandboxConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -491,7 +491,7 @@ def prepare_dataset_pre(dataset: pd.DataFrame, filter_column: str) -> pd.DataFra
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,
@@ -18,8 +18,8 @@ from openhands.core.config import (
LLMConfig,
OpenHandsConfig,
get_agent_config_arg,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.config.agent_config import AgentConfig
from openhands.core.logger import openhands_logger as logger
@@ -197,7 +197,7 @@ def run_evaluator(
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--task-image-name',
type=str,
+2 -2
View File
@@ -19,8 +19,8 @@ from evaluation.utils.shared import (
from openhands.controller.state.state import State
from openhands.core.config import (
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -157,7 +157,7 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,
@@ -31,8 +31,8 @@ from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -565,7 +565,7 @@ SWEGYM_EXCLUDE_IDS = [
]
if __name__ == '__main__':
parser = get_parser()
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,
+4 -3
View File
@@ -263,19 +263,20 @@ def prepare_dataset(
f'Randomly sampling {eval_n_limit} unique instances with random seed 42.'
)
def make_serializable(instance: pd.Series) -> dict:
def make_serializable(instance_dict: dict) -> dict:
import numpy as np
instance_dict = instance.to_dict()
for k, v in instance_dict.items():
if isinstance(v, np.ndarray):
instance_dict[k] = v.tolist()
elif isinstance(v, pd.Timestamp):
instance_dict[k] = str(v)
elif isinstance(v, dict):
instance_dict[k] = make_serializable(v)
return instance_dict
new_dataset = [
make_serializable(instance)
make_serializable(instance.to_dict())
for _, instance in dataset.iterrows()
if str(instance[id_column]) not in finished_ids
]
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { FileService } from "#/api/file-service/file-service.api";
import OpenHands from "#/api/open-hands";
import {
FILE_VARIANTS_1,
FILE_VARIANTS_2,
@@ -10,20 +10,20 @@ import {
* You can find the mock handlers in `frontend/src/mocks/file-service-handlers.ts`.
*/
describe("FileService", () => {
describe("OpenHands File API", () => {
it("should get a list of files", async () => {
await expect(FileService.getFiles("test-conversation-id")).resolves.toEqual(
await expect(OpenHands.getFiles("test-conversation-id")).resolves.toEqual(
FILE_VARIANTS_1,
);
await expect(
FileService.getFiles("test-conversation-id-2"),
OpenHands.getFiles("test-conversation-id-2"),
).resolves.toEqual(FILE_VARIANTS_2);
});
it("should get content of a file", async () => {
await expect(
FileService.getFile("test-conversation-id", "file1.txt"),
OpenHands.getFile("test-conversation-id", "file1.txt"),
).resolves.toEqual("Content of file1.txt");
});
});
@@ -3,8 +3,6 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { LaunchMicroagentModal } from "#/components/features/chat/microagent/launch-microagent-modal";
import { MemoryService } from "#/api/memory-service/memory-service.api";
import { FileService } from "#/api/file-service/file-service.api";
import { I18nKey } from "#/i18n/declaration";
vi.mock("react-router", async () => ({
@@ -85,9 +85,10 @@ describe("ConversationPanel", () => {
vi.clearAllMocks();
vi.restoreAllMocks();
// Setup default mock for getUserConversations
vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue([
...mockConversations,
]);
vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue({
results: [...mockConversations],
next_page_id: null,
});
});
it("should render the conversations", async () => {
@@ -101,7 +102,10 @@ describe("ConversationPanel", () => {
it("should display an empty state when there are no conversations", async () => {
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockResolvedValue([]);
getUserConversationsSpy.mockResolvedValue({
results: [],
next_page_id: null,
});
renderConversationPanel();
@@ -195,7 +199,10 @@ describe("ConversationPanel", () => {
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockImplementation(async () => mockData);
getUserConversationsSpy.mockImplementation(async () => ({
results: mockData,
next_page_id: null,
}));
const deleteUserConversationSpy = vi.spyOn(
OpenHands,
@@ -249,7 +256,10 @@ describe("ConversationPanel", () => {
it("should refetch data on rerenders", async () => {
const user = userEvent.setup();
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockResolvedValue([...mockConversations]);
getUserConversationsSpy.mockResolvedValue({
results: [...mockConversations],
next_page_id: null,
});
function PanelWithToggle() {
const [isOpen, setIsOpen] = React.useState(true);
@@ -343,7 +353,10 @@ describe("ConversationPanel", () => {
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockResolvedValue(mockRunningConversations);
getUserConversationsSpy.mockResolvedValue({
results: mockRunningConversations,
next_page_id: null,
});
renderConversationPanel();
@@ -407,7 +420,10 @@ describe("ConversationPanel", () => {
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockImplementation(async () => mockData);
getUserConversationsSpy.mockImplementation(async () => ({
results: mockData,
next_page_id: null,
}));
const stopConversationSpy = vi.spyOn(OpenHands, "stopConversation");
stopConversationSpy.mockImplementation(async (id: string) => {
@@ -492,7 +508,10 @@ describe("ConversationPanel", () => {
];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockResolvedValue(mockMixedStatusConversations);
getUserConversationsSpy.mockResolvedValue({
results: mockMixedStatusConversations,
next_page_id: null,
});
renderConversationPanel();
@@ -82,5 +82,11 @@ describe("extractModelAndProvider", () => {
model: "claude-opus-4-20250514",
separator: "/",
});
expect(extractModelAndProvider("claude-opus-4-1-20250805")).toEqual({
provider: "anthropic",
model: "claude-opus-4-1-20250805",
separator: "/",
});
});
});
-44
View File
@@ -1,44 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="OpenHands: Code Less, Make More"
/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>OpenHands</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+247 -135
View File
@@ -12,16 +12,16 @@
"@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.7.1",
"@react-router/serve": "^7.7.1",
"@react-router/node": "^7.8.0",
"@react-router/serve": "^7.8.0",
"@react-types/shared": "^3.31.0",
"@reduxjs/toolkit": "^2.8.2",
"@stripe/react-stripe-js": "^3.9.0",
"@stripe/stripe-js": "^7.8.0",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.84.1",
"@vitejs/plugin-react": "^4.7.0",
"@tanstack/react-query": "^5.84.2",
"@vitejs/plugin-react": "^5.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.11.0",
@@ -33,9 +33,9 @@
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.29",
"jose": "^6.0.12",
"lucide-react": "^0.536.0",
"lucide-react": "^0.539.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.258.5",
"posthog-js": "^1.259.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-highlight": "^0.15.0",
@@ -44,7 +44,7 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.7.1",
"react-router": "^7.8.0",
"react-select": "^5.10.2",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
@@ -53,7 +53,7 @@
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"vite": "^7.0.6",
"vite": "^7.1.1",
"web-vitals": "^5.1.0",
"ws": "^8.18.2"
},
@@ -63,7 +63,7 @@
"@babel/types": "^7.28.2",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.54.2",
"@react-router/dev": "^7.7.1",
"@react-router/dev": "^7.8.0",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.83.1",
"@testing-library/dom": "^10.4.1",
@@ -4388,11 +4388,10 @@
}
},
"node_modules/@react-router/dev": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.7.1.tgz",
"integrity": "sha512-ByfgHmAyfx/JQYN/QwUx1sFJlBA5Z3HQAZ638wHSb+m6khWtHqSaKCvPqQh1P00wdEAeV3tX5L1aUM/ceCF6+w==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.8.0.tgz",
"integrity": "sha512-5NA9yLZComM+kCD3zNPL3rjrAFjzzODY8hjAJlpz/6jpyXoF28W8QTSo8rxc56XVNLONM75Y5nq1wzeEcWFFKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.7",
"@babel/generator": "^7.27.5",
@@ -4402,7 +4401,9 @@
"@babel/traverse": "^7.27.7",
"@babel/types": "^7.27.7",
"@npmcli/package-json": "^4.0.1",
"@react-router/node": "7.7.1",
"@react-router/node": "7.8.0",
"@vitejs/plugin-react": "^4.5.2",
"@vitejs/plugin-rsc": "0.4.11",
"arg": "^5.0.1",
"babel-dead-code-elimination": "^1.0.6",
"chokidar": "^4.0.0",
@@ -4429,8 +4430,8 @@
"node": ">=20.0.0"
},
"peerDependencies": {
"@react-router/serve": "^7.7.1",
"react-router": "^7.7.1",
"@react-router/serve": "^7.8.0",
"react-router": "^7.8.0",
"typescript": "^5.1.0",
"vite": "^5.1.0 || ^6.0.0 || ^7.0.0",
"wrangler": "^3.28.2 || ^4.0.0"
@@ -4447,6 +4448,41 @@
}
}
},
"node_modules/@react-router/dev/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
"dev": true
},
"node_modules/@react-router/dev/node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
"dev": true,
"dependencies": {
"@babel/core": "^7.28.0",
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
"@rolldown/pluginutils": "1.0.0-beta.27",
"@types/babel__core": "^7.20.5",
"react-refresh": "^0.17.0"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/@react-router/dev/node_modules/@vitejs/plugin-react/node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@react-router/dev/node_modules/jsesc": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
@@ -4460,33 +4496,10 @@
"node": ">=6"
}
},
"node_modules/@react-router/express": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.7.1.tgz",
"integrity": "sha512-OEZwIM7i/KPSDjwVRg3LqeNIwG41U+SeFOwMjhZRFfyrnwghHfvWsDajf73r4ccMh+RRHcP1GIN6VSU3XZk7MA==",
"license": "MIT",
"dependencies": {
"@react-router/node": "7.7.1"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"express": "^4.17.1 || ^5",
"react-router": "7.7.1",
"typescript": "^5.1.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@react-router/node": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.7.1.tgz",
"integrity": "sha512-EHd6PEcw2nmcJmcYTPA0MmRWSqOaJ/meycfCp0ADA9T/6b7+fUHfr9XcNyf7UeZtYwu4zGyuYfPmLU5ic6Ugyg==",
"license": "MIT",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.8.0.tgz",
"integrity": "sha512-/FFN9vqI2EHPwDCHTvsMInhrYvwJ5SlCeyUr1oWUxH47JyYkooVFks5++M4VkrTgj2ZBsMjPPKy0xRNTQdtBDA==",
"dependencies": {
"@mjackson/node-fetch-server": "^0.2.0"
},
@@ -4494,7 +4507,7 @@
"node": ">=20.0.0"
},
"peerDependencies": {
"react-router": "7.7.1",
"react-router": "7.8.0",
"typescript": "^5.1.0"
},
"peerDependenciesMeta": {
@@ -4504,13 +4517,12 @@
}
},
"node_modules/@react-router/serve": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@react-router/serve/-/serve-7.7.1.tgz",
"integrity": "sha512-LyAiX+oI+6O6j2xWPUoKW+cgayUf3USBosSMv73Jtwi99XUhSDu2MUhM+BB+AbrYRubauZ83QpZTROiXoaf8jA==",
"license": "MIT",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@react-router/serve/-/serve-7.8.0.tgz",
"integrity": "sha512-DokCv1GfOMt9KHu+k3WYY9sP5nOEzq7za+Vi3dWPHoY5oP0wgv8S4DnTPU08ASY8iFaF38NAzapbSFfu6Xfr0Q==",
"dependencies": {
"@react-router/express": "7.7.1",
"@react-router/node": "7.7.1",
"@react-router/express": "7.8.0",
"@react-router/node": "7.8.0",
"compression": "^1.7.4",
"express": "^4.19.2",
"get-port": "5.1.1",
@@ -4524,7 +4536,28 @@
"node": ">=20.0.0"
},
"peerDependencies": {
"react-router": "7.7.1"
"react-router": "7.8.0"
}
},
"node_modules/@react-router/serve/node_modules/@react-router/express": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.8.0.tgz",
"integrity": "sha512-lNUwux5IfMqczIL3gXZ/mauPUoVz65fSLPnUTkP7hkh/P7fcsPtYkmcixuaWb+882lY+Glf157OdoIMbcSMBaA==",
"dependencies": {
"@react-router/node": "7.8.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"express": "^4.17.1 || ^5",
"react-router": "7.8.0",
"typescript": "^5.1.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@react-stately/calendar": {
@@ -5226,10 +5259,9 @@
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
"license": "MIT"
"version": "1.0.0-beta.30",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.30.tgz",
"integrity": "sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw=="
},
"node_modules/@rollup/pluginutils": {
"version": "5.2.0",
@@ -6072,6 +6104,60 @@
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.4.3",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.0.2",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.4.3",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.0.2",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.11",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.4.3",
"@emnapi/runtime": "^1.4.3",
"@tybys/wasm-util": "^0.9.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.9.0",
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.0",
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz",
@@ -6175,10 +6261,9 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "5.84.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.1.tgz",
"integrity": "sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw==",
"license": "MIT",
"version": "5.84.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.84.2.tgz",
"integrity": "sha512-cZadySzROlD2+o8zIfbD978p0IphuQzRWiiH3I2ugnTmz4jbjc0+TdibpwqxlzynEen8OulgAg+rzdNF37s7XQ==",
"dependencies": {
"@tanstack/query-core": "5.83.1"
},
@@ -6951,20 +7036,19 @@
"license": "ISC"
},
"node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
"license": "MIT",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.0.tgz",
"integrity": "sha512-Jx9JfsTa05bYkS9xo0hkofp2dCmp1blrKjw9JONs5BTHOvJCgLbaPSuZLGSVJW6u2qe0tc4eevY0+gSNNi0YCw==",
"dependencies": {
"@babel/core": "^7.28.0",
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
"@rolldown/pluginutils": "1.0.0-beta.27",
"@rolldown/pluginutils": "1.0.0-beta.30",
"@types/babel__core": "^7.20.5",
"react-refresh": "^0.17.0"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
@@ -6979,6 +7063,32 @@
"node": ">=0.10.0"
}
},
"node_modules/@vitejs/plugin-rsc": {
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-rsc/-/plugin-rsc-0.4.11.tgz",
"integrity": "sha512-+4H4wLi+Y9yF58znBfKgGfX8zcqUGt8ngnmNgzrdGdF1SVz7EO0sg7WnhK5fFVHt6fUxsVEjmEabsCWHKPL1Tw==",
"dev": true,
"dependencies": {
"@mjackson/node-fetch-server": "^0.7.0",
"es-module-lexer": "^1.7.0",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17",
"periscopic": "^4.0.2",
"turbo-stream": "^3.1.0",
"vitefu": "^1.1.1"
},
"peerDependencies": {
"react": "*",
"react-dom": "*",
"vite": "*"
}
},
"node_modules/@vitejs/plugin-rsc/node_modules/@mjackson/node-fetch-server": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.7.0.tgz",
"integrity": "sha512-un8diyEBKU3BTVj3GzlTPA1kIjCkGdD+AMYQy31Gf9JCkfoZzwgJ79GUtHrF2BN3XPNMLpubbzPcxys+a3uZEw==",
"dev": true
},
"node_modules/@vitest/coverage-v8": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz",
@@ -7161,7 +7271,6 @@
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
@@ -7174,7 +7283,6 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -7315,8 +7423,7 @@
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/array-includes": {
"version": "3.1.9",
@@ -7681,7 +7788,6 @@
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
@@ -7705,7 +7811,6 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
@@ -7713,8 +7818,7 @@
"node_modules/body-parser/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/brace-expansion": {
"version": "2.0.2",
@@ -8340,7 +8444,6 @@
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
@@ -8352,7 +8455,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -8367,7 +8469,6 @@
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -8375,8 +8476,7 @@
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/core-js": {
"version": "3.45.0",
@@ -8731,7 +8831,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
@@ -8857,7 +8956,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -9210,8 +9308,7 @@
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
@@ -9884,7 +9981,6 @@
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -9923,7 +10019,6 @@
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -9969,7 +10064,6 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
@@ -9977,8 +10071,7 @@
"node_modules/express/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/extend": {
"version": "3.0.2",
@@ -10103,7 +10196,6 @@
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
@@ -10121,7 +10213,6 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
@@ -10129,8 +10220,7 @@
"node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/find-root": {
"version": "1.1.0",
@@ -10267,7 +10357,6 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -10317,7 +10406,6 @@
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -10930,7 +11018,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
@@ -11039,7 +11126,6 @@
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
@@ -11168,7 +11254,6 @@
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
@@ -11517,6 +11602,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/is-reference": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.6"
}
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -12625,10 +12719,9 @@
}
},
"node_modules/lucide-react": {
"version": "0.536.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.536.0.tgz",
"integrity": "sha512-2PgvNa9v+qz4Jt/ni8vPLt4jwoFybXHuubQT8fv4iCW5TjDxkbZjNZZHa485ad73NSEn/jdsEtU57eE1g+ma8A==",
"license": "ISC",
"version": "0.539.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.539.0.tgz",
"integrity": "sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
@@ -12999,7 +13092,6 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -13014,7 +13106,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
@@ -13033,7 +13124,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -13619,7 +13709,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
@@ -14224,7 +14313,6 @@
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
@@ -14427,7 +14515,6 @@
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -14495,8 +14582,7 @@
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
},
"node_modules/path-type": {
"version": "4.0.0",
@@ -14524,6 +14610,17 @@
"node": ">= 14.16"
}
},
"node_modules/periscopic": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-4.0.2.tgz",
"integrity": "sha512-sqpQDUy8vgB7ycLkendSKS6HnVz1Rneoc3Rc+ZBUCe2pbqlVuCC5vF52l0NJ1aiMg/r1qfYF9/myz8CZeI2rjA==",
"dev": true,
"dependencies": {
"@types/estree": "*",
"is-reference": "^3.0.2",
"zimmerframe": "^1.0.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -14648,10 +14745,9 @@
"license": "MIT"
},
"node_modules/posthog-js": {
"version": "1.258.6",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.258.6.tgz",
"integrity": "sha512-vL5AGG+rOoRg3LGquMfBPO55jD4bGl0CiV44SHdHAoBnOVDDAqxczRGDqMdxor+VLx3/ofTFOJ2FNprfAHp70Q==",
"license": "SEE LICENSE IN LICENSE",
"version": "1.259.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.259.0.tgz",
"integrity": "sha512-6usLnJshky8fQ82ask7PIJh4BSFOU0VkRbFg8Zanm/HIlYMG1VOdRWlToA63JXeO7Bzm9TuREq1wFm5U2VEVCg==",
"dependencies": {
"core-js": "^3.38.1",
"fflate": "^0.4.8",
@@ -14825,7 +14921,6 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
@@ -14910,7 +15005,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -14919,7 +15013,6 @@
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
@@ -15126,10 +15219,9 @@
}
},
"node_modules/react-router": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.1.tgz",
"integrity": "sha512-jVKHXoWRIsD/qS6lvGveckwb862EekvapdHJN/cGmzw40KnJH5gg53ujOJ4qX6EKIK9LSBfFed/xiQ5yeXNrUA==",
"license": "MIT",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz",
"integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
@@ -15898,7 +15990,6 @@
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
@@ -15922,7 +16013,6 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
@@ -15930,14 +16020,12 @@
"node_modules/send/node_modules/debug/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -15946,7 +16034,6 @@
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
@@ -16015,8 +16102,7 @@
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/shebang-command": {
"version": "2.0.0",
@@ -17054,7 +17140,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
@@ -17180,6 +17265,12 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/turbo-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-3.1.0.tgz",
"integrity": "sha512-tVI25WEXl4fckNEmrq70xU1XumxUwEx/FZD5AgEcV8ri7Wvrg2o7GEq8U7htrNx3CajciGm+kDyhRf5JB6t7/A==",
"dev": true
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -17210,7 +17301,6 @@
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
@@ -17438,7 +17528,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -17559,7 +17648,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
@@ -17638,16 +17726,15 @@
}
},
"node_modules/vite": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
"integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
"license": "MIT",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.1.tgz",
"integrity": "sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.6",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.40.0",
"rollup": "^4.43.0",
"tinyglobby": "^0.2.14"
},
"bin": {
@@ -17816,6 +17903,25 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/vitefu": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz",
"integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==",
"dev": true,
"workspaces": [
"tests/deps/*",
"tests/projects/*",
"tests/projects/workspace/packages/*"
],
"peerDependencies": {
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
}
},
"node_modules/vitest": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
@@ -18418,6 +18524,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zimmerframe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
"dev": true
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+9 -9
View File
@@ -11,16 +11,16 @@
"@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.7.1",
"@react-router/serve": "^7.7.1",
"@react-router/node": "^7.8.0",
"@react-router/serve": "^7.8.0",
"@react-types/shared": "^3.31.0",
"@reduxjs/toolkit": "^2.8.2",
"@stripe/react-stripe-js": "^3.9.0",
"@stripe/stripe-js": "^7.8.0",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.84.1",
"@vitejs/plugin-react": "^4.7.0",
"@tanstack/react-query": "^5.84.2",
"@vitejs/plugin-react": "^5.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.11.0",
@@ -32,9 +32,9 @@
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.29",
"jose": "^6.0.12",
"lucide-react": "^0.536.0",
"lucide-react": "^0.539.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.258.5",
"posthog-js": "^1.259.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-highlight": "^0.15.0",
@@ -43,7 +43,7 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.7.1",
"react-router": "^7.8.0",
"react-select": "^5.10.2",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
@@ -52,7 +52,7 @@
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"vite": "^7.0.6",
"vite": "^7.1.1",
"web-vitals": "^5.1.0",
"ws": "^8.18.2"
},
@@ -87,7 +87,7 @@
"@babel/types": "^7.28.2",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.54.2",
"@react-router/dev": "^7.7.1",
"@react-router/dev": "^7.8.0",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.83.1",
"@testing-library/dom": "^10.4.1",
@@ -1,66 +0,0 @@
import { openHands } from "../open-hands-axios";
import { GetFilesResponse, GetFileResponse } from "./file-service.types";
import { getConversationUrl } from "../conversation.utils";
import { FileUploadSuccessResponse } from "../open-hands.types";
export class FileService {
/**
* Retrieve the list of files available in the workspace
* @param conversationId ID of the conversation
* @param path Path to list files from. If provided, it lists all the files in the given path
* @returns List of files available in the given path. If path is not provided, it lists all the files in the workspace
*/
static async getFiles(
conversationId: string,
path?: string,
): Promise<GetFilesResponse> {
const url = `${getConversationUrl(conversationId)}/list-files`;
const { data } = await openHands.get<GetFilesResponse>(url, {
params: { path },
});
return data;
}
/**
* Retrieve the content of a file
* @param conversationId ID of the conversation
* @param path Full path of the file to retrieve
* @returns Code content of the file
*/
static async getFile(conversationId: string, path: string): Promise<string> {
const url = `${getConversationUrl(conversationId)}/select-file`;
const { data } = await openHands.get<GetFileResponse>(url, {
params: { file: path },
});
return data.code;
}
/**
* Upload multiple files to the workspace
* @param conversationId ID of the conversation
* @param files List of files.
* @returns list of uploaded files, list of skipped files
*/
static async uploadFiles(
conversationId: string,
files: File[],
): Promise<FileUploadSuccessResponse> {
const formData = new FormData();
for (const file of files) {
formData.append("files", file);
}
const url = `${getConversationUrl(conversationId)}/upload-files`;
const response = await openHands.post<FileUploadSuccessResponse>(
url,
formData,
{
headers: {
"Content-Type": "multipart/form-data",
},
},
);
return response.data;
}
}
@@ -1,5 +0,0 @@
export type GetFilesResponse = string[];
export interface GetFileResponse {
code: string;
}
@@ -1,21 +0,0 @@
import { openHands } from "../open-hands-axios";
interface GetPromptResponse {
status: string;
prompt: string;
}
export class MemoryService {
static async getPrompt(
conversationId: string,
eventId: number,
): Promise<string> {
const { data } = await openHands.get<GetPromptResponse>(
`/api/conversations/${conversationId}/remember_prompt`,
{
params: { event_id: eventId },
},
);
return data.prompt;
}
}
+85 -10
View File
@@ -15,6 +15,9 @@ import {
GetMicroagentPromptResponse,
CreateMicroagent,
MicroagentContentResponse,
FileUploadSuccessResponse,
GetFilesResponse,
GetFileResponse,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
import { ApiSettings, PostApiSettings, Provider } from "#/types/settings";
@@ -280,17 +283,27 @@ class OpenHands {
return data;
}
static async getUserConversations(): Promise<Conversation[]> {
static async getUserConversations(
limit: number = 20,
pageId?: string,
): Promise<ResultSet<Conversation>> {
const params = new URLSearchParams();
params.append("limit", limit.toString());
if (pageId) {
params.append("page_id", pageId);
}
const { data } = await openHands.get<ResultSet<Conversation>>(
"/api/conversations?limit=100",
`/api/conversations?${params.toString()}`,
);
return data.results;
return data;
}
static async searchConversations(
selectedRepository?: string,
conversationTrigger?: string,
limit: number = 20,
limit: number = 100,
): Promise<Conversation[]> {
const params = new URLSearchParams();
params.append("limit", limit.toString());
@@ -618,12 +631,11 @@ class OpenHands {
conversationId: string,
eventId: number,
): Promise<string> {
const { data } = await openHands.get<GetMicroagentPromptResponse>(
`/api/conversations/${conversationId}/remember_prompt`,
{
params: { event_id: eventId },
},
);
const url = `${this.getConversationUrl(conversationId)}/remember-prompt`;
const { data } = await openHands.get<GetMicroagentPromptResponse>(url, {
params: { event_id: eventId },
headers: this.getConversationHeaders(),
});
return data.prompt;
}
@@ -640,6 +652,69 @@ class OpenHands {
return data;
}
/**
* Retrieve the list of files available in the workspace
* @param conversationId ID of the conversation
* @param path Path to list files from. If provided, it lists all the files in the given path
* @returns List of files available in the given path. If path is not provided, it lists all the files in the workspace
*/
static async getFiles(
conversationId: string,
path?: string,
): Promise<GetFilesResponse> {
const url = `${this.getConversationUrl(conversationId)}/list-files`;
const { data } = await openHands.get<GetFilesResponse>(url, {
params: { path },
headers: this.getConversationHeaders(),
});
return data;
}
/**
* Retrieve the content of a file
* @param conversationId ID of the conversation
* @param path Full path of the file to retrieve
* @returns Code content of the file
*/
static async getFile(conversationId: string, path: string): Promise<string> {
const url = `${this.getConversationUrl(conversationId)}/select-file`;
const { data } = await openHands.get<GetFileResponse>(url, {
params: { file: path },
headers: this.getConversationHeaders(),
});
return data.code;
}
/**
* Upload multiple files to the workspace
* @param conversationId ID of the conversation
* @param files List of files.
* @returns list of uploaded files, list of skipped files
*/
static async uploadFiles(
conversationId: string,
files: File[],
): Promise<FileUploadSuccessResponse> {
const formData = new FormData();
for (const file of files) {
formData.append("files", file);
}
const url = `${this.getConversationUrl(conversationId)}/upload-files`;
const response = await openHands.post<FileUploadSuccessResponse>(
url,
formData,
{
headers: {
"Content-Type": "multipart/form-data",
...this.getConversationHeaders(),
},
},
);
return response.data;
}
/**
* Get the user installation IDs
* @param provider The provider to get installation IDs for (github, bitbucket, etc.)
+6
View File
@@ -158,3 +158,9 @@ export interface MicroagentContentResponse {
git_provider: Provider;
triggers: string[];
}
export type GetFilesResponse = string[];
export interface GetFileResponse {
code: string;
}
@@ -3,7 +3,8 @@ import { NavLink, useParams, useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { ConversationCard } from "./conversation-card";
import { useUserConversations } from "#/hooks/query/use-user-conversations";
import { usePaginatedConversations } from "#/hooks/query/use-paginated-conversations";
import { useInfiniteScroll } from "#/hooks/use-infinite-scroll";
import { useDeleteConversation } from "#/hooks/mutation/use-delete-conversation";
import { useStopConversation } from "#/hooks/mutation/use-stop-conversation";
import { ConfirmDeleteModal } from "./confirm-delete-modal";
@@ -40,12 +41,30 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
string | null
>(null);
const { data: conversations, isFetching, error } = useUserConversations();
const {
data,
isFetching,
error,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = usePaginatedConversations();
// Flatten all pages into a single array of conversations
const conversations = data?.pages.flatMap((page) => page.results) ?? [];
const { mutate: deleteConversation } = useDeleteConversation();
const { mutate: stopConversation } = useStopConversation();
const { mutate: updateConversation } = useUpdateConversation();
// Set up infinite scroll
const scrollContainerRef = useInfiniteScroll({
hasNextPage: !!hasNextPage,
isFetchingNextPage,
fetchNextPage,
threshold: 200, // Load more when 200px from bottom
});
const handleDeleteProject = (conversationId: string) => {
setConfirmDeleteModalVisible(true);
setSelectedConversationId(conversationId);
@@ -102,11 +121,16 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
return (
<div
ref={ref}
ref={(node) => {
// TODO: Combine both refs somehow
if (ref.current !== node) ref.current = node;
if (scrollContainerRef.current !== node)
scrollContainerRef.current = node;
}}
data-testid="conversation-panel"
className="w-[350px] h-full border border-neutral-700 bg-base-secondary rounded-xl overflow-y-auto absolute"
>
{isFetching && (
{isFetching && conversations.length === 0 && (
<div className="w-full h-full absolute flex justify-center items-center">
<LoadingSpinner size="small" />
</div>
@@ -156,6 +180,13 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
</NavLink>
))}
{/* Loading indicator for fetching more conversations */}
{isFetchingNextPage && (
<div className="flex justify-center py-4">
<LoadingSpinner size="small" />
</div>
)}
{confirmDeleteModalVisible && (
<ConfirmDeleteModal
onConfirm={() => {
@@ -25,6 +25,7 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
mcp_config: settings.MCP_CONFIG,
enable_proactive_conversation_starters:
settings.ENABLE_PROACTIVE_CONVERSATION_STARTERS,
enable_solvability_analysis: settings.ENABLE_SOLVABILITY_ANALYSIS,
search_api_key: settings.SEARCH_API_KEY?.trim() || "",
max_budget_per_task: settings.MAX_BUDGET_PER_TASK,
git_user_name:
@@ -1,11 +1,11 @@
import { useMutation } from "@tanstack/react-query";
import { FileService } from "#/api/file-service/file-service.api";
import OpenHands from "#/api/open-hands";
export const useUploadFiles = () =>
useMutation({
mutationKey: ["upload-files"],
mutationFn: (variables: { conversationId: string; files: File[] }) =>
FileService.uploadFiles(variables.conversationId!, variables.files),
OpenHands.uploadFiles(variables.conversationId!, variables.files),
onSuccess: async () => {},
meta: {
disableToast: true,
@@ -1,13 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import { useConversationId } from "../use-conversation-id";
import { FileService } from "#/api/file-service/file-service.api";
import OpenHands from "#/api/open-hands";
export const useGetMicroagents = (microagentDirectory: string) => {
const { conversationId } = useConversationId();
return useQuery({
queryKey: ["files", "microagents", conversationId, microagentDirectory],
queryFn: () => FileService.getFiles(conversationId!, microagentDirectory),
queryFn: () => OpenHands.getFiles(conversationId!, microagentDirectory),
enabled: !!conversationId,
select: (data) =>
data.map((fileName) => fileName.replace(microagentDirectory, "")),
@@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { MemoryService } from "#/api/memory-service/memory-service.api";
import OpenHands from "#/api/open-hands";
import { useConversationId } from "../use-conversation-id";
export const useMicroagentPrompt = (eventId: number) => {
@@ -7,7 +7,7 @@ export const useMicroagentPrompt = (eventId: number) => {
return useQuery({
queryKey: ["memory", "prompt", conversationId, eventId],
queryFn: () => MemoryService.getPrompt(conversationId!, eventId),
queryFn: () => OpenHands.getMicroagentPrompt(conversationId!, eventId),
enabled: !!conversationId,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
@@ -0,0 +1,16 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { useIsAuthed } from "./use-is-authed";
export const usePaginatedConversations = (limit: number = 20) => {
const { data: userIsAuthenticated } = useIsAuthed();
return useInfiniteQuery({
queryKey: ["user", "conversations", "paginated", limit],
queryFn: ({ pageParam }) =>
OpenHands.getUserConversations(limit, pageParam),
enabled: !!userIsAuthenticated,
getNextPageParam: (lastPage) => lastPage.next_page_id,
initialPageParam: undefined as string | undefined,
});
};
@@ -4,7 +4,7 @@ import OpenHands from "#/api/open-hands";
export const useSearchConversations = (
selectedRepository?: string,
conversationTrigger?: string,
limit: number = 20,
limit: number = 100,
cacheDisabled: boolean = false,
) =>
useQuery({
+1
View File
@@ -25,6 +25,7 @@ const getSettingsQueryFn = async (): Promise<Settings> => {
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
ENABLE_PROACTIVE_CONVERSATION_STARTERS:
apiSettings.enable_proactive_conversation_starters,
ENABLE_SOLVABILITY_ANALYSIS: apiSettings.enable_solvability_analysis,
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
SEARCH_API_KEY: apiSettings.search_api_key || "",
MAX_BUDGET_PER_TASK: apiSettings.max_budget_per_task,
@@ -1,13 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { useIsAuthed } from "./use-is-authed";
export const useUserConversations = () => {
const { data: userIsAuthenticated } = useIsAuthed();
return useQuery({
queryKey: ["user", "conversations"],
queryFn: OpenHands.getUserConversations,
enabled: !!userIsAuthenticated,
});
};
+42
View File
@@ -0,0 +1,42 @@
import { useEffect, useRef, useCallback } from "react";
interface UseInfiniteScrollOptions {
hasNextPage: boolean;
isFetchingNextPage: boolean;
fetchNextPage: () => void;
threshold?: number;
}
export const useInfiniteScroll = ({
hasNextPage,
isFetchingNextPage,
fetchNextPage,
threshold = 100,
}: UseInfiniteScrollOptions) => {
const containerRef = useRef<HTMLDivElement>(null);
const handleScroll = useCallback(() => {
if (!containerRef.current || isFetchingNextPage || !hasNextPage) {
return;
}
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
const isNearBottom = scrollTop + clientHeight >= scrollHeight - threshold;
if (isNearBottom) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage, threshold]);
useEffect(() => {
const container = containerRef.current;
if (!container) return undefined;
container.addEventListener("scroll", handleScroll);
return () => {
container.removeEventListener("scroll", handleScroll);
};
}, [handleScroll]);
return containerRef;
};
+2
View File
@@ -147,10 +147,12 @@ export enum I18nKey {
SUGGESTIONS$CLEAN_DEPENDENCIES = "SUGGESTIONS$CLEAN_DEPENDENCIES",
SETTINGS$LLM_SETTINGS = "SETTINGS$LLM_SETTINGS",
SETTINGS$GIT_SETTINGS = "SETTINGS$GIT_SETTINGS",
SETTINGS$GIT_SETTINGS_DESCRIPTION = "SETTINGS$GIT_SETTINGS_DESCRIPTION",
SETTINGS$SOUND_NOTIFICATIONS = "SETTINGS$SOUND_NOTIFICATIONS",
SETTINGS$MAX_BUDGET_PER_TASK = "SETTINGS$MAX_BUDGET_PER_TASK",
SETTINGS$MAX_BUDGET_PER_CONVERSATION = "SETTINGS$MAX_BUDGET_PER_CONVERSATION",
SETTINGS$PROACTIVE_CONVERSATION_STARTERS = "SETTINGS$PROACTIVE_CONVERSATION_STARTERS",
SETTINGS$SOLVABILITY_ANALYSIS = "SETTINGS$SOLVABILITY_ANALYSIS",
SETTINGS$SEARCH_API_KEY = "SETTINGS$SEARCH_API_KEY",
SETTINGS$SEARCH_API_KEY_OPTIONAL = "SETTINGS$SEARCH_API_KEY_OPTIONAL",
SETTINGS$SEARCH_API_KEY_INSTRUCTIONS = "SETTINGS$SEARCH_API_KEY_INSTRUCTIONS",
+32
View File
@@ -2351,6 +2351,22 @@
"tr": "Git Ayarları",
"uk": "Git налаштування"
},
"SETTINGS$GIT_SETTINGS_DESCRIPTION": {
"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",
"ja": "サウンド通知",
@@ -2415,6 +2431,22 @@
"tr": "GitHub'da Görevler Öner",
"uk": "Запропонувати завдання на GitHub"
},
"SETTINGS$SOLVABILITY_ANALYSIS": {
"en": "Enable Solvability Analysis",
"ja": "解決可能性分析を有効にする",
"zh-CN": "启用可解决性分析",
"zh-TW": "啟用可解決性分析",
"ko-KR": "해결 가능성 분석 활성화",
"de": "Lösbarkeitsanalyse aktivieren",
"no": "Aktiver løsningsanalyse",
"it": "Abilita analisi di risolvibilità",
"pt": "Ativar análise de solucionabilidade",
"es": "Habilitar análisis de solvencia",
"ar": "تمكين تحليل القابلية للحل",
"fr": "Activer l'analyse de solvabilité",
"tr": "Çözünürlük Analizini Etkinleştir",
"uk": "Увімкнути аналіз розв'язності"
},
"SETTINGS$SEARCH_API_KEY": {
"en": "Search API Key (Tavily)",
"ja": "検索APIキー (Tavily)",
+1
View File
@@ -30,6 +30,7 @@ export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
enable_proactive_conversation_starters:
DEFAULT_SETTINGS.ENABLE_PROACTIVE_CONVERSATION_STARTERS,
enable_solvability_analysis: DEFAULT_SETTINGS.ENABLE_SOLVABILITY_ANALYSIS,
user_consents_to_analytics: DEFAULT_SETTINGS.USER_CONSENTS_TO_ANALYTICS,
max_budget_per_task: DEFAULT_SETTINGS.MAX_BUDGET_PER_TASK,
};
+31 -1
View File
@@ -38,6 +38,10 @@ function AppSettingsScreen() {
proactiveConversationsSwitchHasChanged,
setProactiveConversationsSwitchHasChanged,
] = React.useState(false);
const [
solvabilityAnalysisSwitchHasChanged,
setSolvabilityAnalysisSwitchHasChanged,
] = React.useState(false);
const [maxBudgetPerTaskHasChanged, setMaxBudgetPerTaskHasChanged] =
React.useState(false);
const [gitUserNameHasChanged, setGitUserNameHasChanged] =
@@ -61,6 +65,9 @@ function AppSettingsScreen() {
formData.get("enable-proactive-conversations-switch")?.toString() ===
"on";
const enableSolvabilityAnalysis =
formData.get("enable-solvability-analysis-switch")?.toString() === "on";
const maxBudgetPerTaskValue = formData
.get("max-budget-per-task-input")
?.toString();
@@ -79,6 +86,7 @@ function AppSettingsScreen() {
user_consents_to_analytics: enableAnalytics,
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
ENABLE_PROACTIVE_CONVERSATION_STARTERS: enableProactiveConversations,
ENABLE_SOLVABILITY_ANALYSIS: enableSolvabilityAnalysis,
MAX_BUDGET_PER_TASK: maxBudgetPerTask,
GIT_USER_NAME: gitUserName,
GIT_USER_EMAIL: gitUserEmail,
@@ -136,6 +144,13 @@ function AppSettingsScreen() {
);
};
const checkIfSolvabilityAnalysisSwitchHasChanged = (checked: boolean) => {
const currentSolvabilityAnalysis = !!settings?.ENABLE_SOLVABILITY_ANALYSIS;
setSolvabilityAnalysisSwitchHasChanged(
checked !== currentSolvabilityAnalysis,
);
};
const checkIfMaxBudgetPerTaskHasChanged = (value: string) => {
const newValue = parseMaxBudgetPerTask(value);
const currentValue = settings?.MAX_BUDGET_PER_TASK;
@@ -157,6 +172,7 @@ function AppSettingsScreen() {
!analyticsSwitchHasChanged &&
!soundNotificationsSwitchHasChanged &&
!proactiveConversationsSwitchHasChanged &&
!solvabilityAnalysisSwitchHasChanged &&
!maxBudgetPerTaskHasChanged &&
!gitUserNameHasChanged &&
!gitUserEmailHasChanged;
@@ -209,6 +225,17 @@ function AppSettingsScreen() {
</SettingsSwitch>
)}
{config?.APP_MODE === "saas" && (
<SettingsSwitch
testId="enable-solvability-analysis-switch"
name="enable-solvability-analysis-switch"
defaultIsToggled={!!settings.ENABLE_SOLVABILITY_ANALYSIS}
onToggle={checkIfSolvabilityAnalysisSwitchHasChanged}
>
{t(I18nKey.SETTINGS$SOLVABILITY_ANALYSIS)}
</SettingsSwitch>
)}
<SettingsInput
testId="max-budget-per-task-input"
name="max-budget-per-task-input"
@@ -223,9 +250,12 @@ function AppSettingsScreen() {
/>
<div className="border-t border-t-tertiary pt-6 mt-2">
<h3 className="text-lg font-medium mb-4">
<h3 className="text-lg font-medium mb-2">
{t(I18nKey.SETTINGS$GIT_SETTINGS)}
</h3>
<p className="text-sm text-secondary mb-4">
{t(I18nKey.SETTINGS$GIT_SETTINGS_DESCRIPTION)}
</p>
<div className="flex flex-col gap-6">
<SettingsInput
testId="git-user-name-input"
+1
View File
@@ -17,6 +17,7 @@ export const DEFAULT_SETTINGS: Settings = {
ENABLE_SOUND_NOTIFICATIONS: false,
USER_CONSENTS_TO_ANALYTICS: false,
ENABLE_PROACTIVE_CONVERSATION_STARTERS: false,
ENABLE_SOLVABILITY_ANALYSIS: false,
SEARCH_API_KEY: "",
IS_NEW_USER: true,
MAX_BUDGET_PER_TASK: null,
+2
View File
@@ -43,6 +43,7 @@ export type Settings = {
ENABLE_DEFAULT_CONDENSER: boolean;
ENABLE_SOUND_NOTIFICATIONS: boolean;
ENABLE_PROACTIVE_CONVERSATION_STARTERS: boolean;
ENABLE_SOLVABILITY_ANALYSIS: boolean;
USER_CONSENTS_TO_ANALYTICS: boolean | null;
SEARCH_API_KEY?: string;
IS_NEW_USER?: boolean;
@@ -68,6 +69,7 @@ export type ApiSettings = {
enable_default_condenser: boolean;
enable_sound_notifications: boolean;
enable_proactive_conversation_starters: boolean;
enable_solvability_analysis: boolean;
user_consents_to_analytics: boolean | null;
search_api_key?: string;
provider_tokens_set: Partial<Record<Provider, string | null>>;
+6
View File
@@ -14,6 +14,7 @@ export const VERIFIED_MODELS = [
"claude-3-7-sonnet-20250219",
"claude-sonnet-4-20250514",
"claude-opus-4-20250514",
"claude-opus-4-1-20250805",
"gemini-2.5-pro",
"o4-mini",
"deepseek-chat",
@@ -22,11 +23,13 @@ export const VERIFIED_MODELS = [
"devstral-medium-2507",
"kimi-k2-0711-preview",
"qwen3-coder-480b",
"gpt-5-2025-08-07",
];
// LiteLLM does not return OpenAI models with the provider, so we list them here to set them ourselves for consistency
// (e.g., they return `gpt-4o` instead of `openai/gpt-4o`)
export const VERIFIED_OPENAI_MODELS = [
"gpt-5-2025-08-07",
"gpt-4o",
"gpt-4o-mini",
"gpt-4.1",
@@ -47,6 +50,7 @@ export const VERIFIED_ANTHROPIC_MODELS = [
"claude-3-7-sonnet-20250219",
"claude-sonnet-4-20250514",
"claude-opus-4-20250514",
"claude-opus-4-1-20250805",
];
// LiteLLM does not return the compatible Mistral models with the provider, so we list them here to set them ourselves
@@ -61,7 +65,9 @@ export const VERIFIED_MISTRAL_MODELS = [
// (e.g., they return `claude-sonnet-4-20250514` instead of `openhands/claude-sonnet-4-20250514`)
export const VERIFIED_OPENHANDS_MODELS = [
"claude-sonnet-4-20250514",
"gpt-5-2025-08-07",
"claude-opus-4-20250514",
"claude-opus-4-1-20250805",
"gemini-2.5-pro",
"o3",
"o4-mini",
@@ -106,10 +106,15 @@ class CodeActAgent(Agent):
def _get_tools(self) -> list['ChatCompletionToolParam']:
# For these models, we use short tool descriptions ( < 1024 tokens)
# to avoid hitting the OpenAI token limit for tool descriptions.
SHORT_TOOL_DESCRIPTION_LLM_SUBSTRS = ['gpt-', 'o3', 'o1', 'o4']
SHORT_TOOL_DESCRIPTION_LLM_SUBSTRS = ['gpt-4', 'o3', 'o1', 'o4']
use_short_tool_desc = False
if self.llm is not None:
# For historical reasons, previously OpenAI enforces max function description length of 1k characters
# https://community.openai.com/t/function-call-description-max-length/529902
# But it no longer seems to be an issue recently
# https://community.openai.com/t/was-the-character-limit-for-schema-descriptions-upgraded/1225975
# Tested on GPT-5 and longer description still works. But we still keep the logic to be safe for older models.
use_short_tool_desc = any(
model_substr in self.llm.config.model
for model_substr in SHORT_TOOL_DESCRIPTION_LLM_SUBSTRS
@@ -66,6 +66,11 @@ Your primary role is to assist users by executing commands, modifying code, and
* Use APIs to work with GitHub or other platforms, unless the user asks otherwise or your task requires browsing.
</SECURITY>
<EXTERNAL_SERVICES>
* When interacting with external services like GitHub, GitLab, or Bitbucket, use their respective APIs instead of browser-based interactions whenever possible.
* Only resort to browser-based interactions with these services if specifically requested by the user or if the required operation cannot be performed via API.
</EXTERNAL_SERVICES>
<ENVIRONMENT_SETUP>
* When user asks you to run an application, don't stop if the application is not installed. Instead, please install the application and run the command again.
* If you encounter missing dependencies:
@@ -1,7 +1,6 @@
import sys
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
from openhands.agenthub.codeact_agent.tools.prompt import refine_prompt
from openhands.llm.tool_names import EXECUTE_BASH_TOOL_NAME
_DETAILED_BASH_DESCRIPTION = """Execute a bash command in the terminal within a persistent shell session.
@@ -35,12 +34,6 @@ _SHORT_BASH_DESCRIPTION = """Execute a bash command in the terminal.
* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together."""
def refine_prompt(prompt: str):
if sys.platform == 'win32':
return prompt.replace('bash', 'powershell')
return prompt
def create_cmd_run_tool(
use_short_description: bool = False,
) -> ChatCompletionToolParam:
@@ -0,0 +1,29 @@
import re
import sys
def refine_prompt(prompt: str):
"""
Refines the prompt based on the platform.
On Windows systems, replaces 'bash' with 'powershell' and 'execute_bash' with 'execute_powershell'
to ensure commands work correctly on the Windows platform.
Args:
prompt: The prompt text to refine
Returns:
The refined prompt text
"""
if sys.platform == 'win32':
# Replace 'bash' with 'powershell' including tool names like 'execute_bash'
# First replace 'execute_bash' with 'execute_powershell' to handle tool names
result = re.sub(
r'\bexecute_bash\b', 'execute_powershell', prompt, flags=re.IGNORECASE
)
# Then replace standalone 'bash' with 'powershell'
result = re.sub(
r'(?<!execute_)(?<!_)\bbash\b', 'powershell', result, flags=re.IGNORECASE
)
return result
return prompt
+1
View File
@@ -0,0 +1 @@
"""OpenHands CLI module."""
+54
View File
@@ -0,0 +1,54 @@
"""Main entry point for OpenHands CLI with subcommand support."""
import sys
import openhands
import openhands.cli.suppress_warnings # noqa: F401
from openhands.cli.gui_launcher import launch_gui_server
from openhands.cli.main import run_cli_command
from openhands.core.config import get_cli_parser
from openhands.core.config.arg_utils import get_subparser
def main():
"""Main entry point with subcommand support and backward compatibility."""
parser = get_cli_parser()
# If user only asks for --help or -h without a subcommand
if len(sys.argv) == 2 and sys.argv[1] in ('--help', '-h'):
# Print top-level help
print(parser.format_help())
# Also print help for `cli` subcommand
print('\n' + '=' * 80)
print('CLI command help:\n')
cli_parser = get_subparser(parser, 'cli')
print(cli_parser.format_help())
sys.exit(0)
# Special case: no subcommand provided, simulate "openhands cli"
if len(sys.argv) == 1 or (
len(sys.argv) > 1 and sys.argv[1] not in ['cli', 'serve']
):
# Inject 'cli' as default command
sys.argv.insert(1, 'cli')
args = parser.parse_args()
if hasattr(args, 'version') and args.version:
print(f'OpenHands CLI version: {openhands.get_version()}')
sys.exit(0)
if args.command == 'serve':
launch_gui_server(mount_cwd=args.mount_cwd, gpu=args.gpu)
elif args.command == 'cli' or args.command is None:
run_cli_command(args)
else:
parser.print_help()
sys.exit(1)
if __name__ == '__main__':
main()
+219
View File
@@ -0,0 +1,219 @@
"""GUI launcher for OpenHands CLI."""
import os
import shutil
import subprocess
import sys
from pathlib import Path
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import HTML
from openhands import __version__
def _format_docker_command_for_logging(cmd: list[str]) -> str:
"""Format a Docker command for logging with grey color.
Args:
cmd (list[str]): The Docker command as a list of strings
Returns:
str: The formatted command string in grey HTML color
"""
cmd_str = ' '.join(cmd)
return f'<grey>Running Docker command: {cmd_str}</grey>'
def check_docker_requirements() -> bool:
"""Check if Docker is installed and running.
Returns:
bool: True if Docker is available and running, False otherwise.
"""
# Check if Docker is installed
if not shutil.which('docker'):
print_formatted_text(
HTML('<ansired>❌ Docker is not installed or not in PATH.</ansired>')
)
print_formatted_text(
HTML(
'<grey>Please install Docker first: https://docs.docker.com/get-docker/</grey>'
)
)
return False
# Check if Docker daemon is running
try:
result = subprocess.run(
['docker', 'info'], capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
print_formatted_text(
HTML('<ansired>❌ Docker daemon is not running.</ansired>')
)
print_formatted_text(
HTML('<grey>Please start Docker and try again.</grey>')
)
return False
except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e:
print_formatted_text(
HTML('<ansired>❌ Failed to check Docker status.</ansired>')
)
print_formatted_text(HTML(f'<grey>Error: {e}</grey>'))
return False
return True
def ensure_config_dir_exists() -> Path:
"""Ensure the OpenHands configuration directory exists and return its path."""
config_dir = Path.home() / '.openhands'
config_dir.mkdir(exist_ok=True)
return config_dir
def launch_gui_server(mount_cwd: bool = False, gpu: bool = False) -> None:
"""Launch the OpenHands GUI server using Docker.
Args:
mount_cwd: If True, mount the current working directory into the container.
gpu: If True, enable GPU support by mounting all GPUs into the container via nvidia-docker.
"""
print_formatted_text(
HTML('<ansiblue>🚀 Launching OpenHands GUI server...</ansiblue>')
)
print_formatted_text('')
# Check Docker requirements
if not check_docker_requirements():
sys.exit(1)
# Ensure config directory exists
config_dir = ensure_config_dir_exists()
# Get the current version for the Docker image
version = __version__
runtime_image = f'docker.all-hands.dev/all-hands-ai/runtime:{version}-nikolaik'
app_image = f'docker.all-hands.dev/all-hands-ai/openhands:{version}'
print_formatted_text(HTML('<grey>Pulling required Docker images...</grey>'))
# Pull the runtime image first
pull_cmd = ['docker', 'pull', runtime_image]
print_formatted_text(HTML(_format_docker_command_for_logging(pull_cmd)))
try:
subprocess.run(
pull_cmd,
check=True,
timeout=300, # 5 minutes timeout
)
except subprocess.CalledProcessError:
print_formatted_text(
HTML('<ansired>❌ Failed to pull runtime image.</ansired>')
)
sys.exit(1)
except subprocess.TimeoutExpired:
print_formatted_text(
HTML('<ansired>❌ Timeout while pulling runtime image.</ansired>')
)
sys.exit(1)
print_formatted_text('')
print_formatted_text(
HTML('<ansigreen>✅ Starting OpenHands GUI server...</ansigreen>')
)
print_formatted_text(
HTML('<grey>The server will be available at: http://localhost:3000</grey>')
)
print_formatted_text(HTML('<grey>Press Ctrl+C to stop the server.</grey>'))
print_formatted_text('')
# Build the Docker command
docker_cmd = [
'docker',
'run',
'-it',
'--rm',
'--pull=always',
'-e',
f'SANDBOX_RUNTIME_CONTAINER_IMAGE={runtime_image}',
'-e',
'LOG_ALL_EVENTS=true',
'-v',
'/var/run/docker.sock:/var/run/docker.sock',
'-v',
f'{config_dir}:/.openhands',
]
# Add GPU support if requested
if gpu:
print_formatted_text(
HTML('<ansigreen>🖥️ Enabling GPU support via nvidia-docker...</ansigreen>')
)
# Add the --gpus all flag to enable all GPUs
docker_cmd.insert(2, '--gpus')
docker_cmd.insert(3, 'all')
# Add environment variable to pass GPU support to sandbox containers
docker_cmd.extend(
[
'-e',
'SANDBOX_ENABLE_GPU=true',
]
)
# Add current working directory mount if requested
if mount_cwd:
cwd = Path.cwd()
# Following the documentation at https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem
docker_cmd.extend(
[
'-e',
f'SANDBOX_VOLUMES={cwd}:/workspace:rw',
]
)
# Set user ID for Unix-like systems only
if os.name != 'nt': # Not Windows
try:
user_id = subprocess.check_output(['id', '-u'], text=True).strip()
docker_cmd.extend(['-e', f'SANDBOX_USER_ID={user_id}'])
except (subprocess.CalledProcessError, FileNotFoundError):
# If 'id' command fails or doesn't exist, skip setting user ID
pass
# Print the folder that will be mounted to inform the user
print_formatted_text(
HTML(
f'<ansigreen>📂 Mounting current directory:</ansigreen> <ansiyellow>{cwd}</ansiyellow> <ansigreen>to</ansigreen> <ansiyellow>/workspace</ansiyellow>'
)
)
docker_cmd.extend(
[
'-p',
'3000:3000',
'--add-host',
'host.docker.internal:host-gateway',
'--name',
'openhands-app',
app_image,
]
)
try:
# Log and run the Docker command
print_formatted_text(HTML(_format_docker_command_for_logging(docker_cmd)))
subprocess.run(docker_cmd, check=True)
except subprocess.CalledProcessError as e:
print_formatted_text('')
print_formatted_text(
HTML('<ansired>❌ Failed to start OpenHands GUI server.</ansired>')
)
print_formatted_text(HTML(f'<grey>Error: {e}</grey>'))
sys.exit(1)
except KeyboardInterrupt:
print_formatted_text('')
print_formatted_text(
HTML('<ansigreen>✓ OpenHands GUI server stopped successfully.</ansigreen>')
)
sys.exit(0)
+13 -21
View File
@@ -45,7 +45,6 @@ from openhands.controller import AgentController
from openhands.controller.agent import Agent
from openhands.core.config import (
OpenHandsConfig,
parse_arguments,
setup_config_from_args,
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
@@ -129,12 +128,13 @@ async def run_session(
conversation_instructions: str | None = None,
session_name: str | None = None,
skip_banner: bool = False,
conversation_id: str | None = None,
) -> bool:
reload_microagents = False
new_session_requested = False
exit_reason = ExitReason.INTENTIONAL
sid = generate_sid(config, session_name)
sid = conversation_id or generate_sid(config, session_name)
is_loaded = asyncio.Event()
is_paused = asyncio.Event() # Event to track agent pause requests
always_confirm_mode = False # Flag to enable always confirm mode
@@ -523,10 +523,8 @@ def run_alias_setup_flow(config: OpenHandsConfig) -> None:
print_formatted_text('')
async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
async def main_with_loop(loop: asyncio.AbstractEventLoop, args) -> None:
"""Runs the agent in CLI mode."""
args = parse_arguments()
# Set log level from command line argument if provided
if args.log_level and isinstance(args.log_level, str):
log_level = getattr(logging, str(args.log_level).upper())
@@ -574,13 +572,9 @@ async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
# Use settings from settings store if available and override with command line arguments
if settings:
# Handle agent configuration
if args.agent_cls:
config.default_agent = str(args.agent_cls)
else:
# settings.agent is not None because we check for it in setup_config_from_args
assert settings.agent is not None
config.default_agent = settings.agent
# settings.agent is not None because we check for it in setup_config_from_args
assert settings.agent is not None
config.default_agent = settings.agent
# Handle LLM configuration with proper precedence:
# 1. CLI parameters (-l) have highest precedence (already handled in setup_config_from_args)
@@ -705,6 +699,7 @@ After reviewing the file, please ask the user what they would like to do with it
task_str,
session_name=args.name,
skip_banner=banner_shown,
conversation_id=args.conversation,
)
# If a new session was requested, run it
@@ -717,18 +712,19 @@ After reviewing the file, please ask the user what they would like to do with it
get_runtime_cls(config.runtime).teardown(config)
def main():
def run_cli_command(args):
"""Run the CLI command with proper error handling and cleanup."""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(main_with_loop(loop))
loop.run_until_complete(main_with_loop(loop, args))
except KeyboardInterrupt:
print_formatted_text('⚠️ Session was interrupted: interrupted\n')
except ConnectionRefusedError as e:
print(f'Connection refused: {e}')
print_formatted_text(f'Connection refused: {e}')
sys.exit(1)
except Exception as e:
print(f'An error occurred: {e}')
print_formatted_text(f'An error occurred: {e}')
sys.exit(1)
finally:
try:
@@ -741,9 +737,5 @@ def main():
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
loop.close()
except Exception as e:
print(f'Error during cleanup: {e}')
print_formatted_text(f'Error during cleanup: {e}')
sys.exit(1)
if __name__ == '__main__':
main()
+1 -1
View File
@@ -27,7 +27,7 @@ from openhands.core.config.condenser_config import (
CondenserPipelineConfig,
ConversationWindowCondenserConfig,
)
from openhands.core.config.utils import OH_DEFAULT_AGENT
from openhands.core.config.config_utils import OH_DEFAULT_AGENT
from openhands.memory.condenser.impl.llm_summarizing_condenser import (
LLMSummarizingCondenserConfig,
)
+84 -10
View File
@@ -11,6 +11,7 @@ import threading
import time
from typing import Generator
import markdown # type: ignore
from prompt_toolkit import PromptSession, print_formatted_text
from prompt_toolkit.application import Application
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
@@ -65,6 +66,7 @@ MAX_RECENT_THOUGHTS = 5
# Color and styling constants
COLOR_GOLD = '#FFD700'
COLOR_GREY = '#808080'
COLOR_AGENT_BLUE = '#4682B4' # Steel blue - less saturated, works well on both light and dark backgrounds
DEFAULT_STYLE = Style.from_dict(
{
'gold': COLOR_GOLD,
@@ -236,13 +238,19 @@ def display_mcp_errors() -> None:
# Prompt output display functions
def display_thought_if_new(thought: str) -> None:
"""Display a thought only if it hasn't been displayed recently."""
def display_thought_if_new(thought: str, is_agent_message: bool = False) -> None:
"""
Display a thought only if it hasn't been displayed recently.
Args:
thought: The thought to display
is_agent_message: If True, apply agent styling and markdown rendering
"""
global recent_thoughts
if thought and thought.strip():
# Check if this thought was recently displayed
if thought not in recent_thoughts:
display_message(thought)
display_message(thought, is_agent_message=is_agent_message)
recent_thoughts.append(thought)
# Keep only the most recent thoughts
if len(recent_thoughts) > MAX_RECENT_THOUGHTS:
@@ -255,7 +263,7 @@ def display_event(event: Event, config: OpenHandsConfig) -> None:
if isinstance(event, CmdRunAction):
# For CmdRunAction, display thought first, then command
if hasattr(event, 'thought') and event.thought:
display_message(event.thought)
display_thought_if_new(event.thought)
# Only display the command if it's not already confirmed
# Commands are always shown when AWAITING_CONFIRMATION, so we don't need to show them again when CONFIRMED
@@ -269,14 +277,15 @@ def display_event(event: Event, config: OpenHandsConfig) -> None:
elif isinstance(event, Action):
# For other actions, display thoughts normally
if hasattr(event, 'thought') and event.thought:
display_message(event.thought)
display_thought_if_new(event.thought)
if hasattr(event, 'final_thought') and event.final_thought:
display_message(event.final_thought)
# Display final thoughts with agent styling
display_message(event.final_thought, is_agent_message=True)
if isinstance(event, MessageAction):
if event.source == EventSource.AGENT:
# Check if this message content is a duplicate thought
display_thought_if_new(event.content)
# Display agent messages with styling and markdown rendering
display_thought_if_new(event.content, is_agent_message=True)
elif isinstance(event, CmdOutputObservation):
display_command_output(event.content)
elif isinstance(event, FileEditObservation):
@@ -291,11 +300,76 @@ def display_event(event: Event, config: OpenHandsConfig) -> None:
display_error(event.content)
def display_message(message: str) -> None:
def display_message(message: str, is_agent_message: bool = False) -> None:
"""
Display a message in the terminal with markdown rendering.
Args:
message: The message to display
is_agent_message: If True, apply agent styling (blue color)
"""
message = message.strip()
if message:
print_formatted_text(f'\n{message}')
# Add spacing before the message
print_formatted_text('')
try:
# Convert markdown to HTML for all messages
html_content = convert_markdown_to_html(message)
if is_agent_message:
# Use prompt_toolkit's HTML renderer with the agent color
print_formatted_text(
HTML(f'<style fg="{COLOR_AGENT_BLUE}">{html_content}</style>')
)
else:
# Regular message display with HTML rendering but default color
print_formatted_text(HTML(html_content))
except Exception as e:
# If HTML rendering fails, fall back to plain text
print(f'Warning: HTML rendering failed: {str(e)}', file=sys.stderr)
if is_agent_message:
print_formatted_text(
FormattedText([('fg:' + COLOR_AGENT_BLUE, message)])
)
else:
print_formatted_text(message)
def convert_markdown_to_html(text: str) -> str:
"""
Convert markdown to HTML for prompt_toolkit's HTML renderer using the markdown library.
Args:
text: Markdown text to convert
Returns:
HTML formatted text with custom styling for headers and bullet points
"""
if not text:
return text
# Use the markdown library to convert markdown to HTML
# Enable the 'extra' extension for tables, fenced code, etc.
html = markdown.markdown(text, extensions=['extra'])
# Customize headers
for i in range(1, 7):
# Get the appropriate number of # characters for this heading level
prefix = '#' * i + ' '
# Replace <h1> with the prefix and bold text
html = html.replace(f'<h{i}>', f'<b>{prefix}')
html = html.replace(f'</h{i}>', '</b>\n')
# Customize bullet points to use dashes instead of dots with compact spacing
html = html.replace('<ul>', '')
html = html.replace('</ul>', '')
html = html.replace('<li>', '- ')
html = html.replace('</li>', '')
return html
def display_error(error: str) -> None:
+4
View File
@@ -150,6 +150,7 @@ def organize_models_and_providers(
VERIFIED_PROVIDERS = ['openhands', 'anthropic', 'openai', 'mistral']
VERIFIED_OPENAI_MODELS = [
'gpt-5-2025-08-07',
'o4-mini',
'gpt-4o',
'gpt-4o-mini',
@@ -164,6 +165,7 @@ VERIFIED_OPENAI_MODELS = [
VERIFIED_ANTHROPIC_MODELS = [
'claude-sonnet-4-20250514',
'claude-opus-4-20250514',
'claude-opus-4-1-20250805',
'claude-3-7-sonnet-20250219',
'claude-3-sonnet-20240229',
'claude-3-opus-20240229',
@@ -183,7 +185,9 @@ VERIFIED_MISTRAL_MODELS = [
VERIFIED_OPENHANDS_MODELS = [
'claude-sonnet-4-20250514',
'gpt-5-2025-08-07',
'claude-opus-4-20250514',
'claude-opus-4-1-20250805',
'devstral-small-2507',
'devstral-medium-2507',
'o3',
+8 -2
View File
@@ -1,4 +1,9 @@
from openhands.core.config.agent_config import AgentConfig
from openhands.core.config.arg_utils import (
get_cli_parser,
get_evaluation_parser,
get_headless_parser,
)
from openhands.core.config.cli_config import CLIConfig
from openhands.core.config.config_utils import (
OH_DEFAULT_AGENT,
@@ -15,7 +20,6 @@ from openhands.core.config.utils import (
finalize_config,
get_agent_config_arg,
get_llm_config_arg,
get_parser,
load_from_env,
load_from_toml,
load_openhands_config,
@@ -41,7 +45,9 @@ __all__ = [
'get_agent_config_arg',
'get_llm_config_arg',
'get_field_info',
'get_parser',
'get_cli_parser',
'get_headless_parser',
'get_evaluation_parser',
'parse_arguments',
'setup_config_from_args',
]
+224
View File
@@ -0,0 +1,224 @@
"""Centralized command line argument configuration for OpenHands CLI and headless modes."""
import argparse
from argparse import ArgumentParser, _SubParsersAction
def get_subparser(parser: ArgumentParser, name: str) -> ArgumentParser:
for action in parser._actions:
if isinstance(action, _SubParsersAction):
if name in action.choices:
return action.choices[name]
raise ValueError(f"Subparser '{name}' not found")
def add_common_arguments(parser: argparse.ArgumentParser) -> None:
"""Add common arguments shared between CLI and headless modes."""
parser.add_argument(
'--config-file',
type=str,
default='config.toml',
help='Path to the config file (default: config.toml in the current directory)',
)
parser.add_argument(
'-t',
'--task',
type=str,
default='',
help='The task for the agent to perform',
)
parser.add_argument(
'-f',
'--file',
type=str,
help='Path to a file containing the task. Overrides -t if both are provided.',
)
parser.add_argument(
'-n',
'--name',
help='Session name',
type=str,
default='',
)
parser.add_argument(
'--log-level',
help='Set the log level',
type=str,
default=None,
)
parser.add_argument(
'-l',
'--llm-config',
default=None,
type=str,
help='Replace default LLM ([llm] section in config.toml) config with the specified LLM config, e.g. "llama3" for [llm.llama3] section in config.toml',
)
parser.add_argument(
'--agent-config',
default=None,
type=str,
help='Replace default Agent ([agent] section in config.toml) config with the specified Agent config, e.g. "CodeAct" for [agent.CodeAct] section in config.toml',
)
parser.add_argument(
'-v', '--version', action='store_true', help='Show version information'
)
def add_evaluation_arguments(parser: argparse.ArgumentParser) -> None:
"""Add arguments specific to evaluation mode."""
# Evaluation-specific arguments
parser.add_argument(
'--eval-output-dir',
default='evaluation/evaluation_outputs/outputs',
type=str,
help='The directory to save evaluation output',
)
parser.add_argument(
'--eval-n-limit',
default=None,
type=int,
help='The number of instances to evaluate',
)
parser.add_argument(
'--eval-num-workers',
default=4,
type=int,
help='The number of workers to use for evaluation',
)
parser.add_argument(
'--eval-note',
default=None,
type=str,
help='The note to add to the evaluation directory',
)
parser.add_argument(
'--eval-ids',
default=None,
type=str,
help='The comma-separated list (in quotes) of IDs of the instances to evaluate',
)
def add_headless_specific_arguments(parser: argparse.ArgumentParser) -> None:
"""Add arguments specific to headless mode (full evaluation suite)."""
parser.add_argument(
'-d',
'--directory',
type=str,
help='The working directory for the agent',
)
parser.add_argument(
'-c',
'--agent-cls',
default=None,
type=str,
help='Name of the default agent to use',
)
parser.add_argument(
'-i',
'--max-iterations',
default=None,
type=int,
help='The maximum number of iterations to run the agent',
)
parser.add_argument(
'-b',
'--max-budget-per-task',
type=float,
help='The maximum budget allowed per task, beyond which the agent will stop.',
)
# Additional headless-specific arguments
parser.add_argument(
'--no-auto-continue',
help='Disable auto-continue responses in headless mode (i.e. headless will read from stdin instead of auto-continuing)',
action='store_true',
default=False,
)
parser.add_argument(
'--selected-repo',
help='GitHub repository to clone (format: owner/repo)',
type=str,
default=None,
)
def get_cli_parser() -> argparse.ArgumentParser:
"""Create argument parser for CLI mode with simplified argument set."""
# Create a description with welcome message explaining available commands
description = (
'Welcome to OpenHands: Code Less, Make More\n\n'
'OpenHands supports two main commands:\n'
' serve - Launch the OpenHands GUI server (web interface)\n'
' cli - Run OpenHands in CLI mode (terminal interface)\n\n'
'Running "openhands" without a command is the same as "openhands cli"'
)
parser = argparse.ArgumentParser(
description=description,
prog='openhands',
formatter_class=argparse.RawDescriptionHelpFormatter, # Preserve formatting in description
epilog='For more information about a command, run: openhands COMMAND --help',
)
# Create subparsers
subparsers = parser.add_subparsers(
dest='command',
title='commands',
description='OpenHands supports two main commands:',
metavar='COMMAND',
)
# Add 'serve' subcommand
serve_parser = subparsers.add_parser(
'serve', help='Launch the OpenHands GUI server using Docker (web interface)'
)
serve_parser.add_argument(
'--mount-cwd',
help='Mount the current working directory into the GUI server container',
action='store_true',
default=False,
)
serve_parser.add_argument(
'--gpu',
help='Enable GPU support by mounting all GPUs into the Docker container via nvidia-docker',
action='store_true',
default=False,
)
# Add 'cli' subcommand - import all the existing CLI arguments
cli_parser = subparsers.add_parser(
'cli', help='Run OpenHands in CLI mode (terminal interface)'
)
add_common_arguments(cli_parser)
cli_parser.add_argument(
'--override-cli-mode',
help='Override the default settings for CLI mode',
type=bool,
default=False,
)
parser.add_argument(
'--conversation',
help='The conversation id to continue',
type=str,
default=None,
)
return parser
def get_headless_parser() -> argparse.ArgumentParser:
"""Create argument parser for headless mode with full argument set."""
parser = argparse.ArgumentParser(description='Run the agent via CLI')
add_common_arguments(parser)
add_headless_specific_arguments(parser)
return parser
def get_evaluation_parser() -> argparse.ArgumentParser:
"""Create argument parser for evaluation mode."""
parser = argparse.ArgumentParser(description='Run OpenHands in evaluation mode')
add_common_arguments(parser)
add_headless_specific_arguments(parser)
add_evaluation_arguments(parser)
return parser
+9
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import os
import re
import shlex
@@ -302,6 +304,13 @@ class MCPConfig(BaseModel):
raise ValueError(f'Invalid MCP configuration: {e}')
return mcp_mapping
def merge(self, other: MCPConfig):
return MCPConfig(
sse_servers=self.sse_servers + other.sse_servers,
stdio_servers=self.stdio_servers + other.stdio_servers,
shttp_servers=self.shttp_servers + other.shttp_servers,
)
class OpenHandsMCPConfig:
@staticmethod
@@ -72,6 +72,7 @@ class OpenHandsConfig(BaseModel):
file_store_path: str = Field(default='~/.openhands')
file_store_web_hook_url: str | None = Field(default=None)
file_store_web_hook_headers: dict | None = Field(default=None)
file_store_web_hook_batch: bool = Field(default=False)
enable_browser: bool = Field(default=True)
save_trajectory_path: str | None = Field(default=None)
save_screenshots_in_trajectory: bool = Field(default=False)
+6 -142
View File
@@ -15,15 +15,12 @@ from pydantic import BaseModel, SecretStr, ValidationError
from openhands import __version__
from openhands.core import logger
from openhands.core.config.agent_config import AgentConfig
from openhands.core.config.arg_utils import get_headless_parser
from openhands.core.config.condenser_config import (
CondenserConfig,
condenser_config_from_toml_section,
create_condenser_config,
)
from openhands.core.config.config_utils import (
OH_DEFAULT_AGENT,
OH_MAX_ITERATIONS,
)
from openhands.core.config.extended_config import ExtendedConfig
from openhands.core.config.kubernetes_config import KubernetesConfig
from openhands.core.config.llm_config import LLMConfig
@@ -674,142 +671,9 @@ def get_condenser_config_arg(
return None
# Command line arguments
def get_parser() -> argparse.ArgumentParser:
"""Get the argument parser."""
parser = argparse.ArgumentParser(description='Run the agent via CLI')
# Add version argument
parser.add_argument(
'-v', '--version', action='store_true', help='Show version information'
)
parser.add_argument(
'--config-file',
type=str,
default='config.toml',
help='Path to the config file (default: config.toml in the current directory)',
)
parser.add_argument(
'-d',
'--directory',
type=str,
help='The working directory for the agent',
)
parser.add_argument(
'-t',
'--task',
type=str,
default='',
help='The task for the agent to perform',
)
parser.add_argument(
'-f',
'--file',
type=str,
help='Path to a file containing the task. Overrides -t if both are provided.',
)
parser.add_argument(
'-c',
'--agent-cls',
default=OH_DEFAULT_AGENT,
type=str,
help='Name of the default agent to use',
)
parser.add_argument(
'-i',
'--max-iterations',
default=OH_MAX_ITERATIONS,
type=int,
help='The maximum number of iterations to run the agent',
)
parser.add_argument(
'-b',
'--max-budget-per-task',
type=float,
help='The maximum budget allowed per task, beyond which the agent will stop.',
)
# --eval configs are for evaluations only
parser.add_argument(
'--eval-output-dir',
default='evaluation/evaluation_outputs/outputs',
type=str,
help='The directory to save evaluation output',
)
parser.add_argument(
'--eval-n-limit',
default=None,
type=int,
help='The number of instances to evaluate',
)
parser.add_argument(
'--eval-num-workers',
default=4,
type=int,
help='The number of workers to use for evaluation',
)
parser.add_argument(
'--eval-note',
default=None,
type=str,
help='The note to add to the evaluation directory',
)
parser.add_argument(
'-l',
'--llm-config',
default=None,
type=str,
help='Replace default LLM ([llm] section in config.toml) config with the specified LLM config, e.g. "llama3" for [llm.llama3] section in config.toml',
)
parser.add_argument(
'--agent-config',
default=None,
type=str,
help='Replace default Agent ([agent] section in config.toml) config with the specified Agent config, e.g. "CodeAct" for [agent.CodeAct] section in config.toml',
)
parser.add_argument(
'-n',
'--name',
help='Session name',
type=str,
default='',
)
parser.add_argument(
'--eval-ids',
default=None,
type=str,
help='The comma-separated list (in quotes) of IDs of the instances to evaluate',
)
parser.add_argument(
'--no-auto-continue',
help='Disable auto-continue responses in headless mode (i.e. headless will read from stdin instead of auto-continuing)',
action='store_true',
default=False,
)
parser.add_argument(
'--selected-repo',
help='GitHub repository to clone (format: owner/repo)',
type=str,
default=None,
)
parser.add_argument(
'--override-cli-mode',
help='Override the default settings for CLI mode',
type=bool,
default=False,
)
parser.add_argument(
'--log-level',
help='Set the log level',
type=str,
default=None,
)
return parser
def parse_arguments() -> argparse.Namespace:
"""Parse command line arguments."""
parser = get_parser()
parser = get_headless_parser()
args = parser.parse_args()
if args.version:
@@ -914,17 +778,17 @@ def setup_config_from_args(args: argparse.Namespace) -> OpenHandsConfig:
)
# Override default agent if provided
if args.agent_cls:
if hasattr(args, 'agent_cls') and args.agent_cls:
config.default_agent = args.agent_cls
# Set max iterations and max budget per task if provided, otherwise fall back to config values
if args.max_iterations is not None:
if hasattr(args, 'max_iterations') and args.max_iterations is not None:
config.max_iterations = args.max_iterations
if args.max_budget_per_task is not None:
if hasattr(args, 'max_budget_per_task') and args.max_budget_per_task is not None:
config.max_budget_per_task = args.max_budget_per_task
# Read selected repository in config for use by CLI and main.py
if args.selected_repo is not None:
if hasattr(args, 'selected_repo') and args.selected_repo is not None:
config.sandbox.selected_repo = args.selected_repo
return config
+1 -1
View File
@@ -383,7 +383,7 @@ Do NOT assume the environment is the same as in the example above.
"""
example = example.lstrip()
return example
return refine_prompt(example)
IN_CONTEXT_LEARNING_EXAMPLE_PREFIX = get_example_for_tools
+4
View File
@@ -63,6 +63,7 @@ CACHE_PROMPT_SUPPORTED_MODELS = [
'claude-sonnet-4-20250514',
'claude-sonnet-4',
'claude-opus-4-20250514',
'claude-opus-4-1-20250805',
]
# function calling supporting models
@@ -77,6 +78,7 @@ FUNCTION_CALLING_SUPPORTED_MODELS = [
'claude-sonnet-4-20250514',
'claude-sonnet-4',
'claude-opus-4-20250514',
'claude-opus-4-1-20250805',
'gpt-4o-mini',
'gpt-4o',
'o1-2024-12-17',
@@ -92,6 +94,7 @@ FUNCTION_CALLING_SUPPORTED_MODELS = [
'kimi-k2-instruct',
'Qwen3-Coder-480B-A35B-Instruct',
'qwen3-coder', # this will match both qwen3-coder-480b (openhands provider) and qwen3-coder (for openrouter)
'gpt-5-2025-08-07',
]
REASONING_EFFORT_SUPPORTED_MODELS = [
@@ -105,6 +108,7 @@ REASONING_EFFORT_SUPPORTED_MODELS = [
'o4-mini-2025-04-16',
'gemini-2.5-flash',
'gemini-2.5-pro',
'gpt-5-2025-08-07',
]
MODELS_WITHOUT_STOP_WORDS = [
+1 -1
View File
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from openhands.controller.agent import Agent
from openhands.memory.memory import Memory
from mcp import McpError
@@ -20,7 +21,6 @@ from openhands.events.observation.mcp import MCPObservation
from openhands.events.observation.observation import Observation
from openhands.mcp.client import MCPClient
from openhands.mcp.error_collector import mcp_error_collector
from openhands.memory.memory import Memory
from openhands.runtime.base import Runtime
from openhands.runtime.impl.cli.cli_runtime import CLIRuntime
@@ -9,6 +9,7 @@ import docker
import httpx
import tenacity
from docker.models.containers import Container
from docker.types import DriverConfig, Mount
from openhands.core.config import OpenHandsConfig
from openhands.core.exceptions import (
@@ -258,6 +259,9 @@ class DockerRuntime(ActionExecutionClient):
container_path = parts[1]
# Default mode is 'rw' if not specified
mount_mode = parts[2] if len(parts) > 2 else 'rw'
# Skip overlay mounts here; they will be handled separately via Mount objects
if 'overlay' in mount_mode:
continue
volumes[host_path] = {
'bind': container_path,
@@ -286,6 +290,72 @@ class DockerRuntime(ActionExecutionClient):
return volumes
def _process_overlay_mounts(self) -> list[Mount]:
"""Process overlay mounts specified in sandbox.volumes with mode containing 'overlay'.
Returns:
List of docker.types.Mount objects configured with overlay driver providing
read-only lowerdir with per-container copy-on-write upper/work layers.
"""
overlay_mounts: list[Mount] = []
# No volumes configured
if self.config.sandbox.volumes is None:
return overlay_mounts
# Base directory for overlay upper/work layers from env var
overlay_base = os.environ.get('SANDBOX_VOLUME_OVERLAYS')
if not overlay_base:
# If no base path provided, skip overlay processing
return overlay_mounts
os.makedirs(overlay_base, exist_ok=True)
mount_specs = self.config.sandbox.volumes.split(',')
for idx, mount_spec in enumerate(mount_specs):
parts = mount_spec.split(':')
if len(parts) < 2:
continue
host_path = os.path.abspath(parts[0])
container_path = parts[1]
mount_mode = parts[2] if len(parts) > 2 else 'rw'
if 'overlay' not in mount_mode:
continue
# Prepare upper and work directories unique to this container and mount
overlay_dir = os.path.join(overlay_base, self.container_name, f'{idx}')
upper_dir = os.path.join(overlay_dir, 'upper')
work_dir = os.path.join(overlay_dir, 'work')
os.makedirs(upper_dir, exist_ok=True)
os.makedirs(work_dir, exist_ok=True)
driver_cfg = DriverConfig(
name='local',
options={
'type': 'overlay',
'device': 'overlay',
'o': f'lowerdir={host_path},upperdir={upper_dir},workdir={work_dir}',
},
)
mount = Mount(
target=container_path,
source='', # Anonymous volume
type='volume',
labels={
'app': 'openhands',
'role': 'worker',
'container': self.container_name,
},
driver_config=driver_cfg,
)
overlay_mounts.append(mount)
return overlay_mounts
def init_container(self) -> None:
self.log('debug', 'Preparing to start container...')
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
@@ -409,6 +479,9 @@ class DockerRuntime(ActionExecutionClient):
try:
if self.runtime_container_image is None:
raise ValueError('Runtime container image is not set')
# Process overlay mounts (read-only lower with per-container COW)
overlay_mounts = self._process_overlay_mounts()
self.container = self.docker_client.containers.run(
self.runtime_container_image,
command=command,
@@ -421,6 +494,7 @@ class DockerRuntime(ActionExecutionClient):
detach=True,
environment=environment,
volumes=volumes, # type: ignore
mounts=overlay_mounts, # type: ignore
device_requests=device_requests,
**(self.config.sandbox.docker_runtime_kwargs or {}),
)
@@ -609,7 +683,8 @@ class DockerRuntime(ActionExecutionClient):
def pause(self) -> None:
"""Pause the runtime by stopping the container.
This is different from container.stop() as it ensures environment variables are properly preserved."""
This is different from container.stop() as it ensures environment variables are properly preserved.
"""
if not self.container:
raise RuntimeError('Container not initialized')
@@ -622,7 +697,8 @@ class DockerRuntime(ActionExecutionClient):
def resume(self) -> None:
"""Resume the runtime by starting the container.
This is different from container.start() as it ensures environment variables are properly restored."""
This is different from container.start() as it ensures environment variables are properly restored.
"""
if not self.container:
raise RuntimeError('Container not initialized')
+32 -2
View File
@@ -39,7 +39,10 @@ def split_bash_commands(commands: str) -> list[str]:
f'[warning]: {traceback.format_exc()}\n'
f'The original command will be returned as is.'
)
# If parsing fails, return the original commands
# If parsing fails, check if it's a comment-only command
if _is_comment_only(commands):
# For comment-only input, return it as a single command to preserve original behavior
return [commands]
return [commands]
result: list[str] = []
@@ -75,7 +78,33 @@ def split_bash_commands(commands: str) -> list[str]:
if remaining:
result.append(remaining)
logger.debug(f'BASH PARSING result.append(remaining): {result[-1]}')
return result
# Return only non-comment commands
filtered_result = [cmd for cmd in result if not _is_comment_only(cmd)]
# Special case: if all commands are comments, return them as a single command
# This preserves the original behavior for comment-only input
if not filtered_result and result:
# Combine all comment commands into one
combined_comments = '\n'.join(result)
filtered_result = [combined_comments]
logger.debug(f'BASH PARSING final result: {result} -> {filtered_result}')
return filtered_result
def _is_comment_only(command: str) -> bool:
"""Check if a command consists only of comments.
Args:
command: The command string to check
Returns:
True if the command contains only comments, False otherwise
"""
# Split the command into lines and check if each line is a comment
lines = command.strip().split('\n')
return all(line.strip().startswith('#') for line in lines if line.strip())
def escape_bash_special_chars(command: str) -> str:
@@ -501,6 +530,7 @@ class BashSession:
# Check if the command is a single command or multiple commands
splited_commands = split_bash_commands(command)
if len(splited_commands) > 1:
return ErrorObservation(
content=(
+5
View File
@@ -280,6 +280,11 @@ def prep_build_folder(
),
)
# Copy the 'microagents' directory (Microagents)
shutil.copytree(
Path(project_root, 'microagents'), Path(build_folder, 'code', 'microagents')
)
# Copy pyproject.toml and poetry.lock files
for file in ['pyproject.toml', 'poetry.lock']:
src = Path(openhands_source_dir, file)
@@ -239,7 +239,8 @@ COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code/
# ================================================================
RUN if [ -d /openhands/code/openhands ]; then rm -rf /openhands/code/openhands; fi
COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code/
RUN if [ -d /openhands/code/microagents ]; then rm -rf /openhands/code/microagents; fi
COPY ./code/microagents /openhands/code/microagents
COPY ./code/openhands /openhands/code/openhands
RUN chmod a+rwx /openhands/code/openhands/__init__.py
+17 -17
View File
@@ -10,17 +10,18 @@ from jinja2 import Environment, FileSystemLoader
from pydantic import BaseModel, ConfigDict, Field
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.mcp_config import MCPConfig
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import (
ChangeAgentStateAction,
NullAction,
)
from openhands.events.event_filter import EventFilter
from openhands.events.event_store import EventStore
from openhands.events.observation import (
AgentStateChangedObservation,
NullObservation,
)
from openhands.events.stream import EventStream
from openhands.integrations.provider import (
PROVIDER_TOKEN_TYPE,
ProviderHandler,
@@ -44,11 +45,11 @@ from openhands.server.services.conversation_service import (
create_new_conversation,
setup_init_convo_settings,
)
from openhands.server.session.conversation import ServerConversation
from openhands.server.shared import (
ConversationStoreImpl,
config,
conversation_manager,
file_store,
)
from openhands.server.types import LLMAuthenticationError, MissingSettingsError
from openhands.server.user_auth import (
@@ -60,7 +61,7 @@ from openhands.server.user_auth import (
get_user_settings_store,
)
from openhands.server.user_auth.user_auth import AuthType
from openhands.server.utils import get_conversation as get_conversation_object
from openhands.server.utils import get_conversation as get_conversation_metadata
from openhands.server.utils import get_conversation_store
from openhands.storage.conversation.conversation_store import ConversationStore
from openhands.storage.data_models.conversation_metadata import (
@@ -87,6 +88,7 @@ class InitSessionRequest(BaseModel):
suggested_task: SuggestedTask | None = None
create_microagent: CreateMicroagent | None = None
conversation_instructions: str | None = None
mcp_config: MCPConfig | None = None
# Only nested runtimes require the ability to specify a conversation id, and it could be a security risk
if os.getenv('ALLOW_SET_CONVERSATION_ID', '0') == '1':
conversation_id: str = Field(default_factory=lambda: uuid.uuid4().hex)
@@ -178,6 +180,7 @@ async def new_conversation(
conversation_instructions=conversation_instructions,
git_provider=git_provider,
conversation_id=conversation_id,
mcp_config=data.mcp_config,
)
return ConversationResponse(
@@ -331,23 +334,20 @@ async def delete_conversation(
return True
@app.get('/conversations/{conversation_id}/remember_prompt')
@app.get('/conversations/{conversation_id}/remember-prompt')
async def get_prompt(
conversation_id: str,
event_id: int,
user_settings: SettingsStore = Depends(get_user_settings_store),
conversation: ServerConversation | None = Depends(get_conversation_object),
metadata: ConversationMetadata = Depends(get_conversation_metadata),
):
if conversation is None:
return JSONResponse(
status_code=404,
content={'error': 'Conversation not found.'},
)
# get event stream for the conversation
event_stream = conversation.event_stream
# get event store for the conversation
event_store = EventStore(
sid=conversation_id, file_store=file_store, user_id=metadata.user_id
)
# retrieve the relevant events
stringified_events = _get_contextual_events(event_stream, event_id)
stringified_events = _get_contextual_events(event_store, event_id)
# generate a prompt
settings = await user_settings.load()
@@ -551,7 +551,7 @@ async def stop_conversation(
)
def _get_contextual_events(event_stream: EventStream, event_id: int) -> str:
def _get_contextual_events(event_store: EventStore, event_id: int) -> str:
# find the specified events to learn from
# Get X events around the target event
context_size = 4
@@ -567,7 +567,7 @@ def _get_contextual_events(event_stream: EventStream, event_id: int) -> str:
) # the types of events that can be in an agent's history
# from event_id - context_size to event_id..
context_before = event_stream.search_events(
context_before = event_store.search_events(
start_id=event_id,
filter=agent_event_filter,
reverse=True,
@@ -575,7 +575,7 @@ def _get_contextual_events(event_stream: EventStream, event_id: int) -> str:
)
# from event_id to event_id + context_size + 1
context_after = event_stream.search_events(
context_after = event_store.search_events(
start_id=event_id + 1,
filter=agent_event_filter,
limit=context_size + 1,
@@ -2,6 +2,7 @@ import uuid
from types import MappingProxyType
from typing import Any
from openhands.core.config.mcp_config import MCPConfig
from openhands.core.logger import openhands_logger as logger
from openhands.events.action.message import MessageAction
from openhands.experiments.experiment_manager import ExperimentManagerImpl
@@ -44,6 +45,7 @@ async def create_new_conversation(
attach_convo_id: bool = False,
git_provider: ProviderType | None = None,
conversation_id: str | None = None,
mcp_config: MCPConfig | None = None,
) -> AgentLoopInfo:
logger.info(
'Creating conversation',
@@ -82,6 +84,9 @@ async def create_new_conversation(
session_init_args['selected_branch'] = selected_branch
session_init_args['git_provider'] = git_provider
session_init_args['conversation_instructions'] = conversation_instructions
if mcp_config:
session_init_args['mcp_config'] = mcp_config
conversation_init_data = ConversationInitData(**session_init_args)
logger.info('Loading conversation store')
+15 -5
View File
@@ -124,10 +124,12 @@ class Session:
)
# Set Git user configuration if provided in settings
if hasattr(settings, 'git_user_name') and settings.git_user_name:
self.config.git_user_name = settings.git_user_name
if hasattr(settings, 'git_user_email') and settings.git_user_email:
self.config.git_user_email = settings.git_user_email
git_user_name = getattr(settings, 'git_user_name', None)
if git_user_name is not None:
self.config.git_user_name = git_user_name
git_user_email = getattr(settings, 'git_user_email', None)
if git_user_email is not None:
self.config.git_user_email = git_user_email
max_iterations = settings.max_iterations or self.config.max_iterations
# Prioritize settings over config for max_budget_per_task
@@ -152,6 +154,14 @@ class Session:
self.logger.debug(
f'MCP configuration before setup - self.config.mcp_config: {self.config.mcp}'
)
# Check if settings has custom mcp_config
mcp_config = getattr(settings, 'mcp_config', None)
if mcp_config is not None:
# Use the provided MCP SHTTP servers instead of default setup
self.config.mcp = self.config.mcp.merge(mcp_config)
self.logger.debug(f'Merged custom MCP Config: {mcp_config}')
# Add OpenHands' MCP server by default
openhands_mcp_server, openhands_mcp_stdio_servers = (
OpenHandsMCPConfigImpl.create_default_mcp_server_config(
@@ -163,7 +173,7 @@ class Session:
self.config.mcp.shttp_servers.append(openhands_mcp_server)
self.logger.debug('Added default MCP HTTP server to config')
self.config.mcp.stdio_servers.extend(openhands_mcp_stdio_servers)
self.config.mcp.stdio_servers.extend(openhands_mcp_stdio_servers)
self.logger.debug(
f'MCP configuration after setup - self.config.mcp: {self.config.mcp}'
+5 -4
View File
@@ -27,10 +27,11 @@ assert isinstance(server_config_interface, ServerConfig), (
)
server_config: ServerConfig = server_config_interface
file_store: FileStore = get_file_store(
config.file_store,
config.file_store_path,
config.file_store_web_hook_url,
config.file_store_web_hook_headers,
file_store_type=config.file_store,
file_store_path=config.file_store_path,
file_store_web_hook_url=config.file_store_web_hook_url,
file_store_web_hook_headers=config.file_store_web_hook_headers,
file_store_web_hook_batch=config.file_store_web_hook_batch,
)
client_manager = None
+34
View File
@@ -61,9 +61,12 @@ The `WebHookFileStore` wraps another `FileStore` implementation and sends HTTP r
**Configuration Options:**
- `file_store_web_hook_url`: The base URL for webhook requests
- `file_store_web_hook_headers`: HTTP headers to include in webhook requests
- `file_store_web_hook_batch`: Whether to use batched webhook requests (default: false)
### Protocol Details
#### Standard Webhook Protocol (Non-Batched)
1. **File Write Operation**:
- When a file is written, a POST request is sent to `{base_url}{path}`
- The request body contains the file contents
@@ -73,6 +76,27 @@ The `WebHookFileStore` wraps another `FileStore` implementation and sends HTTP r
- When a file is deleted, a DELETE request is sent to `{base_url}{path}`
- The operation is retried up to 3 times with a 1-second delay between attempts
#### Batched Webhook Protocol
The `BatchedWebHookFileStore` extends the webhook functionality by batching multiple file operations into a single request, which can significantly improve performance when many files are being modified in a short period of time.
1. **Batch Request**:
- A single POST request is sent to `{base_url}` with a JSON array in the body
- Each item in the array contains:
- `method`: "POST" for write operations, "DELETE" for delete operations
- `path`: The file path
- `content`: The file contents (for write operations only)
- `encoding`: "base64" if binary content was base64-encoded (optional)
2. **Batch Triggering**:
- Batches are sent when one of the following conditions is met:
- A timeout period has elapsed (defaults to 5 seconds, configurable via constructor parameter)
- The total size of batched content exceeds a size limit (defaults to 1MB, configurable via constructor parameter)
- The `flush()` method is explicitly called
3. **Error Handling**:
- The batch request is retried up to 3 times with a 1-second delay between attempts
## Configuration
To configure the storage module in OpenHands, use the following configuration options:
@@ -90,4 +114,14 @@ file_store_web_hook_url = "https://example.com/api/files"
# Optional webhook headers (JSON string)
file_store_web_hook_headers = '{"Authorization": "Bearer token"}'
# Optional batched webhook mode (default: false)
file_store_web_hook_batch = true
```
**Batched Webhook Configuration:**
The batched webhook behavior uses predefined constants with the following default values:
- Batch timeout: 5 seconds
- Batch size limit: 1MB (1048576 bytes)
These values can be customized by passing `batch_timeout_seconds` and `batch_size_limit_bytes` parameters to the `BatchedWebHookFileStore` constructor.
+19 -5
View File
@@ -2,6 +2,7 @@ import os
import httpx
from openhands.storage.batched_web_hook import BatchedWebHookFileStore
from openhands.storage.files import FileStore
from openhands.storage.google_cloud import GoogleCloudFileStore
from openhands.storage.local import LocalFileStore
@@ -15,6 +16,7 @@ def get_file_store(
file_store_path: str | None = None,
file_store_web_hook_url: str | None = None,
file_store_web_hook_headers: dict | None = None,
file_store_web_hook_batch: bool = False,
) -> FileStore:
store: FileStore
if file_store_type == 'local':
@@ -35,9 +37,21 @@ def get_file_store(
file_store_web_hook_headers['X-Session-API-Key'] = os.getenv(
'SESSION_API_KEY'
)
store = WebHookFileStore(
store,
file_store_web_hook_url,
httpx.Client(headers=file_store_web_hook_headers or {}),
)
client = httpx.Client(headers=file_store_web_hook_headers or {})
if file_store_web_hook_batch:
# Use batched webhook file store
store = BatchedWebHookFileStore(
store,
file_store_web_hook_url,
client,
)
else:
# Use regular webhook file store
store = WebHookFileStore(
store,
file_store_web_hook_url,
client,
)
return store
+274
View File
@@ -0,0 +1,274 @@
import threading
from typing import Optional, Union
import httpx
import tenacity
from openhands.storage.files import FileStore
from openhands.utils.async_utils import EXECUTOR
# Constants for batching configuration
WEBHOOK_BATCH_TIMEOUT_SECONDS = 5.0
WEBHOOK_BATCH_SIZE_LIMIT_BYTES = 1048576 # 1MB
class BatchedWebHookFileStore(FileStore):
"""
File store which batches updates before sending them to a webhook.
This class wraps another FileStore implementation and sends HTTP requests
to a specified URL when files are written or deleted. Updates are batched
and sent together after a certain amount of time passes or if the content
size exceeds a threshold.
Attributes:
file_store: The underlying FileStore implementation
base_url: The base URL for webhook requests
client: The HTTP client used to make webhook requests
batch_timeout_seconds: Time in seconds after which a batch is sent (default: WEBHOOK_BATCH_TIMEOUT_SECONDS)
batch_size_limit_bytes: Size limit in bytes after which a batch is sent (default: WEBHOOK_BATCH_SIZE_LIMIT_BYTES)
_batch_lock: Lock for thread-safe access to the batch
_batch: Dictionary of pending file updates
_batch_timer: Timer for sending batches after timeout
_batch_size: Current size of the batch in bytes
"""
file_store: FileStore
base_url: str
client: httpx.Client
batch_timeout_seconds: float
batch_size_limit_bytes: int
_batch_lock: threading.Lock
_batch: dict[str, tuple[str, Optional[Union[str, bytes]]]]
_batch_timer: Optional[threading.Timer]
_batch_size: int
def __init__(
self,
file_store: FileStore,
base_url: str,
client: Optional[httpx.Client] = None,
batch_timeout_seconds: Optional[float] = None,
batch_size_limit_bytes: Optional[int] = None,
):
"""
Initialize a BatchedWebHookFileStore.
Args:
file_store: The underlying FileStore implementation
base_url: The base URL for webhook requests
client: Optional HTTP client to use for requests. If None, a new client will be created.
batch_timeout_seconds: Time in seconds after which a batch is sent.
If None, uses the default constant WEBHOOK_BATCH_TIMEOUT_SECONDS.
batch_size_limit_bytes: Size limit in bytes after which a batch is sent.
If None, uses the default constant WEBHOOK_BATCH_SIZE_LIMIT_BYTES.
"""
self.file_store = file_store
self.base_url = base_url
if client is None:
client = httpx.Client()
self.client = client
# Use provided values or default constants
self.batch_timeout_seconds = (
batch_timeout_seconds or WEBHOOK_BATCH_TIMEOUT_SECONDS
)
self.batch_size_limit_bytes = (
batch_size_limit_bytes or WEBHOOK_BATCH_SIZE_LIMIT_BYTES
)
# Initialize batch state
self._batch_lock = threading.Lock()
self._batch = {} # Maps path -> (operation, content)
self._batch_timer = None
self._batch_size = 0
def write(self, path: str, contents: Union[str, bytes]) -> None:
"""
Write contents to a file and queue a webhook update.
Args:
path: The path to write to
contents: The contents to write
"""
self.file_store.write(path, contents)
self._queue_update(path, 'write', contents)
def read(self, path: str) -> str:
"""
Read contents from a file.
Args:
path: The path to read from
Returns:
The contents of the file
"""
return self.file_store.read(path)
def list(self, path: str) -> list[str]:
"""
List files in a directory.
Args:
path: The directory path to list
Returns:
A list of file paths
"""
return self.file_store.list(path)
def delete(self, path: str) -> None:
"""
Delete a file and queue a webhook update.
Args:
path: The path to delete
"""
self.file_store.delete(path)
self._queue_update(path, 'delete', None)
def _queue_update(
self, path: str, operation: str, contents: Optional[Union[str, bytes]]
) -> None:
"""
Queue an update to be sent to the webhook.
Args:
path: The path that was modified
operation: The operation performed ("write" or "delete")
contents: The contents that were written (None for delete operations)
"""
with self._batch_lock:
# Calculate content size
content_size = 0
if contents is not None:
if isinstance(contents, str):
content_size = len(contents.encode('utf-8'))
else:
content_size = len(contents)
# Update batch size calculation
# If this path already exists in the batch, subtract its previous size
if path in self._batch:
prev_op, prev_contents = self._batch[path]
if prev_contents is not None:
if isinstance(prev_contents, str):
self._batch_size -= len(prev_contents.encode('utf-8'))
else:
self._batch_size -= len(prev_contents)
# Add new content size
self._batch_size += content_size
# Add to batch
self._batch[path] = (operation, contents)
# Check if we need to send the batch due to size limit
if self._batch_size >= self.batch_size_limit_bytes:
# Submit to executor to avoid blocking
EXECUTOR.submit(self._send_batch)
return
# Start or reset the timer for sending the batch
if self._batch_timer is not None:
self._batch_timer.cancel()
self._batch_timer = None
timer = threading.Timer(
self.batch_timeout_seconds, self._send_batch_from_timer
)
timer.daemon = True
timer.start()
self._batch_timer = timer
def _send_batch_from_timer(self) -> None:
"""
Send the batch from the timer thread.
This method is called by the timer and submits the actual sending to the executor.
"""
EXECUTOR.submit(self._send_batch)
def _send_batch(self) -> None:
"""
Send the current batch of updates to the webhook as a single request.
This method acquires the batch lock and processes all pending updates in one batch.
"""
batch_to_send: dict[str, tuple[str, Optional[Union[str, bytes]]]] = {}
with self._batch_lock:
if not self._batch:
return
# Copy the batch and clear the current one
batch_to_send = self._batch.copy()
self._batch.clear()
self._batch_size = 0
# Cancel any pending timer
if self._batch_timer is not None:
self._batch_timer.cancel()
self._batch_timer = None
# Process the entire batch in a single request
if batch_to_send:
try:
self._send_batch_request(batch_to_send)
except Exception as e:
# Log the error
print(f'Error sending webhook batch: {e}')
@tenacity.retry(
wait=tenacity.wait_fixed(1),
stop=tenacity.stop_after_attempt(3),
)
def _send_batch_request(
self, batch: dict[str, tuple[str, Optional[Union[str, bytes]]]]
) -> None:
"""
Send a single batch request to the webhook URL with all updates.
This method is retried up to 3 times with a 1-second delay between attempts.
Args:
batch: Dictionary mapping paths to (operation, contents) tuples
Raises:
httpx.HTTPStatusError: If the webhook request fails
"""
# Prepare the batch payload
batch_payload = []
for path, (operation, contents) in batch.items():
item = {
'method': 'POST' if operation == 'write' else 'DELETE',
'path': path,
}
if operation == 'write' and contents is not None:
# Convert bytes to string if needed
if isinstance(contents, bytes):
try:
# Try to decode as UTF-8
item['content'] = contents.decode('utf-8')
except UnicodeDecodeError:
# If not UTF-8, use base64 encoding
import base64
item['content'] = base64.b64encode(contents).decode('ascii')
item['encoding'] = 'base64'
else:
item['content'] = contents
batch_payload.append(item)
# Send the batch as a single request
response = self.client.post(self.base_url, json=batch_payload)
response.raise_for_status()
def flush(self) -> None:
"""
Immediately send any pending updates to the webhook.
This can be called to ensure all updates are sent before shutting down.
"""
self._send_batch()
@@ -106,10 +106,11 @@ class FileConversationStore(ConversationStore):
cls, config: OpenHandsConfig, user_id: str | None
) -> FileConversationStore:
file_store = get_file_store(
config.file_store,
config.file_store_path,
config.file_store_web_hook_url,
config.file_store_web_hook_headers,
file_store_type=config.file_store,
file_store_path=config.file_store_path,
file_store_web_hook_url=config.file_store_web_hook_url,
file_store_web_hook_headers=config.file_store_web_hook_headers,
file_store_web_hook_batch=config.file_store_web_hook_batch,
)
return FileConversationStore(file_store)

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