mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a001491c3 | |||
| 11c11e633a |
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
Executable
+14
@@ -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"
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
Generated
+62
-63
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}')
|
||||
|
||||
Executable
+270
@@ -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
@@ -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");
|
||||
});
|
||||
});
|
||||
+82
@@ -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;
|
||||
|
||||
Generated
+2
-91
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
+13
-10
@@ -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}
|
||||
|
||||
+25
-13
@@ -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(
|
||||
|
||||
@@ -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 }),
|
||||
};
|
||||
}
|
||||
@@ -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,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,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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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,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,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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user