Compare commits

..

7 Commits

Author SHA1 Message Date
openhands
eb348a5f3d Remove KeyboardInterrupt exit behavior from main chat loop
- Change KeyboardInterrupt handler to continue loop instead of exiting
- Let signal handler manage Ctrl+C behavior completely
- Only exit on explicit /exit command or outer KeyboardInterrupt

This ensures that Ctrl+C during agent processing returns to chat loop
instead of exiting the entire application.

Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-03 17:09:16 +00:00
openhands
099dcb787f Fix Ctrl+C behavior to return to chat loop instead of exiting
- Remove os._exit(1) from second Ctrl+C handler
- Reset Ctrl+C counter after force killing process
- Add graceful handling in SimpleProcessRunner for killed processes
- Show user-friendly message that they can continue sending messages

This allows users to stop a running agent and continue with new messages
instead of having to restart the entire CLI application.

Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-03 17:07:47 +00:00
openhands
b3034a0d75 Fix multiprocessing serialization issues in SimpleProcessRunner
- Pass conversation_id and message_data instead of full objects to subprocess
- Recreate conversation and message objects in the subprocess
- Extract text content from Message objects for serialization
- Store conversation_id as string for subprocess recreation

This fixes the 'cannot pickle _asyncio.Future object' error by avoiding
passing non-serializable objects between processes.

Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-03 16:59:29 +00:00
openhands
459e224d37 Fix Message creation to include required role field
- Add role='user' to Message constructor in agent_chat.py
- This fixes the validation error when processing user messages

Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-03 16:57:02 +00:00
openhands
97f13b7100 Fix SimpleProcessRunner to use proper SDK imports
- Replace incorrect openhands.core.main imports with openhands.sdk
- Use existing ConversationRunner from runner.py instead of run_controller
- Update SimpleProcessRunner to accept BaseConversation instead of setup function
- Update agent_chat.py to create conversation first, then pass to SimpleProcessRunner
- Fix process_message to use proper Message object with TextContent

This ensures the openhands-cli remains standalone and only uses the SDK library
as intended, without importing from the main OpenHands codebase.

Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-03 16:51:47 +00:00
openhands
6ecaca5b3c Simplify Ctrl+C handling implementation
- Replace complex ProcessSignalHandler with SimpleSignalHandler
  - Direct signal handling in main process instead of queue communication
  - Simple Ctrl+C counting with immediate force kill on second press
  - Reset functionality to clear count when starting new operations

- Replace ProcessBasedConversationRunner with SimpleProcessRunner
  - Minimal multiprocessing - only process_message runs in subprocess
  - Direct method calls for status, settings, and other operations
  - No unnecessary queue communication

- Update agent_chat.py to use simplified components
  - Reset Ctrl+C count when starting new message processing
  - Direct method calls for commands that don't need process isolation
  - Cleaner error handling and resource cleanup

- Update simple_main.py imports

Fixes issues where second Ctrl+C wouldn't register properly due to
complex queue-based communication and race conditions.

Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-03 16:46:43 +00:00
openhands
5351702d3a Implement improved Ctrl+C handling for OpenHands CLI
- First Ctrl+C attempts graceful pause of agent
- Second Ctrl+C (within 3 seconds) kills process immediately
- Added SignalHandler and ProcessSignalHandler classes for signal management
- Implemented ProcessBasedConversationRunner for separate process execution
- Modified pause_listener to remove Ctrl+C handling (now handled by signal handler)
- Updated agent_chat.py to use process-based runner with new signal management
- Updated simple_main.py to install basic signal handler
- Added comprehensive test script and documentation

Co-authored-by: openhands <openhands@all-hands.dev>
2025-11-03 16:24:23 +00:00
422 changed files with 12400 additions and 17545 deletions

View File

@@ -1 +0,0 @@
This way of running OpenHands is not officially supported. It is maintained by the community.

View File

@@ -7,8 +7,5 @@ git config --global --add safe.directory "$(realpath .)"
# Install `nc`
sudo apt update && sudo apt install netcat -y
# Install `uv` and `uvx`
wget -qO- https://astral.sh/uv/install.sh | sh
# Do common setup tasks
source .openhands/setup.sh

View File

@@ -13,7 +13,6 @@
- [ ] Other (dependency update, docs, typo fixes, etc.)
## Checklist
<!-- AI/LLM AGENTS: This checklist is for a human author to complete. Do NOT check either of the two boxes below. Leave them unchecked until a human has personally reviewed and tested the changes. -->
- [ ] I have read and reviewed the code and I understand what the code is doing.
- [ ] I have tested the code to the best of my ability and ensured it works as expected.

73
.github/scripts/check_version_consistency.py vendored Executable file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
import os
import re
import sys
def find_version_references(directory: str) -> tuple[set[str], set[str]]:
openhands_versions = set()
runtime_versions = set()
version_pattern_openhands = re.compile(r'openhands:(\d{1})\.(\d{2})')
version_pattern_runtime = re.compile(r'runtime:(\d{1})\.(\d{2})')
for root, _, files in os.walk(directory):
# Skip .git directory and docs/build directory
if '.git' in root or 'docs/build' in root:
continue
for file in files:
if file.endswith(
('.md', '.yml', '.yaml', '.txt', '.html', '.py', '.js', '.ts')
):
file_path = os.path.join(root, file)
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Find all openhands version references
matches = version_pattern_openhands.findall(content)
if matches:
print(f'Found openhands version {matches} in {file_path}')
openhands_versions.update(matches)
# Find all runtime version references
matches = version_pattern_runtime.findall(content)
if matches:
print(f'Found runtime version {matches} in {file_path}')
runtime_versions.update(matches)
except Exception as e:
print(f'Error reading {file_path}: {e}', file=sys.stderr)
return openhands_versions, runtime_versions
def main():
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
print(f'Checking version consistency in {repo_root}')
openhands_versions, runtime_versions = find_version_references(repo_root)
print(f'Found openhands versions: {sorted(openhands_versions)}')
print(f'Found runtime versions: {sorted(runtime_versions)}')
exit_code = 0
if len(openhands_versions) > 1:
print('Error: Multiple openhands versions found:', file=sys.stderr)
print('Found versions:', sorted(openhands_versions), file=sys.stderr)
exit_code = 1
elif len(openhands_versions) == 0:
print('Warning: No openhands version references found', file=sys.stderr)
if len(runtime_versions) > 1:
print('Error: Multiple runtime versions found:', file=sys.stderr)
print('Found versions:', sorted(runtime_versions), file=sys.stderr)
exit_code = 1
elif len(runtime_versions) == 0:
print('Warning: No runtime version references found', file=sys.stderr)
sys.exit(exit_code)
if __name__ == '__main__':
main()

View File

@@ -17,6 +17,9 @@ DOCKER_RUN_COMMAND="docker run -it --rm \
--name openhands-app-${SHORT_SHA} \
docker.openhands.dev/openhands/openhands:${SHORT_SHA}"
# Define the uvx command
UVX_RUN_COMMAND="uvx --python 3.12 --from git+https://github.com/OpenHands/OpenHands@${BRANCH_NAME}#subdirectory=openhands-cli openhands"
# Get the current PR body
PR_BODY=$(gh pr view "$PR_NUMBER" --json body --jq .body)
@@ -34,6 +37,11 @@ GUI with Docker:
\`\`\`
${DOCKER_RUN_COMMAND}
\`\`\`
CLI with uvx:
\`\`\`
${UVX_RUN_COMMAND}
\`\`\`
EOF
)
else
@@ -49,6 +57,11 @@ GUI with Docker:
\`\`\`
${DOCKER_RUN_COMMAND}
\`\`\`
CLI with uvx:
\`\`\`
${UVX_RUN_COMMAND}
\`\`\`
EOF
)
fi

View File

@@ -1,65 +0,0 @@
name: Check Package Versions
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
check-package-versions:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Check for any 'rev' fields in pyproject.toml
run: |
python - <<'PY'
import sys, tomllib, pathlib
path = pathlib.Path("pyproject.toml")
if not path.exists():
print("❌ ERROR: pyproject.toml not found")
sys.exit(1)
try:
data = tomllib.loads(path.read_text(encoding="utf-8"))
except Exception as e:
print(f"❌ ERROR: Failed to parse pyproject.toml: {e}")
sys.exit(1)
poetry = data.get("tool", {}).get("poetry", {})
sections = {
"dependencies": poetry.get("dependencies", {}),
}
errors = []
print("🔍 Checking for any dependencies with 'rev' fields...\n")
for section_name, deps in sections.items():
if not isinstance(deps, dict):
continue
for pkg_name, cfg in deps.items():
if isinstance(cfg, dict) and "rev" in cfg:
msg = f" ✖ {pkg_name} in [{section_name}] uses rev='{cfg['rev']}' (NOT ALLOWED)"
print(msg)
errors.append(msg)
else:
print(f" • {pkg_name}: OK")
if errors:
print("\n❌ FAILED: Found dependencies using 'rev' fields:\n" + "\n".join(errors))
print("\nPlease use versioned releases instead, e.g.:")
print(' my-package = "1.0.0"')
sys.exit(1)
print("\n✅ SUCCESS: No 'rev' fields found. All dependencies are using proper versioned releases.")
PY

69
.github/workflows/clean-up.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
# Workflow that cleans up outdated and old workflows to prevent out of disk issues
name: Delete old workflow runs
# This workflow is currently only triggered manually
on:
workflow_dispatch:
inputs:
days:
description: 'Days-worth of runs to keep for each workflow'
required: true
default: '30'
minimum_runs:
description: 'Minimum runs to keep for each workflow'
required: true
default: '10'
delete_workflow_pattern:
description: 'Name or filename of the workflow (if not set, all workflows are targeted)'
required: false
delete_workflow_by_state_pattern:
description: 'Filter workflows by state: active, deleted, disabled_fork, disabled_inactivity, disabled_manually'
required: true
default: "ALL"
type: choice
options:
- "ALL"
- active
- deleted
- disabled_inactivity
- disabled_manually
delete_run_by_conclusion_pattern:
description: 'Remove runs based on conclusion: action_required, cancelled, failure, skipped, success'
required: true
default: 'ALL'
type: choice
options:
- 'ALL'
- 'Unsuccessful: action_required,cancelled,failure,skipped'
- action_required
- cancelled
- failure
- skipped
- success
dry_run:
description: 'Logs simulated changes, no deletions are performed'
required: false
jobs:
del_runs:
runs-on: blacksmith-4vcpu-ubuntu-2204
permissions:
actions: write
contents: read
steps:
- name: Delete workflow runs
uses: Mattraks/delete-workflow-runs@v2
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: ${{ github.event.inputs.days }}
keep_minimum_runs: ${{ github.event.inputs.minimum_runs }}
delete_workflow_pattern: ${{ github.event.inputs.delete_workflow_pattern }}
delete_workflow_by_state_pattern: ${{ github.event.inputs.delete_workflow_by_state_pattern }}
delete_run_by_conclusion_pattern: >-
${{
startsWith(github.event.inputs.delete_run_by_conclusion_pattern, 'Unsuccessful:')
&& 'action_required,cancelled,failure,skipped'
|| github.event.inputs.delete_run_by_conclusion_pattern
}}
dry_run: ${{ github.event.inputs.dry_run }}

View File

@@ -0,0 +1,122 @@
# Workflow that builds and tests the CLI binary executable
name: CLI - Build binary and optionally release
# Run on pushes to main branch and CLI tags, and on pull requests when CLI files change
on:
push:
branches:
- main
tags:
- "*-cli"
pull_request:
paths:
- "openhands-cli/**"
permissions:
contents: write # needed to create releases or upload assets
# Cancel previous runs if a new commit is pushed
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
build-binary:
name: Build binary executable
strategy:
matrix:
include:
# Build on Ubuntu 22.04 for maximum GLIBC compatibility (GLIBC 2.31)
- os: ubuntu-22.04
platform: linux
artifact_name: openhands-cli-linux
# Build on macOS for macOS users
- os: macos-15
platform: macos
artifact_name: openhands-cli-macos
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Install dependencies
working-directory: openhands-cli
run: |
uv sync
- name: Build binary executable
working-directory: openhands-cli
run: |
./build.sh --install-pyinstaller | tee output.log
echo "Full output:"
cat output.log
if grep -q "❌" output.log; then
echo "❌ Found failure marker in output"
exit 1
fi
echo "✅ Build & test finished without ❌ markers"
- name: Verify binary files exist
run: |
if ! ls openhands-cli/dist/openhands* 1> /dev/null 2>&1; then
echo "❌ No binaries found to upload!"
exit 1
fi
echo "✅ Found binaries to upload."
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: openhands-cli/dist/openhands*
retention-days: 30
create-github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: build-binary
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release assets
run: |
mkdir -p release-assets
# Copy binaries with appropriate names for release
if [ -f artifacts/openhands-cli-linux/openhands ]; then
cp artifacts/openhands-cli-linux/openhands release-assets/openhands-linux
fi
if [ -f artifacts/openhands-cli-macos/openhands ]; then
cp artifacts/openhands-cli-macos/openhands release-assets/openhands-macos
fi
ls -la release-assets/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: release-assets/*
draft: true
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

23
.github/workflows/dispatch-to-docs.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Dispatch to docs repo
on:
push:
branches: [main]
paths:
- 'docs/**'
workflow_dispatch:
jobs:
dispatch:
runs-on: ubuntu-latest
strategy:
matrix:
repo: ["OpenHands/docs"]
steps:
- name: Push to docs repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.ALLHANDS_BOT_GITHUB_PAT }}
repository: ${{ matrix.repo }}
event-type: update
client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "module": "openhands", "branch": "main"}'

View File

@@ -86,7 +86,7 @@ jobs:
# Builds the runtime Docker images
ghcr_build_runtime:
name: Build Runtime Image
name: Build Image
runs-on: blacksmith-8vcpu-ubuntu-2204
if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ext-v'))"
permissions:
@@ -256,7 +256,7 @@ jobs:
test_runtime_root:
name: RT Unit Tests (Root)
needs: [ghcr_build_runtime, define-matrix]
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-8vcpu-ubuntu-2204
strategy:
fail-fast: false
matrix:
@@ -298,7 +298,7 @@ jobs:
# We install pytest-xdist in order to run tests across CPUs
poetry run pip install pytest-xdist
# Install to be able to retry on failures for flakey tests
# Install to be able to retry on failures for flaky tests
poetry run pip install pytest-rerunfailures
image_name=ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
@@ -311,14 +311,14 @@ jobs:
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
RUN_AS_OPENHANDS=false \
poetry run pytest -n 5 -raRs --reruns 2 --reruns-delay 3 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
poetry run pytest -n 0 -raRs --reruns 2 --reruns-delay 5 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
env:
DEBUG: "1"
# Run unit tests with the Docker runtime Docker images as openhands user
test_runtime_oh:
name: RT Unit Tests (openhands)
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-8vcpu-ubuntu-2204
needs: [ghcr_build_runtime, define-matrix]
strategy:
matrix:
@@ -370,7 +370,7 @@ jobs:
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
RUN_AS_OPENHANDS=true \
poetry run pytest -n 5 -raRs --reruns 2 --reruns-delay 3 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
poetry run pytest -n 0 -raRs --reruns 2 --reruns-delay 5 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
env:
DEBUG: "1"

199
.github/workflows/integration-runner.yml vendored Normal file
View File

@@ -0,0 +1,199 @@
name: Run Integration Tests
on:
pull_request:
types: [labeled]
workflow_dispatch:
inputs:
reason:
description: 'Reason for manual trigger'
required: true
default: ''
schedule:
- cron: '30 22 * * *' # Runs at 10:30pm UTC every day
env:
N_PROCESSES: 10 # Global configuration for number of parallel processes for evaluation
jobs:
run-integration-tests:
if: github.event.label.name == 'integration-test' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
runs-on: blacksmith-4vcpu-ubuntu-2204
permissions:
contents: "read"
id-token: "write"
pull-requests: "write"
issues: "write"
strategy:
matrix:
python-version: ["3.12"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Setup Node.js
uses: useblacksmith/setup-node@v5
with:
node-version: '22.x'
- name: Comment on PR if 'integration-test' label is present
if: github.event_name == 'pull_request' && github.event.label.name == 'integration-test'
uses: KeisukeYamashita/create-comment@v1
with:
unique: false
comment: |
Hi! I started running the integration tests on your PR. You will receive a comment with the results shortly.
- name: Install Python dependencies using Poetry
run: poetry install --with dev,test,runtime,evaluation
- name: Configure config.toml for testing with Haiku
env:
LLM_MODEL: "litellm_proxy/claude-3-5-haiku-20241022"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MAX_ITERATIONS: 10
run: |
echo "[llm.eval]" > config.toml
echo "model = \"$LLM_MODEL\"" >> config.toml
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
echo "temperature = 0.0" >> config.toml
- name: Build environment
run: make build
- name: Run integration test evaluation for Haiku
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
run: |
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD CodeActAgent '' 10 $N_PROCESSES '' 'haiku_run'
# get integration tests report
REPORT_FILE_HAIKU=$(find evaluation/evaluation_outputs/outputs/integration_tests/CodeActAgent/*haiku*_maxiter_10_N* -name "report.md" -type f | head -n 1)
echo "REPORT_FILE: $REPORT_FILE_HAIKU"
echo "INTEGRATION_TEST_REPORT_HAIKU<<EOF" >> $GITHUB_ENV
cat $REPORT_FILE_HAIKU >> $GITHUB_ENV
echo >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Wait a little bit
run: sleep 10
- name: Configure config.toml for testing with DeepSeek
env:
LLM_MODEL: "litellm_proxy/deepseek-chat"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MAX_ITERATIONS: 10
run: |
echo "[llm.eval]" > config.toml
echo "model = \"$LLM_MODEL\"" >> config.toml
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
echo "temperature = 0.0" >> config.toml
- name: Run integration test evaluation for DeepSeek
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
run: |
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD CodeActAgent '' 10 $N_PROCESSES '' 'deepseek_run'
# get integration tests report
REPORT_FILE_DEEPSEEK=$(find evaluation/evaluation_outputs/outputs/integration_tests/CodeActAgent/deepseek*_maxiter_10_N* -name "report.md" -type f | head -n 1)
echo "REPORT_FILE: $REPORT_FILE_DEEPSEEK"
echo "INTEGRATION_TEST_REPORT_DEEPSEEK<<EOF" >> $GITHUB_ENV
cat $REPORT_FILE_DEEPSEEK >> $GITHUB_ENV
echo >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
# -------------------------------------------------------------
# Run VisualBrowsingAgent tests for DeepSeek, limited to t05 and t06
- name: Wait a little bit (again)
run: sleep 5
- name: Configure config.toml for testing VisualBrowsingAgent (DeepSeek)
env:
LLM_MODEL: "litellm_proxy/deepseek-chat"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MAX_ITERATIONS: 15
run: |
echo "[llm.eval]" > config.toml
echo "model = \"$LLM_MODEL\"" >> config.toml
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
echo "temperature = 0.0" >> config.toml
- name: Run integration test evaluation for VisualBrowsingAgent (DeepSeek)
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
run: |
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD VisualBrowsingAgent '' 15 $N_PROCESSES "t05_simple_browsing,t06_github_pr_browsing.py" 'visualbrowsing_deepseek_run'
# Find and export the visual browsing agent test results
REPORT_FILE_VISUALBROWSING_DEEPSEEK=$(find evaluation/evaluation_outputs/outputs/integration_tests/VisualBrowsingAgent/deepseek*_maxiter_15_N* -name "report.md" -type f | head -n 1)
echo "REPORT_FILE_VISUALBROWSING_DEEPSEEK: $REPORT_FILE_VISUALBROWSING_DEEPSEEK"
echo "INTEGRATION_TEST_REPORT_VISUALBROWSING_DEEPSEEK<<EOF" >> $GITHUB_ENV
cat $REPORT_FILE_VISUALBROWSING_DEEPSEEK >> $GITHUB_ENV
echo >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create archive of evaluation outputs
run: |
TIMESTAMP=$(date +'%y-%m-%d-%H-%M')
cd evaluation/evaluation_outputs/outputs # Change to the outputs directory
tar -czvf ../../../integration_tests_${TIMESTAMP}.tar.gz integration_tests/CodeActAgent/* integration_tests/VisualBrowsingAgent/* # Only include the actual result directories
- name: Upload evaluation results as artifact
uses: actions/upload-artifact@v4
id: upload_results_artifact
with:
name: integration-test-outputs-${{ github.run_id }}-${{ github.run_attempt }}
path: integration_tests_*.tar.gz
- name: Get artifact URLs
run: |
echo "ARTIFACT_URL=${{ steps.upload_results_artifact.outputs.artifact-url }}" >> $GITHUB_ENV
- name: Set timestamp and trigger reason
run: |
echo "TIMESTAMP=$(date +'%Y-%m-%d-%H-%M')" >> $GITHUB_ENV
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "TRIGGER_REASON=pr-${{ github.event.pull_request.number }}" >> $GITHUB_ENV
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "TRIGGER_REASON=manual-${{ github.event.inputs.reason }}" >> $GITHUB_ENV
else
echo "TRIGGER_REASON=nightly-scheduled" >> $GITHUB_ENV
fi
- name: Comment with results and artifact link
id: create_comment
uses: KeisukeYamashita/create-comment@v1
with:
# if triggered by PR, use PR number, otherwise use 9745 as fallback issue number for manual triggers
number: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || 9745 }}
unique: false
comment: |
Trigger by: ${{ github.event_name == 'pull_request' && format('Pull Request (integration-test label on PR #{0})', github.event.pull_request.number) || (github.event_name == 'workflow_dispatch' && format('Manual Trigger: {0}', github.event.inputs.reason)) || 'Nightly Scheduled Run' }}
Commit: ${{ github.sha }}
**Integration Tests Report (Haiku)**
Haiku LLM Test Results:
${{ env.INTEGRATION_TEST_REPORT_HAIKU }}
---
**Integration Tests Report (DeepSeek)**
DeepSeek LLM Test Results:
${{ env.INTEGRATION_TEST_REPORT_DEEPSEEK }}
---
**Integration Tests Report VisualBrowsing (DeepSeek)**
${{ env.INTEGRATION_TEST_REPORT_VISUALBROWSING_DEEPSEEK }}
---
Download testing outputs (includes both Haiku and DeepSeek results): [Download](${{ steps.upload_results_artifact.outputs.artifact-url }})

View File

@@ -72,3 +72,34 @@ jobs:
- name: Run pre-commit hooks
working-directory: ./enterprise
run: pre-commit run --all-files --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
lint-cli-python:
name: Lint CLI python
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up python
uses: useblacksmith/setup-python@v6
with:
python-version: 3.12
cache: "pip"
- name: Install pre-commit
run: pip install pre-commit==4.2.0
- name: Run pre-commit hooks
working-directory: ./openhands-cli
run: pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml
# Check version consistency across documentation
check-version-consistency:
name: Check version consistency
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- name: Set up python
uses: useblacksmith/setup-python@v6
with:
python-version: 3.12
- name: Run version consistency check
run: .github/scripts/check_version_consistency.py

70
.github/workflows/mdx-lint.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
# Workflow that checks MDX format in docs/ folder
name: MDX Lint
# Run on pushes to main and on pull requests that modify docs/ files
on:
push:
branches:
- main
paths:
- 'docs/**/*.mdx'
pull_request:
paths:
- 'docs/**/*.mdx'
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
mdx-lint:
name: Lint MDX files
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- name: Install Node.js 22
uses: useblacksmith/setup-node@v5
with:
node-version: 22
- name: Install MDX dependencies
run: |
npm install @mdx-js/mdx@3 glob@10
- name: Validate MDX files
run: |
node -e "
const {compile} = require('@mdx-js/mdx');
const fs = require('fs');
const path = require('path');
const glob = require('glob');
async function validateMDXFiles() {
const files = glob.sync('docs/**/*.mdx');
console.log('Found', files.length, 'MDX files to validate');
let hasErrors = false;
for (const file of files) {
try {
const content = fs.readFileSync(file, 'utf8');
await compile(content);
console.log('✅ MDX parsing successful for', file);
} catch (err) {
console.error('❌ MDX parsing failed for', file, ':', err.message);
hasErrors = true;
}
}
if (hasErrors) {
console.error('\\n❌ Some MDX files have parsing errors. Please fix them before merging.');
process.exit(1);
} else {
console.log('\\n✅ All MDX files are valid!');
}
}
validateMDXFiles();
"

View File

@@ -48,10 +48,7 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install Python dependencies using Poetry
run: |
poetry install --with dev,test,runtime
poetry run pip install pytest-xdist
poetry run pip install pytest-rerunfailures
run: poetry install --with dev,test,runtime
- name: Build Environment
run: make build
- name: Run Unit Tests
@@ -59,7 +56,7 @@ jobs:
env:
COVERAGE_FILE: ".coverage.${{ matrix.python_version }}"
- name: Run Runtime Tests with CLIRuntime
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -n 5 --reruns 2 --reruns-delay 3 -s tests/runtime/test_bash.py --cov=openhands --cov-branch
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -s tests/runtime/test_bash.py --cov=openhands --cov-branch
env:
COVERAGE_FILE: ".coverage.runtime.${{ matrix.python_version }}"
- name: Store coverage file
@@ -70,7 +67,37 @@ jobs:
.coverage.${{ matrix.python_version }}
.coverage.runtime.${{ matrix.python_version }}
include-hidden-files: true
# Run specific Windows python tests
test-on-windows:
name: Python Tests on Windows
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.12"]
steps:
- uses: actions/checkout@v4
- name: Install pipx
run: pip install pipx
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
- name: Install Python dependencies using Poetry
run: poetry install --with dev,test,runtime
- name: Run Windows unit tests
run: poetry run pytest -svv tests/unit/runtime/utils/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"
test-enterprise:
name: Enterprise Python Unit Tests
runs-on: blacksmith-4vcpu-ubuntu-2404
@@ -101,11 +128,57 @@ jobs:
path: ".coverage.enterprise.${{ matrix.python_version }}"
include-hidden-files: true
# Run CLI unit tests
test-cli-python:
name: CLI Unit Tests
runs-on: blacksmith-4vcpu-ubuntu-2404
strategy:
matrix:
python-version: ["3.12"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Install dependencies
working-directory: ./openhands-cli
run: |
uv sync --group dev
- name: Run CLI unit tests
working-directory: ./openhands-cli
env:
# write coverage to repo root so the merge step finds it
COVERAGE_FILE: "${{ github.workspace }}/.coverage.openhands-cli.${{ matrix.python-version }}"
run: |
uv run pytest --forked -n auto -s \
-p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark \
tests --cov=openhands_cli --cov-branch
- name: Store coverage file
uses: actions/upload-artifact@v4
with:
name: coverage-openhands-cli
path: ".coverage.openhands-cli.${{ matrix.python-version }}"
include-hidden-files: true
coverage-comment:
name: Coverage Comment
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: [test-on-linux, test-enterprise]
needs: [test-on-linux, test-enterprise, test-cli-python]
permissions:
pull-requests: write
@@ -119,6 +192,9 @@ jobs:
pattern: coverage-*
merge-multiple: true
- name: Create symlink for CLI source files
run: ln -sf openhands-cli/openhands_cli openhands_cli
- name: Coverage comment
id: coverage_comment
uses: py-cov-action/python-coverage-comment-action@v3

View File

@@ -10,6 +10,7 @@ on:
type: choice
options:
- app server
- cli
default: app server
push:
tags:
@@ -38,3 +39,36 @@ jobs:
run: ./build.sh
- name: publish
run: poetry publish -u __token__ -p ${{ secrets.PYPI_TOKEN }}
release-cli:
name: Publish CLI to PyPI
runs-on: ubuntu-latest
# Run when manually dispatched for "cli" OR for tag pushes that contain '-cli'
if: |
(github.event_name == 'workflow_dispatch' && github.event.inputs.reason == 'cli')
|| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && contains(github.ref, '-cli'))
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Build CLI package
working-directory: openhands-cli
run: |
# Clean dist directory to avoid conflicts with binary builds
rm -rf dist/
uv build
- name: Publish CLI to PyPI
working-directory: openhands-cli
run: |
uv publish --token ${{ secrets.PYPI_TOKEN_OPENHANDS }}

135
.github/workflows/run-eval.yml vendored Normal file
View File

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

3
.gitignore vendored
View File

@@ -185,9 +185,6 @@ cython_debug/
.repomix
repomix-output.txt
# Emacs backup
*~
# evaluation
evaluation/evaluation_outputs
evaluation/outputs

1
CNAME
View File

@@ -1 +0,0 @@
docs.all-hands.dev

View File

@@ -1,45 +1,43 @@
# The OpenHands Community
# 🙌 The OpenHands Community
OpenHands is a community of engineers, academics, and enthusiasts reimagining software development for an AI-powered world.
The OpenHands community is built around the belief that (1) AI and AI agents are going to fundamentally change the way
we build software, and (2) if this is true, we should do everything we can to make sure that the benefits provided by
such powerful technology are accessible to everyone.
## Mission
If this resonates with you, we'd love to have you join us in our quest!
Its very clear that AI is changing software development. We want the developer community to drive that change organically, through open source.
## 🤝 How to Join
So were not just building friendly interfaces for AI-driven development. Were publishing _building blocks_ that empower developers to create new experiences, tailored to your own habits, needs, and imagination.
Check out our [How to Join the Community section.](https://github.com/OpenHands/OpenHands?tab=readme-ov-file#-how-to-join-the-community)
## Ethos
## 💪 Becoming a Contributor
We have two core values: **high openness** and **high agency**. While we dont expect everyone in the community to embody these values, we want to establish them as norms.
We welcome contributions from everyone! Whether you're a developer, a researcher, or simply enthusiastic about advancing
the field of software engineering with AI, there are many ways to get involved:
### High Openness
- **Code Contributions:** Help us develop new core functionality, improve our agents, improve the frontend and other
interfaces, or anything else that would help make OpenHands better.
- **Research and Evaluation:** Contribute to our understanding of LLMs in software engineering, participate in
evaluating the models, or suggest improvements.
- **Feedback and Testing:** Use the OpenHands toolset, report bugs, suggest features, or provide feedback on usability.
We welcome anyone and everyone into our community by default. You dont have to be a software developer to help us build. You dont have to be pro-AI to help us learn.
For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md).
Our plans, our work, our successes, and our failures are all public record. We want the world to see not just the fruits of our work, but the whole process of growing it.
## Code of Conduct
We welcome thoughtful criticism, whether its a comment on a PR or feedback on the community as a whole.
We have a [Code of Conduct](./CODE_OF_CONDUCT.md) that we expect all contributors to adhere to.
Long story short, we are aiming for an open, welcoming, diverse, inclusive, and healthy community.
All contributors are expected to contribute to building this sort of community.
### High Agency
## 🛠️ Becoming a Maintainer
Everyone should feel empowered to contribute to OpenHands. Whether its by making a PR, hosting an event, sharing feedback, or just asking a question, dont hold back!
For contributors who have made significant and sustained contributions to the project, there is a possibility of joining
the maintainer team. The process for this is as follows:
OpenHands gives everyone the building blocks to create state-of-the-art developer experiences. We experiment constantly and love building new things.
1. Any contributor who has made sustained and high-quality contributions to the codebase can be nominated by any
maintainer. If you feel that you may qualify you can reach out to any of the maintainers that have reviewed your PRs and ask if you can be nominated.
2. Once a maintainer nominates a new maintainer, there will be a discussion period among the maintainers for at least 3 days.
3. If no concerns are raised the nomination will be accepted by acclamation, and if concerns are raised there will be a discussion and possible vote.
Coding, development practices, and communities are changing rapidly. We wont hesitate to change direction and make big bets.
## Relationship to All Hands
OpenHands is supported by the for-profit organization [All Hands AI, Inc](https://www.all-hands.dev/).
All Hands was founded by three of the first major contributors to OpenHands:
- Xingyao Wang, a UIUC PhD candidate who got OpenHands to the top of the SWE-bench leaderboards
- Graham Neubig, a CMU Professor who rallied the academic community around OpenHands
- Robert Brennan, a software engineer who architected the user-facing features of OpenHands
All Hands is an important part of the OpenHands ecosystem. Weve raised over $20M--mainly to hire developers and researchers who can work on OpenHands full-time, and to provide them with expensive infrastructure. ([Join us!](https://allhandsai.applytojob.com/apply/))
But we see OpenHands as much larger, and ultimately more important, than All Hands. When our financial responsibility to investors is at odds with our social responsibility to the community—as it inevitably will be, from time to time—we promise to navigate that conflict thoughtfully and transparently.
At some point, we may transfer custody of OpenHands to an open source foundation. But for now, the [Benevolent Dictator approach](http://www.catb.org/~esr/writings/cathedral-bazaar/homesteading/ar01s16.html) helps us move forward with speed and intention. If we ever forget the “benevolent” part, please: fork us.
Note that just making many PRs does not immediately imply that you will become a maintainer. We will be looking
at sustained high-quality contributions over a period of time, as well as good teamwork and adherence to our [Code of Conduct](./CODE_OF_CONDUCT.md).

View File

@@ -58,7 +58,7 @@ by implementing the [interface specified here](https://github.com/OpenHands/Open
#### Testing
When you write code, it is also good to write tests. Please navigate to the [`./tests`](./tests) folder to see existing test suites.
At the moment, we have these kinds of tests: [`unit`](./tests/unit), [`runtime`](./tests/runtime), and [`end-to-end (e2e)`](./tests/e2e). Please refer to the README for each test suite. These tests also run on GitHub's continuous integration to ensure quality of the project.
At the moment, we have two kinds of tests: [`unit`](./tests/unit) and [`integration`](./evaluation/integration_tests). Please refer to the README for each test suite. These tests also run on GitHub's continuous integration to ensure quality of the project.
## Sending Pull Requests to OpenHands

View File

@@ -91,14 +91,14 @@ make run
#### Option B: Individual Server Startup
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on
backend-related tasks or configurations.
backend-related tasks or configurations.
```bash
make start-backend
```
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related
components or interface enhancements.
components or interface enhancements.
```bash
make start-frontend
```
@@ -110,7 +110,6 @@ You can use OpenHands to develop and improve OpenHands itself! This is a powerfu
#### Quick Start
1. **Build and run OpenHands:**
```bash
export INSTALL_DOCKER=0
export RUNTIME=local
@@ -118,7 +117,6 @@ You can use OpenHands to develop and improve OpenHands itself! This is a powerfu
```
2. **Access the interface:**
- Local development: http://localhost:3001
- Remote/cloud environments: Use the appropriate external URL
@@ -161,7 +159,7 @@ poetry run pytest ./tests/unit/test_*.py
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/openhands/runtime:0.62-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/openhands/runtime:0.60-nikolaik`
## Develop inside Docker container
@@ -201,6 +199,6 @@ Here's a guide to the important documentation files in the repository:
- [/containers/README.md](./containers/README.md): Information about Docker containers and deployment
- [/tests/unit/README.md](./tests/unit/README.md): Guide to writing and running unit tests
- [/evaluation/README.md](./evaluation/README.md): Documentation for the evaluation framework and benchmarks
- [/skills/README.md](./skills/README.md): Information about the skills architecture and implementation
- [/microagents/README.md](./microagents/README.md): Information about the microagents architecture and implementation
- [/openhands/server/README.md](./openhands/server/README.md): Server implementation details and API documentation
- [/openhands/runtime/README.md](./openhands/runtime/README.md): Documentation for the runtime environment and execution model

186
README.md
View File

@@ -1,18 +1,22 @@
<a name="readme-top"></a>
<div align="center">
<img src="https://raw.githubusercontent.com/OpenHands/docs/main/openhands/static/img/logo.png" alt="Logo" width="200">
<h1 align="center" style="border-bottom: none">OpenHands: AI-Driven Development</h1>
<img src="https://raw.githubusercontent.com/All-Hands-AI/docs/main/openhands/static/img/logo.png" alt="Logo" width="200">
<h1 align="center">OpenHands: Code Less, Make More</h1>
</div>
<div align="center">
<a href="https://github.com/OpenHands/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/badge/LICENSE-MIT-20B2AA?style=for-the-badge" alt="MIT License"></a>
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=811504672#gid=811504672"><img src="https://img.shields.io/badge/SWEBench-72.8-00cc00?logoColor=FFE165&style=for-the-badge" alt="Benchmark Score"></a>
<a href="https://github.com/OpenHands/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/OpenHands/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
<a href="https://github.com/OpenHands/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/OpenHands/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/OpenHands/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/OpenHands/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://docs.openhands.dev/sdk"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation"></a>
<a href="https://arxiv.org/abs/2511.03690"><img src="https://img.shields.io/badge/Paper-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Tech Report"></a>
<a href="https://all-hands.dev/joinslack"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://github.com/OpenHands/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits"></a>
<br/>
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation"></a>
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Paper on Arxiv"></a>
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="Evaluation Benchmark Score"></a>
<!-- Keep these links. Translations will automatically update with the README. -->
<a href="https://www.readme-i18n.com/OpenHands/OpenHands?lang=de">Deutsch</a> |
@@ -24,63 +28,157 @@
<a href="https://www.readme-i18n.com/OpenHands/OpenHands?lang=ru">Русский</a> |
<a href="https://www.readme-i18n.com/OpenHands/OpenHands?lang=zh">中文</a>
<hr>
</div>
<hr>
Welcome to OpenHands (formerly OpenDevin), a platform for software development agents powered by AI.
🙌 Welcome to OpenHands, a [community](COMMUNITY.md) focused on AI-driven development. Wed love for you to [join us on Slack](https://dub.sh/openhands).
OpenHands agents can do anything a human developer can: modify code, run commands, browse the web,
call APIs, and yes—even copy code snippets from StackOverflow.
There are a few ways to work with OpenHands:
Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or [sign up for OpenHands Cloud](https://app.all-hands.dev) to get started.
### OpenHands Software Agent SDK
The SDK is a composable Python library that contains all of our agentic tech. It's the engine that powers everything else below.
Define agents in code, then run them locally, or scale to 1000s of agents in the cloud.
> [!IMPORTANT]
> **Upcoming change**: We are renaming our GitHub Org from `All-Hands-AI` to `OpenHands` on October 20th, 2025.
> Check the [tracking issue](https://github.com/All-Hands-AI/OpenHands/issues/11376) for more information.
[Check out the docs](https://docs.openhands.dev/sdk) or [view the source](https://github.com/OpenHands/software-agent-sdk/)
### OpenHands CLI
The CLI is the easiest way to start using OpenHands. The experience will be familiar to anyone who has worked
with e.g. Claude Code or Codex. You can power it with Claude, GPT, or any other LLM.
> [!IMPORTANT]
> Using OpenHands for work? We'd love to chat! Fill out
> [this short form](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> to join our Design Partner program, where you'll get early access to commercial features and the opportunity to provide input on our product roadmap.
[Check out the docs](https://docs.openhands.dev/openhands/usage/run-openhands/cli-mode) or [view the source](https://github.com/OpenHands/OpenHands-CLI)
## ☁️ OpenHands Cloud
The easiest way to get started with OpenHands is on [OpenHands Cloud](https://app.all-hands.dev),
which comes with $20 in free credits for new users.
### OpenHands Local GUI
Use the Local GUI for running agents on your laptop. It comes with a REST API and a single-page React application.
The experience will be familiar to anyone who has used Devin or Jules.
## 💻 Running OpenHands Locally
[Check out the docs](https://docs.openhands.dev/openhands/usage/run-openhands/local-setup) or view the source in this repo.
### Option 1: CLI Launcher (Recommended)
### OpenHands Cloud
This is a deployment of OpenHands GUI, running on hosted infrastructure.
The easiest way to run OpenHands locally is using the CLI launcher with [uv](https://docs.astral.sh/uv/). This provides better isolation from your current project's virtual environment and is required for OpenHands' default MCP servers.
You can try it with a free $10 credit by [signing in with your GitHub account](https://app.all-hands.dev).
**Install uv** (if you haven't already):
OpenHands Cloud comes with source-available features and integrations:
- Integrations with Slack, Jira, and Linear
- Multi-user support
- RBAC and permissions
- Collaboration features (e.g., conversation sharing)
See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/) for the latest installation instructions for your platform.
### OpenHands Enterprise
Large enterprises can work with us to self-host OpenHands Cloud in their own VPC, via Kubernetes.
OpenHands Enterprise can also work with the CLI and SDK above.
**Launch OpenHands**:
```bash
# Launch the GUI server
uvx --python 3.12 openhands serve
OpenHands Enterprise is source-available--you can see all the source code here in the enterprise/ directory,
but you'll need to purchase a license if you want to run it for more than one month.
# Or launch the CLI
uvx --python 3.12 openhands
```
Enterprise contracts also come with extended support and access to our research team.
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000) (for GUI mode)!
Learn more at [openhands.dev/enterprise](https://openhands.dev/enterprise)
### Option 2: Docker
### Everything Else
<details>
<summary>Click to expand Docker command</summary>
Check out our [Product Roadmap](https://github.com/orgs/openhands/projects/1), and feel free to
[open up an issue](https://github.com/OpenHands/OpenHands/issues) if there's something you'd like to see!
You can also run OpenHands directly with Docker:
You might also be interested in our [evaluation infrastructure](https://github.com/OpenHands/benchmarks), our [chrome extension](https://github.com/OpenHands/openhands-chrome-extension/), or our [Theory-of-Mind module](https://github.com/OpenHands/ToM-SWE).
```bash
docker pull docker.openhands.dev/openhands/runtime:0.60-nikolaik
All our work is available under the MIT license, except for the `enterprise/` directory in this repository (see the [enterprise license](enterprise/LICENSE) for details).
The core `openhands` and `agent-server` Docker images are fully MIT-licensed as well.
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:0.60-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.openhands.dev/openhands/openhands:0.60
```
If you need help with anything, or just want to chat, [come find us on Slack](https://dub.sh/openhands).
</details>
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
> [!WARNING]
> On a public network? See our [Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)
> to secure your deployment by restricting network binding and implementing additional security measures.
### Getting Started
When you open the application, you'll be asked to choose an LLM provider and add an API key.
[Anthropic's Claude Sonnet 4.5](https://www.anthropic.com/api) (`anthropic/claude-sonnet-4-5-20250929`)
works best, but you have [many options](https://docs.all-hands.dev/usage/llms).
See the [Running OpenHands](https://docs.all-hands.dev/usage/installation) guide for
system requirements and more information.
## 💡 Other ways to run OpenHands
> [!WARNING]
> OpenHands is meant to be run by a single user on their local workstation.
> It is not appropriate for multi-tenant deployments where multiple users share the same instance. There is no built-in authentication, isolation, or scalability.
>
> If you're interested in running OpenHands in a multi-tenant environment, check out the source-available, commercially-licensed
> [OpenHands Cloud Helm Chart](https://github.com/openHands/OpenHands-cloud)
You can [connect OpenHands to your local filesystem](https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem),
interact with it via a [friendly CLI](https://docs.all-hands.dev/usage/how-to/cli-mode),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/usage/how-to/headless-mode),
or run it on tagged issues with [a github action](https://docs.all-hands.dev/usage/how-to/github-action).
Visit [Running OpenHands](https://docs.all-hands.dev/usage/installation) for more information and setup instructions.
If you want to modify the OpenHands source code, check out [Development.md](https://github.com/OpenHands/OpenHands/blob/main/Development.md).
Having issues? The [Troubleshooting Guide](https://docs.all-hands.dev/usage/troubleshooting) can help.
## 📖 Documentation
To learn more about the project, and for tips on using OpenHands,
check out our [documentation](https://docs.all-hands.dev/usage/getting-started).
There you'll find resources on how to use different LLM providers,
troubleshooting resources, and advanced configuration options.
## 🤝 How to Join the Community
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
through Slack, so this is the best place to start, but we also are happy to have you contact us on Github:
- [Join our Slack workspace](https://all-hands.dev/joinslack) - Here we talk about research, architecture, and future development.
- [Read or post Github Issues](https://github.com/OpenHands/OpenHands/issues) - Check out the issues we're working on, or add your own ideas.
See more about the community in [COMMUNITY.md](./COMMUNITY.md) or find details on contributing in [CONTRIBUTING.md](./CONTRIBUTING.md).
## 📈 Progress
See the monthly OpenHands roadmap [here](https://github.com/orgs/OpenHands/projects/1) (updated at the maintainer's meeting at the end of each month).
<p align="center">
<a href="https://star-history.com/#OpenHands/OpenHands&Date">
<img src="https://api.star-history.com/svg?repos=OpenHands/OpenHands&type=Date" width="500" alt="Star History Chart">
</a>
</p>
## 📜 License
Distributed under the MIT License, with the exception of the `enterprise/` folder. See [`LICENSE`](./LICENSE) for more information.
## 🙏 Acknowledgements
OpenHands is built by a large number of contributors, and every contribution is greatly appreciated! We also build upon other open source projects, and we are deeply thankful for their work.
For a list of open source projects and licenses used in OpenHands, please see our [CREDITS.md](./CREDITS.md) file.
## 📚 Cite
```
@inproceedings{
wang2025openhands,
title={OpenHands: An Open Platform for {AI} Software Developers as Generalist Agents},
author={Xingyao Wang and Boxuan Li and Yufan Song and Frank F. Xu and Xiangru Tang and Mingchen Zhuge and Jiayi Pan and Yueqi Song and Bowen Li and Jaskirat Singh and Hoang H. Tran and Fuqiang Li and Ren Ma and Mingzhang Zheng and Bill Qian and Yanjun Shao and Niklas Muennighoff and Yizhe Zhang and Binyuan Hui and Junyang Lin and Robert Brennan and Hao Peng and Heng Ji and Graham Neubig},
booktitle={The Thirteenth International Conference on Learning Representations},
year={2025},
url={https://openreview.net/forum?id=OJd3ayDDoF}
}
```

View File

@@ -73,7 +73,7 @@ ENV VIRTUAL_ENV=/app/.venv \
COPY --chown=openhands:openhands --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY --chown=openhands:openhands --chmod=770 ./skills ./skills
COPY --chown=openhands:openhands --chmod=770 ./microagents ./microagents
COPY --chown=openhands:openhands --chmod=770 ./openhands ./openhands
COPY --chown=openhands:openhands --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
COPY --chown=openhands:openhands pyproject.toml poetry.lock README.md MANIFEST.in LICENSE ./

View File

@@ -1,7 +1,7 @@
# Develop in Docker
> [!WARNING]
> This way of running OpenHands is not officially supported. It is maintained by the community and may not work.
> This is not officially supported and may not work.
Install [Docker](https://docs.docker.com/engine/install/) on your host machine and run:

View File

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

View File

@@ -3,9 +3,9 @@ repos:
rev: v5.0.0
hooks:
- id: trailing-whitespace
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/)
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/|openhands-cli/)
- id: end-of-file-fixer
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/)
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/|enterprise/|openhands-cli/)
- id: check-yaml
args: ["--allow-multiple-documents"]
- id: debug-statements
@@ -28,12 +28,12 @@ repos:
entry: ruff check --config dev_config/python/ruff.toml
types_or: [python, pyi, jupyter]
args: [--fix, --unsafe-fixes]
exclude: ^(third_party/|enterprise/)
exclude: ^(third_party/|enterprise/|openhands-cli/)
# Run the formatter.
- id: ruff-format
entry: ruff format --config dev_config/python/ruff.toml
types_or: [python, pyi, jupyter]
exclude: ^(third_party/|enterprise/)
exclude: ^(third_party/|enterprise/|openhands-cli/)
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0

View File

@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.openhands.dev/openhands/runtime:0.62-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.openhands.dev/openhands/runtime:0.60-nikolaik}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of ~/.openhands for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -5,8 +5,12 @@ from experiments.constants import (
EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT,
)
from experiments.experiment_versions import (
handle_condenser_max_step_experiment,
handle_system_prompt_experiment,
)
from experiments.experiment_versions._004_condenser_max_step_experiment import (
handle_condenser_max_step_experiment__v1,
)
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
@@ -27,6 +31,10 @@ class SaaSExperimentManager(ExperimentManager):
)
return agent
agent = handle_condenser_max_step_experiment__v1(
user_id, conversation_id, agent
)
if EXPERIMENT_SYSTEM_PROMPT_EXPERIMENT:
agent = agent.model_copy(
update={'system_prompt_filename': 'system_prompt_long_horizon.j2'}
@@ -52,7 +60,20 @@ class SaaSExperimentManager(ExperimentManager):
"""
logger.debug(
'experiment_manager:run_conversation_variant_test:started',
extra={'user_id': user_id, 'conversation_id': conversation_id},
extra={'user_id': user_id},
)
# Skip all experiment processing if the experiment manager is disabled
if not ENABLE_EXPERIMENT_MANAGER:
logger.info(
'experiment_manager:run_conversation_variant_test:skipped',
extra={'reason': 'experiment_manager_disabled'},
)
return conversation_settings
# Apply conversation-scoped experiments
conversation_settings = handle_condenser_max_step_experiment(
user_id, conversation_id, conversation_settings
)
return conversation_settings

View File

@@ -1,71 +0,0 @@
"""add status and updated_at to callback
Revision ID: 080
Revises: 079
Create Date: 2025-11-05 00:00:00.000000
"""
from enum import Enum
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '080'
down_revision: Union[str, None] = '079'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
class EventCallbackStatus(Enum):
ACTIVE = 'ACTIVE'
DISABLED = 'DISABLED'
COMPLETED = 'COMPLETED'
ERROR = 'ERROR'
def upgrade() -> None:
"""Upgrade schema."""
status = sa.Enum(EventCallbackStatus, name='eventcallbackstatus')
status.create(op.get_bind(), checkfirst=True)
op.add_column(
'event_callback',
sa.Column('status', status, nullable=False, server_default='ACTIVE'),
)
op.add_column(
'event_callback',
sa.Column(
'updated_at', sa.DateTime, nullable=False, server_default=sa.func.now()
),
)
op.drop_index('ix_event_callback_result_event_id')
op.drop_column('event_callback_result', 'event_id')
op.add_column(
'event_callback_result', sa.Column('event_id', sa.String, nullable=True)
)
op.create_index(
op.f('ix_event_callback_result_event_id'),
'event_callback_result',
['event_id'],
unique=False,
)
def downgrade() -> None:
"""Downgrade schema."""
op.drop_column('event_callback', 'status')
op.drop_column('event_callback', 'updated_at')
op.drop_index('ix_event_callback_result_event_id')
op.drop_column('event_callback_result', 'event_id')
op.add_column(
'event_callback_result', sa.Column('event_id', sa.UUID, nullable=True)
)
op.create_index(
op.f('ix_event_callback_result_event_id'),
'event_callback_result',
['event_id'],
unique=False,
)
op.execute('DROP TYPE eventcallbackstatus')

View File

@@ -1,41 +0,0 @@
"""add parent_conversation_id to conversation_metadata
Revision ID: 081
Revises: 080
Create Date: 2025-11-06 00:00:00.000000
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '081'
down_revision: Union[str, None] = '080'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.add_column(
'conversation_metadata',
sa.Column('parent_conversation_id', sa.String(), nullable=True),
)
op.create_index(
op.f('ix_conversation_metadata_parent_conversation_id'),
'conversation_metadata',
['parent_conversation_id'],
unique=False,
)
def downgrade() -> None:
"""Downgrade schema."""
op.drop_index(
op.f('ix_conversation_metadata_parent_conversation_id'),
table_name='conversation_metadata',
)
op.drop_column('conversation_metadata', 'parent_conversation_id')

View File

@@ -1,51 +0,0 @@
"""Add SETTING_UP_SKILLS to appconversationstarttaskstatus enum
Revision ID: 082
Revises: 081
Create Date: 2025-11-19 12:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
from sqlalchemy import text
# revision identifiers, used by Alembic.
revision: str = '082'
down_revision: Union[str, Sequence[str], None] = '081'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Add SETTING_UP_SKILLS enum value to appconversationstarttaskstatus."""
# Check if the enum value already exists before adding it
# This handles the case where the enum was created with the value already included
connection = op.get_bind()
result = connection.execute(
text(
"SELECT 1 FROM pg_enum WHERE enumlabel = 'SETTING_UP_SKILLS' "
"AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'appconversationstarttaskstatus')"
)
)
if not result.fetchone():
# Add the new enum value only if it doesn't already exist
op.execute(
"ALTER TYPE appconversationstarttaskstatus ADD VALUE 'SETTING_UP_SKILLS'"
)
def downgrade() -> None:
"""Remove SETTING_UP_SKILLS enum value from appconversationstarttaskstatus.
Note: PostgreSQL doesn't support removing enum values directly.
This would require recreating the enum type and updating all references.
For safety, this downgrade is not implemented.
"""
# PostgreSQL doesn't support removing enum values directly
# This would require a complex migration to recreate the enum
# For now, we'll leave this as a no-op since removing enum values
# is rarely needed and can be dangerous
pass

336
enterprise/poetry.lock generated
View File

@@ -201,20 +201,19 @@ files = [
[[package]]
name = "anthropic"
version = "0.72.0"
version = "0.65.0"
description = "The official Python library for the anthropic API"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "anthropic-0.72.0-py3-none-any.whl", hash = "sha256:0e9f5a7582f038cab8efbb4c959e49ef654a56bfc7ba2da51b5a7b8a84de2e4d"},
{file = "anthropic-0.72.0.tar.gz", hash = "sha256:8971fe76dcffc644f74ac3883069beb1527641115ae0d6eb8fa21c1ce4082f7a"},
{file = "anthropic-0.65.0-py3-none-any.whl", hash = "sha256:ba9d9f82678046c74ddf5698ca06d9f5b0f599cfac922ab0d5921638eb448d98"},
{file = "anthropic-0.65.0.tar.gz", hash = "sha256:6b6b6942574e54342050dfd42b8d856a8366b171daec147df3b80be4722733b9"},
]
[package.dependencies]
anyio = ">=3.5.0,<5"
distro = ">=1.7.0,<2"
docstring-parser = ">=0.15,<1"
google-auth = {version = ">=2,<3", extras = ["requests"], optional = true, markers = "extra == \"vertex\""}
httpx = ">=0.25.0,<1"
jiter = ">=0.4.0,<1"
@@ -223,7 +222,7 @@ sniffio = "*"
typing-extensions = ">=4.10,<5"
[package.extras]
aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"]
aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.8)"]
bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"]
vertex = ["google-auth[requests] (>=2,<3)"]
@@ -682,34 +681,31 @@ crt = ["awscrt (==0.27.6)"]
[[package]]
name = "browser-use"
version = "0.9.5"
version = "0.7.10"
description = "Make websites accessible for AI agents"
optional = false
python-versions = "<4.0,>=3.11"
groups = ["main"]
files = [
{file = "browser_use-0.9.5-py3-none-any.whl", hash = "sha256:4a2e92847204d1ded269026a99cb0cc0e60e38bd2751fa3f58aedd78f00b4e67"},
{file = "browser_use-0.9.5.tar.gz", hash = "sha256:f8285fe253b149d01769a7084883b4cf4db351e2f38e26302c157bcbf14a703f"},
{file = "browser_use-0.7.10-py3-none-any.whl", hash = "sha256:669e12571a0c0c4c93e5fd26abf9e2534eb9bacbc510328aedcab795bd8906a9"},
{file = "browser_use-0.7.10.tar.gz", hash = "sha256:f93ce59e06906c12d120360dee4aa33d83618ddf7c9a575dd0ac517d2de7ccbc"},
]
[package.dependencies]
aiohttp = "3.12.15"
anthropic = ">=0.68.1,<1.0.0"
anthropic = ">=0.58.2,<1.0.0"
anyio = ">=4.9.0"
authlib = ">=1.6.0"
bubus = ">=1.5.6"
cdp-use = ">=1.4.0"
click = ">=8.1.8"
cloudpickle = ">=3.1.1"
google-api-core = ">=2.25.0"
google-api-python-client = ">=2.174.0"
google-auth = ">=2.40.3"
google-auth-oauthlib = ">=1.2.2"
google-genai = ">=1.29.0,<2.0.0"
groq = ">=0.30.0"
html2text = ">=2025.4.15"
httpx = ">=0.28.1"
inquirerpy = ">=0.3.4"
markdownify = ">=1.2.0"
mcp = ">=1.10.1"
ollama = ">=0.5.1"
openai = ">=1.99.2,<2.0.0"
@@ -724,20 +720,16 @@ pypdf = ">=5.7.0"
python-dotenv = ">=1.0.1"
reportlab = ">=4.0.0"
requests = ">=2.32.3"
rich = ">=14.0.0"
screeninfo = {version = ">=0.8.1", markers = "platform_system != \"darwin\""}
typing-extensions = ">=4.12.2"
uuid7 = ">=0.1.0"
[package.extras]
all = ["agentmail (==0.0.59)", "boto3 (>=1.38.45)", "botocore (>=1.37.23)", "imgcat (>=0.6.0)", "langchain-openai (>=0.3.26)", "oci (>=2.126.4)", "textual (>=3.2.0)"]
all = ["agentmail (>=0.0.53)", "boto3 (>=1.38.45)", "botocore (>=1.37.23)", "click (>=8.1.8)", "imgcat (>=0.6.0)", "langchain-openai (>=0.3.26)", "rich (>=14.0.0)", "textual (>=3.2.0)"]
aws = ["boto3 (>=1.38.45)"]
cli = ["textual (>=3.2.0)"]
cli-oci = ["oci (>=2.126.4)", "textual (>=3.2.0)"]
code = ["matplotlib (>=3.9.0)", "numpy (>=2.3.2)", "pandas (>=2.2.0)", "tabulate (>=0.9.0)"]
eval = ["anyio (>=4.9.0)", "datamodel-code-generator (>=0.26.0)", "lmnr[all] (==0.7.17)", "psutil (>=7.0.0)"]
examples = ["agentmail (==0.0.59)", "botocore (>=1.37.23)", "imgcat (>=0.6.0)", "langchain-openai (>=0.3.26)"]
oci = ["oci (>=2.126.4)"]
cli = ["click (>=8.1.8)", "rich (>=14.0.0)", "textual (>=3.2.0)"]
eval = ["anyio (>=4.9.0)", "browserbase (==1.4.0)", "datamodel-code-generator (>=0.26.0)", "hyperbrowser (==0.47.0)", "lmnr[all] (==0.7.10)", "psutil (>=7.0.0)"]
examples = ["agentmail (>=0.0.53)", "botocore (>=1.37.23)", "imgcat (>=0.6.0)", "langchain-openai (>=0.3.26)"]
video = ["imageio[ffmpeg] (>=2.37.0)", "numpy (>=2.3.2)"]
[[package]]
@@ -3533,25 +3525,6 @@ files = [
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
]
[[package]]
name = "inquirerpy"
version = "0.3.4"
description = "Python port of Inquirer.js (A collection of common interactive command-line user interfaces)"
optional = false
python-versions = ">=3.7,<4.0"
groups = ["main"]
files = [
{file = "InquirerPy-0.3.4-py3-none-any.whl", hash = "sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4"},
{file = "InquirerPy-0.3.4.tar.gz", hash = "sha256:89d2ada0111f337483cb41ae31073108b2ec1e618a49d7110b0d7ade89fc197e"},
]
[package.dependencies]
pfzy = ">=0.3.1,<0.4.0"
prompt-toolkit = ">=3.0.1,<4.0.0"
[package.extras]
docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17-beta.43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"]
[[package]]
name = "installer"
version = "0.7.0"
@@ -4607,62 +4580,6 @@ files = [
{file = "llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4"},
]
[[package]]
name = "lmnr"
version = "0.7.20"
description = "Python SDK for Laminar"
optional = false
python-versions = "<4,>=3.10"
groups = ["main"]
files = [
{file = "lmnr-0.7.20-py3-none-any.whl", hash = "sha256:5f9fa7444e6f96c25e097f66484ff29e632bdd1de0e9346948bf5595f4a8af38"},
{file = "lmnr-0.7.20.tar.gz", hash = "sha256:1f484cd618db2d71af65f90a0b8b36d20d80dc91a5138b811575c8677bf7c4fd"},
]
[package.dependencies]
grpcio = ">=1"
httpx = ">=0.24.0"
opentelemetry-api = ">=1.33.0"
opentelemetry-exporter-otlp-proto-grpc = ">=1.33.0"
opentelemetry-exporter-otlp-proto-http = ">=1.33.0"
opentelemetry-instrumentation = ">=0.54b0"
opentelemetry-instrumentation-threading = ">=0.57b0"
opentelemetry-sdk = ">=1.33.0"
opentelemetry-semantic-conventions = ">=0.54b0"
opentelemetry-semantic-conventions-ai = ">=0.4.13"
orjson = ">=3.0.0"
packaging = ">=22.0"
pydantic = ">=2.0.3,<3.0.0"
python-dotenv = ">=1.0"
tenacity = ">=8.0"
tqdm = ">=4.0"
[package.extras]
alephalpha = ["opentelemetry-instrumentation-alephalpha (>=0.47.1)"]
all = ["opentelemetry-instrumentation-alephalpha (>=0.47.1)", "opentelemetry-instrumentation-bedrock (>=0.47.1)", "opentelemetry-instrumentation-chromadb (>=0.47.1)", "opentelemetry-instrumentation-cohere (>=0.47.1)", "opentelemetry-instrumentation-crewai (>=0.47.1)", "opentelemetry-instrumentation-haystack (>=0.47.1)", "opentelemetry-instrumentation-lancedb (>=0.47.1)", "opentelemetry-instrumentation-langchain (>=0.47.1)", "opentelemetry-instrumentation-llamaindex (>=0.47.1)", "opentelemetry-instrumentation-marqo (>=0.47.1)", "opentelemetry-instrumentation-mcp (>=0.47.1)", "opentelemetry-instrumentation-milvus (>=0.47.1)", "opentelemetry-instrumentation-mistralai (>=0.47.1)", "opentelemetry-instrumentation-ollama (>=0.47.1)", "opentelemetry-instrumentation-pinecone (>=0.47.1)", "opentelemetry-instrumentation-qdrant (>=0.47.1)", "opentelemetry-instrumentation-replicate (>=0.47.1)", "opentelemetry-instrumentation-sagemaker (>=0.47.1)", "opentelemetry-instrumentation-together (>=0.47.1)", "opentelemetry-instrumentation-transformers (>=0.47.1)", "opentelemetry-instrumentation-vertexai (>=0.47.1)", "opentelemetry-instrumentation-watsonx (>=0.47.1)", "opentelemetry-instrumentation-weaviate (>=0.47.1)"]
bedrock = ["opentelemetry-instrumentation-bedrock (>=0.47.1)"]
chromadb = ["opentelemetry-instrumentation-chromadb (>=0.47.1)"]
cohere = ["opentelemetry-instrumentation-cohere (>=0.47.1)"]
crewai = ["opentelemetry-instrumentation-crewai (>=0.47.1)"]
haystack = ["opentelemetry-instrumentation-haystack (>=0.47.1)"]
lancedb = ["opentelemetry-instrumentation-lancedb (>=0.47.1)"]
langchain = ["opentelemetry-instrumentation-langchain (>=0.47.1)"]
llamaindex = ["opentelemetry-instrumentation-llamaindex (>=0.47.1)"]
marqo = ["opentelemetry-instrumentation-marqo (>=0.47.1)"]
mcp = ["opentelemetry-instrumentation-mcp (>=0.47.1)"]
milvus = ["opentelemetry-instrumentation-milvus (>=0.47.1)"]
mistralai = ["opentelemetry-instrumentation-mistralai (>=0.47.1)"]
ollama = ["opentelemetry-instrumentation-ollama (>=0.47.1)"]
pinecone = ["opentelemetry-instrumentation-pinecone (>=0.47.1)"]
qdrant = ["opentelemetry-instrumentation-qdrant (>=0.47.1)"]
replicate = ["opentelemetry-instrumentation-replicate (>=0.47.1)"]
sagemaker = ["opentelemetry-instrumentation-sagemaker (>=0.47.1)"]
together = ["opentelemetry-instrumentation-together (>=0.47.1)"]
transformers = ["opentelemetry-instrumentation-transformers (>=0.47.1)"]
vertexai = ["opentelemetry-instrumentation-vertexai (>=0.47.1)"]
watsonx = ["opentelemetry-instrumentation-watsonx (>=0.47.1)"]
weaviate = ["opentelemetry-instrumentation-weaviate (>=0.47.1)"]
[[package]]
name = "lxml"
version = "6.0.1"
@@ -5820,15 +5737,12 @@ llama = ["llama-index (>=0.12.29,<0.13.0)", "llama-index-core (>=0.12.29,<0.13.0
[[package]]
name = "openhands-agent-server"
version = "1.1.0"
version = "1.0.0a4"
description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = [
{file = "openhands_agent_server-1.1.0-py3-none-any.whl", hash = "sha256:59a856883df23488c0723e47655ef21649a321fcd4709a25a4690866eff6ac88"},
{file = "openhands_agent_server-1.1.0.tar.gz", hash = "sha256:e39bebd39afd45cfcfd765005e7c4e5409e46678bd7612ae20bae79f7057b935"},
]
files = []
develop = false
[package.dependencies]
@@ -5845,13 +5759,13 @@ wsproto = ">=1.2.0"
[package.source]
type = "git"
url = "https://github.com/OpenHands/agent-sdk.git"
reference = "15f565b8ac38876e40dc05c08e2b04ccaae4a66d"
resolved_reference = "15f565b8ac38876e40dc05c08e2b04ccaae4a66d"
reference = "3d8af53b2f0259dc98555a4acd4238f90e0afbce"
resolved_reference = "3d8af53b2f0259dc98555a4acd4238f90e0afbce"
subdirectory = "openhands-agent-server"
[[package]]
name = "openhands-ai"
version = "0.0.0-post.5576+ed2ac6040"
version = "0.0.0-post.5456+15c207c40"
description = "OpenHands: Code Less, Make More"
optional = false
python-versions = "^3.12,<3.14"
@@ -5868,7 +5782,6 @@ bashlex = "^0.18"
boto3 = "*"
browsergym-core = "0.13.3"
deprecated = "*"
deprecation = "^2.1.0"
dirhash = "*"
docker = "*"
fastapi = "*"
@@ -5888,14 +5801,13 @@ jupyter_kernel_gateway = "*"
kubernetes = "^33.1.0"
libtmux = ">=0.46.2"
litellm = ">=1.74.3, <1.78.0, !=1.64.4, !=1.67.*"
lmnr = "^0.7.20"
memory-profiler = "^0.61.0"
numpy = "*"
openai = "1.99.9"
openhands-aci = "0.3.2"
openhands-agent-server = "1.1.0"
openhands-sdk = "1.1.0"
openhands-tools = "1.1.0"
openhands-agent-server = {git = "https://github.com/OpenHands/agent-sdk.git", rev = "3d8af53b2f0259dc98555a4acd4238f90e0afbce", subdirectory = "openhands-agent-server"}
openhands-sdk = {git = "https://github.com/OpenHands/agent-sdk.git", rev = "3d8af53b2f0259dc98555a4acd4238f90e0afbce", subdirectory = "openhands-sdk"}
openhands-tools = {git = "https://github.com/OpenHands/agent-sdk.git", rev = "3d8af53b2f0259dc98555a4acd4238f90e0afbce", subdirectory = "openhands-tools"}
opentelemetry-api = "^1.33.1"
opentelemetry-exporter-otlp-proto-grpc = "^1.33.1"
pathspec = "^0.12.1"
@@ -5951,23 +5863,18 @@ url = ".."
[[package]]
name = "openhands-sdk"
version = "1.1.0"
version = "1.0.0a4"
description = "OpenHands SDK - Core functionality for building AI agents"
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = [
{file = "openhands_sdk-1.1.0-py3-none-any.whl", hash = "sha256:4a984ce1687a48cf99a67fdf3d37b116f8b2840743d4807810b5024af6a1d57e"},
{file = "openhands_sdk-1.1.0.tar.gz", hash = "sha256:855e0d8f3657205e4119e50520c17e65b3358b1a923f7a051a82512a54bf426c"},
]
files = []
develop = false
[package.dependencies]
deprecation = ">=2.1.0"
fastmcp = ">=2.11.3"
httpx = ">=0.27.0"
litellm = ">=1.77.7.dev9"
lmnr = ">=0.7.20"
pydantic = ">=2.11.7"
python-frontmatter = ">=1.1.0"
python-json-logger = ">=3.3.0"
@@ -5980,27 +5887,24 @@ boto3 = ["boto3 (>=1.35.0)"]
[package.source]
type = "git"
url = "https://github.com/OpenHands/agent-sdk.git"
reference = "15f565b8ac38876e40dc05c08e2b04ccaae4a66d"
resolved_reference = "15f565b8ac38876e40dc05c08e2b04ccaae4a66d"
reference = "3d8af53b2f0259dc98555a4acd4238f90e0afbce"
resolved_reference = "3d8af53b2f0259dc98555a4acd4238f90e0afbce"
subdirectory = "openhands-sdk"
[[package]]
name = "openhands-tools"
version = "1.1.0"
version = "1.0.0a4"
description = "OpenHands Tools - Runtime tools for AI agents"
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = [
{file = "openhands_tools-1.1.0-py3-none-any.whl", hash = "sha256:767d6746f05edade49263aa24450a037485a3dc23379f56917ef19aad22033f9"},
{file = "openhands_tools-1.1.0.tar.gz", hash = "sha256:c2fadaa4f4e16e9a3df5781ea847565dcae7171584f09ef7c0e1d97c8dfc83f6"},
]
files = []
develop = false
[package.dependencies]
bashlex = ">=0.18"
binaryornot = ">=0.4.4"
browser-use = ">=0.8.0"
browser-use = ">=0.7.7"
cachetools = "*"
func-timeout = ">=4.3.5"
libtmux = ">=0.46.2"
@@ -6010,8 +5914,8 @@ pydantic = ">=2.11.7"
[package.source]
type = "git"
url = "https://github.com/OpenHands/agent-sdk.git"
reference = "15f565b8ac38876e40dc05c08e2b04ccaae4a66d"
resolved_reference = "15f565b8ac38876e40dc05c08e2b04ccaae4a66d"
reference = "3d8af53b2f0259dc98555a4acd4238f90e0afbce"
resolved_reference = "3d8af53b2f0259dc98555a4acd4238f90e0afbce"
subdirectory = "openhands-tools"
[[package]]
@@ -6084,62 +5988,6 @@ opentelemetry-proto = "1.36.0"
opentelemetry-sdk = ">=1.36.0,<1.37.0"
typing-extensions = ">=4.6.0"
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
version = "1.36.0"
description = "OpenTelemetry Collector Protobuf over HTTP Exporter"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "opentelemetry_exporter_otlp_proto_http-1.36.0-py3-none-any.whl", hash = "sha256:3d769f68e2267e7abe4527f70deb6f598f40be3ea34c6adc35789bea94a32902"},
{file = "opentelemetry_exporter_otlp_proto_http-1.36.0.tar.gz", hash = "sha256:dd3637f72f774b9fc9608ab1ac479f8b44d09b6fb5b2f3df68a24ad1da7d356e"},
]
[package.dependencies]
googleapis-common-protos = ">=1.52,<2.0"
opentelemetry-api = ">=1.15,<2.0"
opentelemetry-exporter-otlp-proto-common = "1.36.0"
opentelemetry-proto = "1.36.0"
opentelemetry-sdk = ">=1.36.0,<1.37.0"
requests = ">=2.7,<3.0"
typing-extensions = ">=4.5.0"
[[package]]
name = "opentelemetry-instrumentation"
version = "0.57b0"
description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "opentelemetry_instrumentation-0.57b0-py3-none-any.whl", hash = "sha256:9109280f44882e07cec2850db28210b90600ae9110b42824d196de357cbddf7e"},
{file = "opentelemetry_instrumentation-0.57b0.tar.gz", hash = "sha256:f2a30135ba77cdea2b0e1df272f4163c154e978f57214795d72f40befd4fcf05"},
]
[package.dependencies]
opentelemetry-api = ">=1.4,<2.0"
opentelemetry-semantic-conventions = "0.57b0"
packaging = ">=18.0"
wrapt = ">=1.0.0,<2.0.0"
[[package]]
name = "opentelemetry-instrumentation-threading"
version = "0.57b0"
description = "Thread context propagation support for OpenTelemetry"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "opentelemetry_instrumentation_threading-0.57b0-py3-none-any.whl", hash = "sha256:adfd64857c8c78d6111cf80552311e1713bad64272dd81abdd61f07b892a161b"},
{file = "opentelemetry_instrumentation_threading-0.57b0.tar.gz", hash = "sha256:06fa4c98d6bfe4670e7532497670ac202db42afa647ff770aedce0e422421c6e"},
]
[package.dependencies]
opentelemetry-api = ">=1.12,<2.0"
opentelemetry-instrumentation = "0.57b0"
wrapt = ">=1.0.0,<2.0.0"
[[package]]
name = "opentelemetry-proto"
version = "1.36.0"
@@ -6188,115 +6036,6 @@ files = [
opentelemetry-api = "1.36.0"
typing-extensions = ">=4.5.0"
[[package]]
name = "opentelemetry-semantic-conventions-ai"
version = "0.4.13"
description = "OpenTelemetry Semantic Conventions Extension for Large Language Models"
optional = false
python-versions = "<4,>=3.9"
groups = ["main"]
files = [
{file = "opentelemetry_semantic_conventions_ai-0.4.13-py3-none-any.whl", hash = "sha256:883a30a6bb5deaec0d646912b5f9f6dcbb9f6f72557b73d0f2560bf25d13e2d5"},
{file = "opentelemetry_semantic_conventions_ai-0.4.13.tar.gz", hash = "sha256:94efa9fb4ffac18c45f54a3a338ffeb7eedb7e1bb4d147786e77202e159f0036"},
]
[[package]]
name = "orjson"
version = "3.11.4"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "orjson-3.11.4-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e3aa2118a3ece0d25489cbe48498de8a5d580e42e8d9979f65bf47900a15aba1"},
{file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a69ab657a4e6733133a3dca82768f2f8b884043714e8d2b9ba9f52b6efef5c44"},
{file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3740bffd9816fc0326ddc406098a3a8f387e42223f5f455f2a02a9f834ead80c"},
{file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65fd2f5730b1bf7f350c6dc896173d3460d235c4be007af73986d7cd9a2acd23"},
{file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fdc3ae730541086158d549c97852e2eea6820665d4faf0f41bf99df41bc11ea"},
{file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e10b4d65901da88845516ce9f7f9736f9638d19a1d483b3883dc0182e6e5edba"},
{file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb6a03a678085f64b97f9d4a9ae69376ce91a3a9e9b56a82b1580d8e1d501aff"},
{file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c82e4f0b1c712477317434761fbc28b044c838b6b1240d895607441412371ac"},
{file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d58c166a18f44cc9e2bad03a327dc2d1a3d2e85b847133cfbafd6bfc6719bd79"},
{file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94f206766bf1ea30e1382e4890f763bd1eefddc580e08fec1ccdc20ddd95c827"},
{file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:41bf25fb39a34cf8edb4398818523277ee7096689db352036a9e8437f2f3ee6b"},
{file = "orjson-3.11.4-cp310-cp310-win32.whl", hash = "sha256:fa9627eba4e82f99ca6d29bc967f09aba446ee2b5a1ea728949ede73d313f5d3"},
{file = "orjson-3.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:23ef7abc7fca96632d8174ac115e668c1e931b8fe4dde586e92a500bf1914dcc"},
{file = "orjson-3.11.4-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e59d23cd93ada23ec59a96f215139753fbfe3a4d989549bcb390f8c00370b39"},
{file = "orjson-3.11.4-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5c3aedecfc1beb988c27c79d52ebefab93b6c3921dbec361167e6559aba2d36d"},
{file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9e5301f1c2caa2a9a4a303480d79c9ad73560b2e7761de742ab39fe59d9175"},
{file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8873812c164a90a79f65368f8f96817e59e35d0cc02786a5356f0e2abed78040"},
{file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d7feb0741ebb15204e748f26c9638e6665a5fa93c37a2c73d64f1669b0ddc63"},
{file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ee5487fefee21e6910da4c2ee9eef005bee568a0879834df86f888d2ffbdd9"},
{file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d40d46f348c0321df01507f92b95a377240c4ec31985225a6668f10e2676f9a"},
{file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95713e5fc8af84d8edc75b785d2386f653b63d62b16d681687746734b4dfc0be"},
{file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad73ede24f9083614d6c4ca9a85fe70e33be7bf047ec586ee2363bc7418fe4d7"},
{file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:842289889de515421f3f224ef9c1f1efb199a32d76d8d2ca2706fa8afe749549"},
{file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3b2427ed5791619851c52a1261b45c233930977e7de8cf36de05636c708fa905"},
{file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c36e524af1d29982e9b190573677ea02781456b2e537d5840e4538a5ec41907"},
{file = "orjson-3.11.4-cp311-cp311-win32.whl", hash = "sha256:87255b88756eab4a68ec61837ca754e5d10fa8bc47dc57f75cedfeaec358d54c"},
{file = "orjson-3.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:e2d5d5d798aba9a0e1fede8d853fa899ce2cb930ec0857365f700dffc2c7af6a"},
{file = "orjson-3.11.4-cp311-cp311-win_arm64.whl", hash = "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045"},
{file = "orjson-3.11.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d4371de39319d05d3f482f372720b841c841b52f5385bd99c61ed69d55d9ab50"},
{file = "orjson-3.11.4-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e41fd3b3cac850eaae78232f37325ed7d7436e11c471246b87b2cd294ec94853"},
{file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600e0e9ca042878c7fdf189cf1b028fe2c1418cc9195f6cb9824eb6ed99cb938"},
{file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7bbf9b333f1568ef5da42bc96e18bf30fd7f8d54e9ae066d711056add508e415"},
{file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806363144bb6e7297b8e95870e78d30a649fdc4e23fc84daa80c8ebd366ce44"},
{file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad355e8308493f527d41154e9053b86a5be892b3b359a5c6d5d95cda23601cb2"},
{file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a7517482667fb9f0ff1b2f16fe5829296ed7a655d04d68cd9711a4d8a4e708"},
{file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97eb5942c7395a171cbfecc4ef6701fc3c403e762194683772df4c54cfbb2210"},
{file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:149d95d5e018bdd822e3f38c103b1a7c91f88d38a88aada5c4e9b3a73a244241"},
{file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:624f3951181eb46fc47dea3d221554e98784c823e7069edb5dbd0dc826ac909b"},
{file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:03bfa548cf35e3f8b3a96c4e8e41f753c686ff3d8e182ce275b1751deddab58c"},
{file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:525021896afef44a68148f6ed8a8bf8375553d6066c7f48537657f64823565b9"},
{file = "orjson-3.11.4-cp312-cp312-win32.whl", hash = "sha256:b58430396687ce0f7d9eeb3dd47761ca7d8fda8e9eb92b3077a7a353a75efefa"},
{file = "orjson-3.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:c6dbf422894e1e3c80a177133c0dda260f81428f9de16d61041949f6a2e5c140"},
{file = "orjson-3.11.4-cp312-cp312-win_arm64.whl", hash = "sha256:d38d2bc06d6415852224fcc9c0bfa834c25431e466dc319f0edd56cca81aa96e"},
{file = "orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534"},
{file = "orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff"},
{file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad"},
{file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5"},
{file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a"},
{file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436"},
{file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9"},
{file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73"},
{file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0"},
{file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196"},
{file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a"},
{file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6"},
{file = "orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839"},
{file = "orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a"},
{file = "orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de"},
{file = "orjson-3.11.4-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803"},
{file = "orjson-3.11.4-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54"},
{file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e"},
{file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316"},
{file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1"},
{file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc"},
{file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f"},
{file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf"},
{file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606"},
{file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780"},
{file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23"},
{file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155"},
{file = "orjson-3.11.4-cp314-cp314-win32.whl", hash = "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394"},
{file = "orjson-3.11.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1"},
{file = "orjson-3.11.4-cp314-cp314-win_arm64.whl", hash = "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d"},
{file = "orjson-3.11.4-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:405261b0a8c62bcbd8e2931c26fdc08714faf7025f45531541e2b29e544b545b"},
{file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af02ff34059ee9199a3546f123a6ab4c86caf1708c79042caf0820dc290a6d4f"},
{file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b2eba969ea4203c177c7b38b36c69519e6067ee68c34dc37081fac74c796e10"},
{file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0baa0ea43cfa5b008a28d3c07705cf3ada40e5d347f0f44994a64b1b7b4b5350"},
{file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80fd082f5dcc0e94657c144f1b2a3a6479c44ad50be216cf0c244e567f5eae19"},
{file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e3704d35e47d5bee811fb1cbd8599f0b4009b14d451c4c57be5a7e25eb89a13"},
{file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa447f2b5356779d914658519c874cf3b7629e99e63391ed519c28c8aea4919"},
{file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bba5118143373a86f91dadb8df41d9457498226698ebdf8e11cbb54d5b0e802d"},
{file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:622463ab81d19ef3e06868b576551587de8e4d518892d1afab71e0fbc1f9cffc"},
{file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3e0a700c4b82144b72946b6629968df9762552ee1344bfdb767fecdd634fbd5a"},
{file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6e18a5c15e764e5f3fc569b47872450b4bcea24f2a6354c0a0e95ad21045d5a9"},
{file = "orjson-3.11.4-cp39-cp39-win32.whl", hash = "sha256:fb1c37c71cad991ef4d89c7a634b5ffb4447dbd7ae3ae13e8f5ee7f1775e7ab1"},
{file = "orjson-3.11.4-cp39-cp39-win_amd64.whl", hash = "sha256:e2985ce8b8c42d00492d0ed79f2bd2b6460d00f2fa671dfde4bf2e02f49bf5c6"},
{file = "orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d"},
]
[[package]]
name = "packaging"
version = "25.0"
@@ -6513,21 +6252,6 @@ files = [
[package.dependencies]
ptyprocess = ">=0.5"
[[package]]
name = "pfzy"
version = "0.3.4"
description = "Python port of the fzy fuzzy string matching algorithm"
optional = false
python-versions = ">=3.7,<4.0"
groups = ["main"]
files = [
{file = "pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96"},
{file = "pfzy-0.3.4.tar.gz", hash = "sha256:717ea765dd10b63618e7298b2d98efd819e0b30cd5905c9707223dceeb94b3f1"},
]
[package.extras]
docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17-beta.43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"]
[[package]]
name = "pg8000"
version = "1.31.5"

View File

@@ -17,10 +17,7 @@ GITLAB_APP_CLIENT_ID = os.getenv('GITLAB_APP_CLIENT_ID', '').strip()
GITLAB_APP_CLIENT_SECRET = os.getenv('GITLAB_APP_CLIENT_SECRET', '').strip()
BITBUCKET_APP_CLIENT_ID = os.getenv('BITBUCKET_APP_CLIENT_ID', '').strip()
BITBUCKET_APP_CLIENT_SECRET = os.getenv('BITBUCKET_APP_CLIENT_SECRET', '').strip()
AZURE_DEVOPS_APP_CLIENT_ID = os.getenv('AZURE_DEVOPS_APP_CLIENT_ID', '').strip()
AZURE_DEVOPS_APP_CLIENT_SECRET = os.getenv('AZURE_DEVOPS_APP_CLIENT_SECRET', '').strip()
ENABLE_ENTERPRISE_SSO = os.getenv('ENABLE_ENTERPRISE_SSO', '').strip()
ENABLE_AZURE_DEVOPS = os.environ.get('ENABLE_AZURE_DEVOPS', 'false') == 'true'
ENABLE_JIRA = os.environ.get('ENABLE_JIRA', 'false') == 'true'
ENABLE_JIRA_DC = os.environ.get('ENABLE_JIRA_DC', 'false') == 'true'
ENABLE_LINEAR = os.environ.get('ENABLE_LINEAR', 'false') == 'true'
@@ -33,11 +30,3 @@ JIRA_DC_CLIENT_SECRET = os.getenv('JIRA_DC_CLIENT_SECRET', '').strip()
JIRA_DC_BASE_URL = os.getenv('JIRA_DC_BASE_URL', '').strip()
JIRA_DC_ENABLE_OAUTH = os.getenv('JIRA_DC_ENABLE_OAUTH', '1') in ('1', 'true')
AUTH_URL = os.getenv('AUTH_URL', '').rstrip('/')
ROLE_CHECK_ENABLED = os.getenv('ROLE_CHECK_ENABLED', 'false').lower() in (
'1',
'true',
't',
'yes',
'y',
'on',
)

View File

@@ -8,9 +8,7 @@ import jwt
import requests # type: ignore
from fastapi import HTTPException
from server.auth.constants import (
AZURE_DEVOPS_APP_CLIENT_ID,
BITBUCKET_APP_CLIENT_ID,
ENABLE_AZURE_DEVOPS,
ENABLE_ENTERPRISE_SSO,
ENABLE_JIRA,
ENABLE_JIRA_DC,
@@ -86,7 +84,6 @@ class SaaSServerConfig(ServerConfig):
maintenance_start_time: str = os.environ.get(
'MAINTENANCE_START_TIME', ''
) # Timestamp in EST e.g 2025-07-29T14:18:01.219616-04:00
enable_azure_devops = ENABLE_AZURE_DEVOPS
enable_jira = ENABLE_JIRA
enable_jira_dc = ENABLE_JIRA_DC
enable_linear = ENABLE_LINEAR
@@ -163,9 +160,6 @@ class SaaSServerConfig(ServerConfig):
if BITBUCKET_APP_CLIENT_ID:
providers_configured.append(ProviderType.BITBUCKET)
if AZURE_DEVOPS_APP_CLIENT_ID:
providers_configured.append(ProviderType.AZURE_DEVOPS)
if ENABLE_ENTERPRISE_SSO:
providers_configured.append(ProviderType.ENTERPRISE_SSO)
@@ -177,7 +171,6 @@ class SaaSServerConfig(ServerConfig):
'FEATURE_FLAGS': {
'ENABLE_BILLING': self.enable_billing,
'HIDE_LLM_SETTINGS': self.hide_llm_settings,
'ENABLE_AZURE_DEVOPS': self.enable_azure_devops,
'ENABLE_JIRA': self.enable_jira,
'ENABLE_JIRA_DC': self.enable_jira_dc,
'ENABLE_LINEAR': self.enable_linear,

View File

@@ -50,7 +50,7 @@ SUBSCRIPTION_PRICE_DATA = {
},
}
DEFAULT_INITIAL_BUDGET = float(os.environ.get('DEFAULT_INITIAL_BUDGET', '10'))
DEFAULT_INITIAL_BUDGET = float(os.environ.get('DEFAULT_INITIAL_BUDGET', '20'))
STRIPE_API_KEY = os.environ.get('STRIPE_API_KEY', None)
STRIPE_WEBHOOK_SECRET = os.environ.get('STRIPE_WEBHOOK_SECRET', None)
REQUIRE_PAYMENT = os.environ.get('REQUIRE_PAYMENT', '0') in ('1', 'true')

View File

@@ -12,7 +12,6 @@ from server.auth.constants import (
KEYCLOAK_CLIENT_ID,
KEYCLOAK_REALM_NAME,
KEYCLOAK_SERVER_URL_EXT,
ROLE_CHECK_ENABLED,
)
from server.auth.gitlab_sync import schedule_gitlab_repo_sync
from server.auth.saas_user_auth import SaasUserAuth
@@ -31,7 +30,6 @@ from openhands.server.services.conversation_service import create_provider_token
from openhands.server.shared import config
from openhands.server.user_auth import get_access_token
from openhands.server.user_auth.user_auth import get_user_auth
from openhands.utils.posthog_tracker import track_user_signup_completed
with warnings.catch_warnings():
warnings.simplefilter('ignore')
@@ -134,12 +132,6 @@ async def keycloak_callback(
user_info = await token_manager.get_user_info(keycloak_access_token)
logger.debug(f'user_info: {user_info}')
if ROLE_CHECK_ENABLED and 'roles' not in user_info:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={'error': 'Missing required role'},
)
if 'sub' not in user_info or 'preferred_username' not in user_info:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -370,12 +362,6 @@ async def accept_tos(request: Request):
logger.info(f'User {user_id} accepted TOS')
# Track user signup completion in PostHog
track_user_signup_completed(
user_id=user_id,
signup_timestamp=user_settings.accepted_tos.isoformat(),
)
response = JSONResponse(
status_code=status.HTTP_200_OK, content={'redirect_url': redirect_url}
)

View File

@@ -28,7 +28,6 @@ from storage.subscription_access import SubscriptionAccess
from openhands.server.user_auth import get_user_id
from openhands.utils.http_session import httpx_verify_option
from openhands.utils.posthog_tracker import track_credits_purchased
stripe.api_key = STRIPE_API_KEY
billing_router = APIRouter(prefix='/api/billing')
@@ -458,20 +457,6 @@ async def success_callback(session_id: str, request: Request):
)
session.commit()
# Track credits purchased in PostHog
try:
track_credits_purchased(
user_id=billing_session.user_id,
amount_usd=amount_subtotal / 100, # Convert cents to dollars
credits_added=add_credits,
stripe_session_id=session_id,
)
except Exception as e:
logger.warning(
f'Failed to track credits purchase: {e}',
extra={'user_id': billing_session.user_id, 'error': str(e)},
)
return RedirectResponse(
f'{request.base_url}settings/billing?checkout=success', status_code=302
)

View File

@@ -35,7 +35,6 @@ class SaasConversationStore(ConversationStore):
session.query(StoredConversationMetadata)
.filter(StoredConversationMetadata.user_id == self.user_id)
.filter(StoredConversationMetadata.conversation_id == conversation_id)
.filter(StoredConversationMetadata.conversation_version == 'V0')
)
def _to_external_model(self, conversation_metadata: StoredConversationMetadata):
@@ -60,7 +59,6 @@ class SaasConversationStore(ConversationStore):
kwargs.pop('reasoning_tokens', None)
kwargs.pop('context_window', None)
kwargs.pop('per_turn_token', None)
kwargs.pop('parent_conversation_id', None)
return ConversationMetadata(**kwargs)
@@ -125,7 +123,6 @@ class SaasConversationStore(ConversationStore):
conversations = (
session.query(StoredConversationMetadata)
.filter(StoredConversationMetadata.user_id == self.user_id)
.filter(StoredConversationMetadata.conversation_version == 'V0')
.order_by(StoredConversationMetadata.created_at.desc())
.offset(offset)
.limit(limit + 1)

View File

@@ -97,10 +97,6 @@ class SaasSettingsStore(SettingsStore):
return settings
async def store(self, item: Settings):
# Check if provider is OpenHands and generate API key if needed
if item and self._is_openhands_provider(item):
await self._ensure_openhands_api_key(item)
with self.session_maker() as session:
existing = None
kwargs = {}
@@ -372,30 +368,6 @@ class SaasSettingsStore(SettingsStore):
def _should_encrypt(self, key: str) -> bool:
return key in ('llm_api_key', 'llm_api_key_for_byor', 'search_api_key')
def _is_openhands_provider(self, item: Settings) -> bool:
"""Check if the settings use the OpenHands provider."""
return bool(item.llm_model and item.llm_model.startswith('openhands/'))
async def _ensure_openhands_api_key(self, item: Settings) -> None:
"""Generate and set the OpenHands API key for the given settings.
First checks if an existing key with the OpenHands alias exists,
and reuses it if found. Otherwise, generates a new key.
"""
# Generate new key if none exists
generated_key = await self._generate_openhands_key()
if generated_key:
item.llm_api_key = SecretStr(generated_key)
logger.info(
'saas_settings_store:store:generated_openhands_key',
extra={'user_id': self.user_id},
)
else:
logger.warning(
'saas_settings_store:store:failed_to_generate_openhands_key',
extra={'user_id': self.user_id},
)
async def _create_user_in_lite_llm(
self, client: httpx.AsyncClient, email: str | None, max_budget: int, spend: int
):
@@ -418,55 +390,3 @@ class SaasSettingsStore(SettingsStore):
},
)
return response
async def _generate_openhands_key(self) -> str | None:
"""Generate a new OpenHands provider key for a user."""
if not (LITE_LLM_API_KEY and LITE_LLM_API_URL):
logger.warning(
'saas_settings_store:_generate_openhands_key:litellm_config_not_found',
extra={'user_id': self.user_id},
)
return None
try:
async with httpx.AsyncClient(
verify=httpx_verify_option(),
headers={
'x-goog-api-key': LITE_LLM_API_KEY,
},
) as client:
response = await client.post(
f'{LITE_LLM_API_URL}/key/generate',
json={
'user_id': self.user_id,
'metadata': {'type': 'openhands'},
},
)
response.raise_for_status()
response_json = response.json()
key = response_json.get('key')
if key:
logger.info(
'saas_settings_store:_generate_openhands_key:success',
extra={
'user_id': self.user_id,
'key_length': len(key) if key else 0,
'key_prefix': (
key[:10] + '...' if key and len(key) > 10 else key
),
},
)
return key
else:
logger.error(
'saas_settings_store:_generate_openhands_key:no_key_in_response',
extra={'user_id': self.user_id, 'response_json': response_json},
)
return None
except Exception as e:
logger.exception(
'saas_settings_store:_generate_openhands_key:error',
extra={'user_id': self.user_id, 'error': str(e)},
)
return None

View File

@@ -92,8 +92,11 @@ def test_unknown_variant_returns_original_agent_without_changes(monkeypatch):
assert getattr(result, 'condenser', None) is None
@patch('experiments.experiment_manager.handle_condenser_max_step_experiment__v1')
@patch('experiments.experiment_manager.ENABLE_EXPERIMENT_MANAGER', False)
def test_run_agent_variant_tests_v1_noop_when_manager_disabled():
def test_run_agent_variant_tests_v1_noop_when_manager_disabled(
mock_handle_condenser,
):
"""If ENABLE_EXPERIMENT_MANAGER is False, the method returns the exact same agent and does not call the handler."""
agent = make_agent()
conv_id = uuid4()
@@ -106,6 +109,8 @@ def test_run_agent_variant_tests_v1_noop_when_manager_disabled():
# Same object returned (no copy)
assert result is agent
# Handler should not have been called
mock_handle_condenser.assert_not_called()
@patch('experiments.experiment_manager.ENABLE_EXPERIMENT_MANAGER', True)
@@ -126,3 +131,7 @@ def test_run_agent_variant_tests_v1_calls_handler_and_sets_system_prompt(monkeyp
# Should be a different instance than the original (copied after handler runs)
assert result is not agent
assert result.system_prompt_filename == 'system_prompt_long_horizon.j2'
# The condenser returned by the handler must be preserved after the system-prompt override copy
assert isinstance(result.condenser, LLMSummarizingCondenser)
assert result.condenser.max_size == 80

View File

@@ -243,7 +243,7 @@ async def test_update_settings_with_litellm_default(
# Check that the URL and most of the JSON payload match what we expect
assert call_args['json']['user_email'] == 'testy@tester.com'
assert call_args['json']['models'] == []
assert call_args['json']['max_budget'] == 10.0
assert call_args['json']['max_budget'] == 20.0
assert call_args['json']['user_id'] == 'user-id'
assert call_args['json']['teams'] == ['test_team']
assert call_args['json']['auto_create_key'] is True

View File

@@ -15,7 +15,7 @@ python evaluation/benchmarks/multi_swe_bench/scripts/data/data_change.py
## Docker image download
Please download the multi-swe-bench docker images from [here](https://github.com/multi-swe-bench/multi-swe-bench?tab=readme-ov-file#run-evaluation).
Please download the multi-swe-bench dokcer images from [here](https://github.com/multi-swe-bench/multi-swe-bench?tab=readme-ov-file#run-evaluation).
## Generate patch
@@ -47,7 +47,7 @@ For debugging purposes, you can set `export EVAL_SKIP_MAXIMUM_RETRIES_EXCEEDED=t
The results will be generated in evaluation/evaluation_outputs/outputs/XXX/CodeActAgent/YYY/output.jsonl, you can refer to the [example](examples/output.jsonl).
## Running evaluation
## Runing evaluation
First, install [multi-swe-bench](https://github.com/multi-swe-bench/multi-swe-bench).

View File

@@ -0,0 +1,69 @@
# Integration tests
This directory implements integration tests that [was running in CI](https://github.com/OpenHands/OpenHands/tree/23d3becf1d6f5d07e592f7345750c314a826b4e9/tests/integration).
[PR 3985](https://github.com/OpenHands/OpenHands/pull/3985) introduce LLM-based editing, which requires access to LLM to perform edit. Hence, we remove integration tests from CI and intend to run them as nightly evaluation to ensure the quality of OpenHands softwares.
## To add new tests
Each test is a file named like `tXX_testname.py` where `XX` is a number.
Make sure to name the file for each test to start with `t` and ends with `.py`.
Each test should be structured as a subclass of [`BaseIntegrationTest`](./tests/base.py), where you need to implement `initialize_runtime` that setup the runtime enviornment before test, and `verify_result` that takes in a `Runtime` and history of `Event` and return a `TestResult`. See [t01_fix_simple_typo.py](./tests/t01_fix_simple_typo.py) and [t05_simple_browsing.py](./tests/t05_simple_browsing.py) for two representative examples.
```python
class TestResult(BaseModel):
success: bool
reason: str | None = None
class BaseIntegrationTest(ABC):
"""Base class for integration tests."""
INSTRUCTION: str
@classmethod
@abstractmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
"""Initialize the runtime for the test to run."""
pass
@classmethod
@abstractmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
"""Verify the result of the test.
This method will be called after the agent performs the task on the runtime.
"""
pass
```
## Setup Environment and LLM Configuration
Please follow instruction [here](../README.md#setup) to setup your local
development environment and LLM.
## Start the evaluation
```bash
./evaluation/integration_tests/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [eval-num-workers] [eval_ids]
```
- `model_config`, e.g. `eval_gpt4_1106_preview`, is the config group name for
your LLM settings, as defined in your `config.toml`.
- `git-version`, e.g. `HEAD`, is the git commit hash of the OpenHands version
you would like to evaluate. It could also be a release tag like `0.9.0`.
- `agent`, e.g. `CodeActAgent`, is the name of the agent for benchmarks,
defaulting to `CodeActAgent`.
- `eval_limit`, e.g. `10`, limits the evaluation to the first `eval_limit`
instances. By default, the script evaluates the entire Exercism test set
(133 issues). Note: in order to use `eval_limit`, you must also set `agent`.
- `eval-num-workers`: the number of workers to use for evaluation. Default: `1`.
- `eval_ids`, e.g. `"1,3,10"`, limits the evaluation to instances with the
given IDs (comma separated).
Example:
```bash
./evaluation/integration_tests/scripts/run_infer.sh llm.claude-35-sonnet-eval HEAD CodeActAgent
```

View File

View File

@@ -0,0 +1,251 @@
import asyncio
import importlib.util
import os
import pandas as pd
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_llm_config_for_completions_logging,
)
from evaluation.utils.shared import (
codeact_user_response as fake_user_response,
)
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import MessageAction
from openhands.events.serialization.event import event_to_dict
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
FAKE_RESPONSES = {
'CodeActAgent': fake_user_response,
'VisualBrowsingAgent': fake_user_response,
}
def get_config(
metadata: EvalMetadata,
instance_id: str,
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.platform = 'linux/amd64'
config = get_openhands_config_for_eval(
metadata=metadata,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox_config=sandbox_config,
)
config.debug = True
config.set_llm_config(
update_llm_config_for_completions_logging(
metadata.llm_config, metadata.eval_output_dir, instance_id
)
)
agent_config = AgentConfig(
enable_jupyter=True,
enable_browsing=True,
enable_llm_editor=False,
)
config.set_agent_config(agent_config)
return config
def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
) -> EvalOutput:
config = get_config(metadata, instance.instance_id)
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
reset_logger_for_multiprocessing(logger, str(instance.instance_id), log_dir)
else:
logger.info(
f'\nStarting evaluation for instance {str(instance.instance_id)}.\n'
)
# =============================================
# import test instance
# =============================================
instance_id = instance.instance_id
spec = importlib.util.spec_from_file_location(instance_id, instance.file_path)
test_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(test_module)
assert hasattr(test_module, 'Test'), (
f'Test module {instance_id} does not have a Test class'
)
test_class: type[BaseIntegrationTest] = test_module.Test
assert issubclass(test_class, BaseIntegrationTest), (
f'Test class {instance_id} does not inherit from BaseIntegrationTest'
)
instruction = test_class.INSTRUCTION
# =============================================
# create sandbox and run the agent
# =============================================
runtime: Runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
try:
test_class.initialize_runtime(runtime)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
state: State | None = asyncio.run(
run_controller(
config=config,
initial_user_action=MessageAction(content=instruction),
runtime=runtime,
fake_user_response_fn=FAKE_RESPONSES[metadata.agent_class],
)
)
if state is None:
raise ValueError('State should not be None.')
# # =============================================
# # result evaluation
# # =============================================
histories = state.history
# some basic check
logger.info(f'Total events in history: {len(histories)}')
assert len(histories) > 0, 'History should not be empty'
test_result: TestResult = test_class.verify_result(runtime, histories)
metrics = get_metrics(state)
finally:
runtime.close()
# Save the output
output = EvalOutput(
instance_id=str(instance.instance_id),
instance=instance.to_dict(),
instruction=instruction,
metadata=metadata,
history=[event_to_dict(event) for event in histories],
metrics=metrics,
error=state.last_error if state and state.last_error else None,
test_result=test_result.model_dump(),
)
return output
def load_integration_tests() -> pd.DataFrame:
"""Load tests from python files under ./tests"""
cur_dir = os.path.dirname(os.path.abspath(__file__))
test_dir = os.path.join(cur_dir, 'tests')
test_files = [
os.path.join(test_dir, f)
for f in os.listdir(test_dir)
if f.startswith('t') and f.endswith('.py')
]
df = pd.DataFrame(test_files, columns=['file_path'])
df['instance_id'] = df['file_path'].apply(
lambda x: os.path.basename(x).rstrip('.py')
)
return df
if __name__ == '__main__':
parser = get_evaluation_parser()
args, _ = parser.parse_known_args()
integration_tests = load_integration_tests()
llm_config = None
if args.llm_config:
llm_config = get_llm_config_arg(args.llm_config)
if llm_config is None:
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
'integration_tests',
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
# Parse dataset IDs if provided
eval_ids = None
if args.eval_ids:
eval_ids = str(args.eval_ids).split(',')
logger.info(f'\nUsing specific dataset IDs: {eval_ids}\n')
instances = prepare_dataset(
integration_tests,
output_file,
args.eval_n_limit,
eval_ids=eval_ids,
)
run_evaluation(
instances,
metadata,
output_file,
args.eval_num_workers,
process_instance,
)
df = pd.read_json(output_file, lines=True, orient='records')
# record success and reason
df['success'] = df['test_result'].apply(lambda x: x['success'])
df['reason'] = df['test_result'].apply(lambda x: x['reason'])
logger.info('-' * 100)
logger.info(
f'Success rate: {df["success"].mean():.2%} ({df["success"].sum()}/{len(df)})'
)
logger.info(
'\nEvaluation Results:'
+ '\n'
+ df[['instance_id', 'success', 'reason']].to_string(index=False)
)
logger.info('-' * 100)
# record cost for each instance, with 3 decimal places
# we sum up all the "costs" from the metrics array
df['cost'] = df['metrics'].apply(
lambda m: round(sum(c['cost'] for c in m['costs']), 3)
if m and 'costs' in m
else 0.0
)
# capture the top-level error if present, per instance
df['error_message'] = df.get('error', None)
logger.info(f'Total cost: USD {df["cost"].sum():.2f}')
report_file = os.path.join(metadata.eval_output_dir, 'report.md')
with open(report_file, 'w') as f:
f.write(
f'Success rate: {df["success"].mean():.2%}'
f' ({df["success"].sum()}/{len(df)})\n'
)
f.write(f'\nTotal cost: USD {df["cost"].sum():.2f}\n')
f.write(
df[
['instance_id', 'success', 'reason', 'cost', 'error_message']
].to_markdown(index=False)
)

View File

@@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -eo pipefail
source "evaluation/utils/version_control.sh"
MODEL_CONFIG=$1
COMMIT_HASH=$2
AGENT=$3
EVAL_LIMIT=$4
MAX_ITERATIONS=$5
NUM_WORKERS=$6
EVAL_IDS=$7
if [ -z "$NUM_WORKERS" ]; then
NUM_WORKERS=1
echo "Number of workers not specified, use default $NUM_WORKERS"
fi
checkout_eval_branch
if [ -z "$AGENT" ]; then
echo "Agent not specified, use default CodeActAgent"
AGENT="CodeActAgent"
fi
get_openhands_version
echo "AGENT: $AGENT"
echo "OPENHANDS_VERSION: $OPENHANDS_VERSION"
echo "MODEL_CONFIG: $MODEL_CONFIG"
EVAL_NOTE=$OPENHANDS_VERSION
# Default to NOT use unit tests.
if [ -z "$USE_UNIT_TESTS" ]; then
export USE_UNIT_TESTS=false
fi
echo "USE_UNIT_TESTS: $USE_UNIT_TESTS"
# If use unit tests, set EVAL_NOTE to the commit hash
if [ "$USE_UNIT_TESTS" = true ]; then
EVAL_NOTE=$EVAL_NOTE-w-test
fi
# export PYTHONPATH=evaluation/integration_tests:\$PYTHONPATH
COMMAND="poetry run python evaluation/integration_tests/run_infer.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--max-iterations ${MAX_ITERATIONS:-10} \
--eval-num-workers $NUM_WORKERS \
--eval-note $EVAL_NOTE"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
fi
if [ -n "$EVAL_IDS" ]; then
echo "EVAL_IDS: $EVAL_IDS"
COMMAND="$COMMAND --eval-ids $EVAL_IDS"
fi
# Run the command
eval $COMMAND

View File

@@ -0,0 +1,32 @@
from abc import ABC, abstractmethod
from pydantic import BaseModel
from openhands.events.event import Event
from openhands.runtime.base import Runtime
class TestResult(BaseModel):
success: bool
reason: str | None = None
class BaseIntegrationTest(ABC):
"""Base class for integration tests."""
INSTRUCTION: str
@classmethod
@abstractmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
"""Initialize the runtime for the test to run."""
pass
@classmethod
@abstractmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
"""Verify the result of the test.
This method will be called after the agent performs the task on the runtime.
"""
pass

View File

@@ -0,0 +1,39 @@
import os
import tempfile
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from openhands.events.action import CmdRunAction
from openhands.events.event import Event
from openhands.runtime.base import Runtime
class Test(BaseIntegrationTest):
INSTRUCTION = 'Fix typos in bad.txt.'
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
# create a file with a typo in /workspace/bad.txt
with tempfile.TemporaryDirectory() as temp_dir:
temp_file_path = os.path.join(temp_dir, 'bad.txt')
with open(temp_file_path, 'w') as f:
f.write('This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!')
# Copy the file to the desired location
runtime.copy_to(temp_file_path, '/workspace')
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the file /workspace/bad.txt has been fixed
action = CmdRunAction(command='cat /workspace/bad.txt')
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False, reason=f'Failed to run command: {obs.content}'
)
# check if the file /workspace/bad.txt has been fixed
if (
obs.content.strip().replace('\r\n', '\n')
== 'This is a stupid typo.\nReally?\nNo more typos!\nEnjoy!'
):
return TestResult(success=True)
return TestResult(success=False, reason=f'File not fixed: {obs.content}')

View File

@@ -0,0 +1,40 @@
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from evaluation.utils.shared import assert_and_raise
from openhands.events.action import CmdRunAction
from openhands.events.event import Event
from openhands.runtime.base import Runtime
class Test(BaseIntegrationTest):
INSTRUCTION = "Write a shell script '/workspace/hello.sh' that prints 'hello'."
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
action = CmdRunAction(command='mkdir -p /workspace')
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the file /workspace/hello.sh exists
action = CmdRunAction(command='cat /workspace/hello.sh')
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False,
reason=f'Failed to cat /workspace/hello.sh: {obs.content}.',
)
# execute the script
action = CmdRunAction(command='bash /workspace/hello.sh')
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False,
reason=f'Failed to execute /workspace/hello.sh: {obs.content}.',
)
if obs.content.strip() != 'hello':
return TestResult(
success=False, reason=f'Script did not print "hello": {obs.content}.'
)
return TestResult(success=True)

View File

@@ -0,0 +1,43 @@
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from evaluation.utils.shared import assert_and_raise
from openhands.events.action import CmdRunAction
from openhands.events.event import Event
from openhands.runtime.base import Runtime
class Test(BaseIntegrationTest):
INSTRUCTION = "Use Jupyter IPython to write a text file containing 'hello world' to '/workspace/test.txt'."
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
action = CmdRunAction(command='mkdir -p /workspace')
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the file /workspace/hello.sh exists
action = CmdRunAction(command='cat /workspace/test.txt')
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False,
reason=f'Failed to cat /workspace/test.txt: {obs.content}.',
)
# execute the script
action = CmdRunAction(command='cat /workspace/test.txt')
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False,
reason=f'Failed to cat /workspace/test.txt: {obs.content}.',
)
if 'hello world' not in obs.content.strip():
return TestResult(
success=False,
reason=f'File did not contain "hello world": {obs.content}.',
)
return TestResult(success=True)

View File

@@ -0,0 +1,57 @@
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from evaluation.utils.shared import assert_and_raise
from openhands.events.action import CmdRunAction
from openhands.events.event import Event
from openhands.runtime.base import Runtime
class Test(BaseIntegrationTest):
INSTRUCTION = 'Write a git commit message for the current staging area and commit the changes.'
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
action = CmdRunAction(command='mkdir -p /workspace')
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# git init
action = CmdRunAction(command='git init')
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# create file
action = CmdRunAction(command='echo \'print("hello world")\' > hello.py')
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# git add
cmd_str = 'git add hello.py'
action = CmdRunAction(command=cmd_str)
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the file /workspace/hello.py exists
action = CmdRunAction(command='cat /workspace/hello.py')
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False,
reason=f'Failed to cat /workspace/hello.py: {obs.content}.',
)
# check if the staging area is empty
action = CmdRunAction(command='git status')
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False, reason=f'Failed to git status: {obs.content}.'
)
if 'nothing to commit, working tree clean' in obs.content.strip():
return TestResult(success=True)
return TestResult(
success=False,
reason=f'Failed to check for "nothing to commit, working tree clean": {obs.content}.',
)

View File

@@ -0,0 +1,145 @@
import os
import tempfile
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from evaluation.utils.shared import assert_and_raise
from openhands.events.action import AgentFinishAction, CmdRunAction, MessageAction
from openhands.events.event import Event
from openhands.events.observation import AgentDelegateObservation
from openhands.runtime.base import Runtime
HTML_FILE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Ultimate Answer</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(to right, #1e3c72, #2a5298);
color: #fff;
font-family: 'Arial', sans-serif;
text-align: center;
}
.container {
text-align: center;
padding: 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
h1 {
font-size: 36px;
margin-bottom: 20px;
}
p {
font-size: 18px;
margin-bottom: 30px;
}
#showButton {
padding: 10px 20px;
font-size: 16px;
color: #1e3c72;
background: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s ease;
}
#showButton:hover {
background: #f0f0f0;
}
#result {
margin-top: 20px;
font-size: 24px;
}
</style>
</head>
<body>
<div class="container">
<h1>The Ultimate Answer</h1>
<p>Click the button to reveal the answer to life, the universe, and everything.</p>
<button id="showButton">Click me</button>
<div id="result"></div>
</div>
<script>
document.getElementById('showButton').addEventListener('click', function() {
document.getElementById('result').innerText = 'The answer is OpenHands is all you need!';
});
</script>
</body>
</html>
"""
class Test(BaseIntegrationTest):
INSTRUCTION = 'Browse localhost:8000, and tell me the ultimate answer to life.'
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
action = CmdRunAction(command='mkdir -p /workspace')
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
action = CmdRunAction(command='mkdir -p /tmp/server')
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# create a file with a typo in /workspace/bad.txt
with tempfile.TemporaryDirectory() as temp_dir:
temp_file_path = os.path.join(temp_dir, 'index.html')
with open(temp_file_path, 'w') as f:
f.write(HTML_FILE)
# Copy the file to the desired location
runtime.copy_to(temp_file_path, '/tmp/server')
# create README.md
action = CmdRunAction(
command='cd /tmp/server && nohup python3 -m http.server 8000 &'
)
obs = runtime.run_action(action)
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
from openhands.core.logger import openhands_logger as logger
# check if the "The answer is OpenHands is all you need!" is in any message
message_actions = [
event
for event in histories
if isinstance(
event, (MessageAction, AgentFinishAction, AgentDelegateObservation)
)
]
logger.debug(f'Total message-like events: {len(message_actions)}')
for event in message_actions:
try:
if isinstance(event, AgentDelegateObservation):
content = event.content
elif isinstance(event, AgentFinishAction):
content = event.outputs.get('content', '')
elif isinstance(event, MessageAction):
content = event.content
else:
logger.warning(f'Unexpected event type: {type(event)}')
continue
if 'OpenHands is all you need!' in content:
return TestResult(success=True)
except Exception as e:
logger.error(f'Error processing event: {e}')
logger.debug(
f'Total messages: {len(message_actions)}. Messages: {message_actions}'
)
return TestResult(
success=False,
reason=f'The answer is not found in any message. Total messages: {len(message_actions)}.',
)

View File

@@ -0,0 +1,58 @@
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from openhands.events.action import AgentFinishAction, MessageAction
from openhands.events.event import Event
from openhands.events.observation import AgentDelegateObservation
from openhands.runtime.base import Runtime
class Test(BaseIntegrationTest):
INSTRUCTION = 'Look at https://github.com/OpenHands/OpenHands/pull/8, and tell me what is happening there and what did @asadm suggest.'
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
pass
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
from openhands.core.logger import openhands_logger as logger
# check if the license information is in any message
message_actions = [
event
for event in histories
if isinstance(
event, (MessageAction, AgentFinishAction, AgentDelegateObservation)
)
]
logger.info(f'Total message-like events: {len(message_actions)}')
for event in message_actions:
try:
if isinstance(event, AgentDelegateObservation):
content = event.content
elif isinstance(event, AgentFinishAction):
content = event.outputs.get('content', '')
if event.thought:
content += f'\n\n{event.thought}'
elif isinstance(event, MessageAction):
content = event.content
else:
logger.warning(f'Unexpected event type: {type(event)}')
continue
if (
'non-commercial' in content
or 'MIT' in content
or 'Apache 2.0' in content
):
return TestResult(success=True)
except Exception as e:
logger.error(f'Error processing event: {e}')
logger.debug(
f'Total messages: {len(message_actions)}. Messages: {message_actions}'
)
return TestResult(
success=False,
reason=f'The answer is not found in any message. Total messages: {len(message_actions)}.',
)

View File

@@ -0,0 +1,73 @@
import hashlib
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from openhands.events.action import (
AgentFinishAction,
FileWriteAction,
MessageAction,
)
from openhands.events.event import Event
from openhands.events.observation import AgentDelegateObservation
from openhands.runtime.base import Runtime
class Test(BaseIntegrationTest):
INSTRUCTION = 'Execute the python script /workspace/python_script.py with input "John" and "25" and tell me the secret number.'
SECRET_NUMBER = int(hashlib.sha256(str(25).encode()).hexdigest()[:8], 16) % 1000
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
from openhands.core.logger import openhands_logger as logger
action = FileWriteAction(
path='/workspace/python_script.py',
content=(
'name = input("Enter your name: "); age = input("Enter your age: "); '
'import hashlib; secret = int(hashlib.sha256(str(age).encode()).hexdigest()[:8], 16) % 1000; '
'print(f"Hello {name}, you are {age} years old. Tell you a secret number: {secret}")'
),
)
logger.info(action, extra={'msg_type': 'ACTION'})
observation = runtime.run_action(action)
logger.info(observation, extra={'msg_type': 'OBSERVATION'})
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
from openhands.core.logger import openhands_logger as logger
# check if the license information is in any message
message_actions = [
event
for event in histories
if isinstance(
event, (MessageAction, AgentFinishAction, AgentDelegateObservation)
)
]
logger.info(f'Total message-like events: {len(message_actions)}')
for event in message_actions:
try:
if isinstance(event, AgentDelegateObservation):
content = event.content
elif isinstance(event, AgentFinishAction):
content = event.outputs.get('content', '')
if event.thought:
content += f'\n\n{event.thought}'
elif isinstance(event, MessageAction):
content = event.content
else:
logger.warning(f'Unexpected event type: {type(event)}')
continue
if str(cls.SECRET_NUMBER) in content:
return TestResult(success=True)
except Exception as e:
logger.error(f'Error processing event: {e}')
logger.debug(
f'Total messages: {len(message_actions)}. Messages: {message_actions}'
)
return TestResult(
success=False,
reason=f'The answer is not found in any message. Total messages: {len(message_actions)}.',
)

View File

@@ -123,7 +123,6 @@ describe("ExpandableMessage", () => {
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
ENABLE_AZURE_DEVOPS: false,
},
});
const RouterStub = createRoutesStub([

View File

@@ -33,24 +33,9 @@ describe("AccountSettingsContextMenu", () => {
expect(
screen.getByTestId("account-settings-context-menu"),
).toBeInTheDocument();
expect(screen.getByText("SIDEBAR$DOCS")).toBeInTheDocument();
expect(screen.getByText("ACCOUNT_SETTINGS$LOGOUT")).toBeInTheDocument();
});
it("should render Documentation link with correct attributes", () => {
renderWithRouter(
<AccountSettingsContextMenu
onLogout={onLogoutMock}
onClose={onCloseMock}
/>,
);
const documentationLink = screen.getByText("SIDEBAR$DOCS").closest("a");
expect(documentationLink).toHaveAttribute("href", "https://docs.openhands.dev");
expect(documentationLink).toHaveAttribute("target", "_blank");
expect(documentationLink).toHaveAttribute("rel", "noopener noreferrer");
});
it("should call onLogout when the logout option is clicked", async () => {
renderWithRouter(
<AccountSettingsContextMenu

View File

@@ -8,13 +8,6 @@ vi.mock("#/hooks/use-auth-url", () => ({
useAuthUrl: () => "https://gitlab.com/oauth/authorize",
}));
// Mock the useTracking hook
vi.mock("#/hooks/use-tracking", () => ({
useTracking: () => ({
trackLoginButtonClick: vi.fn(),
}),
}));
describe("AuthModal", () => {
beforeEach(() => {
vi.stubGlobal("location", { href: "" });

View File

@@ -8,11 +8,10 @@ vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
TASK_TRACKING_OBSERVATION$TASK_LIST: "Task List",
TASK_TRACKING_OBSERVATION$TASK_ID: "ID",
TASK_TRACKING_OBSERVATION$TASK_NOTES: "Notes",
TASK_TRACKING_OBSERVATION$RESULT: "Result",
COMMON$TASKS: "Tasks",
"TASK_TRACKING_OBSERVATION$TASK_LIST": "Task List",
"TASK_TRACKING_OBSERVATION$TASK_ID": "ID",
"TASK_TRACKING_OBSERVATION$TASK_NOTES": "Notes",
"TASK_TRACKING_OBSERVATION$RESULT": "Result",
};
return translations[key] || key;
},
@@ -62,26 +61,19 @@ describe("TaskTrackingObservationContent", () => {
it("renders task list when command is 'plan' and tasks exist", () => {
render(<TaskTrackingObservationContent event={mockEvent} />);
expect(screen.getByText("Tasks")).toBeInTheDocument();
expect(screen.getByText("Task List (3 items)")).toBeInTheDocument();
expect(screen.getByText("Implement feature A")).toBeInTheDocument();
expect(screen.getByText("Fix bug B")).toBeInTheDocument();
expect(screen.getByText("Deploy to production")).toBeInTheDocument();
});
it("displays correct status icons and badges", () => {
const { container } = render(
<TaskTrackingObservationContent event={mockEvent} />,
);
render(<TaskTrackingObservationContent event={mockEvent} />);
// Status is represented by icons, not text. Verify task items are rendered with their titles
// which indicates the status icons are present (status affects icon rendering)
expect(screen.getByText("Implement feature A")).toBeInTheDocument();
expect(screen.getByText("Fix bug B")).toBeInTheDocument();
expect(screen.getByText("Deploy to production")).toBeInTheDocument();
// Verify task items are present (they contain the status icons)
const taskItems = container.querySelectorAll('[data-name="item"]');
expect(taskItems).toHaveLength(3);
// Check for status text (the icons are emojis)
expect(screen.getByText("todo")).toBeInTheDocument();
expect(screen.getByText("in progress")).toBeInTheDocument();
expect(screen.getByText("done")).toBeInTheDocument();
});
it("displays task IDs and notes", () => {
@@ -92,9 +84,14 @@ describe("TaskTrackingObservationContent", () => {
expect(screen.getByText("ID: task-3")).toBeInTheDocument();
expect(screen.getByText("Notes: This is a test task")).toBeInTheDocument();
expect(
screen.getByText("Notes: Completed successfully"),
).toBeInTheDocument();
expect(screen.getByText("Notes: Completed successfully")).toBeInTheDocument();
});
it("renders result section when content exists", () => {
render(<TaskTrackingObservationContent event={mockEvent} />);
expect(screen.getByText("Result")).toBeInTheDocument();
expect(screen.getByText("Task tracking operation completed successfully")).toBeInTheDocument();
});
it("does not render task list when command is not 'plan'", () => {
@@ -108,7 +105,7 @@ describe("TaskTrackingObservationContent", () => {
render(<TaskTrackingObservationContent event={eventWithoutPlan} />);
expect(screen.queryByText("Tasks")).not.toBeInTheDocument();
expect(screen.queryByText("Task List")).not.toBeInTheDocument();
});
it("does not render task list when task list is empty", () => {
@@ -122,6 +119,17 @@ describe("TaskTrackingObservationContent", () => {
render(<TaskTrackingObservationContent event={eventWithEmptyTasks} />);
expect(screen.queryByText("Tasks")).not.toBeInTheDocument();
expect(screen.queryByText("Task List")).not.toBeInTheDocument();
});
it("does not render result section when content is empty", () => {
const eventWithoutContent = {
...mockEvent,
content: "",
};
render(<TaskTrackingObservationContent event={eventWithoutContent} />);
expect(screen.queryByText("Result")).not.toBeInTheDocument();
});
});

View File

@@ -13,6 +13,34 @@ vi.mock("#/hooks/use-agent-state", () => ({
useAgentState: vi.fn(),
}));
// Mock the custom hooks
const mockStartConversationMutate = vi.fn();
const mockStopConversationMutate = vi.fn();
vi.mock("#/hooks/mutation/use-unified-start-conversation", () => ({
useUnifiedStartConversation: () => ({
mutate: mockStartConversationMutate,
}),
}));
vi.mock("#/hooks/mutation/use-unified-stop-conversation", () => ({
useUnifiedStopConversation: () => ({
mutate: mockStopConversationMutate,
}),
}));
vi.mock("#/hooks/use-conversation-id", () => ({
useConversationId: () => ({
conversationId: "test-conversation-id",
}),
}));
vi.mock("#/hooks/use-user-providers", () => ({
useUserProviders: () => ({
providers: [],
}),
}));
vi.mock("#/hooks/query/use-task-polling", () => ({
useTaskPolling: () => ({
isTask: false,
@@ -38,12 +66,8 @@ vi.mock("react-i18next", async () => {
COMMON$SERVER_STOPPED: "Server Stopped",
COMMON$ERROR: "Error",
COMMON$STARTING: "Starting",
COMMON$STOPPING: "Stopping...",
COMMON$STOP_RUNTIME: "Stop Runtime",
COMMON$START_RUNTIME: "Start Runtime",
CONVERSATION$ERROR_STARTING_CONVERSATION:
"Error starting conversation",
CONVERSATION$READY: "Ready",
};
return translations[key] || key;
},
@@ -55,6 +79,10 @@ vi.mock("react-i18next", async () => {
});
describe("ServerStatus", () => {
// Mock functions for handlers
const mockHandleStop = vi.fn();
const mockHandleResumeAgent = vi.fn();
// Helper function to mock agent state with specific state
const mockAgentStore = (agentState: AgentState) => {
vi.mocked(useAgentState).mockReturnValue({
@@ -66,91 +94,248 @@ describe("ServerStatus", () => {
vi.clearAllMocks();
});
it("should render server status with RUNNING conversation status", () => {
it("should render server status with different conversation statuses", () => {
// Mock agent store to return RUNNING state
mockAgentStore(AgentState.RUNNING);
renderWithProviders(<ServerStatus conversationStatus="RUNNING" />);
// Test RUNNING status
const { rerender } = renderWithProviders(
<ServerStatus
conversationStatus="RUNNING"
handleStop={mockHandleStop}
handleResumeAgent={mockHandleResumeAgent}
/>,
);
expect(screen.getByText("Running")).toBeInTheDocument();
expect(screen.getByTestId("server-status")).toBeInTheDocument();
// Test STOPPED status
rerender(
<ServerStatus
conversationStatus="STOPPED"
handleStop={mockHandleStop}
handleResumeAgent={mockHandleResumeAgent}
/>,
);
expect(screen.getByText("Server Stopped")).toBeInTheDocument();
// Test STARTING status (shows "Running" due to agent state being RUNNING)
rerender(
<ServerStatus
conversationStatus="STARTING"
handleStop={mockHandleStop}
handleResumeAgent={mockHandleResumeAgent}
/>,
);
expect(screen.getByText("Running")).toBeInTheDocument();
// Test null status (shows "Running" due to agent state being RUNNING)
rerender(
<ServerStatus
conversationStatus={null}
handleStop={mockHandleStop}
handleResumeAgent={mockHandleResumeAgent}
/>,
);
expect(screen.getByText("Running")).toBeInTheDocument();
});
it("should render server status with STOPPED conversation status", () => {
mockAgentStore(AgentState.RUNNING);
it("should show context menu when clicked with RUNNING status", async () => {
const user = userEvent.setup();
renderWithProviders(<ServerStatus conversationStatus="STOPPED" />);
expect(screen.getByTestId("server-status")).toBeInTheDocument();
expect(screen.getByText("Server Stopped")).toBeInTheDocument();
});
it("should render STARTING status when agent state is LOADING", () => {
mockAgentStore(AgentState.LOADING);
renderWithProviders(<ServerStatus conversationStatus="STARTING" />);
expect(screen.getByTestId("server-status")).toBeInTheDocument();
expect(screen.getByText("Starting")).toBeInTheDocument();
});
it("should render STARTING status when agent state is INIT", () => {
mockAgentStore(AgentState.INIT);
renderWithProviders(<ServerStatus conversationStatus="STARTING" />);
expect(screen.getByTestId("server-status")).toBeInTheDocument();
expect(screen.getByText("Starting")).toBeInTheDocument();
});
it("should render ERROR status when agent state is ERROR", () => {
mockAgentStore(AgentState.ERROR);
renderWithProviders(<ServerStatus conversationStatus="RUNNING" />);
expect(screen.getByTestId("server-status")).toBeInTheDocument();
expect(screen.getByText("Error")).toBeInTheDocument();
});
it("should render STOPPING status when isPausing is true", () => {
// Mock agent store to return RUNNING state
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatus conversationStatus="RUNNING" isPausing={true} />,
<ServerStatus
conversationStatus="RUNNING"
handleStop={mockHandleStop}
handleResumeAgent={mockHandleResumeAgent}
/>,
);
expect(screen.getByTestId("server-status")).toBeInTheDocument();
expect(screen.getByText("Stopping...")).toBeInTheDocument();
const statusContainer = screen.getByText("Running").closest("div");
expect(statusContainer).toBeInTheDocument();
await user.click(statusContainer!);
// Context menu should appear
expect(
screen.getByTestId("server-status-context-menu"),
).toBeInTheDocument();
expect(screen.getByTestId("stop-server-button")).toBeInTheDocument();
});
it("should show context menu when clicked with STOPPED status", async () => {
const user = userEvent.setup();
// Mock agent store to return STOPPED state
mockAgentStore(AgentState.STOPPED);
renderWithProviders(
<ServerStatus
conversationStatus="STOPPED"
handleStop={mockHandleStop}
handleResumeAgent={mockHandleResumeAgent}
/>,
);
const statusContainer = screen.getByText("Server Stopped").closest("div");
expect(statusContainer).toBeInTheDocument();
await user.click(statusContainer!);
// Context menu should appear
expect(
screen.getByTestId("server-status-context-menu"),
).toBeInTheDocument();
expect(screen.getByTestId("start-server-button")).toBeInTheDocument();
});
it("should not show context menu when clicked with other statuses", async () => {
const user = userEvent.setup();
// Mock agent store to return RUNNING state
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatus
conversationStatus="STARTING"
handleStop={mockHandleStop}
handleResumeAgent={mockHandleResumeAgent}
/>,
);
const statusContainer = screen.getByText("Running").closest("div");
expect(statusContainer).toBeInTheDocument();
await user.click(statusContainer!);
// Context menu should not appear
expect(
screen.queryByTestId("server-status-context-menu"),
).not.toBeInTheDocument();
});
it("should call stop conversation mutation when stop server is clicked", async () => {
const user = userEvent.setup();
// Clear previous calls
mockHandleStop.mockClear();
// Mock agent store to return RUNNING state
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatus
conversationStatus="RUNNING"
handleStop={mockHandleStop}
handleResumeAgent={mockHandleResumeAgent}
/>,
);
const statusContainer = screen.getByText("Running").closest("div");
await user.click(statusContainer!);
const stopButton = screen.getByTestId("stop-server-button");
await user.click(stopButton);
expect(mockHandleStop).toHaveBeenCalledTimes(1);
});
it("should call start conversation mutation when start server is clicked", async () => {
const user = userEvent.setup();
// Clear previous calls
mockHandleResumeAgent.mockClear();
// Mock agent store to return STOPPED state
mockAgentStore(AgentState.STOPPED);
renderWithProviders(
<ServerStatus
conversationStatus="STOPPED"
handleStop={mockHandleStop}
handleResumeAgent={mockHandleResumeAgent}
/>,
);
const statusContainer = screen.getByText("Server Stopped").closest("div");
await user.click(statusContainer!);
const startButton = screen.getByTestId("start-server-button");
await user.click(startButton);
expect(mockHandleResumeAgent).toHaveBeenCalledTimes(1);
});
it("should close context menu after stop server action", async () => {
const user = userEvent.setup();
// Mock agent store to return RUNNING state
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatus
conversationStatus="RUNNING"
handleStop={mockHandleStop}
handleResumeAgent={mockHandleResumeAgent}
/>,
);
const statusContainer = screen.getByText("Running").closest("div");
await user.click(statusContainer!);
const stopButton = screen.getByTestId("stop-server-button");
await user.click(stopButton);
// Context menu should be closed (handled by the component)
expect(mockHandleStop).toHaveBeenCalledTimes(1);
});
it("should close context menu after start server action", async () => {
const user = userEvent.setup();
// Mock agent store to return STOPPED state
mockAgentStore(AgentState.STOPPED);
renderWithProviders(
<ServerStatus
conversationStatus="STOPPED"
handleStop={mockHandleStop}
handleResumeAgent={mockHandleResumeAgent}
/>,
);
const statusContainer = screen.getByText("Server Stopped").closest("div");
await user.click(statusContainer!);
const startButton = screen.getByTestId("start-server-button");
await user.click(startButton);
// Context menu should be closed
expect(
screen.queryByTestId("server-status-context-menu"),
).not.toBeInTheDocument();
});
it("should handle null conversation status", () => {
mockAgentStore(AgentState.RUNNING);
renderWithProviders(<ServerStatus conversationStatus={null} />);
expect(screen.getByTestId("server-status")).toBeInTheDocument();
expect(screen.getByText("Running")).toBeInTheDocument();
});
it("should apply custom className", () => {
// Mock agent store to return RUNNING state
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatus conversationStatus="RUNNING" className="custom-class" />,
<ServerStatus
conversationStatus={null}
handleStop={mockHandleStop}
handleResumeAgent={mockHandleResumeAgent}
/>,
);
const container = screen.getByTestId("server-status");
expect(container).toHaveClass("custom-class");
const statusText = screen.getByText("Running");
expect(statusText).toBeInTheDocument();
});
});
describe("ServerStatusContextMenu", () => {
// Helper function to mock agent state with specific state
const mockAgentStore = (agentState: AgentState) => {
vi.mocked(useAgentState).mockReturnValue({
curAgentState: agentState,
});
};
const defaultProps = {
onClose: vi.fn(),
conversationStatus: "RUNNING" as ConversationStatus,
@@ -161,8 +346,6 @@ describe("ServerStatusContextMenu", () => {
});
it("should render stop server button when status is RUNNING", () => {
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatusContextMenu
{...defaultProps}
@@ -171,14 +354,11 @@ describe("ServerStatusContextMenu", () => {
/>,
);
expect(screen.getByTestId("server-status")).toBeInTheDocument();
expect(screen.getByTestId("stop-server-button")).toBeInTheDocument();
expect(screen.getByText("Stop Runtime")).toBeInTheDocument();
});
it("should render start server button when status is STOPPED", () => {
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatusContextMenu
{...defaultProps}
@@ -187,14 +367,11 @@ describe("ServerStatusContextMenu", () => {
/>,
);
expect(screen.getByTestId("server-status")).toBeInTheDocument();
expect(screen.getByTestId("start-server-button")).toBeInTheDocument();
expect(screen.getByText("Start Runtime")).toBeInTheDocument();
});
it("should not render stop server button when onStopServer is not provided", () => {
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatusContextMenu
{...defaultProps}
@@ -202,13 +379,10 @@ describe("ServerStatusContextMenu", () => {
/>,
);
expect(screen.getByTestId("server-status")).toBeInTheDocument();
expect(screen.queryByTestId("stop-server-button")).not.toBeInTheDocument();
});
it("should not render start server button when onStartServer is not provided", () => {
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatusContextMenu
{...defaultProps}
@@ -216,14 +390,12 @@ describe("ServerStatusContextMenu", () => {
/>,
);
expect(screen.getByTestId("server-status")).toBeInTheDocument();
expect(screen.queryByTestId("start-server-button")).not.toBeInTheDocument();
});
it("should call onStopServer when stop button is clicked", async () => {
const user = userEvent.setup();
const onStopServer = vi.fn();
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatusContextMenu
@@ -242,7 +414,6 @@ describe("ServerStatusContextMenu", () => {
it("should call onStartServer when start button is clicked", async () => {
const user = userEvent.setup();
const onStartServer = vi.fn();
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatusContextMenu
@@ -259,8 +430,6 @@ describe("ServerStatusContextMenu", () => {
});
it("should render correct text content for stop server button", () => {
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatusContextMenu
{...defaultProps}
@@ -275,8 +444,6 @@ describe("ServerStatusContextMenu", () => {
});
it("should render correct text content for start server button", () => {
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatusContextMenu
{...defaultProps}
@@ -292,7 +459,6 @@ describe("ServerStatusContextMenu", () => {
it("should call onClose when context menu is closed", () => {
const onClose = vi.fn();
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatusContextMenu
@@ -309,8 +475,6 @@ describe("ServerStatusContextMenu", () => {
});
it("should not render any buttons for other conversation statuses", () => {
mockAgentStore(AgentState.RUNNING);
renderWithProviders(
<ServerStatusContextMenu
{...defaultProps}
@@ -318,7 +482,6 @@ describe("ServerStatusContextMenu", () => {
/>,
);
expect(screen.getByTestId("server-status")).toBeInTheDocument();
expect(screen.queryByTestId("stop-server-button")).not.toBeInTheDocument();
expect(screen.queryByTestId("start-server-button")).not.toBeInTheDocument();
});

View File

@@ -71,7 +71,6 @@ beforeEach(() => {
provider_tokens_set: {
github: "some-token",
gitlab: null,
azure_devops: null,
},
});
});

View File

@@ -23,7 +23,6 @@ const MOCK_RESPOSITORIES: GitRepository[] = [
{ id: "2", full_name: "repo2", git_provider: "github", is_public: true },
{ id: "3", full_name: "repo3", git_provider: "gitlab", is_public: true },
{ id: "4", full_name: "repo4", git_provider: "gitlab", is_public: true },
{ id: "5", full_name: "repo5", git_provider: "azure_devops", is_public: true },
];
const renderTaskCard = (task = MOCK_TASK_1) => {

View File

@@ -21,7 +21,6 @@ const mockUseConfig = vi.fn();
const mockUseRepositoryMicroagents = vi.fn();
const mockUseMicroagentManagementConversations = vi.fn();
const mockUseSearchRepositories = vi.fn();
const mockUseCreateConversationAndSubscribeMultiple = vi.fn();
vi.mock("#/hooks/use-user-providers", () => ({
useUserProviders: () => mockUseUserProviders(),
@@ -48,17 +47,6 @@ vi.mock("#/hooks/query/use-search-repositories", () => ({
useSearchRepositories: () => mockUseSearchRepositories(),
}));
vi.mock("#/hooks/use-tracking", () => ({
useTracking: () => ({
trackEvent: vi.fn(),
}),
}));
vi.mock("#/hooks/use-create-conversation-and-subscribe-multiple", () => ({
useCreateConversationAndSubscribeMultiple: () =>
mockUseCreateConversationAndSubscribeMultiple(),
}));
describe("MicroagentManagement", () => {
const RouterStub = createRoutesStub([
{
@@ -321,16 +309,6 @@ describe("MicroagentManagement", () => {
isError: false,
});
mockUseCreateConversationAndSubscribeMultiple.mockReturnValue({
createConversationAndSubscribe: vi.fn(({ onSuccessCallback }) => {
// Immediately call the success callback to close the modal
if (onSuccessCallback) {
onSuccessCallback();
}
}),
isPending: false,
});
// Mock the search repositories hook to return repositories with OpenHands suffixes
const mockSearchResults =
getRepositoriesWithOpenHandsSuffix(mockRepositories);

View File

@@ -38,7 +38,6 @@ describe("PaymentForm", () => {
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
ENABLE_AZURE_DEVOPS: false,
},
});
});

View File

@@ -30,7 +30,7 @@ describe("ImagePreview", () => {
expect(onRemoveMock).toHaveBeenCalledOnce();
});
it("should not display the close button when onRemove is not provided", () => {
it("shoud not display the close button when onRemove is not provided", () => {
render(<ImagePreview src="https://example.com/image.jpg" />);
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});

View File

@@ -268,7 +268,7 @@ describe("useWebSocket", () => {
});
// onError handler should have been called
expect(onErrorSpy).toHaveBeenCalled();
expect(onErrorSpy).toHaveBeenCalledOnce();
});
it("should provide sendMessage function to send messages to WebSocket", async () => {

View File

@@ -1,233 +0,0 @@
import {
describe,
it,
expect,
beforeAll,
afterAll,
afterEach,
vi,
} from "vitest";
import { screen, waitFor, render, cleanup } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createMockAgentErrorEvent } from "#/mocks/mock-ws-helpers";
import { ConversationWebSocketProvider } from "#/contexts/conversation-websocket-context";
import { conversationWebSocketTestSetup } from "./helpers/msw-websocket-setup";
import { ConnectionStatusComponent } from "./helpers/websocket-test-components";
// Mock the tracking function
const mockTrackCreditLimitReached = vi.fn();
// Mock useTracking hook
vi.mock("#/hooks/use-tracking", () => ({
useTracking: () => ({
trackCreditLimitReached: mockTrackCreditLimitReached,
trackLoginButtonClick: vi.fn(),
trackConversationCreated: vi.fn(),
trackPushButtonClick: vi.fn(),
trackPullButtonClick: vi.fn(),
trackCreatePrButtonClick: vi.fn(),
trackGitProviderConnected: vi.fn(),
trackUserSignupCompleted: vi.fn(),
trackCreditsPurchased: vi.fn(),
}),
}));
// Mock useActiveConversation hook
vi.mock("#/hooks/query/use-active-conversation", () => ({
useActiveConversation: () => ({
data: null,
isLoading: false,
error: null,
}),
}));
// MSW WebSocket mock setup
const { wsLink, server: mswServer } = conversationWebSocketTestSetup();
beforeAll(() => {
// The global MSW server from vitest.setup.ts is already running
// We just need to start our WebSocket-specific server
mswServer.listen({ onUnhandledRequest: "bypass" });
});
afterEach(() => {
// Clear all mocks before each test
mockTrackCreditLimitReached.mockClear();
mswServer.resetHandlers();
// Clean up any React components
cleanup();
});
afterAll(async () => {
// Close the WebSocket MSW server
mswServer.close();
// Give time for any pending WebSocket connections to close. This is very important to prevent serious memory leaks
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
});
// Helper function to render components with all necessary providers
function renderWithProviders(
children: React.ReactNode,
conversationId = "test-conversation-123",
conversationUrl = "http://localhost:3000/api/conversations/test-conversation-123",
) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return render(
<QueryClientProvider client={queryClient}>
<ConversationWebSocketProvider
conversationId={conversationId}
conversationUrl={conversationUrl}
sessionApiKey={null}
>
{children}
</ConversationWebSocketProvider>
</QueryClientProvider>,
);
}
describe("PostHog Analytics Tracking", () => {
describe("Credit Limit Tracking", () => {
it("should track credit_limit_reached when AgentErrorEvent contains budget error", async () => {
// Create a mock AgentErrorEvent with budget-related error message
const mockBudgetErrorEvent = createMockAgentErrorEvent({
error: "ExceededBudget: Task exceeded maximum budget of $10.00",
});
// Set up MSW to send the budget error event when connection is established
mswServer.use(
wsLink.addEventListener("connection", ({ client, server }) => {
server.connect();
// Send the mock budget error event after connection
client.send(JSON.stringify(mockBudgetErrorEvent));
}),
);
// Render with all providers
renderWithProviders(<ConnectionStatusComponent />);
// Wait for connection to be established
await waitFor(() => {
expect(screen.getByTestId("connection-state")).toHaveTextContent(
"OPEN",
);
});
// Wait for the tracking event to be captured
await waitFor(() => {
expect(mockTrackCreditLimitReached).toHaveBeenCalledWith(
expect.objectContaining({
conversationId: "test-conversation-123",
}),
);
});
});
it("should track credit_limit_reached when AgentErrorEvent contains 'credit' keyword", async () => {
// Create error with "credit" keyword (case-insensitive)
const mockCreditErrorEvent = createMockAgentErrorEvent({
error: "Insufficient CREDIT to complete this operation",
});
mswServer.use(
wsLink.addEventListener("connection", ({ client, server }) => {
server.connect();
client.send(JSON.stringify(mockCreditErrorEvent));
}),
);
renderWithProviders(<ConnectionStatusComponent />);
await waitFor(() => {
expect(screen.getByTestId("connection-state")).toHaveTextContent(
"OPEN",
);
});
await waitFor(() => {
expect(mockTrackCreditLimitReached).toHaveBeenCalledWith(
expect.objectContaining({
conversationId: "test-conversation-123",
}),
);
});
});
it("should NOT track credit_limit_reached for non-budget errors", async () => {
// Create a regular error without budget/credit keywords
const mockRegularErrorEvent = createMockAgentErrorEvent({
error: "Failed to execute command: Permission denied",
});
mswServer.use(
wsLink.addEventListener("connection", ({ client, server }) => {
server.connect();
client.send(JSON.stringify(mockRegularErrorEvent));
}),
);
renderWithProviders(<ConnectionStatusComponent />);
// Wait for connection and error to be processed
await waitFor(() => {
expect(screen.getByTestId("connection-state")).toHaveTextContent(
"OPEN",
);
});
// Verify that credit_limit_reached was NOT tracked
expect(mockTrackCreditLimitReached).not.toHaveBeenCalled();
});
it("should only track credit_limit_reached once per error event", async () => {
const mockBudgetErrorEvent = createMockAgentErrorEvent({
error: "Budget exceeded: $10.00 limit reached",
});
mswServer.use(
wsLink.addEventListener("connection", ({ client, server }) => {
server.connect();
// Send the same error event twice
client.send(JSON.stringify(mockBudgetErrorEvent));
client.send(
JSON.stringify({ ...mockBudgetErrorEvent, id: "different-id" }),
);
}),
);
renderWithProviders(<ConnectionStatusComponent />);
await waitFor(() => {
expect(screen.getByTestId("connection-state")).toHaveTextContent(
"OPEN",
);
});
await waitFor(() => {
expect(mockTrackCreditLimitReached).toHaveBeenCalledTimes(2);
});
// Both calls should be for credit_limit_reached (once per event)
expect(mockTrackCreditLimitReached).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
conversationId: "test-conversation-123",
}),
);
expect(mockTrackCreditLimitReached).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
conversationId: "test-conversation-123",
}),
);
});
});
});

View File

@@ -80,7 +80,6 @@ describe("frontend/routes/_oh", () => {
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
ENABLE_AZURE_DEVOPS: false,
},
});
@@ -119,7 +118,6 @@ describe("frontend/routes/_oh", () => {
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
ENABLE_AZURE_DEVOPS: false,
},
});
@@ -204,7 +202,6 @@ describe("frontend/routes/_oh", () => {
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
ENABLE_AZURE_DEVOPS: false,
},
});

View File

@@ -1,9 +1,10 @@
import { render, screen } from "@testing-library/react";
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import AcceptTOS from "#/routes/accept-tos";
import * as CaptureConsent from "#/utils/handle-capture-consent";
import * as ToastHandlers from "#/utils/custom-toast-handlers";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { openHands } from "#/api/open-hands-axios";
// Mock the react-router hooks
@@ -43,13 +44,9 @@ const createWrapper = () => {
},
});
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
return Wrapper;
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
describe("AcceptTOS", () => {
@@ -109,10 +106,7 @@ describe("AcceptTOS", () => {
// Wait for the mutation to complete
await new Promise(process.nextTick);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(
expect.anything(),
true,
);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
expect(openHands.post).toHaveBeenCalledWith("/api/accept_tos", {
redirect_url: "/dashboard",
});

View File

@@ -46,21 +46,6 @@ describe("Content", () => {
});
});
it("should render analytics toggle as enabled when server returns null (opt-in by default)", async () => {
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
user_consents_to_analytics: null,
});
renderAppSettingsScreen();
await waitFor(() => {
const analytics = screen.getByTestId("enable-analytics-switch");
expect(analytics).toBeChecked();
});
});
it("should render the language options", async () => {
renderAppSettingsScreen();
@@ -178,10 +163,7 @@ describe("Form submission", () => {
await userEvent.click(submit);
await waitFor(() =>
expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(
expect.anything(),
true,
),
expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(true),
);
});
@@ -206,10 +188,7 @@ describe("Form submission", () => {
await userEvent.click(submit);
await waitFor(() =>
expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(
expect.anything(),
false,
),
expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(false),
);
});

View File

@@ -24,7 +24,6 @@ const VALID_OSS_CONFIG: GetConfigResponse = {
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
ENABLE_AZURE_DEVOPS: false,
},
};
@@ -38,7 +37,6 @@ const VALID_SAAS_CONFIG: GetConfigResponse = {
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
ENABLE_AZURE_DEVOPS: false,
},
};
@@ -126,9 +124,6 @@ describe("Content", () => {
await screen.findByTestId("bitbucket-token-input");
await screen.findByTestId("bitbucket-token-help-anchor");
await screen.findByTestId("azure-devops-token-input");
await screen.findByTestId("azure-devops-token-help-anchor");
getConfigSpy.mockResolvedValue(VALID_SAAS_CONFIG);
queryClient.invalidateQueries();
rerender();
@@ -154,13 +149,6 @@ describe("Content", () => {
expect(
screen.queryByTestId("bitbucket-token-help-anchor"),
).not.toBeInTheDocument();
expect(
screen.queryByTestId("azure-devops-token-input"),
).not.toBeInTheDocument();
expect(
screen.queryByTestId("azure-devops-token-help-anchor"),
).not.toBeInTheDocument();
});
});
@@ -299,7 +287,6 @@ describe("Form submission", () => {
github: { token: "test-token", host: "" },
gitlab: { token: "", host: "" },
bitbucket: { token: "", host: "" },
azure_devops: { token: "", host: "" },
});
});
@@ -321,7 +308,6 @@ describe("Form submission", () => {
github: { token: "", host: "" },
gitlab: { token: "test-token", host: "" },
bitbucket: { token: "", host: "" },
azure_devops: { token: "", host: "" },
});
});
@@ -343,29 +329,6 @@ describe("Form submission", () => {
github: { token: "", host: "" },
gitlab: { token: "", host: "" },
bitbucket: { token: "test-token", host: "" },
azure_devops: { token: "", host: "" },
});
});
it("should save the Azure DevOps token", async () => {
const saveProvidersSpy = vi.spyOn(SecretsService, "addGitProvider");
saveProvidersSpy.mockImplementation(() => Promise.resolve(true));
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
renderGitSettingsScreen();
const azureDevOpsInput = await screen.findByTestId("azure-devops-token-input");
const submit = await screen.findByTestId("submit-button");
await userEvent.type(azureDevOpsInput, "test-token");
await userEvent.click(submit);
expect(saveProvidersSpy).toHaveBeenCalledWith({
github: { token: "", host: "" },
gitlab: { token: "", host: "" },
bitbucket: { token: "", host: "" },
azure_devops: { token: "test-token", host: "" },
});
});

View File

@@ -404,7 +404,6 @@ describe("Settings 404", () => {
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
ENABLE_AZURE_DEVOPS: false,
},
});
const error = createAxiosNotFoundErrorObject();
@@ -430,7 +429,6 @@ describe("Setup Payment modal", () => {
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
ENABLE_AZURE_DEVOPS: false,
},
});
const error = createAxiosNotFoundErrorObject();

View File

@@ -73,7 +73,6 @@ describe("Settings Billing", () => {
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
ENABLE_AZURE_DEVOPS: false,
},
},
isLoading: false,
@@ -129,7 +128,6 @@ describe("Settings Billing", () => {
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
ENABLE_AZURE_DEVOPS: false,
},
},
isLoading: false,
@@ -154,7 +152,6 @@ describe("Settings Billing", () => {
ENABLE_JIRA: false,
ENABLE_JIRA_DC: false,
ENABLE_LINEAR: false,
ENABLE_AZURE_DEVOPS: false,
},
},
isLoading: false,

View File

@@ -55,7 +55,7 @@ const mockObservationEvent: ObservationEvent = {
tool_call_id: "call_123",
observation: {
kind: "ExecuteBashObservation",
content: [{ type: "text", text: "hello\n" }],
output: "hello\n",
command: "echo hello",
exit_code: 0,
error: false,

View File

@@ -7,7 +7,6 @@ describe("convertRawProvidersToList", () => {
const example1: Partial<Record<Provider, string | null>> | undefined = {
github: "test-token",
gitlab: "test-token",
azure_devops: "test-token",
};
const example2: Partial<Record<Provider, string | null>> | undefined = {
github: "",
@@ -15,13 +14,9 @@ describe("convertRawProvidersToList", () => {
const example3: Partial<Record<Provider, string | null>> | undefined = {
gitlab: null,
};
const example4: Partial<Record<Provider, string | null>> | undefined = {
azure_devops: "test-token",
};
expect(convertRawProvidersToList(example1)).toEqual(["github", "gitlab", "azure_devops"]);
expect(convertRawProvidersToList(example1)).toEqual(["github", "gitlab"]);
expect(convertRawProvidersToList(example2)).toEqual(["github"]);
expect(convertRawProvidersToList(example3)).toEqual(["gitlab"]);
expect(convertRawProvidersToList(example4)).toEqual(["azure_devops"]);
});
});

View File

@@ -32,7 +32,6 @@ describe("Error Handler", () => {
const error = {
message: "Test error",
source: "test",
posthog,
};
trackError(error);
@@ -53,7 +52,6 @@ describe("Error Handler", () => {
extra: "info",
details: { foo: "bar" },
},
posthog,
};
trackError(error);
@@ -75,7 +73,6 @@ describe("Error Handler", () => {
const error = {
message: "Toast error",
source: "toast-test",
posthog,
};
showErrorToast(error);
@@ -97,7 +94,6 @@ describe("Error Handler", () => {
message: "Toast error",
source: "toast-test",
metadata: { context: "testing" },
posthog,
};
showErrorToast(error);
@@ -117,7 +113,6 @@ describe("Error Handler", () => {
message: "Agent error",
source: "agent-status",
metadata: { id: "error.agent" },
posthog,
});
expect(posthog.captureException).toHaveBeenCalledWith(
@@ -132,7 +127,6 @@ describe("Error Handler", () => {
message: "Server error",
source: "server",
metadata: { error_code: 500, details: "Internal error" },
posthog,
});
expect(posthog.captureException).toHaveBeenCalledWith(
@@ -151,7 +145,6 @@ describe("Error Handler", () => {
message: error.message,
source: "feedback",
metadata: { conversationId: "123", error },
posthog,
});
expect(posthog.captureException).toHaveBeenCalledWith(
@@ -171,7 +164,6 @@ describe("Error Handler", () => {
message: "Chat error",
source: "chat-test",
msgId: "123",
posthog,
};
showChatError(error);

View File

@@ -13,14 +13,14 @@ describe("handleCaptureConsent", () => {
});
it("should opt out of of capturing", () => {
handleCaptureConsent(posthog, false);
handleCaptureConsent(false);
expect(optOutSpy).toHaveBeenCalled();
expect(optInSpy).not.toHaveBeenCalled();
});
it("should opt in to capturing if the user consents", () => {
handleCaptureConsent(posthog, true);
handleCaptureConsent(true);
expect(optInSpy).toHaveBeenCalled();
expect(optOutSpy).not.toHaveBeenCalled();
@@ -28,7 +28,7 @@ describe("handleCaptureConsent", () => {
it("should not opt in to capturing if the user is already opted in", () => {
hasOptedInSpy.mockReturnValueOnce(true);
handleCaptureConsent(posthog, true);
handleCaptureConsent(true);
expect(optInSpy).not.toHaveBeenCalled();
expect(optOutSpy).not.toHaveBeenCalled();
@@ -36,7 +36,7 @@ describe("handleCaptureConsent", () => {
it("should not opt out of capturing if the user is already opted out", () => {
hasOptedOutSpy.mockReturnValueOnce(true);
handleCaptureConsent(posthog, false);
handleCaptureConsent(false);
expect(optOutSpy).not.toHaveBeenCalled();
expect(optInSpy).not.toHaveBeenCalled();

View File

@@ -17,7 +17,7 @@ describe("handleEventForUI", () => {
tool_call_id: "call_123",
observation: {
kind: "ExecuteBashObservation",
content: [{ type: "text", text: "hello\n" }],
output: "hello\n",
command: "echo hello",
exit_code: 0,
error: false,

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,16 @@
{
"name": "openhands-frontend",
"version": "0.62.0",
"version": "0.60.0",
"private": true,
"type": "module",
"engines": {
"node": ">=22.0.0"
},
"dependencies": {
"@heroui/react": "2.8.5",
"@heroui/react": "^2.8.4",
"@heroui/use-infinite-scroll": "^2.2.11",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@posthog/react": "^1.4.0",
"@react-router/node": "^7.9.3",
"@react-router/serve": "^7.9.3",
"@react-types/shared": "^3.32.0",
@@ -38,7 +37,7 @@
"jose": "^6.1.0",
"lucide-react": "^0.544.0",
"monaco-editor": "^0.53.0",
"posthog-js": "^1.290.0",
"posthog-js": "^1.268.8",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-highlight": "^0.15.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,7 +1,32 @@
<svg width="1365" height="1365" viewBox="0 -24 148 148" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M71.7542 16.863V2.97414C71.7542 1.82355 72.6872 0.890503 73.8378 0.890503C74.9884 0.890503 75.9214 1.82355 75.9214 2.97414V16.863C75.9214 18.0136 74.9884 18.9466 73.8378 18.9466C72.6872 18.9466 71.7542 18.0136 71.7542 16.863Z" fill="black"/>
<path d="M82.5272 18.9329L89.4716 6.90477C90.0469 5.90832 91.3215 5.5668 92.3179 6.1421C93.3144 6.7174 93.6559 7.99197 93.0806 8.98841L86.1362 21.0165C85.5609 22.0129 84.2863 22.3545 83.2899 21.7792C82.2934 21.2039 81.9519 19.9293 82.5272 18.9329Z" fill="black"/>
<path d="M65.1481 18.9329L58.2037 6.90477C57.6284 5.90832 56.3538 5.5668 55.3574 6.1421C54.3609 6.7174 54.0194 7.99197 54.5947 8.98841L61.5391 21.0165C62.1144 22.0129 63.389 22.3545 64.3854 21.7792C65.3819 21.2039 65.7234 19.9293 65.1481 18.9329Z" fill="black"/>
<path d="M140.606 62.0292C140.606 58.409 141.583 47.6748 141.89 44.1323C142.097 41.7374 141.809 40.4247 141.424 39.7542C141.141 39.2626 140.699 38.915 139.634 38.8436C138.865 38.7921 138.027 39.0114 137.401 39.5761C136.814 40.1052 136.159 41.1682 136.159 43.3176L136.155 43.4388L135.198 59.758C135.164 60.3451 134.883 60.8911 134.424 61.2599C133.966 61.6284 133.374 61.7859 132.793 61.6941L122.764 60.1068L111.948 58.6703C110.949 58.5376 110.188 57.7084 110.142 56.7016L109.561 44.1323C109.535 43.621 109.51 43.1141 109.484 42.6146C109.241 37.9294 109.022 33.7805 109.022 32.4282C109.022 28.3859 108.338 26.6806 107.74 25.9634C107.263 25.3915 106.577 25.1402 105.11 25.1402C104.583 25.1402 104.212 25.2481 103.933 25.4111C103.659 25.5714 103.346 25.8587 103.049 26.4208C102.41 27.6257 101.945 29.891 102.118 33.8479C102.342 38.9804 102.692 42.8146 103.035 46.2718C103.377 49.7231 103.718 52.8561 103.908 56.4971C104.204 62.1966 104.178 66.1256 103.945 68.7924C103.828 70.124 103.656 71.1996 103.423 72.0501C103.202 72.8558 102.871 73.6757 102.296 74.2887C101.6 75.0303 100.608 75.3844 99.577 75.136C98.7592 74.9389 98.1847 74.4215 97.8706 74.0916C97.2141 73.4017 96.7501 72.5106 96.568 72.0512C95.5097 69.3812 92.2352 63.1808 87.8023 59.6811C86.5089 58.6599 85.5666 58.3652 84.9736 58.3204C84.4148 58.2783 84.0094 58.4436 83.6909 58.6967C83.34 58.9756 83.0781 59.3811 82.9479 59.7643C82.9019 59.8999 82.8823 59.9968 82.8741 60.0584C84.0759 62.0865 88.8421 69.5222 91.0896 77.069C92.7648 82.6941 96.8038 88.4259 99.8194 90.8809C102.74 93.258 107.988 94.7313 113.9 95.0218C119.756 95.3095 125.788 94.4121 130.033 92.5092C138.233 88.8334 139.903 80.7382 140.651 77.2292C141.232 74.5057 141.243 71.5987 141.087 68.9009C141.01 67.5551 140.894 66.2969 140.793 65.1373C140.695 64.0105 140.606 62.9215 140.606 62.0292ZM120.986 27.0953C120.986 25.8314 120.648 24.7049 120.089 23.9514C119.583 23.27 118.84 22.7987 117.646 22.7984C116.668 22.7982 116.011 22.9187 115.546 23.1167C115.13 23.2943 114.781 23.5699 114.463 24.0831C113.73 25.2671 113.192 27.6455 113.189 32.384L113.721 43.9088C113.91 47.5661 114.106 51.4922 114.235 54.7707L120.986 55.6666V27.0953ZM125.153 56.2652L131.172 57.218L131.992 43.267V32.5083C131.992 31.031 131.39 30.1275 130.678 29.5489C129.884 28.9039 128.957 28.6731 128.519 28.6731C127.722 28.6731 126.899 28.797 126.306 29.2179C125.849 29.5421 125.153 30.3087 125.153 32.5083V56.2652ZM136.159 35.4278C137.406 34.8069 138.74 34.6083 139.912 34.6868C142.037 34.8292 143.91 35.718 145.037 37.6779C146.06 39.4592 146.273 41.8136 146.041 44.4927C145.72 48.1949 144.772 58.6457 144.772 62.0292C144.772 62.708 144.843 63.6116 144.944 64.7758C145.042 65.907 145.165 67.2389 145.247 68.6606C145.411 71.4987 145.422 74.8383 144.727 78.0987C144.002 81.4953 142.041 91.6918 131.738 96.3108C126.731 98.5551 120.002 99.4936 113.696 99.1838C107.445 98.8767 101.128 97.3189 97.1887 94.1122C93.4809 91.0937 88.9938 84.6307 87.0962 78.2589C84.9529 71.0619 80.3109 63.9646 79.1527 61.9533C78.4706 60.7689 78.684 59.3628 79.0019 58.4258C79.3607 57.3688 80.0554 56.2631 81.0993 55.4337C82.1758 54.5784 83.6043 54.0377 85.2876 54.1647C86.9369 54.2893 88.6462 55.0393 90.3834 56.4107C94.8541 59.9401 98.1342 65.5082 99.7424 68.9231C99.759 68.7664 99.779 68.6024 99.7941 68.4298C100.003 66.0435 100.039 62.3344 99.7467 56.7132C99.5635 53.1942 99.2356 50.1809 98.8888 46.6828C98.5425 43.1904 98.184 39.2713 97.955 34.0302C97.7722 29.8481 98.2012 26.6722 99.3672 24.471C99.9716 23.3302 100.79 22.4223 101.83 21.814C102.866 21.2087 103.995 20.974 105.11 20.974C106.759 20.974 108.813 21.2062 110.448 22.7678C110.593 22.4576 110.75 22.1652 110.921 21.8899C111.676 20.6698 112.681 19.8084 113.912 19.2835C115.095 18.7791 116.378 18.6309 117.646 18.6311C120.195 18.6315 122.164 19.7567 123.434 21.4683C124.256 22.576 124.75 23.8775 124.985 25.1982C126.338 24.5876 127.691 24.5068 128.519 24.5068C129.933 24.5068 131.784 25.0791 133.305 26.3154C134.908 27.6179 136.159 29.6733 136.159 32.5083V35.4278Z" fill="black"/>
<path d="M7.15661 62.0292C7.15661 58.409 6.17994 47.6748 5.87291 44.1323C5.6654 41.7374 5.95357 40.4247 6.33875 39.7542C6.62116 39.2626 7.06336 38.915 8.12834 38.8436C8.89759 38.7921 9.73544 39.0114 10.3616 39.5761C10.9484 40.1052 11.6032 41.1682 11.6032 43.3176L11.6074 43.4388L12.5644 59.758C12.5988 60.3451 12.8798 60.8911 13.338 61.2599C13.7961 61.6284 14.3887 61.7859 14.9695 61.6941L24.9988 60.1068L35.8143 58.6703C36.8135 58.5376 37.5741 57.7084 37.6208 56.7016L38.2015 44.1323C38.2279 43.621 38.2525 43.1141 38.2784 42.6146C38.5218 37.9294 38.7401 33.7805 38.7401 32.4282C38.7401 28.3859 39.4246 26.6806 40.0227 25.9634C40.4996 25.3915 41.185 25.1402 42.6523 25.1402C43.1794 25.1402 43.5505 25.2481 43.8295 25.4111C44.1038 25.5714 44.416 25.8587 44.7138 26.4208C45.3521 27.6257 45.8173 29.891 45.6444 33.8479C45.4201 38.9804 45.0703 42.8146 44.7275 46.2718C44.3853 49.7231 44.0443 52.8561 43.8548 56.4971C43.5582 62.1966 43.5847 66.1256 43.8179 68.7924C43.9344 70.124 44.1069 71.1996 44.3396 72.0501C44.5601 72.8558 44.891 73.6757 45.4663 74.2887C46.1625 75.0303 47.1546 75.3844 48.1855 75.136C49.0033 74.9389 49.5778 74.4215 49.8918 74.0916C50.5484 73.4017 51.0123 72.5106 51.1945 72.0512C52.2527 69.3812 55.5272 63.1808 59.9601 59.6811C61.2536 58.6599 62.1958 58.3652 62.7889 58.3204C63.3476 58.2783 63.753 58.4436 64.0715 58.6967C64.4225 58.9756 64.6844 59.3811 64.8146 59.7643C64.8606 59.8999 64.8801 59.9968 64.8883 60.0584C63.6866 62.0865 58.9204 69.5222 56.6729 77.069C54.9977 82.6941 50.9586 88.4259 47.9431 90.8809C45.0229 93.258 39.7747 94.7313 33.8624 95.0218C28.0068 95.3095 21.9748 94.4121 17.7297 92.5092C9.52988 88.8334 7.85961 80.7382 7.11129 77.2292C6.53054 74.5057 6.5195 71.5987 6.67496 68.9009C6.75251 67.5551 6.86809 66.2969 6.96901 65.1373C7.06707 64.0105 7.1566 62.9215 7.15661 62.0292ZM26.7768 27.0953C26.7768 25.8314 27.1147 24.7049 27.6737 23.9514C28.1792 23.27 28.9221 22.7987 30.1167 22.7984C31.0942 22.7982 31.7518 22.9187 32.2162 23.1167C32.6326 23.2943 32.9817 23.5699 33.2996 24.0831C34.0328 25.2671 34.5705 27.6455 34.5738 32.384L34.0416 43.9088C33.8524 47.5661 33.6565 51.4922 33.5273 54.7707L26.7768 55.6666V27.0953ZM22.6095 56.2652L16.5904 57.218L15.7705 43.267V32.5083C15.7705 31.031 16.3726 30.1275 17.0847 29.5489C17.8785 28.9039 18.8058 28.6731 19.2432 28.6731C20.0404 28.6731 20.8634 28.797 21.4565 29.2179C21.9131 29.5421 22.6095 30.3087 22.6095 32.5083V56.2652ZM11.6032 35.4278C10.3568 34.8069 9.02265 34.6083 7.8501 34.6868C5.72541 34.8292 3.85197 35.718 2.72584 37.6779C1.70247 39.4592 1.48924 41.8136 1.72143 44.4927C2.0423 48.1949 2.99038 58.6457 2.99038 62.0292C2.99037 62.708 2.91991 63.6116 2.81859 64.7758C2.72014 65.907 2.59699 67.2389 2.51505 68.6606C2.3515 71.4987 2.34041 74.8383 3.0357 78.0987C3.76005 81.4953 5.72154 91.6918 16.0245 96.3108C21.0311 98.5551 27.7601 99.4936 34.0669 99.1838C40.3172 98.8767 46.6346 97.3189 50.5737 94.1122C54.2816 91.0937 58.7686 84.6307 60.6662 78.2589C62.8095 71.0619 67.4515 63.9646 68.6098 61.9533C69.2919 60.7689 69.0785 59.3628 68.7605 58.4258C68.4018 57.3688 67.707 56.2631 66.6632 55.4337C65.5867 54.5784 64.1582 54.0377 62.4748 54.1647C60.8256 54.2893 59.1162 55.0393 57.379 56.4107C52.9083 59.9401 49.6283 65.5082 48.02 68.9231C48.0034 68.7664 47.9835 68.6024 47.9684 68.4298C47.7597 66.0435 47.7232 62.3344 48.0158 56.7132C48.1989 53.1942 48.5269 50.1809 48.8737 46.6828C49.22 43.1904 49.5784 39.2713 49.8075 34.0302C49.9903 29.8481 49.5612 26.6722 48.3952 24.471C47.7909 23.3302 46.9729 22.4223 45.9321 21.814C44.8964 21.2087 43.7676 20.974 42.6523 20.974C41.0038 20.974 38.9497 21.2062 37.3141 22.7678C37.1698 22.4576 37.0124 22.1652 36.8419 21.8899C36.0863 20.6698 35.0817 19.8084 33.8508 19.2835C32.6679 18.7791 31.3849 18.6309 30.1167 18.6311C27.5677 18.6315 25.5986 19.7567 24.3285 21.4683C23.5066 22.576 23.0121 23.8775 22.7771 25.1982C21.4247 24.5876 20.0718 24.5068 19.2432 24.5068C17.8298 24.5068 15.9788 25.0791 14.4573 26.3154C12.8542 27.6179 11.6032 29.6733 11.6032 32.5083V35.4278Z" fill="black"/>
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1365 1365" width="1365" height="1365">
<title>safari-pinned-tab-svg</title>
<defs>
<clipPath clipPathUnits="userSpaceOnUse" id="cp1">
<path d="m655.2 313.1v822.23h-622.69v-822.23z"/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="cp2">
<path d="m1308.84 304v828.5h-617.24v-828.5z"/>
</clipPath>
</defs>
<style>
.s0 { fill: none }
.s1 { fill: #000000 }
</style>
<g id="surface1">
<path class="s0" d="m1258.6 499.2c-40.8-26.1-68 13.8-64.7 68.1l-0.3 0.4c0.1-56.6-7.3-119.2-31.7-169.8-8.7-17.9-26.2-47.4-61-33.5-15.2 6.1-29 24.4-21.9 71.7 0 0 8 49.7 6.5 112.2v0.8c-9.9-172.2-47.3-224.7-100.7-221.3-17.1 3.1-40.5 10.8-32.6 63.8 0 0 8.5 55.2 11.3 99.2l0.2 2.2h-0.2c-25.2-94.9-59-96.2-83.5-92.5-22.3 3.3-46.6 27.3-34.3 74.7 38.6 148.4 31 327.2 28.2 352.9-7.9-17.6-10.3-31.4-21.3-50.7-43.9-77.1-64.8-82.8-90.4-84-25.5-1.1-53 15.2-51.2 46.3 1.9 31.1 17.1 36.3 38.7 79.6 16.9 33.8 21.7 78 55.7 158.4 28.1 66.5 101.6 139.5 235.6 130.8 108.5-3.7 270.6-43.2 242.4-302.5-7-45-1.7-82.7 1.9-121.4 5.8-60 14.1-159.4-26.7-185.5z"/>
<path class="s0" d="m580.9 695.3c-25.7 1.7-46.4 7.7-89 85.7-10.6 19.4-12.7 33.3-20.3 50.9-3.3-25.5-14-204.2 21.9-353.4 11.4-47.5-13.3-71-35.6-73.9-24.6-3.3-58.5-1.3-81.9 94.6h-0.3l0.4-2.8c1.9-44 9.5-99.4 9.5-99.4 6.9-53.1-16.6-60.3-33.7-63.2-53.4-2.3-89.7 50.4-96.7 221.1h-0.2c-2.4-61.8 4.6-111 4.6-111 6.3-47.5-7.9-65.5-23.2-71.3-35-13.3-52 16.6-60.3 34.7-23.6 51-29.9 113.7-28.7 170.3l-0.4-0.4c2.4-54.2-25.6-93.7-65.9-66.9-40.3 26.9-30.1 126.2-23.4 186 4.5 38.6 10.4 76.2 4.1 121.4-23.5 259.7 139.3 296.1 247.8 297.8 134.1 6.1 206.3-68.3 233.3-135.4 32.5-80.9 36.6-125.3 52.8-159.3 20.8-43.8 36-49.2 37.3-80.3 1.3-31.1-26.5-46.9-52-45.3z"/>
<g id="Clip-Path" clip-path="url(#cp1)">
<g>
<path fill-rule="evenodd" class="s1" d="m634.4 695.7c11.9 12 17.8 27.9 17.1 45.7-1 24.6-9.5 37.8-19.4 53.2-5.8 9-12.3 19.2-19.8 34.9-6.6 13.8-11.2 30.4-17 51.5-7.6 27.3-17 61.2-35.3 106.7-14.1 35.1-71.4 146.1-231.3 147.6q-9.8 0.1-20-0.3c-93.6-1.5-164.2-28.5-209.3-80.6-46.9-54-65.8-134.2-56.4-238.4l0.1-0.9c5.2-37.6 1.5-69.5-2.6-103.3-0.5-4.4-1-8.7-1.5-13.1-9.4-82.5-15.4-173.1 31.8-204.6 27.9-18.5 49.3-11.1 59.6-4.9 1 0.6 1.9 1.2 2.9 1.9 4.5-32.3 12.6-63.9 25.6-92.2 11.2-24.2 37.1-62.2 83.8-44.5 7.6 3 17.3 8.8 24.8 20.2 6.7-14.7 14.5-26.6 23.5-35.8 16.6-17.2 37.4-25.4 61.6-24.3l2.1 0.2c39.2 6.5 55.9 35 49.4 84.9 0 0 0 0.3 0 0.6 20-17 40.4-16.9 56.1-14.8 17.2 2.2 32.8 12.1 42.8 27.2 8.6 13 17.1 35.8 8.7 70.7-22.3 92.9-26.1 198.5-25.2 268.8 36.3-61.5 59.9-74.1 93.2-76.2 20.8-1.3 41.3 6 54.8 19.8zm-316.9-329.4c-35.1 36.2-49.7 148.5-43.4 333.7 19.5-3 39.5-5.2 59.6-6.5 4.7-91.2 13.3-153.7 23.7-197q0-0.5 0-0.9c2-44.4 9.3-98.9 9.7-101.2 4.7-36.3-5.7-39.2-17-41.1-13.2-0.3-23.5 3.8-32.5 13zm-124.6 49.5c-50 108.3-16.2 277.2-8.5 303.8 16.1-4.8 33.8-9.2 52.4-13-2.1-57.7-2.3-108.1-0.6-151.9-2.3-62.3 4.4-111.4 4.7-113.5 1.8-13 4.2-44.5-11.1-50.3-10.9-4.1-22.8-5.7-36.9 24.8zm407.9 357.3c8.9-13.8 12.6-19.5 13.2-33.3 0.2-6.7-1.6-12-5.9-16.3-5.9-6.1-16-9.4-26.1-8.8-17.3 1.2-33.6 2.2-73.8 75.9-5.6 10.4-8.5 18.9-11.8 28.6-2.1 6.3-4.4 13.2-7.7 20.8-0.1 0.3-0.2 0.6-0.4 0.8-1.4 3.3-3 6.8-4.8 10.4-17.6 34.2-46.2 53.4-76.5 51.6-10.4-0.7-18.2-9.9-17.6-20.6 0.6-10.7 9.6-18.7 19.8-18.2 18 1.1 33.1-15.3 41.2-31.1 0.7-1.4 1.3-2.8 2-4.2-4-41.1-11.8-210.7 22.9-354.8 3.9-16.5 2.8-30.1-3.3-39.3-5.6-8.4-13.4-10.3-16.5-10.7-11.2-1.5-19.3-0.9-27.8 6.4-20.5 17.8-46.8 77.8-56.4 261.8 5.8 0 11.6 0 17.3 0.2 10.3 0.3 18.5 9.2 18.2 19.9-0.3 10.7-8.8 19.1-19.2 18.9-95.8-2.8-204.4 22.8-250.2 47.9-2.8 1.5-5.7 2.3-8.6 2.3-6.8 0.1-13.4-3.7-16.8-10.3-4.8-9.5-1.4-21.2 7.8-26.3 8.1-4.4 17.9-8.9 28.9-13.1-6.7-22.4-18.6-82-20.1-150.6-0.3-1.5-0.5-3-0.4-4.6 1.2-29.4-7.6-48.4-16.4-53.6-5.2-3.1-12.1-1.8-20.6 3.9-31.6 21-19.4 127.4-14.9 167.4 0.5 4.3 1 8.6 1.5 12.9 4.1 34.7 8.4 70.6 2.6 113-8.3 92.7 7.5 162.9 47 208.5 38.4 44.2 98 66.3 182.4 67.6 151.5 7.1 203.3-92.6 215.7-123.3 17.4-43.4 26.5-76.2 33.8-102.5 6.4-22.9 11.4-41 19.5-58 8.5-17.9 16.1-29.7 22.2-39.2z"/>
</g>
</g>
<g id="Clip-Path" clip-path="url(#cp2)">
<g>
<path fill-rule="evenodd" class="s1" d="m1301.9 802.9l0.1 0.9c11.3 104-6.3 184.6-52.2 239.5-44.1 52.8-114.2 81.3-208.3 84.5q-10.2 0.7-19.9 0.8c-159.5 1.6-218.8-108.4-233.5-143.2-19.1-45.1-29.1-78.9-37.2-106-6.2-21-11.1-37.5-18-51.2-7.7-15.5-14.4-25.6-20.4-34.5-10.1-15.1-18.8-28.2-20.3-52.8-1.1-17.7 4.5-33.6 16.2-45.9 13.3-14 33.8-21.8 54.5-20.9 33.3 1.5 57.1 13.7 94.5 74.5-0.3-70.4-6-175.9-30-268.4-9-34.8-0.9-57.7 7.5-70.8 9.7-15.3 25.1-25.5 42.2-28.1 15.7-2.3 36.2-2.9 56.4 13.8 0-0.2 0-0.4 0-0.4-7.4-49.9 8.8-78.8 47.9-86l2.1-0.3c24.1-1.6 45 6.3 62 23.2 9.1 9 17.1 20.7 24.1 35.3 7.4-11.6 16.9-17.6 24.5-20.6 46.4-18.7 72.9 18.9 84.5 42.9 13.5 28 22.1 59.5 27.3 91.6 0.9-0.6 1.8-1.3 2.8-1.9 10.2-6.3 31.5-14.3 59.7 3.8 47.8 30.6 43.4 121.3 35.5 203.9-0.4 4.4-0.8 8.7-1.3 13.1-3.4 33.9-6.6 65.9-0.8 103.3zm-197.6-256.5c2.5 43.7 3.2 94.2 2.1 151.9 18.7 3.5 36.4 7.5 52.7 12 7.2-26.7 37.9-196.3-14-303.7-14.6-30.1-26.5-28.4-37.4-24.1-15.1 6.1-12.2 37.5-10.2 50.7 0.4 1.9 8 50.9 6.8 113.2zm-117.5-199.2c-11.4 2.2-21.6 5.2-16.3 41.6 0.4 2.1 8.8 56.4 11.5 100.8q0 0.5 0 0.9c11.2 43.2 21 105.4 27.2 196.6 20.2 0.9 40.2 2.7 59.7 5.3 3-185.3-13.5-297.2-49.3-332.8-9.1-9.1-19.5-13-32.7-12.4zm278.5 348.5c0.5-4.3 0.9-8.6 1.3-13 3.9-40.1 14.1-146.6-17.9-167-8.6-5.5-15.5-6.7-20.6-3.5-8.7 5.4-17.2 24.5-15.4 53.9 0.1 1.5 0 3.1-0.3 4.6-0.4 68.5-11.2 128.4-17.5 150.9 11.1 4.1 20.9 8.3 29.1 12.6 9.3 4.9 13 16.6 8.3 26.2-3.3 6.7-9.8 10.6-16.6 10.6-2.9 0-5.9-0.6-8.6-2.1-46.2-24.3-155.4-47.7-251-43.2-10.5 0.3-19.2-7.7-19.6-18.5-0.5-10.7 7.5-19.8 17.9-20.2 5.6-0.3 11.4-0.5 17.2-0.6-12.8-183.8-40.2-243.3-61-260.7-8.6-7.1-16.8-7.6-27.9-5.9-3.2 0.5-10.9 2.5-16.4 11.1-5.9 9.3-6.8 22.9-2.5 39.3 37.3 143.5 32.4 313.2 29.2 354.3 0.7 1.4 1.3 2.7 2.1 4.2 8.4 15.6 23.7 31.7 41.7 30.3 10.3-0.7 19.3 7.2 20.1 17.9 0.8 10.6-6.9 20-17.2 20.8-30.3 2.5-59.3-16.3-77.4-50.1-2-3.6-3.6-7-5.1-10.3-0.1-0.2-0.2-0.5-0.3-0.7-3.5-7.5-5.9-14.5-8.2-20.8-3.5-9.6-6.4-18-12.3-28.3-41.6-72.9-57.9-73.7-75.1-74.5-10.2-0.5-20.2 3.1-26.1 9.3-4.1 4.3-5.9 9.7-5.5 16.4 0.8 13.8 4.6 19.4 13.7 33.1 6.3 9.4 14.1 21 23 38.7 8.3 16.9 13.7 34.8 20.5 57.7 7.7 26.2 17.4 58.7 35.6 101.8 12.9 30.5 66.7 129.1 217.3 119.2 84.9-2.9 144.2-26.2 181.7-71.1 38.7-46.3 53.2-116.8 43.3-209.4-6.6-42.3-3-78.2 0.5-113z"/>
</g>
</g>
<path class="s1" d="m739.8 434.2c-3 0-6.1-0.6-9-2-10.5-5-15.2-18-10.3-28.9 15.7-35.6 38.8-68.4 66.6-94.9 8.5-8.1 21.9-7.6 29.7 1.3 7.9 8.9 7.4 22.7-1.2 30.8-23.7 22.7-43.4 50.6-56.8 81-3.6 7.9-11.1 12.6-19 12.7z"/>
<path class="s1" d="m668.8 421.6c-10.9 0.1-20.3-8.5-21.2-20-4-51.4-4.2-103.5-0.4-154.8 0.9-12 11.1-21 22.6-20.1 11.6 0.9 20.3 11.3 19.4 23.3-3.6 49.1-3.4 98.9 0.4 148 1 12-7.7 22.5-19.3 23.5-0.5 0-1 0-1.5 0z"/>
<path class="s1" d="m596.2 435.1c-9.4 0.1-18.2-6.5-20.6-16.4-8.9-36.3-25.9-70.8-48.9-99.7-7.4-9.3-6.1-23 2.8-30.7 9-7.6 22.3-6.3 29.7 3 26.9 33.8 46.7 74.2 57.2 116.7 2.9 11.6-4 23.5-15.2 26.5-1.8 0.4-3.4 0.6-5.1 0.7z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -11,6 +11,7 @@ import type {
V1AppConversationStartTask,
V1AppConversationStartTaskPage,
V1AppConversation,
V1SandboxInfo,
} from "./v1-conversation-service.types";
class V1ConversationService {
@@ -60,8 +61,6 @@ class V1ConversationService {
selected_branch?: string,
conversationInstructions?: string,
trigger?: ConversationTrigger,
parent_conversation_id?: string,
agent_type?: "default" | "plan",
): Promise<V1AppConversationStartTask> {
const body: V1AppConversationStartRequest = {
selected_repository: selectedRepository,
@@ -69,8 +68,6 @@ class V1ConversationService {
selected_branch,
title: conversationInstructions,
trigger,
parent_conversation_id: parent_conversation_id || null,
agent_type,
};
// Add initial message if provided
@@ -115,11 +112,11 @@ class V1ConversationService {
* Search for start tasks (ongoing tasks that haven't completed yet)
* Use this to find tasks that were started but the user navigated away
*
* Note: Backend supports filtering by limit and created_at__gte. To filter by repository/trigger,
* Note: Backend only supports filtering by limit. To filter by repository/trigger,
* filter the results client-side after fetching.
*
* @param limit Maximum number of tasks to return (max 100)
* @returns Array of start tasks from the last 20 minutes
* @returns Array of start tasks
*/
static async searchStartTasks(
limit: number = 100,
@@ -127,10 +124,6 @@ class V1ConversationService {
const params = new URLSearchParams();
params.append("limit", limit.toString());
// Only get tasks from the last 20 minutes
const twentyMinutesAgo = new Date(Date.now() - 20 * 60 * 1000);
params.append("created_at__gte", twentyMinutesAgo.toISOString());
const { data } = await openHands.get<V1AppConversationStartTaskPage>(
`/api/v1/app-conversations/start-tasks/search?${params.toString()}`,
);
@@ -220,6 +213,36 @@ class V1ConversationService {
return data;
}
/**
* Pause a V1 sandbox
* Calls the /api/v1/sandboxes/{id}/pause endpoint
*
* @param sandboxId The sandbox ID to pause
* @returns Success response
*/
static async pauseSandbox(sandboxId: string): Promise<{ success: boolean }> {
const { data } = await openHands.post<{ success: boolean }>(
`/api/v1/sandboxes/${sandboxId}/pause`,
{},
);
return data;
}
/**
* Resume a V1 sandbox
* Calls the /api/v1/sandboxes/{id}/resume endpoint
*
* @param sandboxId The sandbox ID to resume
* @returns Success response
*/
static async resumeSandbox(sandboxId: string): Promise<{ success: boolean }> {
const { data } = await openHands.post<{ success: boolean }>(
`/api/v1/sandboxes/${sandboxId}/resume`,
{},
);
return data;
}
/**
* Batch get V1 app conversations by their IDs
* Returns null for any missing conversations
@@ -246,6 +269,32 @@ class V1ConversationService {
return data;
}
/**
* Batch get V1 sandboxes by their IDs
* Returns null for any missing sandboxes
*
* @param ids Array of sandbox IDs (max 100)
* @returns Array of sandboxes or null for missing ones
*/
static async batchGetSandboxes(
ids: string[],
): Promise<(V1SandboxInfo | null)[]> {
if (ids.length === 0) {
return [];
}
if (ids.length > 100) {
throw new Error("Cannot request more than 100 sandboxes at once");
}
const params = new URLSearchParams();
ids.forEach((id) => params.append("id", id));
const { data } = await openHands.get<(V1SandboxInfo | null)[]>(
`/api/v1/sandboxes?${params.toString()}`,
);
return data;
}
/**
* Upload a single file to the V1 conversation workspace
* V1 API endpoint: POST /api/file/upload/{path}
@@ -296,6 +345,24 @@ class V1ConversationService {
const { data } = await openHands.get<{ runtime_id: string }>(url);
return data;
}
/**
* Get the count of events for a conversation
* Uses the V1 API endpoint: GET /api/v1/events/count
*
* @param conversationId The conversation ID to get event count for
* @returns The number of events in the conversation
*/
static async getEventCount(conversationId: string): Promise<number> {
const params = new URLSearchParams();
params.append("conversation_id__eq", conversationId);
const { data } = await openHands.get<number>(
`/api/v1/events/count?${params.toString()}`,
);
return data;
}
}
export default V1ConversationService;

View File

@@ -1,6 +1,5 @@
import { ConversationTrigger } from "../open-hands.types";
import { Provider } from "#/types/settings";
import { V1SandboxStatus } from "../sandbox-service/sandbox-service.types";
// V1 API Types for requests
// Note: This represents the serialized API format, not the internal TextContent/ImageContent types
@@ -30,8 +29,6 @@ export interface V1AppConversationStartRequest {
title?: string | null;
trigger?: ConversationTrigger | null;
pr_number?: number[];
parent_conversation_id?: string | null;
agent_type?: "default" | "plan";
}
export type V1AppConversationStartTaskStatus =
@@ -40,7 +37,6 @@ export type V1AppConversationStartTaskStatus =
| "PREPARING_REPOSITORY"
| "RUNNING_SETUP_SCRIPT"
| "SETTING_UP_GIT_HOOKS"
| "SETTING_UP_SKILLS"
| "STARTING_CONVERSATION"
| "READY"
| "ERROR";
@@ -68,7 +64,14 @@ export interface V1AppConversationStartTaskPage {
next_page_id: string | null;
}
export type V1ConversationExecutionStatus =
export type V1SandboxStatus =
| "MISSING"
| "STARTING"
| "RUNNING"
| "STOPPED"
| "PAUSED";
export type V1AgentExecutionStatus =
| "RUNNING"
| "AWAITING_USER_INPUT"
| "AWAITING_USER_CONFIRMATION"
@@ -91,7 +94,22 @@ export interface V1AppConversation {
created_at: string;
updated_at: string;
sandbox_status: V1SandboxStatus;
execution_status: V1ConversationExecutionStatus | null;
agent_status: V1AgentExecutionStatus | null;
conversation_url: string | null;
session_api_key: string | null;
}
export interface V1ExposedUrl {
name: string;
url: string;
}
export interface V1SandboxInfo {
id: string;
created_by_user_id: string | null;
sandbox_spec_id: string;
status: V1SandboxStatus;
session_api_key: string | null;
exposed_urls: V1ExposedUrl[] | null;
created_at: string;
}

View File

@@ -5,7 +5,6 @@ import type {
ConfirmationResponseRequest,
ConfirmationResponseResponse,
} from "./event-service.types";
import { openHands } from "../open-hands-axios";
class EventService {
/**
@@ -37,14 +36,6 @@ class EventService {
return data;
}
static async getEventCount(conversationId: string): Promise<number> {
const params = new URLSearchParams();
params.append("conversation_id__eq", conversationId);
const { data } = await openHands.get<number>(
`/api/v1/events/count?${params.toString()}`,
);
return data;
}
}
export default EventService;

View File

@@ -77,7 +77,6 @@ export interface Conversation {
session_api_key: string | null;
pr_number?: number[] | null;
conversation_version?: "V0" | "V1";
sub_conversation_ids?: string[];
}
export interface ResultSet<T> {

View File

@@ -13,7 +13,6 @@ export interface GetConfigResponse {
ENABLE_JIRA: boolean;
ENABLE_JIRA_DC: boolean;
ENABLE_LINEAR: boolean;
ENABLE_AZURE_DEVOPS: boolean;
};
MAINTENANCE?: {
startTime: string;

View File

@@ -1,52 +0,0 @@
// sandbox-service.api.ts
// This file contains API methods for /api/v1/sandboxes endpoints.
import { openHands } from "../open-hands-axios";
import type { V1SandboxInfo } from "./sandbox-service.types";
export class SandboxService {
/**
* Pause a V1 sandbox
* Calls the /api/v1/sandboxes/{id}/pause endpoint
*/
static async pauseSandbox(sandboxId: string): Promise<{ success: boolean }> {
const { data } = await openHands.post<{ success: boolean }>(
`/api/v1/sandboxes/${sandboxId}/pause`,
{},
);
return data;
}
/**
* Resume a V1 sandbox
* Calls the /api/v1/sandboxes/{id}/resume endpoint
*/
static async resumeSandbox(sandboxId: string): Promise<{ success: boolean }> {
const { data } = await openHands.post<{ success: boolean }>(
`/api/v1/sandboxes/${sandboxId}/resume`,
{},
);
return data;
}
/**
* Batch get V1 sandboxes by their IDs
* Returns null for any missing sandboxes
*/
static async batchGetSandboxes(
ids: string[],
): Promise<(V1SandboxInfo | null)[]> {
if (ids.length === 0) {
return [];
}
if (ids.length > 100) {
throw new Error("Cannot request more than 100 sandboxes at once");
}
const params = new URLSearchParams();
ids.forEach((id) => params.append("id", id));
const { data } = await openHands.get<(V1SandboxInfo | null)[]>(
`/api/v1/sandboxes?${params.toString()}`,
);
return data;
}
}

View File

@@ -1,24 +0,0 @@
// sandbox-service.types.ts
// This file contains types for Sandbox API.
export type V1SandboxStatus =
| "MISSING"
| "STARTING"
| "RUNNING"
| "STOPPED"
| "PAUSED";
export interface V1ExposedUrl {
name: string;
url: string;
}
export interface V1SandboxInfo {
id: string;
created_by_user_id: string | null;
sandbox_spec_id: string;
status: V1SandboxStatus;
session_api_key: string | null;
exposed_urls: V1ExposedUrl[] | null;
created_at: string;
}

View File

@@ -1 +0,0 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="m22 18-5 4-8-3v3l-4.19-5.75 12.91 1.05v-10.96l4.28-.69zm-17.19-1.75v-7.29l12.91-2.62-7.12-4.34v2.84l-6.63 1.92-1.97 2.62v5.69z"/></svg>

Before

Width:  |  Height:  |  Size: 248 B

View File

@@ -1,9 +1,16 @@
<svg width="47" height="30" viewBox="0 0 148 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M71.754 16.863V2.97414C71.754 1.82355 72.6871 0.890503 73.8377 0.890503C74.9883 0.890503 75.9213 1.82355 75.9213 2.97414V16.863C75.9213 18.0136 74.9883 18.9466 73.8377 18.9466C72.6871 18.9466 71.754 18.0136 71.754 16.863Z" fill="black"/>
<path d="M82.5273 18.9329L89.4717 6.90477C90.047 5.90832 91.3215 5.5668 92.318 6.1421C93.3144 6.7174 93.6559 7.99197 93.0806 8.98841L86.1362 21.0165C85.5609 22.0129 84.2864 22.3545 83.2899 21.7792C82.2935 21.2039 81.952 19.9293 82.5273 18.9329Z" fill="black"/>
<path d="M65.1481 18.9329L58.2037 6.90477C57.6284 5.90832 56.3538 5.5668 55.3574 6.1421C54.3609 6.7174 54.0194 7.99197 54.5947 8.98841L61.5391 21.0165C62.1144 22.0129 63.389 22.3545 64.3854 21.7792C65.3819 21.2039 65.7234 19.9293 65.1481 18.9329Z" fill="black"/>
<path d="M98.5045 92.4969C95.1427 89.7602 90.8793 83.6628 89.0929 77.664C86.8598 70.1655 82.0309 62.777 80.9578 60.9138C80.0056 59.2602 83.0313 53.2605 89.0929 58.0459C93.9421 61.8742 97.3878 68.466 98.5045 71.2833C98.9566 72.4239 103.006 79.2475 101.827 56.6051C101.455 49.445 100.49 44.3126 100.036 33.9388C99.6807 25.7997 101.824 23.057 105.11 23.057C108.396 23.057 111.106 24.1244 111.106 32.4283C111.106 22.8682 113.154 20.7141 117.646 20.7147C121.39 20.7153 123.069 23.9077 123.069 27.0952V32.5084C123.069 27.0952 126.669 26.5896 128.519 26.5896C130.37 26.5896 134.076 28.1957 134.076 32.5084V43.317C134.076 38.116 137.324 36.601 139.773 36.765C142.963 36.9786 144.405 39.2382 143.965 44.3126C143.651 47.9349 142.689 58.5273 142.689 62.0291C142.689 65.1712 143.965 71.6801 142.689 77.664C141.953 81.1168 140.137 90.2628 130.885 94.4102C121.634 98.5575 105.364 98.0807 98.5045 92.4969Z" fill="#FFFF8B"/>
<path d="M140.606 62.0292C140.606 58.409 141.583 47.6748 141.89 44.1323C142.097 41.7374 141.809 40.4247 141.424 39.7542C141.141 39.2626 140.699 38.915 139.634 38.8436C138.865 38.7921 138.027 39.0114 137.401 39.5761C136.814 40.1052 136.159 41.1682 136.159 43.3176L136.155 43.4388L135.198 59.758C135.164 60.3451 134.883 60.8911 134.424 61.2599C133.966 61.6284 133.374 61.7859 132.793 61.6941L122.764 60.1068L111.948 58.6703C110.949 58.5376 110.188 57.7084 110.142 56.7016L109.545 43.8055L109.54 43.6959C109.274 38.5609 109.022 33.8767 109.022 32.4282C109.022 28.3859 108.338 26.6806 107.74 25.9634C107.263 25.3915 106.577 25.1402 105.11 25.1402C104.583 25.1402 104.212 25.2481 103.933 25.4111C103.659 25.5714 103.346 25.8587 103.049 26.4208C102.41 27.6257 101.945 29.891 102.118 33.8479C102.342 38.9804 102.692 42.8146 103.035 46.2718C103.377 49.7231 103.718 52.8561 103.908 56.4971C104.204 62.1966 104.178 66.1256 103.945 68.7924C103.828 70.124 103.656 71.1996 103.423 72.0501C103.202 72.8558 102.871 73.6757 102.296 74.2887C101.6 75.0303 100.608 75.3844 99.577 75.136C98.7592 74.9389 98.1847 74.4215 97.8706 74.0916C97.2141 73.4017 96.7501 72.5106 96.568 72.0512C95.5097 69.3812 92.2352 63.1808 87.8023 59.6811C86.5089 58.6599 85.5666 58.3652 84.9736 58.3204C84.4148 58.2783 84.0094 58.4436 83.6909 58.6967C83.34 58.9756 83.0781 59.3811 82.9479 59.7643C82.9019 59.8999 82.8823 59.9968 82.8741 60.0584C84.0759 62.0865 88.8421 69.5222 91.0896 77.069C92.7648 82.6941 96.8038 88.4259 99.8194 90.8809C102.74 93.258 107.988 94.7313 113.9 95.0218C119.756 95.3095 125.788 94.4121 130.033 92.5092C138.233 88.8334 139.903 80.7382 140.651 77.2292C141.232 74.5057 141.243 71.5987 141.087 68.9009C141.01 67.5551 140.894 66.2969 140.793 65.1373C140.695 64.0105 140.606 62.9215 140.606 62.0292ZM120.986 27.0953C120.986 25.8314 120.648 24.7049 120.089 23.9514C119.583 23.27 118.84 22.7987 117.646 22.7984C116.668 22.7982 116.011 22.9187 115.546 23.1167C115.13 23.2943 114.781 23.5699 114.463 24.0831C113.73 25.2671 113.192 27.6455 113.189 32.384L113.707 43.6021C113.901 47.3443 114.103 51.3994 114.236 54.7707L120.986 55.6666V27.0953ZM125.153 56.2652L131.172 57.218L131.992 43.267V32.5083C131.992 31.031 131.39 30.1275 130.678 29.5489C129.884 28.9039 128.957 28.6731 128.519 28.6731C127.722 28.6731 126.899 28.797 126.306 29.2179C125.849 29.5421 125.153 30.3087 125.153 32.5083V56.2652ZM136.159 35.4278C137.406 34.8069 138.74 34.6083 139.912 34.6868C142.037 34.8292 143.91 35.718 145.037 37.6779C146.06 39.4592 146.273 41.8136 146.041 44.4927C145.72 48.1949 144.772 58.6457 144.772 62.0292C144.772 62.708 144.843 63.6116 144.944 64.7758C145.042 65.907 145.165 67.2389 145.247 68.6606C145.411 71.4987 145.422 74.8383 144.727 78.0987C144.002 81.4953 142.041 91.6918 131.738 96.3108C126.731 98.5551 120.002 99.4936 113.696 99.1838C107.445 98.8767 101.128 97.3189 97.1887 94.1122C93.4809 91.0936 88.9938 84.6307 87.0962 78.2589C84.9529 71.0619 80.3109 63.9646 79.1527 61.9533C78.4706 60.7689 78.684 59.3628 79.0019 58.4258C79.3607 57.3688 80.0554 56.2631 81.0993 55.4337C82.1758 54.5784 83.6043 54.0377 85.2876 54.1647C86.9369 54.2893 88.6462 55.0393 90.3834 56.4107C94.8541 59.9401 98.1342 65.5082 99.7424 68.9231C99.759 68.7664 99.779 68.6024 99.7941 68.4298C100.003 66.0435 100.039 62.3344 99.7467 56.7132C99.5635 53.1942 99.2356 50.1809 98.8888 46.6828C98.5425 43.1904 98.184 39.2713 97.955 34.0302C97.7722 29.8481 98.2012 26.6722 99.3672 24.471C99.9716 23.3302 100.79 22.4223 101.83 21.814C102.866 21.2087 103.995 20.974 105.11 20.974C106.759 20.974 108.813 21.2062 110.448 22.7678C110.593 22.4576 110.75 22.1652 110.921 21.8899C111.676 20.6698 112.681 19.8084 113.912 19.2835C115.095 18.7791 116.378 18.6309 117.646 18.6311C120.195 18.6315 122.165 19.7565 123.435 21.4683C124.257 22.576 124.75 23.8776 124.985 25.1982C126.338 24.5876 127.691 24.5068 128.519 24.5068C129.933 24.5068 131.784 25.0791 133.305 26.3154C134.908 27.6179 136.159 29.6733 136.159 32.5083V35.4278Z" fill="black"/>
<path d="M49.258 92.4969C52.6198 89.7602 56.8832 83.6628 58.6696 77.664C60.9027 70.1655 65.7316 62.777 66.8047 60.9138C67.757 59.2602 64.7312 53.2605 58.6696 58.0459C53.8204 61.8742 50.3747 68.466 49.258 71.2833C48.8059 72.4239 44.7566 79.2475 45.935 56.6051C46.3077 49.445 47.2728 44.3126 47.7261 33.9388C48.0818 25.7997 45.9381 23.057 42.6523 23.057C39.3666 23.057 36.6568 24.1244 36.6568 32.4283C36.6568 22.8682 34.6087 20.7141 30.1168 20.7147C26.3728 20.7153 24.6934 23.9077 24.6934 27.0952V32.5084C24.6934 27.0952 21.0938 26.5896 19.243 26.5896C17.3923 26.5896 13.687 28.1957 13.687 32.5084V43.317C13.687 38.116 10.4388 36.601 7.98968 36.765C4.79943 36.9786 3.3574 39.2382 3.79721 44.3126C4.11115 47.9349 5.07331 58.5273 5.07331 62.0291C5.07331 65.1712 3.79721 71.6801 5.07331 77.664C5.80964 81.1168 7.62551 90.2628 16.8772 94.4102C26.129 98.5575 42.3987 98.0807 49.258 92.4969Z" fill="#FFFF8B"/>
<path d="M7.15667 62.0292C7.15667 58.409 6.18001 47.6748 5.87297 44.1323C5.66546 41.7374 5.95363 40.4247 6.33881 39.7542C6.62122 39.2626 7.06342 38.915 8.1284 38.8436C8.89765 38.7921 9.7355 39.0114 10.3617 39.5761C10.9484 40.1052 11.6032 41.1682 11.6032 43.3176L11.6075 43.4388L12.5644 59.758C12.5989 60.3451 12.8798 60.8911 13.338 61.2599C13.7961 61.6284 14.3888 61.7859 14.9695 61.6941L24.9988 60.1068L35.8144 58.6703C36.8136 58.5376 37.5741 57.7084 37.6208 56.7016L38.2174 43.8055L38.2226 43.6959C38.4887 38.5609 38.7401 33.8767 38.7401 32.4282C38.7401 28.3859 39.4246 26.6806 40.0228 25.9634C40.4997 25.3915 41.1851 25.1402 42.6523 25.1402C43.1795 25.1402 43.5506 25.2481 43.8296 25.4111C44.1038 25.5714 44.416 25.8587 44.7139 26.4208C45.3521 27.6257 45.8174 29.891 45.6445 33.8479C45.4202 38.9804 45.0703 42.8146 44.7276 46.2718C44.3854 49.7231 44.0444 52.8561 43.8549 56.4971C43.5583 62.1966 43.5848 66.1256 43.818 68.7924C43.9345 70.124 44.107 71.1996 44.3397 72.0501C44.5602 72.8558 44.891 73.6757 45.4664 74.2887C46.1626 75.0303 47.1547 75.3844 48.1855 75.136C49.0033 74.9389 49.5779 74.4215 49.8919 74.0916C50.5484 73.4017 51.0124 72.5106 51.1945 72.0512C52.2528 69.3812 55.5273 63.1808 59.9602 59.6811C61.2536 58.6599 62.1959 58.3652 62.7889 58.3204C63.3477 58.2783 63.7531 58.4436 64.0716 58.6967C64.4225 58.9756 64.6844 59.3811 64.8146 59.7643C64.8606 59.8999 64.8802 59.9968 64.8884 60.0584C63.6866 62.0865 58.9205 69.5222 56.6729 77.069C54.9978 82.6941 50.9587 88.4259 47.9431 90.8809C45.0229 93.258 39.7748 94.7313 33.8625 95.0218C28.0069 95.3095 21.9748 94.4121 17.7298 92.5092C9.52994 88.8334 7.85968 80.7382 7.11135 77.2292C6.5306 74.5057 6.51956 71.5987 6.67502 68.9009C6.75257 67.5551 6.86815 66.2969 6.96907 65.1373C7.06713 64.0105 7.15666 62.9215 7.15667 62.0292ZM26.7768 27.0953C26.7768 25.8314 27.1148 24.7049 27.6737 23.9514C28.1793 23.27 28.9221 22.7987 30.1168 22.7984C31.0942 22.7982 31.7519 22.9187 32.2162 23.1167C32.6327 23.2943 32.9818 23.5699 33.2997 24.0831C34.0329 25.2671 34.5706 27.6455 34.5739 32.384L34.0554 43.6021C33.8615 47.3443 33.6592 51.3994 33.5263 54.7707L26.7768 55.6666V27.0953ZM22.6096 56.2652L16.5905 57.218L15.7705 43.267V32.5083C15.7705 31.031 16.3726 30.1275 17.0848 29.5489C17.8786 28.9039 18.8059 28.6731 19.2433 28.6731C20.0405 28.6731 20.8635 28.797 21.4565 29.2179C21.9131 29.5421 22.6095 30.3087 22.6096 32.5083V56.2652ZM11.6032 35.4278C10.3568 34.8069 9.02271 34.6083 7.85016 34.6868C5.72547 34.8292 3.85203 35.718 2.7259 37.6779C1.70253 39.4592 1.4893 41.8136 1.7215 44.4927C2.04237 48.1949 2.99044 58.6457 2.99044 62.0292C2.99043 62.708 2.91997 63.6116 2.81865 64.7758C2.7202 65.907 2.59705 67.2389 2.51511 68.6606C2.35156 71.4987 2.34047 74.8383 3.03576 78.0987C3.76011 81.4953 5.7216 91.6918 16.0245 96.3108C21.0312 98.5551 27.7601 99.4936 34.0669 99.1838C40.3173 98.8767 46.6346 97.3189 50.5738 94.1122C54.2816 91.0936 58.7687 84.6307 60.6663 78.2589C62.8096 71.0619 67.4516 63.9646 68.6099 61.9533C69.292 60.7689 69.0785 59.3628 68.7606 58.4258C68.4018 57.3688 67.7071 56.2631 66.6632 55.4337C65.5867 54.5784 64.1582 54.0377 62.4749 54.1647C60.8256 54.2893 59.1163 55.0393 57.3791 56.4107C52.9084 59.9401 49.6283 65.5082 48.0201 68.9231C48.0035 68.7664 47.9835 68.6024 47.9684 68.4298C47.7597 66.0435 47.7233 62.3344 48.0159 56.7132C48.199 53.1942 48.5269 50.1809 48.8738 46.6828C49.22 43.1904 49.5785 39.2713 49.8076 34.0302C49.9903 29.8481 49.5613 26.6722 48.3953 24.471C47.7909 23.3302 46.9729 22.4223 45.9322 21.814C44.8964 21.2087 43.7676 20.974 42.6523 20.974C41.0038 20.974 38.9498 21.2062 37.3141 22.7678C37.1699 22.4576 37.0125 22.1652 36.842 21.8899C36.0864 20.6698 35.0817 19.8084 33.8509 19.2835C32.668 18.7791 31.3849 18.6309 30.1168 18.6311C27.5676 18.6315 25.5976 19.7565 24.3275 21.4683C23.5057 22.576 23.0121 23.8776 22.7771 25.1982C21.4248 24.5876 20.0718 24.5068 19.2433 24.5068C17.8299 24.5068 15.9789 25.0791 14.4573 26.3154C12.8543 27.6179 11.6033 29.6733 11.6032 32.5083V35.4278Z" fill="black"/>
<svg xmlns="http://www.w3.org/2000/svg" width="47" height="30" viewBox="0 0 47 30" fill="none">
<g clip-path="url(#clip0_10905_18559)">
<path d="M44.731 8.9991C43.271 8.13859 42.2956 9.4574 42.4152 11.248L42.4031 11.2616C42.4071 9.39165 42.1435 7.32642 41.2675 5.65567C40.9573 5.06395 40.3287 4.09128 39.0856 4.54957C38.5402 4.75068 38.0454 5.35594 38.3009 6.9184C38.3009 6.9184 38.5848 8.55821 38.532 10.6196V10.6486C38.1772 4.96339 36.8388 3.22883 34.9246 3.34099C34.3122 3.44541 33.4748 3.69873 33.7566 5.44683C33.7566 5.44683 34.0628 7.27034 34.1622 8.72258L34.1683 8.79606H34.1622C33.2618 5.66147 32.0492 5.61893 31.1712 5.74076C30.3743 5.85098 29.5044 6.64381 29.9444 8.20627C31.3253 13.1083 31.0556 19.012 30.9522 19.857C30.6703 19.2789 30.5831 18.8206 30.1918 18.1863C28.6182 15.6396 27.87 15.452 26.9514 15.4133C26.0389 15.3746 25.0534 15.9141 25.1183 16.941C25.1852 17.9678 25.7307 18.1379 26.5053 19.5689C27.1096 20.6827 27.2819 22.1427 28.4986 24.7958C29.5064 26.9925 32.1405 29.402 36.9382 29.1158C40.8255 28.992 46.631 27.6887 45.6212 19.13C45.3697 17.6429 45.5583 16.3976 45.6901 15.1213C45.8949 13.1412 46.195 9.85962 44.733 8.99717L44.731 8.9991Z" fill="#FFE165"/>
<path d="M20.458 15.4707C19.5395 15.5268 18.7973 15.7259 17.2724 18.2998C16.8932 18.9398 16.8161 19.4 16.5444 19.9821C16.4248 19.139 16.0415 13.2411 17.3272 8.31587C17.7368 6.74761 16.8526 5.97024 16.0537 5.87356C15.1736 5.7672 13.959 5.83101 13.1195 8.99654H13.1094L13.1215 8.90566C13.1925 7.45149 13.4642 5.62411 13.4642 5.62411C13.7096 3.87021 12.8701 3.63236 12.2557 3.5376C10.3455 3.46025 9.04367 5.20255 8.79222 10.8375H8.78817C8.70097 8.79737 8.95039 7.17303 8.95039 7.17303C9.17547 5.60477 8.66853 5.00918 8.119 4.81774C6.86786 4.38071 6.25749 5.36498 5.95941 5.96251C5.11585 7.64873 4.89077 9.71783 4.93133 11.5878L4.91916 11.5742C5.0023 9.78164 4.0026 8.48023 2.55882 9.36589C1.11504 10.2535 1.47802 13.5292 1.72135 15.5055C1.87952 16.7798 2.09041 18.0213 1.86735 19.5122C1.02379 28.0864 6.85366 29.2872 10.7429 29.3433C15.5447 29.5464 18.1322 27.0886 19.0974 24.8745C20.2613 22.202 20.4074 20.7382 20.9893 19.6147C21.7355 18.1702 22.279 17.9904 22.3256 16.9635C22.3723 15.9367 21.3766 15.4146 20.4641 15.4688L20.458 15.4707Z" fill="#FFE165"/>
<path d="M22.3819 15.4845C21.8952 15.0301 21.1632 14.7884 20.419 14.8309C19.2266 14.9025 18.3811 15.3182 17.0813 17.3487C17.0468 15.0262 17.1826 11.5397 17.9816 8.47281C18.2817 7.3203 17.9796 6.56808 17.6713 6.14072C17.3124 5.64182 16.7548 5.31308 16.1383 5.2396C15.5766 5.17192 14.8426 5.16805 14.1268 5.72884C14.1268 5.7211 14.1288 5.71143 14.1288 5.71143C14.36 4.06389 13.7638 3.12023 12.3586 2.90751L12.2815 2.89978C11.4156 2.86304 10.6735 3.13376 10.0753 3.70228C9.75488 4.00588 9.47707 4.39843 9.23577 4.88379C8.96607 4.50672 8.61932 4.31527 8.34557 4.21859C6.67265 3.63267 5.74799 4.88766 5.34649 5.68823C4.8801 6.62029 4.59012 7.66451 4.4279 8.73C4.39343 8.70873 4.36098 8.68746 4.32651 8.66812C3.95746 8.46508 3.18893 8.21756 2.19126 8.83055C0.500091 9.8709 0.715036 12.8605 1.05165 15.5832C1.0699 15.7282 1.08815 15.8713 1.1064 16.0163C1.25037 17.1321 1.38623 18.186 1.19968 19.4255L1.19562 19.4564C0.85698 22.8966 1.53629 25.5438 3.21529 27.3287C4.8294 29.0458 7.35804 29.9392 10.71 29.9876C10.9553 29.9972 11.1946 30.0011 11.4278 29.9992C17.1543 29.9489 19.2084 26.2845 19.7133 25.1242C20.3663 23.6236 20.7049 22.504 20.9746 21.6029C21.1835 20.9067 21.3497 20.3576 21.585 19.9012C21.8526 19.383 22.0878 19.0465 22.2947 18.7487C22.6475 18.2421 22.9517 17.805 22.9882 16.9929C23.0145 16.405 22.8036 15.8829 22.3758 15.4845H22.3819ZM11.0263 4.61114C11.3487 4.30561 11.7198 4.17024 12.1902 4.17991C12.5978 4.24373 12.9669 4.33848 12.7986 5.5374C12.7864 5.61281 12.5228 7.41312 12.4518 8.87889C12.4518 8.88856 12.4518 8.89823 12.4518 8.9079C12.0807 10.3389 11.7705 12.4002 11.6042 15.413C10.8844 15.4555 10.1665 15.529 9.46896 15.6257C9.24388 9.51316 9.76502 5.80619 11.0243 4.61114H11.0263ZM6.56315 6.24128C7.06807 5.23573 7.49188 5.28601 7.88527 5.42331C8.43074 5.61475 8.34557 6.65316 8.28271 7.08439C8.27257 7.154 8.02924 8.77254 8.11441 10.832C8.05155 12.2765 8.05966 13.9414 8.13468 15.8462C7.46754 15.9718 6.83488 16.1169 6.25696 16.2735C5.98321 15.3956 4.77262 9.81869 6.56315 6.24321V6.24128ZM21.1794 18.039C20.9604 18.3523 20.6887 18.7429 20.3825 19.3346C20.0925 19.8935 19.9141 20.4929 19.6849 21.249C19.4233 22.1173 19.0969 23.1982 18.4743 24.6311C18.0323 25.6444 16.1748 28.9356 10.7505 28.7036C7.7271 28.661 5.58982 27.9301 4.21701 26.4701C2.80162 24.9657 2.23587 22.649 2.53395 19.5879C2.74079 18.1879 2.5887 17.0025 2.44068 15.8578C2.42243 15.7147 2.40418 15.5735 2.38593 15.4304C2.2237 14.1097 1.78976 10.5999 2.91923 9.90571C3.2234 9.71814 3.47282 9.6756 3.65735 9.77615C3.97165 9.94825 4.28798 10.5748 4.24337 11.5455C4.24135 11.5977 4.24743 11.648 4.25757 11.6983C4.31435 13.9608 4.73815 15.9293 4.97946 16.668C4.58404 16.8092 4.23526 16.9561 3.94326 17.1031C3.61476 17.2694 3.49107 17.6561 3.66546 17.9694C3.78712 18.1879 4.02235 18.3117 4.26568 18.3097C4.3691 18.3097 4.47454 18.2846 4.5739 18.2343C6.21438 17.4047 10.1057 16.5616 13.5347 16.6525C13.9078 16.6583 14.214 16.3837 14.2241 16.0299C14.2342 15.676 13.9422 15.3821 13.5712 15.3724C13.3664 15.3666 13.1595 15.3666 12.9527 15.3666C13.2954 9.29078 14.2383 7.3087 14.9724 6.72278C15.2765 6.48106 15.5665 6.46172 15.968 6.51007C16.0795 6.5236 16.3594 6.58548 16.5601 6.86394C16.7771 7.16754 16.8176 7.61616 16.6757 8.16148C15.4347 12.9204 15.7145 18.5166 15.8565 19.8741C15.8321 19.9205 15.8098 19.9669 15.7835 20.0153C15.4935 20.5355 14.9541 21.0769 14.3113 21.0402C13.9443 21.0228 13.6219 21.2896 13.5996 21.6416C13.5772 21.9954 13.8591 22.299 14.2302 22.3203C15.3171 22.3822 16.3411 21.746 16.9697 20.6186C17.0366 20.4987 17.0934 20.3846 17.1441 20.2744C17.1482 20.2667 17.1522 20.257 17.1563 20.2493C17.2739 19.9979 17.3591 19.7678 17.4341 19.5609C17.5517 19.2399 17.6531 18.9614 17.8559 18.6172C19.2956 16.1846 19.8796 16.1497 20.4981 16.113C20.861 16.0917 21.222 16.202 21.4349 16.4031C21.587 16.5442 21.6539 16.7202 21.6438 16.9406C21.6235 17.3951 21.4917 17.5846 21.1733 18.0409L21.1794 18.039Z" fill="#0D0F11"/>
<path d="M46.2793 19.0284C46.0704 17.7928 46.186 16.7369 46.3077 15.6193C46.3239 15.4742 46.3401 15.3311 46.3543 15.1861C46.6382 12.4595 46.7964 9.46417 45.0829 8.45476C44.073 7.85916 43.3086 8.12022 42.9436 8.32906C42.9091 8.3484 42.8766 8.3716 42.8422 8.39288C42.6576 7.33125 42.3494 6.29284 41.8648 5.36851C41.4491 4.57568 40.5021 3.33615 38.8393 3.95108C38.5676 4.05164 38.2269 4.24888 37.9633 4.63176C37.7119 4.15026 37.426 3.76351 37.0995 3.46571C36.4912 2.9088 35.7429 2.64968 34.8791 2.70189L34.802 2.70962C33.4008 2.94747 32.8229 3.9008 33.0865 5.54835C33.0865 5.54835 33.0865 5.55608 33.0885 5.56188C32.3626 5.0127 31.6285 5.03011 31.0689 5.10746C30.4545 5.19254 29.9029 5.53094 29.5541 6.03565C29.256 6.46881 28.9661 7.2249 29.2885 8.3716C30.1483 11.425 30.351 14.9096 30.3612 17.232C29.0228 15.2248 28.1692 14.8245 26.9768 14.7742C26.2346 14.7433 25.5026 15.0005 25.0261 15.4626C24.6063 15.8687 24.4056 16.3947 24.4441 16.9806C24.4968 17.7908 24.8091 18.224 25.1721 18.7229C25.385 19.0168 25.6263 19.3494 25.9041 19.8619C26.1495 20.3144 26.3259 20.8597 26.549 21.552C26.8369 22.4473 27.1958 23.5611 27.8792 25.0501C28.4064 26.2007 30.5315 29.8303 36.2417 29.7781C36.4729 29.7761 36.7122 29.7684 36.9555 29.7529C40.3257 29.6466 42.8361 28.7068 44.4178 26.9625C46.0603 25.1487 46.6889 22.4898 46.2853 19.0555L46.2813 19.0246L46.2793 19.0284ZM38.961 6.82075C38.89 6.38372 38.7826 5.34724 39.326 5.14806C39.7153 5.00303 40.1412 4.94696 40.6643 5.94283C42.5238 9.48737 41.4227 15.0855 41.1652 15.9673C40.5832 15.8204 39.9485 15.6869 39.2794 15.5728C39.3159 13.6681 39.2915 12.0012 39.2023 10.5587C39.2469 8.49923 38.9732 6.88456 38.961 6.82075ZM34.9967 3.98009C35.4692 3.96075 35.8423 4.09031 36.1687 4.39197C37.4503 5.56575 38.0444 9.26112 37.937 15.3775C37.2374 15.2924 36.5196 15.2325 35.7977 15.2016C35.5746 12.1907 35.2238 10.1371 34.8243 8.71194C34.8243 8.70227 34.8243 8.69261 34.8243 8.68294C34.725 7.21716 34.4249 5.42266 34.4127 5.35304C34.22 4.15219 34.5871 4.05164 34.9947 3.98009H34.9967ZM44.9511 19.2179C45.308 22.2732 44.7868 24.5995 43.4018 26.1291C42.0574 27.6123 39.9343 28.3819 36.8927 28.4786C31.4988 28.8035 29.5724 25.5471 29.1121 24.5415C28.4591 23.1183 28.1124 22.0451 27.8346 21.1807C27.5912 20.4265 27.4006 19.8329 27.1005 19.2779C26.7842 18.692 26.5043 18.3071 26.2793 17.9977C25.9528 17.5472 25.8169 17.3596 25.7865 16.9052C25.7723 16.6847 25.8372 16.5068 25.9852 16.3637C26.1961 16.1588 26.553 16.0408 26.918 16.0582C27.5365 16.0853 28.1205 16.1085 29.6089 18.516C29.8198 18.8563 29.9252 19.1328 30.0489 19.4519C30.13 19.6588 30.2192 19.8889 30.3429 20.1384C30.347 20.1461 30.349 20.1539 30.3531 20.1597C30.4078 20.2699 30.4666 20.382 30.5356 20.5019C31.1865 21.6177 32.2227 22.2365 33.3075 22.1553C33.6766 22.1282 33.9544 21.8188 33.926 21.4669C33.8976 21.1149 33.5752 20.8539 33.2041 20.8771C32.5613 20.9235 32.0118 20.3917 31.7117 19.8773C31.6833 19.829 31.661 19.7845 31.6367 19.7381C31.7522 18.3806 31.9246 12.7786 30.5903 8.04287C30.4362 7.49949 30.4687 7.05086 30.6795 6.7434C30.8762 6.46107 31.154 6.39339 31.2656 6.37792C31.665 6.32184 31.957 6.33731 32.2653 6.57323C33.0115 7.14755 33.9929 9.11223 34.4512 15.1803C34.2444 15.1822 34.0376 15.188 33.8348 15.1977C33.4637 15.2132 33.1778 15.5129 33.194 15.8668C33.2102 16.2206 33.5184 16.4875 33.8956 16.4778C37.3205 16.327 41.2301 17.1005 42.8848 17.903C42.9841 17.9513 43.0896 17.9726 43.195 17.9726C43.4383 17.9707 43.6715 17.843 43.7891 17.6207C43.9575 17.3055 43.8257 16.9187 43.4931 16.7582C43.1991 16.6151 42.8462 16.4759 42.4488 16.3405C42.6759 15.598 43.0632 13.6217 43.0754 11.3592C43.0855 11.309 43.0896 11.2587 43.0855 11.2065C43.0206 10.2377 43.3268 9.60533 43.6371 9.42742C43.8196 9.323 44.069 9.36168 44.3772 9.54345C45.5209 10.2183 45.1559 13.7339 45.018 15.0585C45.0038 15.2016 44.9876 15.3427 44.9713 15.4858C44.8456 16.6345 44.7158 17.8198 44.9511 19.2179Z" fill="#0D0F11"/>
<path d="M26.1508 6.85319C26.0434 6.85319 25.9339 6.83386 25.8304 6.78745C25.4512 6.62114 25.285 6.19379 25.4594 5.83218C26.0231 4.6584 26.8484 3.57551 27.844 2.70146C28.1502 2.43267 28.6288 2.45007 28.9106 2.744C29.1925 3.036 29.1742 3.49236 28.866 3.76115C28.0164 4.50757 27.3127 5.4319 26.8301 6.43357C26.7044 6.69463 26.4347 6.85126 26.1508 6.85319Z" fill="#F9F7F2"/>
<path d="M23.608 6.43744C23.2166 6.44131 22.8821 6.15511 22.8496 5.7761C22.7056 4.08021 22.6996 2.36112 22.8354 0.665235C22.8679 0.268818 23.2308 -0.0270433 23.6445 0.0019628C24.0602 0.0329026 24.3704 0.377108 24.34 0.773524C24.2103 2.394 24.2163 4.03767 24.3542 5.65814C24.3887 6.05456 24.0784 6.40263 23.6628 6.43357C23.6445 6.43357 23.6263 6.4355 23.608 6.4355V6.43744Z" fill="#F9F7F2"/>
<path d="M21.0084 6.88414C20.6697 6.888 20.3575 6.66949 20.2703 6.34269C19.9499 5.14377 19.3436 4.0048 18.5183 3.05147C18.2526 2.74401 18.2993 2.29151 18.6197 2.03819C18.9421 1.78487 19.4166 1.82935 19.6822 2.13488C20.6474 3.25258 21.3572 4.58492 21.7303 5.98688C21.8337 6.3717 21.5883 6.76425 21.1848 6.86287C21.124 6.87834 21.0652 6.88414 21.0043 6.88607L21.0084 6.88414Z" fill="#F9F7F2"/>
</g>
<defs>
<clipPath id="clip0_10905_18559">
<rect width="45.7143" height="30" fill="white" transform="translate(0.818359)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,5 +1,4 @@
import { useTranslation } from "react-i18next";
import { usePostHog } from "posthog-js/react";
import {
BaseModalTitle,
BaseModalDescription,
@@ -18,7 +17,6 @@ interface AnalyticsConsentFormModalProps {
export function AnalyticsConsentFormModal({
onClose,
}: AnalyticsConsentFormModalProps) {
const posthog = usePostHog();
const { t } = useTranslation();
const { mutate: saveUserSettings } = useSaveSettings();
@@ -31,7 +29,7 @@ export function AnalyticsConsentFormModal({
{ user_consents_to_analytics: analytics },
{
onSuccess: () => {
handleCaptureConsent(posthog, analytics);
handleCaptureConsent(analytics);
onClose();
},
},

View File

@@ -1,202 +0,0 @@
import React, { useMemo, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Typography } from "#/ui/typography";
import { I18nKey } from "#/i18n/declaration";
import CodeTagIcon from "#/icons/code-tag.svg?react";
import ChevronDownSmallIcon from "#/icons/chevron-down-small.svg?react";
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
import { useConversationStore } from "#/state/conversation-store";
import { ChangeAgentContextMenu } from "./change-agent-context-menu";
import { cn } from "#/utils/utils";
import { USE_PLANNING_AGENT } from "#/utils/feature-flags";
import { useAgentState } from "#/hooks/use-agent-state";
import { AgentState } from "#/types/agent-state";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
import { useUnifiedWebSocketStatus } from "#/hooks/use-unified-websocket-status";
import { useSubConversationTaskPolling } from "#/hooks/query/use-sub-conversation-task-polling";
export function ChangeAgentButton() {
const [contextMenuOpen, setContextMenuOpen] = useState<boolean>(false);
const {
conversationMode,
setConversationMode,
setSubConversationTaskId,
subConversationTaskId,
} = useConversationStore();
const webSocketStatus = useUnifiedWebSocketStatus();
const isWebSocketConnected = webSocketStatus === "CONNECTED";
const shouldUsePlanningAgent = USE_PLANNING_AGENT();
const { curAgentState } = useAgentState();
const { t } = useTranslation();
const isAgentRunning = curAgentState === AgentState.RUNNING;
const { data: conversation } = useActiveConversation();
const { mutate: createConversation, isPending: isCreatingConversation } =
useCreateConversation();
// Poll sub-conversation task and invalidate parent conversation when ready
useSubConversationTaskPolling(
subConversationTaskId,
conversation?.conversation_id || null,
);
// Close context menu when agent starts running
useEffect(() => {
if ((isAgentRunning || !isWebSocketConnected) && contextMenuOpen) {
setContextMenuOpen(false);
}
}, [isAgentRunning, contextMenuOpen, isWebSocketConnected]);
const handlePlanClick = (
event: React.MouseEvent<HTMLButtonElement> | KeyboardEvent,
) => {
event.preventDefault();
event.stopPropagation();
// Set conversation mode to "plan" immediately
setConversationMode("plan");
// Check if sub_conversation_ids is not empty
if (
(conversation?.sub_conversation_ids &&
conversation.sub_conversation_ids.length > 0) ||
!conversation?.conversation_id
) {
// Do nothing if both conditions are true
return;
}
// Create a new sub-conversation if we have a current conversation ID
createConversation(
{
parentConversationId: conversation.conversation_id,
agentType: "plan",
},
{
onSuccess: (data) => {
displaySuccessToast(
t(I18nKey.PLANNING_AGENTT$PLANNING_AGENT_INITIALIZED),
);
// Track the task ID to poll for sub-conversation creation
if (data.v1_task_id) {
setSubConversationTaskId(data.v1_task_id);
}
},
},
);
};
const isButtonDisabled =
isAgentRunning ||
isCreatingConversation ||
!isWebSocketConnected ||
!shouldUsePlanningAgent;
// Handle Shift + Tab keyboard shortcut to cycle through modes
useEffect(() => {
if (isButtonDisabled) {
return undefined;
}
const handleKeyDown = (event: KeyboardEvent) => {
// Check for Shift + Tab combination
if (event.shiftKey && event.key === "Tab") {
// Prevent default tab navigation behavior
event.preventDefault();
event.stopPropagation();
// Cycle between modes: code -> plan -> code
const nextMode = conversationMode === "code" ? "plan" : "code";
if (nextMode === "plan") {
handlePlanClick(event);
} else {
setConversationMode(nextMode);
}
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [
isButtonDisabled,
conversationMode,
setConversationMode,
handlePlanClick,
]);
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
setContextMenuOpen(!contextMenuOpen);
};
const handleCodeClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
setConversationMode("code");
};
const isExecutionAgent = conversationMode === "code";
const buttonLabel = useMemo(() => {
if (isExecutionAgent) {
return t(I18nKey.COMMON$CODE);
}
return t(I18nKey.COMMON$PLAN);
}, [isExecutionAgent, t]);
const buttonIcon = useMemo(() => {
if (isExecutionAgent) {
return <CodeTagIcon width={18} height={18} color="#737373" />;
}
return <LessonPlanIcon width={18} height={18} color="#ffffff" />;
}, [isExecutionAgent]);
if (!shouldUsePlanningAgent) {
return null;
}
return (
<div className="relative">
<button
type="button"
onClick={handleButtonClick}
disabled={isButtonDisabled}
className={cn(
"flex items-center border border-[#4B505F] rounded-[100px] transition-opacity",
!isExecutionAgent && "border-[#597FF4] bg-[#4A67BD]",
isButtonDisabled
? "opacity-50 cursor-not-allowed"
: "cursor-pointer hover:opacity-80",
)}
>
<div className="flex items-center gap-1 pl-1.5">
{buttonIcon}
<Typography.Text className="text-white text-2.75 not-italic font-normal leading-5">
{buttonLabel}
</Typography.Text>
</div>
<ChevronDownSmallIcon width={24} height={24} color="#ffffff" />
</button>
{contextMenuOpen && (
<ChangeAgentContextMenu
onClose={() => setContextMenuOpen(false)}
onCodeClick={handleCodeClick}
onPlanClick={handlePlanClick}
/>
)}
</div>
);
}

View File

@@ -1,76 +0,0 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import CodeTagIcon from "#/icons/code-tag.svg?react";
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
import { ContextMenu } from "#/ui/context-menu";
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
import { ContextMenuIconTextWithDescription } from "../context-menu/context-menu-icon-text-with-description";
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
import { cn } from "#/utils/utils";
const contextMenuListItemClassName = cn(
"cursor-pointer p-0 h-auto hover:bg-transparent",
);
interface ChangeAgentContextMenuProps {
onClose: () => void;
onCodeClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onPlanClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
export function ChangeAgentContextMenu({
onClose,
onCodeClick,
onPlanClick,
}: ChangeAgentContextMenuProps) {
const { t } = useTranslation();
const menuRef = useClickOutsideElement<HTMLUListElement>(onClose);
const handleCodeClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
onCodeClick?.(event);
onClose();
};
const handlePlanClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
onPlanClick?.(event);
onClose();
};
return (
<ContextMenu
ref={menuRef}
testId="change-agent-context-menu"
position="top"
alignment="left"
className="min-h-fit mb-2 min-w-[195px] max-w-[195px] gap-0"
>
<ContextMenuListItem
testId="code-option"
onClick={handleCodeClick}
className={contextMenuListItemClassName}
>
<ContextMenuIconTextWithDescription
icon={CodeTagIcon}
title={t(I18nKey.COMMON$CODE)}
description={t(I18nKey.COMMON$CODE_AGENT_DESCRIPTION)}
/>
</ContextMenuListItem>
<ContextMenuListItem
testId="plan-option"
onClick={handlePlanClick}
className={contextMenuListItemClassName}
>
<ContextMenuIconTextWithDescription
icon={LessonPlanIcon}
title={t(I18nKey.COMMON$PLAN)}
description={t(I18nKey.COMMON$PLAN_AGENT_DESCRIPTION)}
/>
</ContextMenuListItem>
</ContextMenu>
);
}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { usePostHog } from "posthog-js/react";
import posthog from "posthog-js";
import { useParams } from "react-router";
import { useTranslation } from "react-i18next";
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
@@ -60,7 +60,6 @@ function getEntryPoint(
}
export function ChatInterface() {
const posthog = usePostHog();
const { setMessageToSend } = useConversationStore();
const { data: conversation } = useActiveConversation();
const { errorMessage } = useErrorMessageStore();
@@ -98,29 +97,24 @@ export function ChatInterface() {
const isV1Conversation = conversation?.conversation_version === "V1";
// Track when we should show V1 messages (after DOM has rendered)
const [showV1Messages, setShowV1Messages] = React.useState(false);
const prevV1LoadingRef = React.useRef(
// Instantly scroll to bottom when history loading completes
const prevLoadingHistoryRef = React.useRef(
conversationWebSocket?.isLoadingHistory,
);
// Wait for DOM to render before showing V1 messages
React.useEffect(() => {
const wasLoading = prevV1LoadingRef.current;
const wasLoading = prevLoadingHistoryRef.current;
const isLoading = conversationWebSocket?.isLoadingHistory;
if (wasLoading && !isLoading) {
// Loading just finished - wait for next frame to ensure DOM is ready
requestAnimationFrame(() => {
setShowV1Messages(true);
// When history loading transitions from true to false, instantly scroll to bottom
if (wasLoading && !isLoading && scrollRef.current) {
scrollRef.current.scrollTo({
top: scrollRef.current.scrollHeight,
behavior: "instant",
});
} else if (isLoading) {
// Reset when loading starts
setShowV1Messages(false);
}
prevV1LoadingRef.current = isLoading;
}, [conversationWebSocket?.isLoadingHistory]);
prevLoadingHistoryRef.current = isLoading;
}, [conversationWebSocket?.isLoadingHistory, scrollRef]);
// Filter V0 events
const v0Events = storeEvents
@@ -258,7 +252,7 @@ export function ChatInterface() {
</div>
)}
{(conversationWebSocket?.isLoadingHistory || !showV1Messages) &&
{conversationWebSocket?.isLoadingHistory &&
isV1Conversation &&
!isTask && (
<div className="flex justify-center">
@@ -275,7 +269,7 @@ export function ChatInterface() {
/>
)}
{showV1Messages && v1UserEventsExist && (
{!conversationWebSocket?.isLoadingHistory && v1UserEventsExist && (
<V1Messages messages={v1UiEvents} allEvents={v1FullEvents} />
)}
</div>

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