Compare commits

..

2 Commits

Author SHA1 Message Date
openhands 2a001491c3 Fix: Add init-db.sh to create additional databases for Keycloak
Keycloak requires its own database. This script creates the keycloak
and litellm databases during PostgreSQL initialization.

Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-15 06:16:59 +00:00
openhands 11c11e633a Add local development setup for enterprise server
This adds Docker Compose configuration and documentation for running
OpenHands Enterprise locally with all required services:

- docker-compose.local.yml: Orchestrates PostgreSQL, Redis, Keycloak, and LiteLLM
- litellm-config.yaml: LiteLLM proxy configuration with model definitions
- .env.example: Example environment variables for local development
- LOCAL_DEV_SETUP.md: Comprehensive setup documentation
- setup-local.sh: Interactive setup helper script

Co-authored-by: openhands <openhands@all-hands.dev>
2026-01-15 06:06:18 +00:00
159 changed files with 1292 additions and 876 deletions
+118
View File
@@ -0,0 +1,118 @@
# ============================================
# OpenHands Enterprise Local Development
# Environment Variables Example
# ============================================
# Copy this to .env and fill in your values
# ============================================
# Database Configuration
# ============================================
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASS=postgres
DB_NAME=openhands
# ============================================
# Redis Configuration
# ============================================
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
# ============================================
# Keycloak Configuration
# ============================================
# For local development with the docker-compose setup
KEYCLOAK_SERVER_URL=http://localhost:8080
KEYCLOAK_SERVER_URL_EXT=http://localhost:8080
KEYCLOAK_REALM_NAME=openhands
KEYCLOAK_CLIENT_ID=openhands-client
KEYCLOAK_CLIENT_SECRET=your-client-secret
KEYCLOAK_PROVIDER_NAME=github
KEYCLOAK_ADMIN_PASSWORD=admin
# ============================================
# LiteLLM Configuration
# ============================================
LITE_LLM_API_URL=http://localhost:4000
LITE_LLM_API_KEY=sk-local-dev-master-key
LITELLM_DEFAULT_MODEL=litellm_proxy/claude-sonnet-4-20250514
# ============================================
# OpenHands Configuration
# ============================================
OPENHANDS_CONFIG_CLS=server.config.SaaSServerConfig
OPENHANDS_GITHUB_SERVICE_CLS=integrations.github.github_service.SaaSGitHubService
OPENHANDS_GITLAB_SERVICE_CLS=integrations.gitlab.gitlab_service.SaaSGitLabService
OPENHANDS_BITBUCKET_SERVICE_CLS=integrations.bitbucket.bitbucket_service.SaaSBitBucketService
OPENHANDS_CONVERSATION_VALIDATOR_CLS=storage.saas_conversation_validator.SaasConversationValidator
# ============================================
# GitHub App Configuration (Optional)
# Required for GitHub OAuth and GitHub integration features
# Create a GitHub App at: https://github.com/settings/apps
# ============================================
# GITHUB_APP_CLIENT_ID=
# GITHUB_APP_CLIENT_SECRET=
# GITHUB_APP_WEBHOOK_SECRET=
# GITHUB_APP_PRIVATE_KEY=
# ============================================
# GitLab App Configuration (Optional)
# Required for GitLab OAuth and integration features
# ============================================
# GITLAB_APP_CLIENT_ID=
# GITLAB_APP_CLIENT_SECRET=
# ============================================
# Bitbucket App Configuration (Optional)
# ============================================
# BITBUCKET_APP_CLIENT_ID=
# BITBUCKET_APP_CLIENT_SECRET=
# ============================================
# Feature Flags
# ============================================
ENABLE_BILLING=false
HIDE_LLM_SETTINGS=false
ENABLE_JIRA=false
ENABLE_JIRA_DC=false
ENABLE_LINEAR=false
LOCAL_DEPLOYMENT=true
# ============================================
# Frontend Configuration
# ============================================
# Path to the frontend build directory
# FRONTEND_DIRECTORY=../frontend/build
# ============================================
# Logging
# ============================================
LOG_PLAIN_TEXT=1
# ============================================
# Runtime Configuration
# ============================================
SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/openhands/runtime:main-nikolaik
# ============================================
# PostHog (Analytics - can use dummy for local)
# ============================================
POSTHOG_CLIENT_KEY=test
# ============================================
# LLM API Keys (for LiteLLM proxy)
# Add these to litellm container environment
# ============================================
# OPENAI_API_KEY=
# ANTHROPIC_API_KEY=
# GOOGLE_API_KEY=
# ============================================
# CORS Configuration
# ============================================
WEB_HOST=localhost:3000
PERMITTED_CORS_ORIGINS=http://localhost:3000,http://localhost:3001
+323
View File
@@ -0,0 +1,323 @@
# OpenHands Enterprise Local Development Setup
This guide provides instructions for setting up OpenHands Enterprise locally for development.
## Prerequisites
- **Docker** and **Docker Compose** (v2+)
- **Python 3.12+**
- **Poetry** (Python package manager)
- **Node.js 18+** and **npm** (for frontend)
- An LLM API key (OpenAI, Anthropic, or other supported provider)
## Quick Start
### 1. Start Infrastructure Services
Start all required services using Docker Compose:
```bash
cd enterprise
# Start PostgreSQL, Redis, Keycloak, and LiteLLM
docker-compose -f docker-compose.local.yml up -d
```
This will start:
- **PostgreSQL** on port `5432` (database for OpenHands and Keycloak)
- **Redis** on port `6379` (caching and pub/sub)
- **Keycloak** on port `8080` (authentication)
- **LiteLLM** on port `4000` (LLM proxy)
### 2. Configure Environment Variables
Copy the example environment file and configure it:
```bash
cp .env.example .env
```
Edit `.env` and set your LLM API keys. For LiteLLM to work, you need at least one of:
- `OPENAI_API_KEY` - for GPT models
- `ANTHROPIC_API_KEY` - for Claude models
Update the docker-compose to include your API keys:
```bash
# Add to litellm service environment in docker-compose.local.yml
# Or create a .env file for docker-compose
# Then restart litellm
docker-compose -f docker-compose.local.yml up -d litellm
```
### 3. Initialize the Database
Run Alembic migrations to set up the database schema:
```bash
cd enterprise
poetry install
poetry run alembic upgrade head
```
### 4. Configure Keycloak (Authentication)
#### Option A: Skip Keycloak (Simplified Setup)
For basic local development without authentication, you can skip Keycloak by not setting `GITHUB_APP_CLIENT_ID` and related variables. The app will still work but OAuth login won't be available.
#### Option B: Set Up Keycloak Realm
1. Access Keycloak Admin Console at `http://localhost:8080`
2. Login with `admin` / `admin`
3. Create a new realm called `openhands`
4. Create a client:
- Client ID: `openhands-client`
- Client authentication: ON
- Valid redirect URIs: `http://localhost:3000/*`, `http://localhost:3001/*`
5. Copy the client secret and update your `.env`:
```
KEYCLOAK_CLIENT_SECRET=<your-client-secret>
```
### 5. Build the Frontend
```bash
# From the repository root (not enterprise/)
cd ..
make build-frontend
# OR
cd frontend && npm install && npm run build
```
### 6. Start the Enterprise Backend
```bash
cd enterprise
# Option A: Using Make
OPENHANDS_PATH=../ make start-backend
# Option B: Using Poetry directly
FRONTEND_DIRECTORY=../frontend/build poetry run uvicorn saas_server:app --host 127.0.0.1 --port 3000 --reload
```
### 7. Access the Application
- **Frontend**: http://localhost:3000 (or 3001 if using dev server)
- **Backend API**: http://localhost:3000/api
- **Keycloak Admin**: http://localhost:8080
- **LiteLLM Proxy**: http://localhost:4000
## Individual Service Setup
If you prefer to run services individually:
### PostgreSQL
```bash
docker run -d \
--name openhands-postgres \
-p 5432:5432 \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=openhands \
postgres:15
```
### Redis
```bash
docker run -d \
--name openhands-redis \
-p 6379:6379 \
redis:7-alpine
```
### Keycloak
```bash
docker run -d \
--name openhands-keycloak \
-p 8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
-e KC_HTTP_ENABLED=true \
-e KC_HOSTNAME_STRICT=false \
quay.io/keycloak/keycloak:26.1.1 start-dev
```
### LiteLLM
```bash
# Create litellm-config.yaml first (see file in this directory)
docker run -d \
--name openhands-litellm \
-p 4000:4000 \
-v $(pwd)/litellm-config.yaml:/app/config.yaml \
-e LITELLM_MASTER_KEY=sk-local-dev-master-key \
-e OPENAI_API_KEY=your-openai-key \
-e ANTHROPIC_API_KEY=your-anthropic-key \
ghcr.io/berriai/litellm:main-latest \
--config /app/config.yaml --port 4000 --host 0.0.0.0
```
## Configuration Details
### Environment Variables Reference
| Variable | Description | Default |
|----------|-------------|---------|
| `DB_HOST` | PostgreSQL host | `localhost` |
| `DB_PORT` | PostgreSQL port | `5432` |
| `DB_USER` | PostgreSQL user | `postgres` |
| `DB_PASS` | PostgreSQL password | `postgres` |
| `DB_NAME` | PostgreSQL database name | `openhands` |
| `REDIS_HOST` | Redis host | `localhost` |
| `REDIS_PORT` | Redis port | `6379` |
| `KEYCLOAK_SERVER_URL` | Keycloak internal URL | - |
| `KEYCLOAK_SERVER_URL_EXT` | Keycloak external URL | - |
| `KEYCLOAK_REALM_NAME` | Keycloak realm | - |
| `KEYCLOAK_CLIENT_ID` | Keycloak client ID | - |
| `KEYCLOAK_CLIENT_SECRET` | Keycloak client secret | - |
| `LITE_LLM_API_URL` | LiteLLM proxy URL | `http://localhost:4000` |
| `LITE_LLM_API_KEY` | LiteLLM master key | - |
| `LITELLM_DEFAULT_MODEL` | Default model to use | `litellm_proxy/claude-sonnet-4-20250514` |
| `OPENHANDS_CONFIG_CLS` | Server config class | `server.config.SaaSServerConfig` |
| `LOCAL_DEPLOYMENT` | Flag for local mode | `true` |
### LiteLLM Configuration
The `litellm-config.yaml` file defines which LLM models are available through the proxy. You can add or modify models as needed.
To test if LiteLLM is working:
```bash
curl http://localhost:4000/v1/models
```
### Keycloak Realm Setup for GitHub OAuth
To enable GitHub OAuth through Keycloak:
1. Create a GitHub OAuth App at https://github.com/settings/developers
2. In Keycloak:
- Go to Identity Providers > Add provider > GitHub
- Enter your GitHub Client ID and Secret
- Set redirect URI to match your Keycloak URL
## Troubleshooting
### Database Connection Issues
```bash
# Check if PostgreSQL is running
docker ps | grep postgres
# Check logs
docker logs openhands-postgres
# Test connection
psql -h localhost -U postgres -d openhands
```
### Redis Connection Issues
```bash
# Check if Redis is running
docker ps | grep redis
# Test connection
redis-cli -h localhost ping
```
### Keycloak Issues
```bash
# Check logs
docker logs openhands-keycloak
# Access admin console
open http://localhost:8080/admin
```
### LiteLLM Issues
```bash
# Check logs
docker logs openhands-litellm
# Check health
curl http://localhost:4000/health
# List available models
curl http://localhost:4000/v1/models
```
### Common Issues
1. **"Connection refused" errors**: Ensure all Docker containers are running and ports are not blocked.
2. **Authentication failures**: Check Keycloak configuration and client credentials.
3. **LLM errors**: Verify your API keys are correctly set in the LiteLLM environment.
4. **Database migration errors**: Ensure PostgreSQL is running before running migrations.
## Stopping Services
```bash
# Stop all services
docker-compose -f docker-compose.local.yml down
# Stop and remove volumes (clears all data)
docker-compose -f docker-compose.local.yml down -v
```
## Development Tips
1. **Hot reloading**: Use `--reload` flag with uvicorn for automatic server restarts.
2. **Frontend development**: Run the frontend dev server separately for faster iteration:
```bash
cd ../frontend && npm run dev
```
3. **Database changes**: After modifying models, create a new migration:
```bash
poetry run alembic revision --autogenerate -m "description"
poetry run alembic upgrade head
```
4. **Debugging**: Set `LOG_PLAIN_TEXT=1` for readable logs.
## Minimal Setup (Without Authentication)
For the simplest possible setup without OAuth:
```bash
# Start only PostgreSQL and Redis
docker run -d --name openhands-postgres -p 5432:5432 \
-e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=openhands postgres:15
docker run -d --name openhands-redis -p 6379:6379 redis:7-alpine
# Create minimal .env
cat > .env << 'EOF'
DB_HOST=localhost
REDIS_HOST=localhost
OPENHANDS_CONFIG_CLS=server.config.SaaSServerConfig
LOCAL_DEPLOYMENT=true
POSTHOG_CLIENT_KEY=test
LOG_PLAIN_TEXT=1
EOF
# Run migrations and start server
poetry install
poetry run alembic upgrade head
FRONTEND_DIRECTORY=../frontend/build poetry run uvicorn saas_server:app --host 0.0.0.0 --port 3000 --reload
```
This provides a working enterprise server without Keycloak authentication or LiteLLM proxy. You can configure LLM directly through the UI or environment variables.
+108
View File
@@ -0,0 +1,108 @@
# Docker Compose for running OpenHands Enterprise locally
# Usage: docker-compose -f docker-compose.local.yml up -d
services:
# PostgreSQL database
postgres:
image: postgres:15
container_name: openhands-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: openhands
# Create additional databases for keycloak and litellm
POSTGRES_MULTIPLE_DATABASES: keycloak,litellm
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
# Redis for caching and pub/sub
redis:
image: redis:7-alpine
container_name: openhands-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
# Keycloak for authentication
keycloak:
image: quay.io/keycloak/keycloak:26.1.1
container_name: openhands-keycloak
environment:
# Admin credentials
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
# Database configuration (using embedded H2 for simplicity)
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: postgres
KC_DB_PASSWORD: postgres
# Features
KC_FEATURES: token-exchange
KC_HEALTH_ENABLED: true
KC_METRICS_ENABLED: true
# HTTP settings (for local dev, disable HTTPS requirement)
KC_HTTP_ENABLED: true
KC_HTTP_PORT: 8080
KC_HOSTNAME_STRICT: false
KC_HOSTNAME_STRICT_HTTPS: false
KC_PROXY_HEADERS: xforwarded
ports:
- "8080:8080"
command: start-dev
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080;echo -e 'GET /health/ready HTTP/1.1\r\nhost: localhost\r\nConnection: close\r\n\r\n' >&3;if [ $? -eq 0 ]; then echo 'Healthcheck Successful';exit 0;else echo 'Healthcheck Failed';exit 1;fi;"]
interval: 10s
timeout: 10s
retries: 10
start_period: 30s
# LiteLLM Proxy for LLM routing
litellm:
image: ghcr.io/berriai/litellm:main-latest
container_name: openhands-litellm
environment:
# Master key for admin access
LITELLM_MASTER_KEY: sk-local-dev-master-key
# Database for tracking spend/keys (using same postgres)
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/litellm
# General settings
LITELLM_LOG: DEBUG
ports:
- "4000:4000"
volumes:
- ./litellm-config.yaml:/app/config.yaml
command: --config /app/config.yaml --port 4000 --host 0.0.0.0 --detailed_debug
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
interval: 10s
timeout: 10s
retries: 5
start_period: 15s
volumes:
postgres_data:
redis_data:
networks:
default:
name: openhands-network
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# Initialize additional databases for OpenHands Enterprise
set -e
# Create keycloak database
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE DATABASE keycloak;
CREATE DATABASE litellm;
GRANT ALL PRIVILEGES ON DATABASE keycloak TO $POSTGRES_USER;
GRANT ALL PRIVILEGES ON DATABASE litellm TO $POSTGRES_USER;
EOSQL
echo "Additional databases created: keycloak, litellm"
+1 -3
View File
@@ -29,9 +29,7 @@ class ResolverUserContext(UserContext):
return UserInfo(id=user_id)
async def get_authenticated_git_url(
self, repository: str, is_optional: bool = False
) -> str:
async def get_authenticated_git_url(self, repository: str) -> str:
# This would need to be implemented based on the git provider tokens
# For now, return a basic HTTPS URL
return f'https://github.com/{repository}.git'
+76
View File
@@ -0,0 +1,76 @@
# LiteLLM Configuration for Local Development
# Documentation: https://docs.litellm.ai/docs/proxy/configs
general_settings:
master_key: sk-local-dev-master-key
database_url: postgresql://postgres:postgres@postgres:5432/litellm
model_list:
# OpenAI Models
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: gpt-4o-mini
litellm_params:
model: openai/gpt-4o-mini
api_key: os.environ/OPENAI_API_KEY
- model_name: gpt-4-turbo
litellm_params:
model: openai/gpt-4-turbo
api_key: os.environ/OPENAI_API_KEY
# Anthropic Claude Models
- model_name: claude-3-5-sonnet-20241022
litellm_params:
model: anthropic/claude-3-5-sonnet-20241022
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: claude-3-7-sonnet-20250219
litellm_params:
model: anthropic/claude-3-7-sonnet-20250219
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: claude-sonnet-4-20250514
litellm_params:
model: anthropic/claude-sonnet-4-20250514
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: claude-opus-4-5-20251101
litellm_params:
model: anthropic/claude-opus-4-5-20251101
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: claude-3-opus-20240229
litellm_params:
model: anthropic/claude-3-opus-20240229
api_key: os.environ/ANTHROPIC_API_KEY
# Google Gemini Models (optional)
- model_name: gemini-1.5-pro
litellm_params:
model: gemini/gemini-1.5-pro
api_key: os.environ/GOOGLE_API_KEY
- model_name: gemini-2.0-flash
litellm_params:
model: gemini/gemini-2.0-flash
api_key: os.environ/GOOGLE_API_KEY
litellm_settings:
# Enable request/response logging
set_verbose: true
# Fallback behavior
drop_params: true
# Default max tokens if not specified
max_tokens: 4096
router_settings:
# Enable retries on failure
num_retries: 2
# Timeout for each request
timeout: 600
# Routing strategy
routing_strategy: simple-shuffle
+62 -63
View File
@@ -1178,7 +1178,7 @@ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\"", dev = "os_name == \"nt\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
markers = {main = "platform_system == \"Windows\" or os_name == \"nt\" or sys_platform == \"win32\"", dev = "os_name == \"nt\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
[[package]]
name = "comm"
@@ -2264,14 +2264,14 @@ files = [
[[package]]
name = "filelock"
version = "3.20.3"
version = "3.19.1"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.10"
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1"},
{file = "filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1"},
{file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"},
{file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"},
]
[[package]]
@@ -5858,14 +5858,14 @@ 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.8.2"
version = "1.8.1"
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.8.2-py3-none-any.whl", hash = "sha256:e9abb2e0fe970715537d0e0fc1aea3dd64bb9e8b531f70cb72b3d4e486aaa46a"},
{file = "openhands_agent_server-1.8.2.tar.gz", hash = "sha256:43db2371ee84b100ac921396338dee74359fceeb5c9400c90530bcc5730144c3"},
{file = "openhands_agent_server-1.8.1-py3-none-any.whl", hash = "sha256:c0dfe620184633a173094ffaa77b0d13124ea7bf84e7b534b1641e5fc5fd0256"},
{file = "openhands_agent_server-1.8.1.tar.gz", hash = "sha256:08adfe26d867ff0cb0c1e87bb0ad6e058c9a97374964ba6a9860ea35d32764a0"},
]
[package.dependencies]
@@ -5891,89 +5891,89 @@ files = []
develop = true
[package.dependencies]
aiohttp = ">=3.9,<3.11.13 || >3.11.13"
aiohttp = ">=3.9.0,!=3.11.13"
anthropic = {version = "*", extras = ["vertex"]}
anyio = "4.9"
asyncpg = ">=0.30"
bashlex = ">=0.18"
anyio = "4.9.0"
asyncpg = "^0.30.0"
bashlex = "^0.18"
boto3 = "*"
browsergym-core = "0.13.3"
deprecated = "*"
deprecation = ">=2.1"
deprecation = "^2.1.0"
dirhash = "*"
docker = "*"
fastapi = "*"
fastmcp = ">=2.12.4"
google-api-python-client = ">=2.164"
fastmcp = "^2.12.4"
google-api-python-client = "^2.164.0"
google-auth-httplib2 = "*"
google-auth-oauthlib = "*"
google-cloud-aiplatform = "*"
google-genai = "*"
html2text = "*"
httpx-aiohttp = ">=0.1.8"
ipywidgets = ">=8.1.5"
jinja2 = ">=3.1.6"
httpx-aiohttp = "^0.1.8"
ipywidgets = "^8.1.5"
jinja2 = "^3.1.6"
joblib = "*"
json-repair = "*"
jupyter-kernel-gateway = "*"
kubernetes = ">=33.1"
jupyter_kernel_gateway = "*"
kubernetes = "^33.1.0"
libtmux = ">=0.46.2"
litellm = ">=1.74.3"
lmnr = ">=0.7.20"
memory-profiler = ">=0.61"
litellm = ">=1.74.3, !=1.64.4, !=1.67.*"
lmnr = "^0.7.20"
memory-profiler = "^0.61.0"
numpy = "*"
openai = "2.8"
openai = "2.8.0"
openhands-aci = "0.3.2"
openhands-agent-server = "1.8.2"
openhands-sdk = "1.8.2"
openhands-tools = "1.8.2"
opentelemetry-api = ">=1.33.1"
opentelemetry-exporter-otlp-proto-grpc = ">=1.33.1"
pathspec = ">=0.12.1"
openhands-agent-server = "1.8.1"
openhands-sdk = "1.8.1"
openhands-tools = "1.8.1"
opentelemetry-api = "^1.33.1"
opentelemetry-exporter-otlp-proto-grpc = "^1.33.1"
pathspec = "^0.12.1"
pexpect = "*"
pg8000 = ">=1.31.5"
pillow = ">=11.3"
playwright = ">=1.55"
poetry = ">=2.1.2"
prompt-toolkit = ">=3.0.50"
protobuf = ">=5,<6"
pg8000 = "^1.31.5"
pillow = "^11.3.0"
playwright = "^1.55.0"
poetry = "^2.1.2"
prompt-toolkit = "^3.0.50"
protobuf = "^5.0.0,<6.0.0"
psutil = "*"
pybase62 = ">=1"
pygithub = ">=2.5"
pyjwt = ">=2.9"
pybase62 = "^1.0.0"
pygithub = "^2.5.0"
pyjwt = "^2.9.0"
pylatexenc = "*"
pypdf = ">=6"
pypdf = "^6.0.0"
python-docx = "*"
python-dotenv = "*"
python-frontmatter = ">=1.1"
python-frontmatter = "^1.1.0"
python-jose = {version = ">=3.3", extras = ["cryptography"]}
python-json-logger = ">=3.2.1"
python-json-logger = "^3.2.1"
python-multipart = "*"
python-pptx = "*"
python-socketio = ">=5.11.4"
python-socketio = "^5.11.4"
pythonnet = "*"
pyyaml = ">=6.0.2"
qtconsole = ">=5.6.1"
rapidfuzz = ">=3.9"
redis = ">=5.2,<7"
requests = ">=2.32.5"
pyyaml = "^6.0.2"
qtconsole = "^5.6.1"
rapidfuzz = "^3.9.0"
redis = ">=5.2,<7.0"
requests = "^2.32.5"
setuptools = ">=78.1.1"
shellingham = ">=1.5.4"
sqlalchemy = {version = ">=2.0.40", extras = ["asyncio"]}
sse-starlette = ">=3.0.2"
starlette = ">=0.48"
tenacity = ">=8.5,<10"
shellingham = "^1.5.4"
sqlalchemy = {version = "^2.0.40", extras = ["asyncio"]}
sse-starlette = "^3.0.2"
starlette = "^0.48.0"
tenacity = ">=8.5,<10.0"
termcolor = "*"
toml = "*"
tornado = ">=6.5"
types-toml = "*"
urllib3 = ">=2.6.3"
urllib3 = "^2.6.3"
uvicorn = "*"
whatthepatch = ">=1.0.6"
whatthepatch = "^1.0.6"
zope-interface = "7.2"
[package.extras]
third-party-runtimes = ["daytona (==0.24.2)", "e2b-code-interpreter (>=2)", "modal (>=0.66.26,<1.2)", "runloop-api-client (==0.50)"]
third-party-runtimes = ["daytona (==0.24.2)", "e2b-code-interpreter (>=2.0.0,<3.0.0)", "modal (>=0.66.26,<1.2.0)", "runloop-api-client (==0.50.0)"]
[package.source]
type = "directory"
@@ -5981,20 +5981,19 @@ url = ".."
[[package]]
name = "openhands-sdk"
version = "1.8.2"
version = "1.8.1"
description = "OpenHands SDK - Core functionality for building AI agents"
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = [
{file = "openhands_sdk-1.8.2-py3-none-any.whl", hash = "sha256:b4fad9581865ce222a3e6722384e4df56113db01bd34c2d2d408dfd9695365c0"},
{file = "openhands_sdk-1.8.2.tar.gz", hash = "sha256:5bfb17c8b9515210d121249deb1f3d0dc407c3737edc55b5e73330b4571d61e3"},
{file = "openhands_sdk-1.8.1-py3-none-any.whl", hash = "sha256:133275f56321585c016b4718d56c8fc7bb834f4ef7cab1ef66b0c71c49d47d1d"},
{file = "openhands_sdk-1.8.1.tar.gz", hash = "sha256:9e2baa6c512ac4c2bc1c2c0bf8b1dbdb0267d794a8b86b7306a4656fc0cb8b0b"},
]
[package.dependencies]
deprecation = ">=2.1.0"
fastmcp = ">=2.11.3"
filelock = ">=3.20.1"
httpx = ">=0.27.0"
litellm = ">=1.80.10"
lmnr = ">=0.7.24"
@@ -6009,14 +6008,14 @@ boto3 = ["boto3 (>=1.35.0)"]
[[package]]
name = "openhands-tools"
version = "1.8.2"
version = "1.8.1"
description = "OpenHands Tools - Runtime tools for AI agents"
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = [
{file = "openhands_tools-1.8.2-py3-none-any.whl", hash = "sha256:283f0c1fdd316914559cd16ade792383715478a8f5a73f7166daffc34bf9e5af"},
{file = "openhands_tools-1.8.2.tar.gz", hash = "sha256:eae416e3867f7cb595129a33a4b9237886c4b8a075d2bc7618da55963f2747d5"},
{file = "openhands_tools-1.8.1-py3-none-any.whl", hash = "sha256:9404b17edb8960d4af3a4439e6f68e37c92c59d0705f13096e4a8ff9b6ffc472"},
{file = "openhands_tools-1.8.1.tar.gz", hash = "sha256:e59fcd9ca3baa6266e92020646c4c5f5266f57761f434770cf0cd458b1a33cb0"},
]
[package.dependencies]
-2
View File
@@ -39,8 +39,6 @@ ROLE_CHECK_ENABLED = os.getenv('ROLE_CHECK_ENABLED', 'false').lower() in (
'on',
)
DUPLICATE_EMAIL_CHECK = os.getenv('DUPLICATE_EMAIL_CHECK', 'true') in ('1', 'true')
# reCAPTCHA Enterprise
RECAPTCHA_PROJECT_ID = os.getenv('RECAPTCHA_PROJECT_ID', '').strip()
RECAPTCHA_SITE_KEY = os.getenv('RECAPTCHA_SITE_KEY', '').strip()
-5
View File
@@ -19,7 +19,6 @@ from keycloak.exceptions import (
from server.auth.constants import (
BITBUCKET_APP_CLIENT_ID,
BITBUCKET_APP_CLIENT_SECRET,
DUPLICATE_EMAIL_CHECK,
GITHUB_APP_CLIENT_ID,
GITHUB_APP_CLIENT_SECRET,
GITLAB_APP_CLIENT_ID,
@@ -647,10 +646,6 @@ class TokenManager:
if not email:
return False
# We have the option to skip the duplicate email check in test environments
if not DUPLICATE_EMAIL_CHECK:
return False
base_email = extract_base_email(email)
if not base_email:
logger.warning(f'Could not extract base email from: {email}')
+270
View File
@@ -0,0 +1,270 @@
#!/bin/bash
# OpenHands Enterprise Local Development Setup Script
# This script helps set up the local development environment
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENTERPRISE_DIR="$SCRIPT_DIR"
OPENHANDS_DIR="$(dirname "$ENTERPRISE_DIR")"
echo -e "${BLUE}╔═══════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ OpenHands Enterprise Local Development Setup ║${NC}"
echo -e "${BLUE}╚═══════════════════════════════════════════════════════════╝${NC}"
echo ""
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Function to check if a Docker container is running
container_running() {
docker ps --format '{{.Names}}' | grep -q "^$1$"
}
# Function to wait for a service
wait_for_service() {
local host=$1
local port=$2
local name=$3
local max_attempts=30
local attempt=1
echo -ne " Waiting for $name to be ready..."
while ! nc -z "$host" "$port" 2>/dev/null; do
if [ $attempt -ge $max_attempts ]; then
echo -e " ${RED}FAILED${NC}"
echo -e " ${RED}$name did not become ready in time${NC}"
return 1
fi
sleep 2
attempt=$((attempt + 1))
echo -ne "."
done
echo -e " ${GREEN}READY${NC}"
}
# Check prerequisites
echo -e "${YELLOW}Checking prerequisites...${NC}"
if ! command_exists docker; then
echo -e " ${RED}✗ Docker is not installed${NC}"
echo " Please install Docker: https://docs.docker.com/get-docker/"
exit 1
fi
echo -e " ${GREEN}✓ Docker${NC}"
if ! command_exists docker-compose && ! docker compose version >/dev/null 2>&1; then
echo -e " ${RED}✗ Docker Compose is not installed${NC}"
exit 1
fi
echo -e " ${GREEN}✓ Docker Compose${NC}"
if ! command_exists poetry; then
echo -e " ${RED}✗ Poetry is not installed${NC}"
echo " Install with: curl -sSL https://install.python-poetry.org | python3 -"
exit 1
fi
echo -e " ${GREEN}✓ Poetry${NC}"
echo ""
# Parse arguments
MINIMAL=false
FULL=false
START_ONLY=false
STOP=false
while [[ $# -gt 0 ]]; do
case $1 in
--minimal)
MINIMAL=true
shift
;;
--full)
FULL=true
shift
;;
--start)
START_ONLY=true
shift
;;
--stop)
STOP=true
shift
;;
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --minimal Start only PostgreSQL and Redis (no Keycloak/LiteLLM)"
echo " --full Start all services including Keycloak and LiteLLM"
echo " --start Only start services, skip installation"
echo " --stop Stop all services"
echo " --help Show this help message"
exit 0
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
# Stop services
if [ "$STOP" = true ]; then
echo -e "${YELLOW}Stopping services...${NC}"
cd "$ENTERPRISE_DIR"
if [ -f docker-compose.local.yml ]; then
docker compose -f docker-compose.local.yml down 2>/dev/null || true
fi
docker rm -f openhands-postgres openhands-redis openhands-keycloak openhands-litellm 2>/dev/null || true
echo -e "${GREEN}Services stopped${NC}"
exit 0
fi
# Determine mode
if [ "$MINIMAL" = false ] && [ "$FULL" = false ]; then
echo -e "${YELLOW}Select setup mode:${NC}"
echo " 1) Minimal (PostgreSQL + Redis only)"
echo " 2) Full (PostgreSQL + Redis + Keycloak + LiteLLM)"
read -p "Enter choice [1]: " choice
choice=${choice:-1}
if [ "$choice" = "1" ]; then
MINIMAL=true
else
FULL=true
fi
fi
echo ""
# Start services
if [ "$MINIMAL" = true ]; then
echo -e "${YELLOW}Starting minimal services (PostgreSQL + Redis)...${NC}"
# PostgreSQL
if ! container_running openhands-postgres; then
echo -e " Starting PostgreSQL..."
docker run -d \
--name openhands-postgres \
-p 5432:5432 \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=openhands \
postgres:15 >/dev/null
else
echo -e " ${GREEN}PostgreSQL already running${NC}"
fi
# Redis
if ! container_running openhands-redis; then
echo -e " Starting Redis..."
docker run -d \
--name openhands-redis \
-p 6379:6379 \
redis:7-alpine >/dev/null
else
echo -e " ${GREEN}Redis already running${NC}"
fi
wait_for_service localhost 5432 "PostgreSQL"
wait_for_service localhost 6379 "Redis"
else
echo -e "${YELLOW}Starting full services (PostgreSQL + Redis + Keycloak + LiteLLM)...${NC}"
cd "$ENTERPRISE_DIR"
# Use docker compose
if docker compose version >/dev/null 2>&1; then
docker compose -f docker-compose.local.yml up -d
else
docker-compose -f docker-compose.local.yml up -d
fi
wait_for_service localhost 5432 "PostgreSQL"
wait_for_service localhost 6379 "Redis"
wait_for_service localhost 8080 "Keycloak"
wait_for_service localhost 4000 "LiteLLM"
fi
if [ "$START_ONLY" = true ]; then
echo ""
echo -e "${GREEN}Services started!${NC}"
exit 0
fi
echo ""
# Create .env file if it doesn't exist
echo -e "${YELLOW}Configuring environment...${NC}"
cd "$ENTERPRISE_DIR"
if [ ! -f .env ]; then
cp .env.example .env
echo -e " ${GREEN}Created .env file from template${NC}"
echo -e " ${YELLOW}Please edit .env to add your LLM API keys!${NC}"
else
echo -e " ${GREEN}.env file already exists${NC}"
fi
echo ""
# Install Python dependencies
echo -e "${YELLOW}Installing Python dependencies...${NC}"
cd "$ENTERPRISE_DIR"
poetry install
echo ""
# Run database migrations
echo -e "${YELLOW}Running database migrations...${NC}"
poetry run alembic upgrade head
echo ""
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Setup Complete! ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "Services running:"
echo -e " • PostgreSQL: ${BLUE}localhost:5432${NC}"
echo -e " • Redis: ${BLUE}localhost:6379${NC}"
if [ "$FULL" = true ]; then
echo -e " • Keycloak: ${BLUE}http://localhost:8080${NC} (admin/admin)"
echo -e " • LiteLLM: ${BLUE}http://localhost:4000${NC}"
fi
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo ""
echo "1. Edit .env and add your LLM API keys (OPENAI_API_KEY or ANTHROPIC_API_KEY)"
echo ""
if [ "$FULL" = true ]; then
echo "2. Configure LiteLLM with your API keys:"
echo " docker exec -it openhands-litellm /bin/sh"
echo " # Then set environment variables"
echo ""
echo "3. (Optional) Configure Keycloak realm for OAuth"
echo ""
fi
echo "3. Build the frontend (from repo root):"
echo " cd .. && make build-frontend"
echo ""
echo "4. Start the backend server:"
echo " cd enterprise"
echo " OPENHANDS_PATH=../ make start-backend"
echo ""
echo "5. Access the application at: ${BLUE}http://localhost:3000${NC}"
echo ""
echo "To stop all services: $0 --stop"
+1 -1
View File
@@ -20,7 +20,7 @@ This is the frontend of the OpenHands project. It is a React application that pr
### Prerequisites
- Node.js 22.12.x or later
- Node.js 20.x or later
- `npm`, `bun`, or any other package manager that supports the `package.json` file
### Installation
@@ -1,34 +0,0 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { ErrorMessageBanner } from "#/components/features/chat/error-message-banner";
describe("ErrorMessageBanner", () => {
it("calls onDismiss when the close button is clicked", async () => {
const user = userEvent.setup();
const onDismiss = vi.fn();
render(
<ErrorMessageBanner
message="Something went wrong"
onDismiss={onDismiss}
/>,
);
await user.click(screen.getByLabelText("BUTTON$CLOSE"));
expect(onDismiss).toHaveBeenCalledTimes(1);
});
it("shows a View More / View Less toggle for long messages", async () => {
const user = userEvent.setup();
const longMessage = "a".repeat(400);
render(<ErrorMessageBanner message={longMessage} />);
const toggle = screen.getByTestId("error-message-banner-toggle");
expect(toggle).toHaveTextContent("COMMON$VIEW_MORE");
await user.click(toggle);
expect(toggle).toHaveTextContent("COMMON$VIEW_LESS");
});
});
@@ -0,0 +1,82 @@
import { describe, it, expect, afterEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter, Route, Routes } from "react-router";
import { ConversationTabsContextMenu } from "#/components/features/conversation/conversation-tabs/conversation-tabs-context-menu";
function renderWithRouter(conversationId: string, onClose: () => void) {
return render(
<MemoryRouter initialEntries={[`/conversations/${conversationId}`]}>
<Routes>
<Route
path="/conversations/:conversationId"
element={<ConversationTabsContextMenu isOpen onClose={onClose} />}
/>
</Routes>
</MemoryRouter>,
);
}
describe("ConversationTabsContextMenu", () => {
afterEach(() => {
localStorage.clear();
});
it("should use per-conversation localStorage key for unpinned tabs", async () => {
const user = userEvent.setup();
const onClose = () => {};
// Render for conversation-1
const { unmount } = renderWithRouter("conversation-1", onClose);
// Unpin the terminal tab in conversation-1
const terminalItem = screen.getByText("COMMON$TERMINAL");
await user.click(terminalItem);
// Verify localStorage key is per-conversation
const stored1 = JSON.parse(
localStorage.getItem("conversation-unpinned-tabs-conversation-1") || "[]",
);
expect(stored1).toContain("terminal");
unmount();
// Switch to conversation-2
renderWithRouter("conversation-2", onClose);
// conversation-2 should have its own empty state
const stored2 = JSON.parse(
localStorage.getItem("conversation-unpinned-tabs-conversation-2") || "[]",
);
expect(stored2).toEqual([]);
// conversation-1 state should still have terminal unpinned
const stored1Again = JSON.parse(
localStorage.getItem("conversation-unpinned-tabs-conversation-1") || "[]",
);
expect(stored1Again).toContain("terminal");
});
it("should toggle tab pin state when clicked", async () => {
const user = userEvent.setup();
const onClose = () => {};
renderWithRouter("conversation-1", onClose);
const terminalItem = screen.getByText("COMMON$TERMINAL");
// Click to unpin
await user.click(terminalItem);
let stored = JSON.parse(
localStorage.getItem("conversation-unpinned-tabs-conversation-1") || "[]",
);
expect(stored).toContain("terminal");
// Click again to pin
await user.click(terminalItem);
stored = JSON.parse(
localStorage.getItem("conversation-unpinned-tabs-conversation-1") || "[]",
);
expect(stored).not.toContain("terminal");
});
});
@@ -1,90 +0,0 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter } from "react-router";
import { ConversationTabs } from "#/components/features/conversation/conversation-tabs/conversation-tabs";
import { ConversationTabsContextMenu } from "#/components/features/conversation/conversation-tabs/conversation-tabs-context-menu";
const TASK_CONVERSATION_ID = "task-ec03fb2ab8604517b24af632b058c2fd";
const REAL_CONVERSATION_ID = "conv-abc123";
vi.mock("#/utils/feature-flags", () => ({
USE_PLANNING_AGENT: () => false,
}));
let mockConversationId = TASK_CONVERSATION_ID;
vi.mock("#/hooks/use-conversation-id", () => ({
useConversationId: () => ({ conversationId: mockConversationId }),
}));
const createWrapper = (conversationId: string) => {
return ({ children }: { children: React.ReactNode }) => (
<MemoryRouter initialEntries={[`/conversations/${conversationId}`]}>
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
</MemoryRouter>
);
};
describe("ConversationTabs localStorage behavior", () => {
beforeEach(() => {
localStorage.clear();
vi.resetAllMocks();
mockConversationId = TASK_CONVERSATION_ID;
});
describe("task-prefixed conversation IDs", () => {
it("should not create localStorage entries for task-prefixed conversation IDs", () => {
render(<ConversationTabs />, {
wrapper: createWrapper(TASK_CONVERSATION_ID),
});
expect(
localStorage.getItem(`conversation-state-${TASK_CONVERSATION_ID}`),
).toBeNull();
});
});
describe("consolidated localStorage key", () => {
it("should use a single consolidated key for tab state", async () => {
mockConversationId = REAL_CONVERSATION_ID;
const user = userEvent.setup();
render(<ConversationTabs />, {
wrapper: createWrapper(REAL_CONVERSATION_ID),
});
const changesTab = screen.getByText("COMMON$CHANGES");
await user.click(changesTab);
const consolidatedKey = `conversation-state-${REAL_CONVERSATION_ID}`;
const storedState = localStorage.getItem(consolidatedKey);
expect(storedState).not.toBeNull();
const parsed = JSON.parse(storedState!);
expect(parsed).toHaveProperty("selectedTab");
expect(parsed).toHaveProperty("rightPanelShown");
expect(parsed).toHaveProperty("unpinnedTabs");
});
it("should store unpinned tabs in consolidated key via context menu", async () => {
mockConversationId = REAL_CONVERSATION_ID;
const user = userEvent.setup();
render(<ConversationTabsContextMenu isOpen={true} onClose={vi.fn()} />);
const terminalItem = screen.getByText("COMMON$TERMINAL");
await user.click(terminalItem);
const consolidatedKey = `conversation-state-${REAL_CONVERSATION_ID}`;
const storedState = localStorage.getItem(consolidatedKey);
expect(storedState).not.toBeNull();
const parsed = JSON.parse(storedState!);
expect(parsed.unpinnedTabs).toContain("terminal");
});
});
});
@@ -1,38 +0,0 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import {
clearConversationLocalStorage,
LOCAL_STORAGE_KEYS,
} from "#/utils/conversation-local-storage";
describe("conversation localStorage utilities", () => {
beforeEach(() => {
localStorage.clear();
});
describe("clearConversationLocalStorage", () => {
it("removes the consolidated conversation-state localStorage entry", () => {
const conversationId = "conv-123";
// Set up the consolidated key
const consolidatedKey = `${LOCAL_STORAGE_KEYS.CONVERSATION_STATE}-${conversationId}`;
localStorage.setItem(
consolidatedKey,
JSON.stringify({
selectedTab: "editor",
rightPanelShown: true,
unpinnedTabs: [],
}),
);
clearConversationLocalStorage(conversationId);
expect(localStorage.getItem(consolidatedKey)).toBeNull();
});
it("does not throw if conversation keys do not exist", () => {
expect(() => {
clearConversationLocalStorage("non-existent-id");
}).not.toThrow();
});
});
});
@@ -15,11 +15,10 @@ import { MemoryRouter, Route, Routes } from "react-router";
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
import { useBrowserStore } from "#/stores/browser-store";
import { useCommandStore } from "#/stores/command-store";
import { useErrorMessageStore } from "#/stores/error-message-store";
import {
createMockMessageEvent,
createMockUserMessageEvent,
createMockConversationErrorEvent,
createMockAgentErrorEvent,
createMockBrowserObservationEvent,
createMockBrowserNavigateActionEvent,
createMockExecuteBashActionEvent,
@@ -52,9 +51,6 @@ afterEach(() => {
mswServer.resetHandlers();
// Clean up any React components
cleanup();
// Reset stores to prevent state leakage between tests
useErrorMessageStore.getState().removeErrorMessage();
useEventStore.getState().clearEvents();
});
afterAll(async () => {
@@ -281,23 +277,16 @@ describe("Conversation WebSocket Handler", () => {
// 5. Error Handling Tests
describe("Error Handling & Recovery", () => {
beforeEach(() => {
// Clear stores before each error handling test to prevent state leakage
useErrorMessageStore.getState().removeErrorMessage();
useEventStore.getState().clearEvents();
});
it("should update error message store on ConversationErrorEvent", async () => {
// ConversationErrorEvent represents infrastructure/authentication errors
// that should be shown as a banner to the user.
const mockConversationErrorEvent = createMockConversationErrorEvent();
it("should update error message store on AgentErrorEvent", async () => {
// Create a mock AgentErrorEvent to send through WebSocket
const mockAgentErrorEvent = createMockAgentErrorEvent();
// Set up MSW to send the error event when connection is established
mswServer.use(
wsLink.addEventListener("connection", ({ client, server }) => {
server.connect();
// Send the mock error event after connection
client.send(JSON.stringify(mockConversationErrorEvent));
client.send(JSON.stringify(mockAgentErrorEvent));
}),
);
@@ -310,7 +299,7 @@ describe("Conversation WebSocket Handler", () => {
// Wait for connection and error event processing
await waitFor(() => {
expect(screen.getByTestId("error-message")).toHaveTextContent(
"Your session has expired. Please log in again.",
"Failed to execute command: Permission denied",
);
});
});
@@ -449,60 +438,6 @@ describe("Conversation WebSocket Handler", () => {
);
});
it("should clear error message when a successful event is received after a ConversationErrorEvent", async () => {
// This test verifies that error banners disappear when follow-up messages
// are sent and received. Only ConversationErrorEvent sets the error banner,
// and any non-error event should clear it.
const conversationId = "test-conversation-error-clear";
// Set up MSW to mock event count API and send events
mswServer.use(
http.get(
`http://localhost:3000/api/conversations/${conversationId}/events/count`,
() => HttpResponse.json(2),
),
wsLink.addEventListener("connection", ({ client, server }) => {
server.connect();
// Send a ConversationErrorEvent first (this sets the error banner)
const mockConversationErrorEvent = createMockConversationErrorEvent();
client.send(JSON.stringify(mockConversationErrorEvent));
// Send a successful (non-error) event immediately after
// This simulates the user sending a follow-up message and receiving a response
const mockSuccessEvent = createMockMessageEvent({
id: "success-event-after-error",
});
client.send(JSON.stringify(mockSuccessEvent));
}),
);
// Verify error message store is initially empty
expect(useErrorMessageStore.getState().errorMessage).toBeNull();
// Render with WebSocket context (minimal component just to trigger connection)
renderWithWebSocketContext(
<ConnectionStatusComponent />,
conversationId,
`http://localhost:3000/api/conversations/${conversationId}`,
);
// Wait for connection
await waitFor(() => {
expect(screen.getByTestId("connection-state")).toHaveTextContent(
"OPEN",
);
});
// Wait for both events to be received and error to be cleared
// The error was set by the first event (ConversationErrorEvent),
// then cleared by the second successful event (MessageEvent).
await waitFor(() => {
expect(useEventStore.getState().events.length).toBe(2);
expect(useErrorMessageStore.getState().errorMessage).toBeNull();
});
});
it("should not create duplicate events when WebSocket reconnects with resend_all=true", async () => {
const conversationId = "test-conversation-reconnect";
let connectionCount = 0;
+2 -91
View File
@@ -89,7 +89,7 @@
"vitest": "^4.0.14"
},
"engines": {
"node": ">=22.12.0"
"node": ">=22.0.0"
}
},
"node_modules/@acemir/cssom": {
@@ -192,7 +192,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -732,7 +731,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -779,7 +777,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -2348,7 +2345,6 @@
"version": "2.4.25",
"resolved": "https://registry.npmjs.org/@heroui/system/-/system-2.4.25.tgz",
"integrity": "sha512-F6UUoGTQ+Qas5wYkCzLjXE7u74Z9ygO0u0+dkTW7zCaY7ds65CcmvZ/ahKz2ES3Tk6TNks1MJSyaQ9rFLs8AqA==",
"peer": true,
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/system-rsc": "2.3.21",
@@ -2428,7 +2424,6 @@
"version": "2.4.25",
"resolved": "https://registry.npmjs.org/@heroui/theme/-/theme-2.4.25.tgz",
"integrity": "sha512-nTptYhO1V9rMoh9SJDnMfaSmFuoXvbem1UuwgHcraRtqy/TIVBPqv26JEGzSoUCL194TDGOJpqrpMuab/PdXcw==",
"peer": true,
"dependencies": {
"@heroui/shared-utils": "2.1.12",
"color": "^4.2.3",
@@ -5431,7 +5426,6 @@
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
@@ -5890,7 +5884,6 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -6071,14 +6064,6 @@
"undici-types": "~7.16.0"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/@types/prismjs": {
"version": "1.26.5",
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
@@ -6099,7 +6084,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -6140,7 +6124,6 @@
"integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.18.0",
@@ -6198,7 +6181,6 @@
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/types": "7.18.0",
@@ -6737,7 +6719,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -7132,52 +7113,6 @@
"@babel/types": "^7.23.6"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/babel-plugin-macros/node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/babel-plugin-macros/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
"license": "ISC",
"optional": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@@ -7318,7 +7253,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -8007,8 +7941,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@@ -8726,7 +8659,6 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -8850,7 +8782,6 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -8931,7 +8862,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -9023,7 +8953,6 @@
"integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"aria-query": "^5.3.2",
"array-includes": "^3.1.8",
@@ -9118,7 +9047,6 @@
"integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"array-includes": "^3.1.8",
"array.prototype.findlast": "^1.2.5",
@@ -9152,7 +9080,6 @@
"integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -9420,7 +9347,6 @@
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -10415,7 +10341,6 @@
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4"
},
@@ -11177,7 +11102,6 @@
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz",
"integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
"dev": true,
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.28",
"@asamuzakjp/dom-selector": "^6.7.6",
@@ -12884,7 +12808,6 @@
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT",
"peer": true,
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
@@ -12977,7 +12900,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@inquirer/confirm": "^5.0.0",
"@mswjs/interceptors": "^0.40.0",
@@ -13702,7 +13624,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -13780,7 +13701,6 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -14004,7 +13924,6 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -14053,7 +13972,6 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -14166,7 +14084,6 @@
"version": "7.12.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
"integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
"peer": true,
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
@@ -14528,7 +14445,6 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -15504,7 +15420,6 @@
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
@@ -15631,7 +15546,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15933,7 +15847,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16238,7 +16151,6 @@
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -16409,7 +16321,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
+2 -2
View File
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"engines": {
"node": ">=22.12.0"
"node": ">=22.0.0"
},
"dependencies": {
"@heroui/react": "2.8.7",
@@ -121,7 +121,7 @@
},
"packageManager": "npm@10.5.0",
"volta": {
"node": "22.12.0"
"node": "22.0.0"
},
"msw": {
"workerDirectory": [
@@ -66,7 +66,7 @@ export function ChatInterface() {
const posthog = usePostHog();
const { setMessageToSend } = useConversationStore();
const { data: conversation } = useActiveConversation();
const { errorMessage, removeErrorMessage } = useErrorMessageStore();
const { errorMessage } = useErrorMessageStore();
const { isLoadingMessages } = useWsClient();
const { isTask, taskStatus, taskDetail } = useTaskPolling();
const conversationWebSocket = useConversationWebSocket();
@@ -342,12 +342,7 @@ export function ChatInterface() {
{!hitBottom && <ScrollToBottomButton onClick={scrollDomToBottom} />}
</div>
{errorMessage && (
<ErrorMessageBanner
message={errorMessage}
onDismiss={removeErrorMessage}
/>
)}
{errorMessage && <ErrorMessageBanner message={errorMessage} />}
<InteractiveChatBox onSubmit={handleSendMessage} />
</div>
@@ -1,87 +1,30 @@
import React from "react";
import { Trans, useTranslation } from "react-i18next";
import { Trans } from "react-i18next";
import { Link } from "react-router";
import { X } from "lucide-react";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
import i18n from "#/i18n";
interface ErrorMessageBannerProps {
message: string;
onDismiss?: () => void;
}
const DEFAULT_MAX_COLLAPSED_CHARS = 220;
export function ErrorMessageBanner({
message,
onDismiss,
}: ErrorMessageBannerProps) {
const { t, i18n } = useTranslation();
const [isExpanded, setIsExpanded] = React.useState(false);
const isI18nKey = i18n.exists(message);
const displayTextForLength = isI18nKey ? String(t(message)) : message;
const shouldShowToggle =
displayTextForLength.length > DEFAULT_MAX_COLLAPSED_CHARS;
const isCollapsed = shouldShowToggle && !isExpanded;
export function ErrorMessageBanner({ message }: ErrorMessageBannerProps) {
return (
<div
className="w-full rounded-lg p-2 border border-[#FF0006] bg-[#4A0709] flex gap-2 items-start text-white"
data-testid="error-message-banner"
>
<div className="min-w-0 flex-1">
<div
className={cn(
"whitespace-pre-wrap wrap-break-words",
isCollapsed && "line-clamp-3",
)}
data-testid="error-message-banner-content"
>
{isI18nKey ? (
<Trans
i18nKey={message}
components={{
a: (
<Link
className="underline font-bold cursor-pointer"
to="/settings/billing"
>
link
</Link>
),
}}
/>
) : (
message
)}
</div>
{shouldShowToggle && (
<button
type="button"
className="mt-1 text-xs underline font-semibold cursor-pointer"
onClick={() => setIsExpanded((prev) => !prev)}
data-testid="error-message-banner-toggle"
>
{isExpanded
? t(I18nKey.COMMON$VIEW_LESS)
: t(I18nKey.COMMON$VIEW_MORE)}
</button>
)}
</div>
{onDismiss && (
<button
type="button"
onClick={onDismiss}
className="shrink-0 rounded-md p-1 hover:bg-black/10 cursor-pointer"
aria-label={t(I18nKey.BUTTON$CLOSE)}
data-testid="error-message-banner-dismiss"
>
<X className="h-4 w-4" />
</button>
<div className="w-full rounded-lg p-2 text-black border border-red-800 bg-red-500">
{i18n.exists(message) ? (
<Trans
i18nKey={message}
components={{
a: (
<Link
className="underline font-bold cursor-pointer"
to="/settings/billing"
>
link
</Link>
),
}}
/>
) : (
message
)}
</div>
);
@@ -1,9 +1,8 @@
import { useTranslation } from "react-i18next";
import { useLocalStorage } from "@uidotdev/usehooks";
import { ContextMenu } from "#/ui/context-menu";
import { ContextMenuListItem } from "../../context-menu/context-menu-list-item";
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
import { useConversationId } from "#/hooks/use-conversation-id";
import { useConversationLocalStorageState } from "#/utils/conversation-local-storage";
import { I18nKey } from "#/i18n/declaration";
import TerminalIcon from "#/icons/terminal.svg?react";
import GlobeIcon from "#/icons/globe.svg?react";
@@ -14,6 +13,7 @@ import PillIcon from "#/icons/pill.svg?react";
import PillFillIcon from "#/icons/pill-fill.svg?react";
import { USE_PLANNING_AGENT } from "#/utils/feature-flags";
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
import { useConversationId } from "#/hooks/use-conversation-id";
interface ConversationTabsContextMenuProps {
isOpen: boolean;
@@ -27,8 +27,11 @@ export function ConversationTabsContextMenu({
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
const { t } = useTranslation();
const { conversationId } = useConversationId();
const { state, setUnpinnedTabs } =
useConversationLocalStorageState(conversationId);
const [unpinnedTabs, setUnpinnedTabs] = useLocalStorage<string[]>(
`conversation-unpinned-tabs-${conversationId}`,
[],
);
const shouldUsePlanningAgent = USE_PLANNING_AGENT();
@@ -51,11 +54,11 @@ export function ConversationTabsContextMenu({
if (!isOpen) return null;
const handleTabClick = (tab: string) => {
if (state.unpinnedTabs.includes(tab)) {
setUnpinnedTabs(state.unpinnedTabs.filter((item) => item !== tab));
} else {
setUnpinnedTabs([...state.unpinnedTabs, tab]);
}
setUnpinnedTabs((prev) =>
prev.includes(tab)
? prev.filter((tabItem) => tabItem !== tab)
: [...prev, tab],
);
};
return (
@@ -66,7 +69,7 @@ export function ConversationTabsContextMenu({
className="mt-2 w-fit z-[9999]"
>
{tabConfig.map(({ tab, icon: Icon, i18nKey }) => {
const pinned = !state.unpinnedTabs.includes(tab);
const pinned = !unpinnedTabs.includes(tab);
return (
<ContextMenuListItem
key={tab}
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocalStorage } from "@uidotdev/usehooks";
import TerminalIcon from "#/icons/terminal.svg?react";
import GlobeIcon from "#/icons/globe.svg?react";
import ServerIcon from "#/icons/server.svg?react";
@@ -8,7 +9,6 @@ import VSCodeIcon from "#/icons/vscode.svg?react";
import ThreeDotsVerticalIcon from "#/icons/three-dots-vertical.svg?react";
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
import { cn } from "#/utils/utils";
import { useConversationLocalStorageState } from "#/utils/conversation-local-storage";
import { ConversationTabNav } from "./conversation-tab-nav";
import { ChatActionTooltip } from "../../chat/chat-action-tooltip";
import { I18nKey } from "#/i18n/declaration";
@@ -32,11 +32,23 @@ export function ConversationTabs() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const {
state: persistedState,
setSelectedTab: setPersistedSelectedTab,
setRightPanelShown: setPersistedRightPanelShown,
} = useConversationLocalStorageState(conversationId);
// Persist selectedTab and isRightPanelShown in localStorage per conversation
const [persistedSelectedTab, setPersistedSelectedTab] =
useLocalStorage<ConversationTab | null>(
`conversation-selected-tab-${conversationId}`,
"editor",
);
const [persistedIsRightPanelShown, setPersistedIsRightPanelShown] =
useLocalStorage<boolean>(
`conversation-right-panel-shown-${conversationId}`,
true,
);
const [persistedUnpinnedTabs] = useLocalStorage<string[]>(
`conversation-unpinned-tabs-${conversationId}`,
[],
);
const shouldUsePlanningAgent = USE_PLANNING_AGENT();
@@ -49,13 +61,13 @@ export function ConversationTabs() {
// Initialize Zustand state from localStorage on component mount
useEffect(() => {
// Initialize selectedTab from localStorage if available
setSelectedTab(persistedState.selectedTab);
setHasRightPanelToggled(persistedState.rightPanelShown);
setSelectedTab(persistedSelectedTab);
setHasRightPanelToggled(persistedIsRightPanelShown);
}, [
setSelectedTab,
setHasRightPanelToggled,
persistedState.selectedTab,
persistedState.rightPanelShown,
persistedSelectedTab,
persistedIsRightPanelShown,
]);
useEffect(() => {
@@ -77,13 +89,13 @@ export function ConversationTabs() {
if (selectedTab === tab && isRightPanelShown) {
// If clicking the same active tab, close the drawer
setHasRightPanelToggled(false);
setPersistedRightPanelShown(false);
setPersistedIsRightPanelShown(false);
} else {
// If clicking a different tab or drawer is closed, open drawer and select tab
onTabChange(tab);
if (!isRightPanelShown) {
setHasRightPanelToggled(true);
setPersistedRightPanelShown(true);
setPersistedIsRightPanelShown(true);
}
}
};
@@ -154,7 +166,7 @@ export function ConversationTabs() {
// Filter out unpinned tabs
const visibleTabs = tabs.filter(
(tab) => !persistedState.unpinnedTabs.includes(tab.tabValue),
(tab) => !persistedUnpinnedTabs.includes(tab.tabValue),
);
return (
@@ -329,17 +329,16 @@ export function ConversationWebSocketProvider({
if (isV1Event(event)) {
addEvent(event);
// Handle ConversationErrorEvent specifically - show error banner
// AgentErrorEvent errors are displayed inline in the chat, not as banners
// Handle ConversationErrorEvent specifically
if (isConversationErrorEvent(event)) {
setErrorMessage(event.detail);
} else {
// Clear error message on any non-ConversationErrorEvent
removeErrorMessage();
}
// Track credit limit reached if AgentErrorEvent has budget-related error
// Handle AgentErrorEvent specifically
if (isAgentErrorEvent(event)) {
setErrorMessage(event.error);
// Track credit limit reached if the error is budget-related
if (isBudgetOrCreditError(event.error)) {
trackCreditLimitReached({
conversationId: conversationId || "unknown",
@@ -418,7 +417,6 @@ export function ConversationWebSocketProvider({
isLoadingHistoryMain,
expectedEventCountMain,
setErrorMessage,
removeErrorMessage,
removeOptimisticUserMessage,
queryClient,
conversationId,
@@ -426,7 +424,6 @@ export function ConversationWebSocketProvider({
appendInput,
appendOutput,
updateMetricsFromStats,
trackCreditLimitReached,
],
);
@@ -1,6 +1,5 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { clearConversationLocalStorage } from "#/utils/conversation-local-storage";
export const useDeleteConversation = () => {
const queryClient = useQueryClient();
@@ -25,11 +24,6 @@ export const useDeleteConversation = () => {
return { previousConversations };
},
onSuccess: (_, variables) => {
clearConversationLocalStorage(variables.conversationId);
},
onError: (err, variables, context) => {
if (context?.previousConversations) {
queryClient.setQueryData(
-17
View File
@@ -7,7 +7,6 @@ import {
import { AgentStateChangeObservation } from "#/types/core/observations";
import { MessageEvent } from "#/types/v1/core";
import { AgentErrorEvent } from "#/types/v1/core/events/observation-event";
import { ConversationErrorEvent } from "#/types/v1/core/events/conversation-state-event";
import { MockSessionMessaage } from "./session-history.mock";
export const generateAgentStateChangeObservation = (
@@ -237,19 +236,3 @@ export const createMockBrowserNavigateActionEvent = (
llm_response_id: "llm-response-789",
security_risk: { level: "low" },
});
/**
* Creates a mock ConversationErrorEvent for testing conversation-level error handling
* These are infrastructure/authentication errors that should show error banners
*/
export const createMockConversationErrorEvent = (
overrides: Partial<ConversationErrorEvent> = {},
): ConversationErrorEvent => ({
id: "conversation-error-123",
timestamp: new Date().toISOString(),
source: "environment",
kind: "ConversationErrorEvent",
code: "AuthenticationError",
detail: "Your session has expired. Please log in again.",
...overrides,
});
@@ -62,11 +62,6 @@ export interface ConversationState {
}
interface ConversationStateUpdateEventBase extends BaseEvent {
/**
* Discriminator field for type guards
*/
kind: "ConversationStateUpdateEvent";
/**
* The source is always "environment" for conversation state update events
*/
@@ -110,11 +105,6 @@ export type ConversationStateUpdateEvent =
// Conversation error event - contains error information
export interface ConversationErrorEvent extends BaseEvent {
/**
* Discriminator field for type guards
*/
kind: "ConversationErrorEvent";
/**
* The source is always "environment" for conversation error events
*/
@@ -1,110 +0,0 @@
import { useState } from "react";
import type { ConversationTab } from "#/stores/conversation-store";
export const LOCAL_STORAGE_KEYS = {
CONVERSATION_STATE: "conversation-state",
} as const;
/**
* Consolidated conversation state stored in a single localStorage key.
*/
export interface ConversationState {
selectedTab: ConversationTab | null;
rightPanelShown: boolean;
unpinnedTabs: string[];
}
const DEFAULT_CONVERSATION_STATE: ConversationState = {
selectedTab: "editor",
rightPanelShown: true,
unpinnedTabs: [],
};
/**
* Check if a conversation ID is a temporary task ID that should not be persisted.
* Task IDs have the format "task-{uuid}" and are used during V1 conversation initialization.
*/
export function isTaskConversationId(conversationId: string): boolean {
return conversationId.startsWith("task-");
}
/**
* Get the full conversation state from localStorage.
*/
export function getConversationState(
conversationId: string,
): ConversationState {
if (isTaskConversationId(conversationId)) {
return DEFAULT_CONVERSATION_STATE;
}
try {
const key = `${LOCAL_STORAGE_KEYS.CONVERSATION_STATE}-${conversationId}`;
const item = localStorage.getItem(key);
if (item !== null) {
return { ...DEFAULT_CONVERSATION_STATE, ...JSON.parse(item) };
}
return DEFAULT_CONVERSATION_STATE;
} catch {
return DEFAULT_CONVERSATION_STATE;
}
}
/**
* Set the conversation state in localStorage, merging with existing state.
*/
export function setConversationState(
conversationId: string,
updates: Partial<ConversationState>,
): void {
if (isTaskConversationId(conversationId)) {
return;
}
try {
const key = `${LOCAL_STORAGE_KEYS.CONVERSATION_STATE}-${conversationId}`;
const currentState = getConversationState(conversationId);
const newState = { ...currentState, ...updates };
localStorage.setItem(key, JSON.stringify(newState));
} catch (err) {
console.warn("Failed to set conversation localStorage", err);
}
}
export function clearConversationLocalStorage(conversationId: string) {
try {
const key = `${LOCAL_STORAGE_KEYS.CONVERSATION_STATE}-${conversationId}`;
localStorage.removeItem(key);
} catch (err) {
console.warn(
"Failed to clear conversation localStorage",
conversationId,
err,
);
}
}
/**
* React hook for conversation-scoped localStorage state.
* Returns the full state and individual setters for each property.
*/
export function useConversationLocalStorageState(conversationId: string): {
state: ConversationState;
setSelectedTab: (tab: ConversationTab | null) => void;
setRightPanelShown: (shown: boolean) => void;
setUnpinnedTabs: (tabs: string[]) => void;
} {
const [state, setState] = useState<ConversationState>(() =>
getConversationState(conversationId),
);
const updateState = (updates: Partial<ConversationState>) => {
setState((prev) => ({ ...prev, ...updates }));
setConversationState(conversationId, updates);
};
return {
state,
setSelectedTab: (tab) => updateState({ selectedTab: tab }),
setRightPanelShown: (shown) => updateState({ rightPanelShown: shown }),
setUnpinnedTabs: (tabs) => updateState({ unpinnedTabs: tabs }),
};
}
+3
View File
@@ -43,6 +43,7 @@ export default defineConfig(({ mode }) => {
"i18next-browser-languagedetector",
"react-i18next",
"axios",
"date-fns",
"@uidotdev/usehooks",
"react-icons/fa6",
"react-icons/fa",
@@ -50,6 +51,8 @@ export default defineConfig(({ mode }) => {
"tailwind-merge",
"@heroui/react",
"lucide-react",
"react-select",
"react-select/async",
"@microlink/react-json-view",
"socket.io-client",
// These are discovered when launching conversations:
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -30,20 +30,6 @@ GLOBAL_SKILLS_DIR = os.path.join(
WORK_HOSTS_SKILL = """The user has access to the following hosts for accessing a web application,
each of which has a corresponding port:"""
WORK_HOSTS_SKILL_FOOTER = """
When starting a web server, use the corresponding ports via environment variables:
- $WORKER_1 for the first port
- $WORKER_2 for the second port
**CRITICAL: You MUST enable CORS and bind to 0.0.0.0.** Without CORS headers, the App tab cannot detect your server and will show an empty state.
Example (Flask):
```python
from flask_cors import CORS
CORS(app)
app.run(host='0.0.0.0', port=int(os.environ.get('WORKER_1', 12000)))
```"""
def _find_and_load_global_skill_files(skill_dir: Path) -> list[Skill]:
"""Find and load all .md files from the global skills directory.
@@ -87,7 +73,6 @@ def load_sandbox_skills(sandbox: SandboxInfo) -> list[Skill]:
content_list = [WORK_HOSTS_SKILL]
for url in urls:
content_list.append(f'* {url.url} (port {url.port})')
content_list.append(WORK_HOSTS_SKILL_FOOTER)
content = '\n'.join(content_list)
return [Skill(name='work_hosts', content=content, trigger=None)]
@@ -465,9 +450,7 @@ async def _get_org_repository_url(
Authenticated Git URL if successful, None otherwise
"""
try:
remote_url = await user_context.get_authenticated_git_url(
org_openhands_repo, is_optional=True
)
remote_url = await user_context.get_authenticated_git_url(org_openhands_repo)
return remote_url
except AuthenticationError as e:
_logger.debug(
@@ -272,12 +272,6 @@ class RemoteSandboxService(SandboxService):
# we are probably in local development and the only url in use is localhost
environment[ALLOW_CORS_ORIGINS_VARIABLE] = self.web_url
# Add worker port environment variables so the agent knows which ports to use
# for web applications. These match the ports exposed via the WORKER_1 and
# WORKER_2 URLs.
environment[WORKER_1] = str(WORKER_1_PORT)
environment[WORKER_2] = str(WORKER_2_PORT)
return environment
async def search_sandboxes(
@@ -13,7 +13,7 @@ from openhands.sdk.utils.models import DiscriminatedUnionMixin
# The version of the agent server to use for deployments.
# Typically this will be the same as the values from the pyproject.toml
AGENT_SERVER_IMAGE = 'ghcr.io/openhands/agent-server:10fff69-python'
AGENT_SERVER_IMAGE = 'ghcr.io/openhands/agent-server:7c91cbe-python'
class SandboxSpecService(ABC):
@@ -63,13 +63,9 @@ class AuthUserContext(UserContext):
self._provider_handler = provider_handler
return provider_handler
async def get_authenticated_git_url(
self, repository: str, is_optional: bool = False
) -> str:
async def get_authenticated_git_url(self, repository: str) -> str:
provider_handler = await self.get_provider_handler()
url = await provider_handler.get_authenticated_git_url(
repository, is_optional=is_optional
)
url = await provider_handler.get_authenticated_git_url(repository)
return url
async def get_latest_token(self, provider_type: ProviderType) -> str | None:
@@ -21,9 +21,7 @@ class SpecifyUserContext(UserContext):
async def get_user_info(self) -> UserInfo:
raise NotImplementedError()
async def get_authenticated_git_url(
self, repository: str, is_optional: bool = False
) -> str:
async def get_authenticated_git_url(self, repository: str) -> str:
raise NotImplementedError()
async def get_provider_tokens(self) -> PROVIDER_TOKEN_TYPE | None:
+2 -10
View File
@@ -23,16 +23,8 @@ class UserContext(ABC):
"""Get the user info."""
@abstractmethod
async def get_authenticated_git_url(
self, repository: str, is_optional: bool = False
) -> str:
"""Get an authenticated git URL for a repository.
Args:
repository: Repository name (owner/repo)
is_optional: If True, logs at debug level instead of error level
when repository is not found. Use for optional repositories.
"""
async def get_authenticated_git_url(self, repository: str) -> str:
"""Get the provider tokens for the user"""
@abstractmethod
async def get_provider_tokens(self) -> PROVIDER_TOKEN_TYPE | None:
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
@@ -57,9 +57,7 @@ class ForgejoMixinBase(BaseGitService, HTTPClient):
self.base_url = self.BASE_URL # Backwards compatibility for existing usage
parsed = urlparse(self.BASE_URL)
self.base_domain = parsed.netloc or self.DEFAULT_DOMAIN
# Preserve the protocol from BASE_URL (http or https)
protocol = parsed.scheme or 'https'
self.web_base_url = f'{protocol}://{self.base_domain}'.rstrip('/')
self.web_base_url = f'https://{self.base_domain}'.rstrip('/')
@property
def provider(self) -> str:
+6 -24
View File
@@ -675,22 +675,6 @@ class ProviderHandler:
if provider != ProviderType.AZURE_DEVOPS:
domain = self.provider_tokens[provider].host or domain
# Detect protocol before normalizing domain
# Default to https, but preserve http if explicitly specified
protocol = 'https'
if domain and domain.strip().startswith('http://'):
# Check if insecure HTTP access is allowed
allow_insecure = os.environ.get(
'ALLOW_INSECURE_GIT_ACCESS', 'false'
).lower() in ('true', '1', 'yes')
if not allow_insecure:
raise ValueError(
'Attempting to connect to an insecure git repository over HTTP. '
"If you'd like to allow this nonetheless, set "
'ALLOW_INSECURE_GIT_ACCESS=true as an environment variable.'
)
protocol = 'http'
# Normalize domain to prevent double protocols or path segments
if domain:
domain = domain.strip()
@@ -706,18 +690,16 @@ class ProviderHandler:
token_value = git_token.get_secret_value()
if provider == ProviderType.GITLAB:
remote_url = (
f'{protocol}://oauth2:{token_value}@{domain}/{repo_name}.git'
f'https://oauth2:{token_value}@{domain}/{repo_name}.git'
)
elif provider == ProviderType.BITBUCKET:
# For Bitbucket, handle username:app_password format
if ':' in token_value:
# App token format: username:app_password
remote_url = (
f'{protocol}://{token_value}@{domain}/{repo_name}.git'
)
remote_url = f'https://{token_value}@{domain}/{repo_name}.git'
else:
# Access token format: use x-token-auth
remote_url = f'{protocol}://x-token-auth:{token_value}@{domain}/{repo_name}.git'
remote_url = f'https://x-token-auth:{token_value}@{domain}/{repo_name}.git'
elif provider == ProviderType.AZURE_DEVOPS:
# Azure DevOps uses PAT with Basic auth
# Format: https://{anything}:{PAT}@dev.azure.com/{org}/{project}/_git/{repo}
@@ -777,11 +759,11 @@ class ProviderHandler:
)
else:
# GitHub, Forgejo
remote_url = f'{protocol}://{token_value}@{domain}/{repo_name}.git'
remote_url = f'https://{token_value}@{domain}/{repo_name}.git'
else:
remote_url = f'{protocol}://{domain}/{repo_name}.git'
remote_url = f'https://{domain}/{repo_name}.git'
else:
remote_url = f'{protocol}://{domain}/{repo_name}.git'
remote_url = f'https://{domain}/{repo_name}.git'
return remote_url
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk
+1 -1
View File
@@ -1,4 +1,4 @@
# IMPORTANT: LEGACY V0 CODE - Deprecated since version 1.0.0, scheduled for removal April 1, 2026
# IMPORTANT: LEGACY V0 CODE
# This file is part of the legacy (V0) implementation of OpenHands and will be removed soon as we complete the migration to V1.
# OpenHands V1 uses the Software Agent SDK for the agentic core and runs a new application server. Please refer to:
# - V1 agentic core (SDK): https://github.com/OpenHands/software-agent-sdk

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