mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5ea2ac478 |
@@ -7,8 +7,5 @@ git config --global --add safe.directory "$(realpath .)"
|
||||
# Install `nc`
|
||||
sudo apt update && sudo apt install netcat -y
|
||||
|
||||
# Install `uv` and `uvx`
|
||||
wget -qO- https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# Do common setup tasks
|
||||
source .openhands/setup.sh
|
||||
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def find_version_references(directory: str) -> tuple[set[str], set[str]]:
|
||||
openhands_versions = set()
|
||||
runtime_versions = set()
|
||||
|
||||
version_pattern_openhands = re.compile(r'openhands:(\d{1})\.(\d{2})')
|
||||
version_pattern_runtime = re.compile(r'runtime:(\d{1})\.(\d{2})')
|
||||
|
||||
for root, _, files in os.walk(directory):
|
||||
# Skip .git directory and docs/build directory
|
||||
if '.git' in root or 'docs/build' in root:
|
||||
continue
|
||||
|
||||
for file in files:
|
||||
if file.endswith(
|
||||
('.md', '.yml', '.yaml', '.txt', '.html', '.py', '.js', '.ts')
|
||||
):
|
||||
file_path = os.path.join(root, file)
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Find all openhands version references
|
||||
matches = version_pattern_openhands.findall(content)
|
||||
if matches:
|
||||
print(f'Found openhands version {matches} in {file_path}')
|
||||
openhands_versions.update(matches)
|
||||
|
||||
# Find all runtime version references
|
||||
matches = version_pattern_runtime.findall(content)
|
||||
if matches:
|
||||
print(f'Found runtime version {matches} in {file_path}')
|
||||
runtime_versions.update(matches)
|
||||
except Exception as e:
|
||||
print(f'Error reading {file_path}: {e}', file=sys.stderr)
|
||||
|
||||
return openhands_versions, runtime_versions
|
||||
|
||||
|
||||
def main():
|
||||
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
print(f'Checking version consistency in {repo_root}')
|
||||
openhands_versions, runtime_versions = find_version_references(repo_root)
|
||||
|
||||
print(f'Found openhands versions: {sorted(openhands_versions)}')
|
||||
print(f'Found runtime versions: {sorted(runtime_versions)}')
|
||||
|
||||
exit_code = 0
|
||||
|
||||
if len(openhands_versions) > 1:
|
||||
print('Error: Multiple openhands versions found:', file=sys.stderr)
|
||||
print('Found versions:', sorted(openhands_versions), file=sys.stderr)
|
||||
exit_code = 1
|
||||
elif len(openhands_versions) == 0:
|
||||
print('Warning: No openhands version references found', file=sys.stderr)
|
||||
|
||||
if len(runtime_versions) > 1:
|
||||
print('Error: Multiple runtime versions found:', file=sys.stderr)
|
||||
print('Found versions:', sorted(runtime_versions), file=sys.stderr)
|
||||
exit_code = 1
|
||||
elif len(runtime_versions) == 0:
|
||||
print('Warning: No runtime version references found', file=sys.stderr)
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -71,14 +71,6 @@ jobs:
|
||||
|
||||
echo "✅ Build & test finished without ❌ markers"
|
||||
|
||||
- name: Verify binary files exist
|
||||
run: |
|
||||
if ! ls openhands-cli/dist/openhands* 1> /dev/null 2>&1; then
|
||||
echo "❌ No binaries found to upload!"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Found binaries to upload."
|
||||
|
||||
- name: Upload binary artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
|
||||
# Builds the runtime Docker images
|
||||
ghcr_build_runtime:
|
||||
name: Build Runtime Image
|
||||
name: Build Image
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2204
|
||||
if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ext-v'))"
|
||||
permissions:
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
test_runtime_root:
|
||||
name: RT Unit Tests (Root)
|
||||
needs: [ghcr_build_runtime, define-matrix]
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2204
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -298,7 +298,7 @@ jobs:
|
||||
# We install pytest-xdist in order to run tests across CPUs
|
||||
poetry run pip install pytest-xdist
|
||||
|
||||
# Install to be able to retry on failures for flakey tests
|
||||
# Install to be able to retry on failures for flaky tests
|
||||
poetry run pip install pytest-rerunfailures
|
||||
|
||||
image_name=ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
|
||||
@@ -311,14 +311,14 @@ jobs:
|
||||
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
|
||||
TEST_IN_CI=true \
|
||||
RUN_AS_OPENHANDS=false \
|
||||
poetry run pytest -n 5 -raRs --reruns 2 --reruns-delay 3 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
|
||||
poetry run pytest -n 0 -raRs --reruns 2 --reruns-delay 5 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
|
||||
env:
|
||||
DEBUG: "1"
|
||||
|
||||
# Run unit tests with the Docker runtime Docker images as openhands user
|
||||
test_runtime_oh:
|
||||
name: RT Unit Tests (openhands)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2204
|
||||
needs: [ghcr_build_runtime, define-matrix]
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -370,7 +370,7 @@ jobs:
|
||||
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
|
||||
TEST_IN_CI=true \
|
||||
RUN_AS_OPENHANDS=true \
|
||||
poetry run pytest -n 5 -raRs --reruns 2 --reruns-delay 3 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
|
||||
poetry run pytest -n 0 -raRs --reruns 2 --reruns-delay 5 -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
|
||||
env:
|
||||
DEBUG: "1"
|
||||
|
||||
|
||||
@@ -90,3 +90,16 @@ jobs:
|
||||
- name: Run pre-commit hooks
|
||||
working-directory: ./openhands-cli
|
||||
run: pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml
|
||||
|
||||
# Check version consistency across documentation
|
||||
check-version-consistency:
|
||||
name: Check version consistency
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
with:
|
||||
python-version: 3.12
|
||||
- name: Run version consistency check
|
||||
run: .github/scripts/check_version_consistency.py
|
||||
|
||||
@@ -48,10 +48,7 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "poetry"
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: |
|
||||
poetry install --with dev,test,runtime
|
||||
poetry run pip install pytest-xdist
|
||||
poetry run pip install pytest-rerunfailures
|
||||
run: poetry install --with dev,test,runtime
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
- name: Run Unit Tests
|
||||
@@ -59,7 +56,7 @@ jobs:
|
||||
env:
|
||||
COVERAGE_FILE: ".coverage.${{ matrix.python_version }}"
|
||||
- name: Run Runtime Tests with CLIRuntime
|
||||
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -n 5 --reruns 2 --reruns-delay 3 -s tests/runtime/test_bash.py --cov=openhands --cov-branch
|
||||
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -s tests/runtime/test_bash.py --cov=openhands --cov-branch
|
||||
env:
|
||||
COVERAGE_FILE: ".coverage.runtime.${{ matrix.python_version }}"
|
||||
- name: Store coverage file
|
||||
@@ -91,7 +88,7 @@ jobs:
|
||||
- name: Install Python dependencies using Poetry
|
||||
run: poetry install --with dev,test,runtime
|
||||
- name: Run Windows unit tests
|
||||
run: poetry run pytest -svv tests/runtime//test_windows_bash.py
|
||||
run: poetry run pytest -svv tests/unit/runtime/utils/test_windows_bash.py
|
||||
env:
|
||||
PYTHONPATH: ".;$env:PYTHONPATH"
|
||||
DEBUG: "1"
|
||||
@@ -176,6 +173,7 @@ jobs:
|
||||
path: ".coverage.openhands-cli.${{ matrix.python-version }}"
|
||||
include-hidden-files: true
|
||||
|
||||
|
||||
coverage-comment:
|
||||
name: Coverage Comment
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
+1
-1
@@ -159,7 +159,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
|
||||
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/openhands/runtime:0.61-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/openhands/runtime:0.60-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or [sign up for
|
||||
|
||||
## ☁️ OpenHands Cloud
|
||||
The easiest way to get started with OpenHands is on [OpenHands Cloud](https://app.all-hands.dev),
|
||||
which comes with $10 in free credits for new users.
|
||||
which comes with $20 in free credits for new users.
|
||||
|
||||
## 💻 Running OpenHands Locally
|
||||
|
||||
@@ -82,17 +82,17 @@ You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)
|
||||
You can also run OpenHands directly with Docker:
|
||||
|
||||
```bash
|
||||
docker pull docker.openhands.dev/openhands/runtime:0.61-nikolaik
|
||||
docker pull docker.openhands.dev/openhands/runtime:0.60-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:0.61-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:0.60-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.openhands.dev/openhands/openhands:0.61
|
||||
docker.openhands.dev/openhands/openhands:0.60
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
- DOCKER_HOST_ADDR=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/openhands/runtime:0.61-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/openhands/runtime:0.60-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.openhands.dev/openhands/runtime:0.61-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.openhands.dev/openhands/runtime:0.60-nikolaik}
|
||||
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of ~/.openhands for this user
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
"""add status and updated_at to callback
|
||||
|
||||
Revision ID: 080
|
||||
Revises: 079
|
||||
Create Date: 2025-11-05 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '080'
|
||||
down_revision: Union[str, None] = '079'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
class EventCallbackStatus(Enum):
|
||||
ACTIVE = 'ACTIVE'
|
||||
DISABLED = 'DISABLED'
|
||||
COMPLETED = 'COMPLETED'
|
||||
ERROR = 'ERROR'
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
status = sa.Enum(EventCallbackStatus, name='eventcallbackstatus')
|
||||
status.create(op.get_bind(), checkfirst=True)
|
||||
op.add_column(
|
||||
'event_callback',
|
||||
sa.Column('status', status, nullable=False, server_default='ACTIVE'),
|
||||
)
|
||||
op.add_column(
|
||||
'event_callback',
|
||||
sa.Column(
|
||||
'updated_at', sa.DateTime, nullable=False, server_default=sa.func.now()
|
||||
),
|
||||
)
|
||||
op.drop_index('ix_event_callback_result_event_id')
|
||||
op.drop_column('event_callback_result', 'event_id')
|
||||
op.add_column(
|
||||
'event_callback_result', sa.Column('event_id', sa.String, nullable=True)
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_event_callback_result_event_id'),
|
||||
'event_callback_result',
|
||||
['event_id'],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_column('event_callback', 'status')
|
||||
op.drop_column('event_callback', 'updated_at')
|
||||
op.drop_index('ix_event_callback_result_event_id')
|
||||
op.drop_column('event_callback_result', 'event_id')
|
||||
op.add_column(
|
||||
'event_callback_result', sa.Column('event_id', sa.UUID, nullable=True)
|
||||
)
|
||||
op.create_index(
|
||||
op.f('ix_event_callback_result_event_id'),
|
||||
'event_callback_result',
|
||||
['event_id'],
|
||||
unique=False,
|
||||
)
|
||||
op.execute('DROP TYPE eventcallbackstatus')
|
||||
Generated
+30
-295
@@ -201,20 +201,19 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "anthropic"
|
||||
version = "0.72.0"
|
||||
version = "0.65.0"
|
||||
description = "The official Python library for the anthropic API"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "anthropic-0.72.0-py3-none-any.whl", hash = "sha256:0e9f5a7582f038cab8efbb4c959e49ef654a56bfc7ba2da51b5a7b8a84de2e4d"},
|
||||
{file = "anthropic-0.72.0.tar.gz", hash = "sha256:8971fe76dcffc644f74ac3883069beb1527641115ae0d6eb8fa21c1ce4082f7a"},
|
||||
{file = "anthropic-0.65.0-py3-none-any.whl", hash = "sha256:ba9d9f82678046c74ddf5698ca06d9f5b0f599cfac922ab0d5921638eb448d98"},
|
||||
{file = "anthropic-0.65.0.tar.gz", hash = "sha256:6b6b6942574e54342050dfd42b8d856a8366b171daec147df3b80be4722733b9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.5.0,<5"
|
||||
distro = ">=1.7.0,<2"
|
||||
docstring-parser = ">=0.15,<1"
|
||||
google-auth = {version = ">=2,<3", extras = ["requests"], optional = true, markers = "extra == \"vertex\""}
|
||||
httpx = ">=0.25.0,<1"
|
||||
jiter = ">=0.4.0,<1"
|
||||
@@ -223,7 +222,7 @@ sniffio = "*"
|
||||
typing-extensions = ">=4.10,<5"
|
||||
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"]
|
||||
aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.8)"]
|
||||
bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"]
|
||||
vertex = ["google-auth[requests] (>=2,<3)"]
|
||||
|
||||
@@ -682,34 +681,31 @@ crt = ["awscrt (==0.27.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "browser-use"
|
||||
version = "0.9.5"
|
||||
version = "0.7.10"
|
||||
description = "Make websites accessible for AI agents"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.11"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "browser_use-0.9.5-py3-none-any.whl", hash = "sha256:4a2e92847204d1ded269026a99cb0cc0e60e38bd2751fa3f58aedd78f00b4e67"},
|
||||
{file = "browser_use-0.9.5.tar.gz", hash = "sha256:f8285fe253b149d01769a7084883b4cf4db351e2f38e26302c157bcbf14a703f"},
|
||||
{file = "browser_use-0.7.10-py3-none-any.whl", hash = "sha256:669e12571a0c0c4c93e5fd26abf9e2534eb9bacbc510328aedcab795bd8906a9"},
|
||||
{file = "browser_use-0.7.10.tar.gz", hash = "sha256:f93ce59e06906c12d120360dee4aa33d83618ddf7c9a575dd0ac517d2de7ccbc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = "3.12.15"
|
||||
anthropic = ">=0.68.1,<1.0.0"
|
||||
anthropic = ">=0.58.2,<1.0.0"
|
||||
anyio = ">=4.9.0"
|
||||
authlib = ">=1.6.0"
|
||||
bubus = ">=1.5.6"
|
||||
cdp-use = ">=1.4.0"
|
||||
click = ">=8.1.8"
|
||||
cloudpickle = ">=3.1.1"
|
||||
google-api-core = ">=2.25.0"
|
||||
google-api-python-client = ">=2.174.0"
|
||||
google-auth = ">=2.40.3"
|
||||
google-auth-oauthlib = ">=1.2.2"
|
||||
google-genai = ">=1.29.0,<2.0.0"
|
||||
groq = ">=0.30.0"
|
||||
html2text = ">=2025.4.15"
|
||||
httpx = ">=0.28.1"
|
||||
inquirerpy = ">=0.3.4"
|
||||
markdownify = ">=1.2.0"
|
||||
mcp = ">=1.10.1"
|
||||
ollama = ">=0.5.1"
|
||||
openai = ">=1.99.2,<2.0.0"
|
||||
@@ -724,20 +720,16 @@ pypdf = ">=5.7.0"
|
||||
python-dotenv = ">=1.0.1"
|
||||
reportlab = ">=4.0.0"
|
||||
requests = ">=2.32.3"
|
||||
rich = ">=14.0.0"
|
||||
screeninfo = {version = ">=0.8.1", markers = "platform_system != \"darwin\""}
|
||||
typing-extensions = ">=4.12.2"
|
||||
uuid7 = ">=0.1.0"
|
||||
|
||||
[package.extras]
|
||||
all = ["agentmail (==0.0.59)", "boto3 (>=1.38.45)", "botocore (>=1.37.23)", "imgcat (>=0.6.0)", "langchain-openai (>=0.3.26)", "oci (>=2.126.4)", "textual (>=3.2.0)"]
|
||||
all = ["agentmail (>=0.0.53)", "boto3 (>=1.38.45)", "botocore (>=1.37.23)", "click (>=8.1.8)", "imgcat (>=0.6.0)", "langchain-openai (>=0.3.26)", "rich (>=14.0.0)", "textual (>=3.2.0)"]
|
||||
aws = ["boto3 (>=1.38.45)"]
|
||||
cli = ["textual (>=3.2.0)"]
|
||||
cli-oci = ["oci (>=2.126.4)", "textual (>=3.2.0)"]
|
||||
code = ["matplotlib (>=3.9.0)", "numpy (>=2.3.2)", "pandas (>=2.2.0)", "tabulate (>=0.9.0)"]
|
||||
eval = ["anyio (>=4.9.0)", "datamodel-code-generator (>=0.26.0)", "lmnr[all] (==0.7.17)", "psutil (>=7.0.0)"]
|
||||
examples = ["agentmail (==0.0.59)", "botocore (>=1.37.23)", "imgcat (>=0.6.0)", "langchain-openai (>=0.3.26)"]
|
||||
oci = ["oci (>=2.126.4)"]
|
||||
cli = ["click (>=8.1.8)", "rich (>=14.0.0)", "textual (>=3.2.0)"]
|
||||
eval = ["anyio (>=4.9.0)", "browserbase (==1.4.0)", "datamodel-code-generator (>=0.26.0)", "hyperbrowser (==0.47.0)", "lmnr[all] (==0.7.10)", "psutil (>=7.0.0)"]
|
||||
examples = ["agentmail (>=0.0.53)", "botocore (>=1.37.23)", "imgcat (>=0.6.0)", "langchain-openai (>=0.3.26)"]
|
||||
video = ["imageio[ffmpeg] (>=2.37.0)", "numpy (>=2.3.2)"]
|
||||
|
||||
[[package]]
|
||||
@@ -3533,25 +3525,6 @@ files = [
|
||||
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inquirerpy"
|
||||
version = "0.3.4"
|
||||
description = "Python port of Inquirer.js (A collection of common interactive command-line user interfaces)"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "InquirerPy-0.3.4-py3-none-any.whl", hash = "sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4"},
|
||||
{file = "InquirerPy-0.3.4.tar.gz", hash = "sha256:89d2ada0111f337483cb41ae31073108b2ec1e618a49d7110b0d7ade89fc197e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pfzy = ">=0.3.1,<0.4.0"
|
||||
prompt-toolkit = ">=3.0.1,<4.0.0"
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17-beta.43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "installer"
|
||||
version = "0.7.0"
|
||||
@@ -4607,62 +4580,6 @@ files = [
|
||||
{file = "llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lmnr"
|
||||
version = "0.7.20"
|
||||
description = "Python SDK for Laminar"
|
||||
optional = false
|
||||
python-versions = "<4,>=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "lmnr-0.7.20-py3-none-any.whl", hash = "sha256:5f9fa7444e6f96c25e097f66484ff29e632bdd1de0e9346948bf5595f4a8af38"},
|
||||
{file = "lmnr-0.7.20.tar.gz", hash = "sha256:1f484cd618db2d71af65f90a0b8b36d20d80dc91a5138b811575c8677bf7c4fd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
grpcio = ">=1"
|
||||
httpx = ">=0.24.0"
|
||||
opentelemetry-api = ">=1.33.0"
|
||||
opentelemetry-exporter-otlp-proto-grpc = ">=1.33.0"
|
||||
opentelemetry-exporter-otlp-proto-http = ">=1.33.0"
|
||||
opentelemetry-instrumentation = ">=0.54b0"
|
||||
opentelemetry-instrumentation-threading = ">=0.57b0"
|
||||
opentelemetry-sdk = ">=1.33.0"
|
||||
opentelemetry-semantic-conventions = ">=0.54b0"
|
||||
opentelemetry-semantic-conventions-ai = ">=0.4.13"
|
||||
orjson = ">=3.0.0"
|
||||
packaging = ">=22.0"
|
||||
pydantic = ">=2.0.3,<3.0.0"
|
||||
python-dotenv = ">=1.0"
|
||||
tenacity = ">=8.0"
|
||||
tqdm = ">=4.0"
|
||||
|
||||
[package.extras]
|
||||
alephalpha = ["opentelemetry-instrumentation-alephalpha (>=0.47.1)"]
|
||||
all = ["opentelemetry-instrumentation-alephalpha (>=0.47.1)", "opentelemetry-instrumentation-bedrock (>=0.47.1)", "opentelemetry-instrumentation-chromadb (>=0.47.1)", "opentelemetry-instrumentation-cohere (>=0.47.1)", "opentelemetry-instrumentation-crewai (>=0.47.1)", "opentelemetry-instrumentation-haystack (>=0.47.1)", "opentelemetry-instrumentation-lancedb (>=0.47.1)", "opentelemetry-instrumentation-langchain (>=0.47.1)", "opentelemetry-instrumentation-llamaindex (>=0.47.1)", "opentelemetry-instrumentation-marqo (>=0.47.1)", "opentelemetry-instrumentation-mcp (>=0.47.1)", "opentelemetry-instrumentation-milvus (>=0.47.1)", "opentelemetry-instrumentation-mistralai (>=0.47.1)", "opentelemetry-instrumentation-ollama (>=0.47.1)", "opentelemetry-instrumentation-pinecone (>=0.47.1)", "opentelemetry-instrumentation-qdrant (>=0.47.1)", "opentelemetry-instrumentation-replicate (>=0.47.1)", "opentelemetry-instrumentation-sagemaker (>=0.47.1)", "opentelemetry-instrumentation-together (>=0.47.1)", "opentelemetry-instrumentation-transformers (>=0.47.1)", "opentelemetry-instrumentation-vertexai (>=0.47.1)", "opentelemetry-instrumentation-watsonx (>=0.47.1)", "opentelemetry-instrumentation-weaviate (>=0.47.1)"]
|
||||
bedrock = ["opentelemetry-instrumentation-bedrock (>=0.47.1)"]
|
||||
chromadb = ["opentelemetry-instrumentation-chromadb (>=0.47.1)"]
|
||||
cohere = ["opentelemetry-instrumentation-cohere (>=0.47.1)"]
|
||||
crewai = ["opentelemetry-instrumentation-crewai (>=0.47.1)"]
|
||||
haystack = ["opentelemetry-instrumentation-haystack (>=0.47.1)"]
|
||||
lancedb = ["opentelemetry-instrumentation-lancedb (>=0.47.1)"]
|
||||
langchain = ["opentelemetry-instrumentation-langchain (>=0.47.1)"]
|
||||
llamaindex = ["opentelemetry-instrumentation-llamaindex (>=0.47.1)"]
|
||||
marqo = ["opentelemetry-instrumentation-marqo (>=0.47.1)"]
|
||||
mcp = ["opentelemetry-instrumentation-mcp (>=0.47.1)"]
|
||||
milvus = ["opentelemetry-instrumentation-milvus (>=0.47.1)"]
|
||||
mistralai = ["opentelemetry-instrumentation-mistralai (>=0.47.1)"]
|
||||
ollama = ["opentelemetry-instrumentation-ollama (>=0.47.1)"]
|
||||
pinecone = ["opentelemetry-instrumentation-pinecone (>=0.47.1)"]
|
||||
qdrant = ["opentelemetry-instrumentation-qdrant (>=0.47.1)"]
|
||||
replicate = ["opentelemetry-instrumentation-replicate (>=0.47.1)"]
|
||||
sagemaker = ["opentelemetry-instrumentation-sagemaker (>=0.47.1)"]
|
||||
together = ["opentelemetry-instrumentation-together (>=0.47.1)"]
|
||||
transformers = ["opentelemetry-instrumentation-transformers (>=0.47.1)"]
|
||||
vertexai = ["opentelemetry-instrumentation-vertexai (>=0.47.1)"]
|
||||
watsonx = ["opentelemetry-instrumentation-watsonx (>=0.47.1)"]
|
||||
weaviate = ["opentelemetry-instrumentation-weaviate (>=0.47.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "6.0.1"
|
||||
@@ -5820,7 +5737,7 @@ 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.0.0a5"
|
||||
version = "1.0.0a4"
|
||||
description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
|
||||
optional = false
|
||||
python-versions = ">=3.12"
|
||||
@@ -5841,14 +5758,14 @@ wsproto = ">=1.2.0"
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/OpenHands/software-agent-sdk.git"
|
||||
reference = "d5995c31c55e488d4ab0372d292973bc6fad71f1"
|
||||
resolved_reference = "d5995c31c55e488d4ab0372d292973bc6fad71f1"
|
||||
url = "https://github.com/OpenHands/agent-sdk.git"
|
||||
reference = "3d8af53b2f0259dc98555a4acd4238f90e0afbce"
|
||||
resolved_reference = "3d8af53b2f0259dc98555a4acd4238f90e0afbce"
|
||||
subdirectory = "openhands-agent-server"
|
||||
|
||||
[[package]]
|
||||
name = "openhands-ai"
|
||||
version = "0.0.0-post.5514+7c9e66194"
|
||||
version = "0.59.0"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
optional = false
|
||||
python-versions = "^3.12,<3.14"
|
||||
@@ -5884,14 +5801,13 @@ jupyter_kernel_gateway = "*"
|
||||
kubernetes = "^33.1.0"
|
||||
libtmux = ">=0.46.2"
|
||||
litellm = ">=1.74.3, <1.78.0, !=1.64.4, !=1.67.*"
|
||||
lmnr = "^0.7.20"
|
||||
memory-profiler = "^0.61.0"
|
||||
numpy = "*"
|
||||
openai = "1.99.9"
|
||||
openhands-aci = "0.3.2"
|
||||
openhands-agent-server = {git = "https://github.com/OpenHands/software-agent-sdk.git", rev = "d5995c31c55e488d4ab0372d292973bc6fad71f1", subdirectory = "openhands-agent-server"}
|
||||
openhands-sdk = {git = "https://github.com/OpenHands/software-agent-sdk.git", rev = "d5995c31c55e488d4ab0372d292973bc6fad71f1", subdirectory = "openhands-sdk"}
|
||||
openhands-tools = {git = "https://github.com/OpenHands/software-agent-sdk.git", rev = "d5995c31c55e488d4ab0372d292973bc6fad71f1", subdirectory = "openhands-tools"}
|
||||
openhands-agent-server = {git = "https://github.com/OpenHands/agent-sdk.git", rev = "3d8af53b2f0259dc98555a4acd4238f90e0afbce", subdirectory = "openhands-agent-server"}
|
||||
openhands-sdk = {git = "https://github.com/OpenHands/agent-sdk.git", rev = "3d8af53b2f0259dc98555a4acd4238f90e0afbce", subdirectory = "openhands-sdk"}
|
||||
openhands-tools = {git = "https://github.com/OpenHands/agent-sdk.git", rev = "3d8af53b2f0259dc98555a4acd4238f90e0afbce", subdirectory = "openhands-tools"}
|
||||
opentelemetry-api = "^1.33.1"
|
||||
opentelemetry-exporter-otlp-proto-grpc = "^1.33.1"
|
||||
pathspec = "^0.12.1"
|
||||
@@ -5947,7 +5863,7 @@ url = ".."
|
||||
|
||||
[[package]]
|
||||
name = "openhands-sdk"
|
||||
version = "1.0.0a5"
|
||||
version = "1.0.0a4"
|
||||
description = "OpenHands SDK - Core functionality for building AI agents"
|
||||
optional = false
|
||||
python-versions = ">=3.12"
|
||||
@@ -5959,7 +5875,6 @@ develop = false
|
||||
fastmcp = ">=2.11.3"
|
||||
httpx = ">=0.27.0"
|
||||
litellm = ">=1.77.7.dev9"
|
||||
lmnr = ">=0.7.20"
|
||||
pydantic = ">=2.11.7"
|
||||
python-frontmatter = ">=1.1.0"
|
||||
python-json-logger = ">=3.3.0"
|
||||
@@ -5971,14 +5886,14 @@ boto3 = ["boto3 (>=1.35.0)"]
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/OpenHands/software-agent-sdk.git"
|
||||
reference = "d5995c31c55e488d4ab0372d292973bc6fad71f1"
|
||||
resolved_reference = "d5995c31c55e488d4ab0372d292973bc6fad71f1"
|
||||
url = "https://github.com/OpenHands/agent-sdk.git"
|
||||
reference = "3d8af53b2f0259dc98555a4acd4238f90e0afbce"
|
||||
resolved_reference = "3d8af53b2f0259dc98555a4acd4238f90e0afbce"
|
||||
subdirectory = "openhands-sdk"
|
||||
|
||||
[[package]]
|
||||
name = "openhands-tools"
|
||||
version = "1.0.0a5"
|
||||
version = "1.0.0a4"
|
||||
description = "OpenHands Tools - Runtime tools for AI agents"
|
||||
optional = false
|
||||
python-versions = ">=3.12"
|
||||
@@ -5989,7 +5904,7 @@ develop = false
|
||||
[package.dependencies]
|
||||
bashlex = ">=0.18"
|
||||
binaryornot = ">=0.4.4"
|
||||
browser-use = ">=0.8.0"
|
||||
browser-use = ">=0.7.7"
|
||||
cachetools = "*"
|
||||
func-timeout = ">=4.3.5"
|
||||
libtmux = ">=0.46.2"
|
||||
@@ -5998,9 +5913,9 @@ pydantic = ">=2.11.7"
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/OpenHands/software-agent-sdk.git"
|
||||
reference = "d5995c31c55e488d4ab0372d292973bc6fad71f1"
|
||||
resolved_reference = "d5995c31c55e488d4ab0372d292973bc6fad71f1"
|
||||
url = "https://github.com/OpenHands/agent-sdk.git"
|
||||
reference = "3d8af53b2f0259dc98555a4acd4238f90e0afbce"
|
||||
resolved_reference = "3d8af53b2f0259dc98555a4acd4238f90e0afbce"
|
||||
subdirectory = "openhands-tools"
|
||||
|
||||
[[package]]
|
||||
@@ -6073,62 +5988,6 @@ opentelemetry-proto = "1.36.0"
|
||||
opentelemetry-sdk = ">=1.36.0,<1.37.0"
|
||||
typing-extensions = ">=4.6.0"
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-exporter-otlp-proto-http"
|
||||
version = "1.36.0"
|
||||
description = "OpenTelemetry Collector Protobuf over HTTP Exporter"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "opentelemetry_exporter_otlp_proto_http-1.36.0-py3-none-any.whl", hash = "sha256:3d769f68e2267e7abe4527f70deb6f598f40be3ea34c6adc35789bea94a32902"},
|
||||
{file = "opentelemetry_exporter_otlp_proto_http-1.36.0.tar.gz", hash = "sha256:dd3637f72f774b9fc9608ab1ac479f8b44d09b6fb5b2f3df68a24ad1da7d356e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
googleapis-common-protos = ">=1.52,<2.0"
|
||||
opentelemetry-api = ">=1.15,<2.0"
|
||||
opentelemetry-exporter-otlp-proto-common = "1.36.0"
|
||||
opentelemetry-proto = "1.36.0"
|
||||
opentelemetry-sdk = ">=1.36.0,<1.37.0"
|
||||
requests = ">=2.7,<3.0"
|
||||
typing-extensions = ">=4.5.0"
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-instrumentation"
|
||||
version = "0.57b0"
|
||||
description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "opentelemetry_instrumentation-0.57b0-py3-none-any.whl", hash = "sha256:9109280f44882e07cec2850db28210b90600ae9110b42824d196de357cbddf7e"},
|
||||
{file = "opentelemetry_instrumentation-0.57b0.tar.gz", hash = "sha256:f2a30135ba77cdea2b0e1df272f4163c154e978f57214795d72f40befd4fcf05"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
opentelemetry-api = ">=1.4,<2.0"
|
||||
opentelemetry-semantic-conventions = "0.57b0"
|
||||
packaging = ">=18.0"
|
||||
wrapt = ">=1.0.0,<2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-instrumentation-threading"
|
||||
version = "0.57b0"
|
||||
description = "Thread context propagation support for OpenTelemetry"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "opentelemetry_instrumentation_threading-0.57b0-py3-none-any.whl", hash = "sha256:adfd64857c8c78d6111cf80552311e1713bad64272dd81abdd61f07b892a161b"},
|
||||
{file = "opentelemetry_instrumentation_threading-0.57b0.tar.gz", hash = "sha256:06fa4c98d6bfe4670e7532497670ac202db42afa647ff770aedce0e422421c6e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
opentelemetry-api = ">=1.12,<2.0"
|
||||
opentelemetry-instrumentation = "0.57b0"
|
||||
wrapt = ">=1.0.0,<2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-proto"
|
||||
version = "1.36.0"
|
||||
@@ -6177,115 +6036,6 @@ files = [
|
||||
opentelemetry-api = "1.36.0"
|
||||
typing-extensions = ">=4.5.0"
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-semantic-conventions-ai"
|
||||
version = "0.4.13"
|
||||
description = "OpenTelemetry Semantic Conventions Extension for Large Language Models"
|
||||
optional = false
|
||||
python-versions = "<4,>=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "opentelemetry_semantic_conventions_ai-0.4.13-py3-none-any.whl", hash = "sha256:883a30a6bb5deaec0d646912b5f9f6dcbb9f6f72557b73d0f2560bf25d13e2d5"},
|
||||
{file = "opentelemetry_semantic_conventions_ai-0.4.13.tar.gz", hash = "sha256:94efa9fb4ffac18c45f54a3a338ffeb7eedb7e1bb4d147786e77202e159f0036"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.11.4"
|
||||
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "orjson-3.11.4-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e3aa2118a3ece0d25489cbe48498de8a5d580e42e8d9979f65bf47900a15aba1"},
|
||||
{file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a69ab657a4e6733133a3dca82768f2f8b884043714e8d2b9ba9f52b6efef5c44"},
|
||||
{file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3740bffd9816fc0326ddc406098a3a8f387e42223f5f455f2a02a9f834ead80c"},
|
||||
{file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65fd2f5730b1bf7f350c6dc896173d3460d235c4be007af73986d7cd9a2acd23"},
|
||||
{file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fdc3ae730541086158d549c97852e2eea6820665d4faf0f41bf99df41bc11ea"},
|
||||
{file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e10b4d65901da88845516ce9f7f9736f9638d19a1d483b3883dc0182e6e5edba"},
|
||||
{file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb6a03a678085f64b97f9d4a9ae69376ce91a3a9e9b56a82b1580d8e1d501aff"},
|
||||
{file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c82e4f0b1c712477317434761fbc28b044c838b6b1240d895607441412371ac"},
|
||||
{file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d58c166a18f44cc9e2bad03a327dc2d1a3d2e85b847133cfbafd6bfc6719bd79"},
|
||||
{file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94f206766bf1ea30e1382e4890f763bd1eefddc580e08fec1ccdc20ddd95c827"},
|
||||
{file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:41bf25fb39a34cf8edb4398818523277ee7096689db352036a9e8437f2f3ee6b"},
|
||||
{file = "orjson-3.11.4-cp310-cp310-win32.whl", hash = "sha256:fa9627eba4e82f99ca6d29bc967f09aba446ee2b5a1ea728949ede73d313f5d3"},
|
||||
{file = "orjson-3.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:23ef7abc7fca96632d8174ac115e668c1e931b8fe4dde586e92a500bf1914dcc"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e59d23cd93ada23ec59a96f215139753fbfe3a4d989549bcb390f8c00370b39"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5c3aedecfc1beb988c27c79d52ebefab93b6c3921dbec361167e6559aba2d36d"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9e5301f1c2caa2a9a4a303480d79c9ad73560b2e7761de742ab39fe59d9175"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8873812c164a90a79f65368f8f96817e59e35d0cc02786a5356f0e2abed78040"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d7feb0741ebb15204e748f26c9638e6665a5fa93c37a2c73d64f1669b0ddc63"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ee5487fefee21e6910da4c2ee9eef005bee568a0879834df86f888d2ffbdd9"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d40d46f348c0321df01507f92b95a377240c4ec31985225a6668f10e2676f9a"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95713e5fc8af84d8edc75b785d2386f653b63d62b16d681687746734b4dfc0be"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad73ede24f9083614d6c4ca9a85fe70e33be7bf047ec586ee2363bc7418fe4d7"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:842289889de515421f3f224ef9c1f1efb199a32d76d8d2ca2706fa8afe749549"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3b2427ed5791619851c52a1261b45c233930977e7de8cf36de05636c708fa905"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c36e524af1d29982e9b190573677ea02781456b2e537d5840e4538a5ec41907"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-win32.whl", hash = "sha256:87255b88756eab4a68ec61837ca754e5d10fa8bc47dc57f75cedfeaec358d54c"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:e2d5d5d798aba9a0e1fede8d853fa899ce2cb930ec0857365f700dffc2c7af6a"},
|
||||
{file = "orjson-3.11.4-cp311-cp311-win_arm64.whl", hash = "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d4371de39319d05d3f482f372720b841c841b52f5385bd99c61ed69d55d9ab50"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e41fd3b3cac850eaae78232f37325ed7d7436e11c471246b87b2cd294ec94853"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600e0e9ca042878c7fdf189cf1b028fe2c1418cc9195f6cb9824eb6ed99cb938"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7bbf9b333f1568ef5da42bc96e18bf30fd7f8d54e9ae066d711056add508e415"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806363144bb6e7297b8e95870e78d30a649fdc4e23fc84daa80c8ebd366ce44"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad355e8308493f527d41154e9053b86a5be892b3b359a5c6d5d95cda23601cb2"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a7517482667fb9f0ff1b2f16fe5829296ed7a655d04d68cd9711a4d8a4e708"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97eb5942c7395a171cbfecc4ef6701fc3c403e762194683772df4c54cfbb2210"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:149d95d5e018bdd822e3f38c103b1a7c91f88d38a88aada5c4e9b3a73a244241"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:624f3951181eb46fc47dea3d221554e98784c823e7069edb5dbd0dc826ac909b"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:03bfa548cf35e3f8b3a96c4e8e41f753c686ff3d8e182ce275b1751deddab58c"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:525021896afef44a68148f6ed8a8bf8375553d6066c7f48537657f64823565b9"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-win32.whl", hash = "sha256:b58430396687ce0f7d9eeb3dd47761ca7d8fda8e9eb92b3077a7a353a75efefa"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:c6dbf422894e1e3c80a177133c0dda260f81428f9de16d61041949f6a2e5c140"},
|
||||
{file = "orjson-3.11.4-cp312-cp312-win_arm64.whl", hash = "sha256:d38d2bc06d6415852224fcc9c0bfa834c25431e466dc319f0edd56cca81aa96e"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a"},
|
||||
{file = "orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-win32.whl", hash = "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1"},
|
||||
{file = "orjson-3.11.4-cp314-cp314-win_arm64.whl", hash = "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d"},
|
||||
{file = "orjson-3.11.4-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:405261b0a8c62bcbd8e2931c26fdc08714faf7025f45531541e2b29e544b545b"},
|
||||
{file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af02ff34059ee9199a3546f123a6ab4c86caf1708c79042caf0820dc290a6d4f"},
|
||||
{file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b2eba969ea4203c177c7b38b36c69519e6067ee68c34dc37081fac74c796e10"},
|
||||
{file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0baa0ea43cfa5b008a28d3c07705cf3ada40e5d347f0f44994a64b1b7b4b5350"},
|
||||
{file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80fd082f5dcc0e94657c144f1b2a3a6479c44ad50be216cf0c244e567f5eae19"},
|
||||
{file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e3704d35e47d5bee811fb1cbd8599f0b4009b14d451c4c57be5a7e25eb89a13"},
|
||||
{file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa447f2b5356779d914658519c874cf3b7629e99e63391ed519c28c8aea4919"},
|
||||
{file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bba5118143373a86f91dadb8df41d9457498226698ebdf8e11cbb54d5b0e802d"},
|
||||
{file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:622463ab81d19ef3e06868b576551587de8e4d518892d1afab71e0fbc1f9cffc"},
|
||||
{file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3e0a700c4b82144b72946b6629968df9762552ee1344bfdb767fecdd634fbd5a"},
|
||||
{file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6e18a5c15e764e5f3fc569b47872450b4bcea24f2a6354c0a0e95ad21045d5a9"},
|
||||
{file = "orjson-3.11.4-cp39-cp39-win32.whl", hash = "sha256:fb1c37c71cad991ef4d89c7a634b5ffb4447dbd7ae3ae13e8f5ee7f1775e7ab1"},
|
||||
{file = "orjson-3.11.4-cp39-cp39-win_amd64.whl", hash = "sha256:e2985ce8b8c42d00492d0ed79f2bd2b6460d00f2fa671dfde4bf2e02f49bf5c6"},
|
||||
{file = "orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
@@ -6502,21 +6252,6 @@ files = [
|
||||
[package.dependencies]
|
||||
ptyprocess = ">=0.5"
|
||||
|
||||
[[package]]
|
||||
name = "pfzy"
|
||||
version = "0.3.4"
|
||||
description = "Python port of the fzy fuzzy string matching algorithm"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pfzy-0.3.4-py3-none-any.whl", hash = "sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96"},
|
||||
{file = "pfzy-0.3.4.tar.gz", hash = "sha256:717ea765dd10b63618e7298b2d98efd819e0b30cd5905c9707223dceeb94b3f1"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17-beta.43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pg8000"
|
||||
version = "1.31.5"
|
||||
|
||||
@@ -50,7 +50,7 @@ SUBSCRIPTION_PRICE_DATA = {
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_INITIAL_BUDGET = float(os.environ.get('DEFAULT_INITIAL_BUDGET', '10'))
|
||||
DEFAULT_INITIAL_BUDGET = float(os.environ.get('DEFAULT_INITIAL_BUDGET', '20'))
|
||||
STRIPE_API_KEY = os.environ.get('STRIPE_API_KEY', None)
|
||||
STRIPE_WEBHOOK_SECRET = os.environ.get('STRIPE_WEBHOOK_SECRET', None)
|
||||
REQUIRE_PAYMENT = os.environ.get('REQUIRE_PAYMENT', '0') in ('1', 'true')
|
||||
|
||||
@@ -35,7 +35,6 @@ class SaasConversationStore(ConversationStore):
|
||||
session.query(StoredConversationMetadata)
|
||||
.filter(StoredConversationMetadata.user_id == self.user_id)
|
||||
.filter(StoredConversationMetadata.conversation_id == conversation_id)
|
||||
.filter(StoredConversationMetadata.conversation_version == 'V0')
|
||||
)
|
||||
|
||||
def _to_external_model(self, conversation_metadata: StoredConversationMetadata):
|
||||
@@ -124,7 +123,6 @@ class SaasConversationStore(ConversationStore):
|
||||
conversations = (
|
||||
session.query(StoredConversationMetadata)
|
||||
.filter(StoredConversationMetadata.user_id == self.user_id)
|
||||
.filter(StoredConversationMetadata.conversation_version == 'V0')
|
||||
.order_by(StoredConversationMetadata.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit + 1)
|
||||
|
||||
@@ -243,7 +243,7 @@ async def test_update_settings_with_litellm_default(
|
||||
# Check that the URL and most of the JSON payload match what we expect
|
||||
assert call_args['json']['user_email'] == 'testy@tester.com'
|
||||
assert call_args['json']['models'] == []
|
||||
assert call_args['json']['max_budget'] == 10.0
|
||||
assert call_args['json']['max_budget'] == 20.0
|
||||
assert call_args['json']['user_id'] == 'user-id'
|
||||
assert call_args['json']['teams'] == ['test_team']
|
||||
assert call_args['json']['auto_create_key'] is True
|
||||
|
||||
@@ -8,13 +8,6 @@ vi.mock("#/hooks/use-auth-url", () => ({
|
||||
useAuthUrl: () => "https://gitlab.com/oauth/authorize",
|
||||
}));
|
||||
|
||||
// Mock the useTracking hook
|
||||
vi.mock("#/hooks/use-tracking", () => ({
|
||||
useTracking: () => ({
|
||||
trackLoginButtonClick: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("AuthModal", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("location", { href: "" });
|
||||
|
||||
@@ -13,6 +13,34 @@ vi.mock("#/hooks/use-agent-state", () => ({
|
||||
useAgentState: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the custom hooks
|
||||
const mockStartConversationMutate = vi.fn();
|
||||
const mockStopConversationMutate = vi.fn();
|
||||
|
||||
vi.mock("#/hooks/mutation/use-unified-start-conversation", () => ({
|
||||
useUnifiedStartConversation: () => ({
|
||||
mutate: mockStartConversationMutate,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/mutation/use-unified-stop-conversation", () => ({
|
||||
useUnifiedStopConversation: () => ({
|
||||
mutate: mockStopConversationMutate,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-conversation-id", () => ({
|
||||
useConversationId: () => ({
|
||||
conversationId: "test-conversation-id",
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-user-providers", () => ({
|
||||
useUserProviders: () => ({
|
||||
providers: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/query/use-task-polling", () => ({
|
||||
useTaskPolling: () => ({
|
||||
isTask: false,
|
||||
@@ -38,12 +66,8 @@ vi.mock("react-i18next", async () => {
|
||||
COMMON$SERVER_STOPPED: "Server Stopped",
|
||||
COMMON$ERROR: "Error",
|
||||
COMMON$STARTING: "Starting",
|
||||
COMMON$STOPPING: "Stopping...",
|
||||
COMMON$STOP_RUNTIME: "Stop Runtime",
|
||||
COMMON$START_RUNTIME: "Start Runtime",
|
||||
CONVERSATION$ERROR_STARTING_CONVERSATION:
|
||||
"Error starting conversation",
|
||||
CONVERSATION$READY: "Ready",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
@@ -55,6 +79,10 @@ vi.mock("react-i18next", async () => {
|
||||
});
|
||||
|
||||
describe("ServerStatus", () => {
|
||||
// Mock functions for handlers
|
||||
const mockHandleStop = vi.fn();
|
||||
const mockHandleResumeAgent = vi.fn();
|
||||
|
||||
// Helper function to mock agent state with specific state
|
||||
const mockAgentStore = (agentState: AgentState) => {
|
||||
vi.mocked(useAgentState).mockReturnValue({
|
||||
@@ -66,91 +94,248 @@ describe("ServerStatus", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render server status with RUNNING conversation status", () => {
|
||||
it("should render server status with different conversation statuses", () => {
|
||||
// Mock agent store to return RUNNING state
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus="RUNNING" />);
|
||||
// Test RUNNING status
|
||||
const { rerender } = renderWithProviders(
|
||||
<ServerStatus
|
||||
conversationStatus="RUNNING"
|
||||
handleStop={mockHandleStop}
|
||||
handleResumeAgent={mockHandleResumeAgent}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Running")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId("server-status")).toBeInTheDocument();
|
||||
// Test STOPPED status
|
||||
rerender(
|
||||
<ServerStatus
|
||||
conversationStatus="STOPPED"
|
||||
handleStop={mockHandleStop}
|
||||
handleResumeAgent={mockHandleResumeAgent}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Server Stopped")).toBeInTheDocument();
|
||||
|
||||
// Test STARTING status (shows "Running" due to agent state being RUNNING)
|
||||
rerender(
|
||||
<ServerStatus
|
||||
conversationStatus="STARTING"
|
||||
handleStop={mockHandleStop}
|
||||
handleResumeAgent={mockHandleResumeAgent}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Running")).toBeInTheDocument();
|
||||
|
||||
// Test null status (shows "Running" due to agent state being RUNNING)
|
||||
rerender(
|
||||
<ServerStatus
|
||||
conversationStatus={null}
|
||||
handleStop={mockHandleStop}
|
||||
handleResumeAgent={mockHandleResumeAgent}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Running")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render server status with STOPPED conversation status", () => {
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
it("should show context menu when clicked with RUNNING status", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus="STOPPED" />);
|
||||
|
||||
expect(screen.getByTestId("server-status")).toBeInTheDocument();
|
||||
expect(screen.getByText("Server Stopped")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render STARTING status when agent state is LOADING", () => {
|
||||
mockAgentStore(AgentState.LOADING);
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus="STARTING" />);
|
||||
|
||||
expect(screen.getByTestId("server-status")).toBeInTheDocument();
|
||||
expect(screen.getByText("Starting")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render STARTING status when agent state is INIT", () => {
|
||||
mockAgentStore(AgentState.INIT);
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus="STARTING" />);
|
||||
|
||||
expect(screen.getByTestId("server-status")).toBeInTheDocument();
|
||||
expect(screen.getByText("Starting")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render ERROR status when agent state is ERROR", () => {
|
||||
mockAgentStore(AgentState.ERROR);
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus="RUNNING" />);
|
||||
|
||||
expect(screen.getByTestId("server-status")).toBeInTheDocument();
|
||||
expect(screen.getByText("Error")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render STOPPING status when isPausing is true", () => {
|
||||
// Mock agent store to return RUNNING state
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatus conversationStatus="RUNNING" isPausing={true} />,
|
||||
<ServerStatus
|
||||
conversationStatus="RUNNING"
|
||||
handleStop={mockHandleStop}
|
||||
handleResumeAgent={mockHandleResumeAgent}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("server-status")).toBeInTheDocument();
|
||||
expect(screen.getByText("Stopping...")).toBeInTheDocument();
|
||||
const statusContainer = screen.getByText("Running").closest("div");
|
||||
expect(statusContainer).toBeInTheDocument();
|
||||
|
||||
await user.click(statusContainer!);
|
||||
|
||||
// Context menu should appear
|
||||
expect(
|
||||
screen.getByTestId("server-status-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId("stop-server-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show context menu when clicked with STOPPED status", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock agent store to return STOPPED state
|
||||
mockAgentStore(AgentState.STOPPED);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatus
|
||||
conversationStatus="STOPPED"
|
||||
handleStop={mockHandleStop}
|
||||
handleResumeAgent={mockHandleResumeAgent}
|
||||
/>,
|
||||
);
|
||||
|
||||
const statusContainer = screen.getByText("Server Stopped").closest("div");
|
||||
expect(statusContainer).toBeInTheDocument();
|
||||
|
||||
await user.click(statusContainer!);
|
||||
|
||||
// Context menu should appear
|
||||
expect(
|
||||
screen.getByTestId("server-status-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId("start-server-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show context menu when clicked with other statuses", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock agent store to return RUNNING state
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatus
|
||||
conversationStatus="STARTING"
|
||||
handleStop={mockHandleStop}
|
||||
handleResumeAgent={mockHandleResumeAgent}
|
||||
/>,
|
||||
);
|
||||
|
||||
const statusContainer = screen.getByText("Running").closest("div");
|
||||
expect(statusContainer).toBeInTheDocument();
|
||||
|
||||
await user.click(statusContainer!);
|
||||
|
||||
// Context menu should not appear
|
||||
expect(
|
||||
screen.queryByTestId("server-status-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call stop conversation mutation when stop server is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Clear previous calls
|
||||
mockHandleStop.mockClear();
|
||||
|
||||
// Mock agent store to return RUNNING state
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatus
|
||||
conversationStatus="RUNNING"
|
||||
handleStop={mockHandleStop}
|
||||
handleResumeAgent={mockHandleResumeAgent}
|
||||
/>,
|
||||
);
|
||||
|
||||
const statusContainer = screen.getByText("Running").closest("div");
|
||||
await user.click(statusContainer!);
|
||||
|
||||
const stopButton = screen.getByTestId("stop-server-button");
|
||||
await user.click(stopButton);
|
||||
|
||||
expect(mockHandleStop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call start conversation mutation when start server is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Clear previous calls
|
||||
mockHandleResumeAgent.mockClear();
|
||||
|
||||
// Mock agent store to return STOPPED state
|
||||
mockAgentStore(AgentState.STOPPED);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatus
|
||||
conversationStatus="STOPPED"
|
||||
handleStop={mockHandleStop}
|
||||
handleResumeAgent={mockHandleResumeAgent}
|
||||
/>,
|
||||
);
|
||||
|
||||
const statusContainer = screen.getByText("Server Stopped").closest("div");
|
||||
await user.click(statusContainer!);
|
||||
|
||||
const startButton = screen.getByTestId("start-server-button");
|
||||
await user.click(startButton);
|
||||
|
||||
expect(mockHandleResumeAgent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should close context menu after stop server action", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock agent store to return RUNNING state
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatus
|
||||
conversationStatus="RUNNING"
|
||||
handleStop={mockHandleStop}
|
||||
handleResumeAgent={mockHandleResumeAgent}
|
||||
/>,
|
||||
);
|
||||
|
||||
const statusContainer = screen.getByText("Running").closest("div");
|
||||
await user.click(statusContainer!);
|
||||
|
||||
const stopButton = screen.getByTestId("stop-server-button");
|
||||
await user.click(stopButton);
|
||||
|
||||
// Context menu should be closed (handled by the component)
|
||||
expect(mockHandleStop).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should close context menu after start server action", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock agent store to return STOPPED state
|
||||
mockAgentStore(AgentState.STOPPED);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatus
|
||||
conversationStatus="STOPPED"
|
||||
handleStop={mockHandleStop}
|
||||
handleResumeAgent={mockHandleResumeAgent}
|
||||
/>,
|
||||
);
|
||||
|
||||
const statusContainer = screen.getByText("Server Stopped").closest("div");
|
||||
await user.click(statusContainer!);
|
||||
|
||||
const startButton = screen.getByTestId("start-server-button");
|
||||
await user.click(startButton);
|
||||
|
||||
// Context menu should be closed
|
||||
expect(
|
||||
screen.queryByTestId("server-status-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle null conversation status", () => {
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus={null} />);
|
||||
|
||||
expect(screen.getByTestId("server-status")).toBeInTheDocument();
|
||||
expect(screen.getByText("Running")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should apply custom className", () => {
|
||||
// Mock agent store to return RUNNING state
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatus conversationStatus="RUNNING" className="custom-class" />,
|
||||
<ServerStatus
|
||||
conversationStatus={null}
|
||||
handleStop={mockHandleStop}
|
||||
handleResumeAgent={mockHandleResumeAgent}
|
||||
/>,
|
||||
);
|
||||
|
||||
const container = screen.getByTestId("server-status");
|
||||
expect(container).toHaveClass("custom-class");
|
||||
const statusText = screen.getByText("Running");
|
||||
expect(statusText).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ServerStatusContextMenu", () => {
|
||||
// Helper function to mock agent state with specific state
|
||||
const mockAgentStore = (agentState: AgentState) => {
|
||||
vi.mocked(useAgentState).mockReturnValue({
|
||||
curAgentState: agentState,
|
||||
});
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onClose: vi.fn(),
|
||||
conversationStatus: "RUNNING" as ConversationStatus,
|
||||
@@ -161,8 +346,6 @@ describe("ServerStatusContextMenu", () => {
|
||||
});
|
||||
|
||||
it("should render stop server button when status is RUNNING", () => {
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatusContextMenu
|
||||
{...defaultProps}
|
||||
@@ -171,14 +354,11 @@ describe("ServerStatusContextMenu", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("server-status")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("stop-server-button")).toBeInTheDocument();
|
||||
expect(screen.getByText("Stop Runtime")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render start server button when status is STOPPED", () => {
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatusContextMenu
|
||||
{...defaultProps}
|
||||
@@ -187,14 +367,11 @@ describe("ServerStatusContextMenu", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("server-status")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("start-server-button")).toBeInTheDocument();
|
||||
expect(screen.getByText("Start Runtime")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render stop server button when onStopServer is not provided", () => {
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatusContextMenu
|
||||
{...defaultProps}
|
||||
@@ -202,13 +379,10 @@ describe("ServerStatusContextMenu", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("server-status")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("stop-server-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render start server button when onStartServer is not provided", () => {
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatusContextMenu
|
||||
{...defaultProps}
|
||||
@@ -216,14 +390,12 @@ describe("ServerStatusContextMenu", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("server-status")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("start-server-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onStopServer when stop button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onStopServer = vi.fn();
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatusContextMenu
|
||||
@@ -242,7 +414,6 @@ describe("ServerStatusContextMenu", () => {
|
||||
it("should call onStartServer when start button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onStartServer = vi.fn();
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatusContextMenu
|
||||
@@ -259,8 +430,6 @@ describe("ServerStatusContextMenu", () => {
|
||||
});
|
||||
|
||||
it("should render correct text content for stop server button", () => {
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatusContextMenu
|
||||
{...defaultProps}
|
||||
@@ -275,8 +444,6 @@ describe("ServerStatusContextMenu", () => {
|
||||
});
|
||||
|
||||
it("should render correct text content for start server button", () => {
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatusContextMenu
|
||||
{...defaultProps}
|
||||
@@ -292,7 +459,6 @@ describe("ServerStatusContextMenu", () => {
|
||||
|
||||
it("should call onClose when context menu is closed", () => {
|
||||
const onClose = vi.fn();
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatusContextMenu
|
||||
@@ -309,8 +475,6 @@ describe("ServerStatusContextMenu", () => {
|
||||
});
|
||||
|
||||
it("should not render any buttons for other conversation statuses", () => {
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(
|
||||
<ServerStatusContextMenu
|
||||
{...defaultProps}
|
||||
@@ -318,7 +482,6 @@ describe("ServerStatusContextMenu", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("server-status")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("stop-server-button")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("start-server-button")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
-22
@@ -21,7 +21,6 @@ const mockUseConfig = vi.fn();
|
||||
const mockUseRepositoryMicroagents = vi.fn();
|
||||
const mockUseMicroagentManagementConversations = vi.fn();
|
||||
const mockUseSearchRepositories = vi.fn();
|
||||
const mockUseCreateConversationAndSubscribeMultiple = vi.fn();
|
||||
|
||||
vi.mock("#/hooks/use-user-providers", () => ({
|
||||
useUserProviders: () => mockUseUserProviders(),
|
||||
@@ -48,17 +47,6 @@ vi.mock("#/hooks/query/use-search-repositories", () => ({
|
||||
useSearchRepositories: () => mockUseSearchRepositories(),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-tracking", () => ({
|
||||
useTracking: () => ({
|
||||
trackEvent: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/use-create-conversation-and-subscribe-multiple", () => ({
|
||||
useCreateConversationAndSubscribeMultiple: () =>
|
||||
mockUseCreateConversationAndSubscribeMultiple(),
|
||||
}));
|
||||
|
||||
describe("MicroagentManagement", () => {
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
@@ -321,16 +309,6 @@ describe("MicroagentManagement", () => {
|
||||
isError: false,
|
||||
});
|
||||
|
||||
mockUseCreateConversationAndSubscribeMultiple.mockReturnValue({
|
||||
createConversationAndSubscribe: vi.fn(({ onSuccessCallback }) => {
|
||||
// Immediately call the success callback to close the modal
|
||||
if (onSuccessCallback) {
|
||||
onSuccessCallback();
|
||||
}
|
||||
}),
|
||||
isPending: false,
|
||||
});
|
||||
|
||||
// Mock the search repositories hook to return repositories with OpenHands suffixes
|
||||
const mockSearchResults =
|
||||
getRepositoriesWithOpenHandsSuffix(mockRepositories);
|
||||
|
||||
@@ -268,7 +268,7 @@ describe("useWebSocket", () => {
|
||||
});
|
||||
|
||||
// onError handler should have been called
|
||||
expect(onErrorSpy).toHaveBeenCalled();
|
||||
expect(onErrorSpy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should provide sendMessage function to send messages to WebSocket", async () => {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.61.0",
|
||||
"version": "0.60.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.61.0",
|
||||
"version": "0.60.0",
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.4",
|
||||
"@heroui/use-infinite-scroll": "^2.2.11",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.61.0",
|
||||
"version": "0.60.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
V1AppConversationStartTask,
|
||||
V1AppConversationStartTaskPage,
|
||||
V1AppConversation,
|
||||
V1SandboxInfo,
|
||||
} from "./v1-conversation-service.types";
|
||||
|
||||
class V1ConversationService {
|
||||
@@ -212,6 +213,36 @@ class V1ConversationService {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause a V1 sandbox
|
||||
* Calls the /api/v1/sandboxes/{id}/pause endpoint
|
||||
*
|
||||
* @param sandboxId The sandbox ID to pause
|
||||
* @returns Success response
|
||||
*/
|
||||
static async pauseSandbox(sandboxId: string): Promise<{ success: boolean }> {
|
||||
const { data } = await openHands.post<{ success: boolean }>(
|
||||
`/api/v1/sandboxes/${sandboxId}/pause`,
|
||||
{},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume a V1 sandbox
|
||||
* Calls the /api/v1/sandboxes/{id}/resume endpoint
|
||||
*
|
||||
* @param sandboxId The sandbox ID to resume
|
||||
* @returns Success response
|
||||
*/
|
||||
static async resumeSandbox(sandboxId: string): Promise<{ success: boolean }> {
|
||||
const { data } = await openHands.post<{ success: boolean }>(
|
||||
`/api/v1/sandboxes/${sandboxId}/resume`,
|
||||
{},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch get V1 app conversations by their IDs
|
||||
* Returns null for any missing conversations
|
||||
@@ -238,6 +269,32 @@ class V1ConversationService {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch get V1 sandboxes by their IDs
|
||||
* Returns null for any missing sandboxes
|
||||
*
|
||||
* @param ids Array of sandbox IDs (max 100)
|
||||
* @returns Array of sandboxes or null for missing ones
|
||||
*/
|
||||
static async batchGetSandboxes(
|
||||
ids: string[],
|
||||
): Promise<(V1SandboxInfo | null)[]> {
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (ids.length > 100) {
|
||||
throw new Error("Cannot request more than 100 sandboxes at once");
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
ids.forEach((id) => params.append("id", id));
|
||||
|
||||
const { data } = await openHands.get<(V1SandboxInfo | null)[]>(
|
||||
`/api/v1/sandboxes?${params.toString()}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a single file to the V1 conversation workspace
|
||||
* V1 API endpoint: POST /api/file/upload/{path}
|
||||
@@ -288,6 +345,24 @@ class V1ConversationService {
|
||||
const { data } = await openHands.get<{ runtime_id: string }>(url);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of events for a conversation
|
||||
* Uses the V1 API endpoint: GET /api/v1/events/count
|
||||
*
|
||||
* @param conversationId The conversation ID to get event count for
|
||||
* @returns The number of events in the conversation
|
||||
*/
|
||||
static async getEventCount(conversationId: string): Promise<number> {
|
||||
const params = new URLSearchParams();
|
||||
params.append("conversation_id__eq", conversationId);
|
||||
|
||||
const { data } = await openHands.get<number>(
|
||||
`/api/v1/events/count?${params.toString()}`,
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export default V1ConversationService;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ConversationTrigger } from "../open-hands.types";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { V1SandboxStatus } from "../sandbox-service/sandbox-service.types";
|
||||
|
||||
// V1 API Types for requests
|
||||
// Note: This represents the serialized API format, not the internal TextContent/ImageContent types
|
||||
@@ -65,7 +64,14 @@ export interface V1AppConversationStartTaskPage {
|
||||
next_page_id: string | null;
|
||||
}
|
||||
|
||||
export type V1ConversationExecutionStatus =
|
||||
export type V1SandboxStatus =
|
||||
| "MISSING"
|
||||
| "STARTING"
|
||||
| "RUNNING"
|
||||
| "STOPPED"
|
||||
| "PAUSED";
|
||||
|
||||
export type V1AgentExecutionStatus =
|
||||
| "RUNNING"
|
||||
| "AWAITING_USER_INPUT"
|
||||
| "AWAITING_USER_CONFIRMATION"
|
||||
@@ -88,7 +94,22 @@ export interface V1AppConversation {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
sandbox_status: V1SandboxStatus;
|
||||
execution_status: V1ConversationExecutionStatus | null;
|
||||
agent_status: V1AgentExecutionStatus | null;
|
||||
conversation_url: string | null;
|
||||
session_api_key: string | null;
|
||||
}
|
||||
|
||||
export interface V1ExposedUrl {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface V1SandboxInfo {
|
||||
id: string;
|
||||
created_by_user_id: string | null;
|
||||
sandbox_spec_id: string;
|
||||
status: V1SandboxStatus;
|
||||
session_api_key: string | null;
|
||||
exposed_urls: V1ExposedUrl[] | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
ConfirmationResponseRequest,
|
||||
ConfirmationResponseResponse,
|
||||
} from "./event-service.types";
|
||||
import { openHands } from "../open-hands-axios";
|
||||
|
||||
class EventService {
|
||||
/**
|
||||
@@ -37,14 +36,6 @@ class EventService {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getEventCount(conversationId: string): Promise<number> {
|
||||
const params = new URLSearchParams();
|
||||
params.append("conversation_id__eq", conversationId);
|
||||
const { data } = await openHands.get<number>(
|
||||
`/api/v1/events/count?${params.toString()}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export default EventService;
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// sandbox-service.api.ts
|
||||
// This file contains API methods for /api/v1/sandboxes endpoints.
|
||||
|
||||
import { openHands } from "../open-hands-axios";
|
||||
import type { V1SandboxInfo } from "./sandbox-service.types";
|
||||
|
||||
export class SandboxService {
|
||||
/**
|
||||
* Pause a V1 sandbox
|
||||
* Calls the /api/v1/sandboxes/{id}/pause endpoint
|
||||
*/
|
||||
static async pauseSandbox(sandboxId: string): Promise<{ success: boolean }> {
|
||||
const { data } = await openHands.post<{ success: boolean }>(
|
||||
`/api/v1/sandboxes/${sandboxId}/pause`,
|
||||
{},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume a V1 sandbox
|
||||
* Calls the /api/v1/sandboxes/{id}/resume endpoint
|
||||
*/
|
||||
static async resumeSandbox(sandboxId: string): Promise<{ success: boolean }> {
|
||||
const { data } = await openHands.post<{ success: boolean }>(
|
||||
`/api/v1/sandboxes/${sandboxId}/resume`,
|
||||
{},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch get V1 sandboxes by their IDs
|
||||
* Returns null for any missing sandboxes
|
||||
*/
|
||||
static async batchGetSandboxes(
|
||||
ids: string[],
|
||||
): Promise<(V1SandboxInfo | null)[]> {
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (ids.length > 100) {
|
||||
throw new Error("Cannot request more than 100 sandboxes at once");
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
ids.forEach((id) => params.append("id", id));
|
||||
const { data } = await openHands.get<(V1SandboxInfo | null)[]>(
|
||||
`/api/v1/sandboxes?${params.toString()}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// sandbox-service.types.ts
|
||||
// This file contains types for Sandbox API.
|
||||
|
||||
export type V1SandboxStatus =
|
||||
| "MISSING"
|
||||
| "STARTING"
|
||||
| "RUNNING"
|
||||
| "STOPPED"
|
||||
| "PAUSED";
|
||||
|
||||
export interface V1ExposedUrl {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface V1SandboxInfo {
|
||||
id: string;
|
||||
created_by_user_id: string | null;
|
||||
sandbox_spec_id: string;
|
||||
status: V1SandboxStatus;
|
||||
session_api_key: string | null;
|
||||
exposed_urls: V1ExposedUrl[] | null;
|
||||
created_at: string;
|
||||
}
|
||||
@@ -68,7 +68,6 @@ export function ChatInterface() {
|
||||
const conversationWebSocket = useConversationWebSocket();
|
||||
const { send } = useSendMessage();
|
||||
const storeEvents = useEventStore((state) => state.events);
|
||||
const uiEvents = useEventStore((state) => state.uiEvents);
|
||||
const { setOptimisticUserMessage, getOptimisticUserMessage } =
|
||||
useOptimisticUserMessageStore();
|
||||
const { t } = useTranslation();
|
||||
@@ -97,29 +96,24 @@ export function ChatInterface() {
|
||||
|
||||
const isV1Conversation = conversation?.conversation_version === "V1";
|
||||
|
||||
// Track when we should show V1 messages (after DOM has rendered)
|
||||
const [showV1Messages, setShowV1Messages] = React.useState(false);
|
||||
const prevV1LoadingRef = React.useRef(
|
||||
// Instantly scroll to bottom when history loading completes
|
||||
const prevLoadingHistoryRef = React.useRef(
|
||||
conversationWebSocket?.isLoadingHistory,
|
||||
);
|
||||
|
||||
// Wait for DOM to render before showing V1 messages
|
||||
React.useEffect(() => {
|
||||
const wasLoading = prevV1LoadingRef.current;
|
||||
const wasLoading = prevLoadingHistoryRef.current;
|
||||
const isLoading = conversationWebSocket?.isLoadingHistory;
|
||||
|
||||
if (wasLoading && !isLoading) {
|
||||
// Loading just finished - wait for next frame to ensure DOM is ready
|
||||
requestAnimationFrame(() => {
|
||||
setShowV1Messages(true);
|
||||
// When history loading transitions from true to false, instantly scroll to bottom
|
||||
if (wasLoading && !isLoading && scrollRef.current) {
|
||||
scrollRef.current.scrollTo({
|
||||
top: scrollRef.current.scrollHeight,
|
||||
behavior: "instant",
|
||||
});
|
||||
} else if (isLoading) {
|
||||
// Reset when loading starts
|
||||
setShowV1Messages(false);
|
||||
}
|
||||
|
||||
prevV1LoadingRef.current = isLoading;
|
||||
}, [conversationWebSocket?.isLoadingHistory]);
|
||||
prevLoadingHistoryRef.current = isLoading;
|
||||
}, [conversationWebSocket?.isLoadingHistory, scrollRef]);
|
||||
|
||||
// Filter V0 events
|
||||
const v0Events = storeEvents
|
||||
@@ -127,13 +121,11 @@ export function ChatInterface() {
|
||||
.filter(isActionOrObservation)
|
||||
.filter(shouldRenderEvent);
|
||||
|
||||
// Filter V1 events - use uiEvents for rendering (actions replaced by observations)
|
||||
const v1UiEvents = uiEvents.filter(isV1Event).filter(shouldRenderV1Event);
|
||||
// Keep full v1 events for lookups (includes both actions and observations)
|
||||
const v1FullEvents = storeEvents.filter(isV1Event);
|
||||
// Filter V1 events
|
||||
const v1Events = storeEvents.filter(isV1Event).filter(shouldRenderV1Event);
|
||||
|
||||
// Combined events count for tracking
|
||||
const totalEvents = v0Events.length || v1UiEvents.length;
|
||||
const totalEvents = v0Events.length || v1Events.length;
|
||||
|
||||
// Check if there are any substantive agent actions (not just system messages)
|
||||
const hasSubstantiveAgentActions = React.useMemo(
|
||||
@@ -231,7 +223,7 @@ export function ChatInterface() {
|
||||
};
|
||||
|
||||
const v0UserEventsExist = hasUserEvent(v0Events);
|
||||
const v1UserEventsExist = hasV1UserEvent(v1FullEvents);
|
||||
const v1UserEventsExist = hasV1UserEvent(v1Events);
|
||||
const userEventsExist = v0UserEventsExist || v1UserEventsExist;
|
||||
|
||||
return (
|
||||
@@ -257,7 +249,7 @@ export function ChatInterface() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(conversationWebSocket?.isLoadingHistory || !showV1Messages) &&
|
||||
{conversationWebSocket?.isLoadingHistory &&
|
||||
isV1Conversation &&
|
||||
!isTask && (
|
||||
<div className="flex justify-center">
|
||||
@@ -274,8 +266,8 @@ export function ChatInterface() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showV1Messages && v1UserEventsExist && (
|
||||
<V1Messages messages={v1UiEvents} allEvents={v1FullEvents} />
|
||||
{!conversationWebSocket?.isLoadingHistory && v1UserEventsExist && (
|
||||
<V1Messages messages={v1Events} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { ServerStatus } from "#/components/features/controls/server-status";
|
||||
import { AgentStatus } from "#/components/features/controls/agent-status";
|
||||
import { Tools } from "../../controls/tools";
|
||||
import { useUnifiedPauseConversationSandbox } from "#/hooks/mutation/use-unified-stop-conversation";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useUnifiedResumeConversationSandbox } from "#/hooks/mutation/use-unified-start-conversation";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { useSendMessage } from "#/hooks/use-send-message";
|
||||
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
||||
@@ -10,23 +14,32 @@ import { useV1PauseConversation } from "#/hooks/mutation/use-v1-pause-conversati
|
||||
import { useV1ResumeConversation } from "#/hooks/mutation/use-v1-resume-conversation";
|
||||
|
||||
interface ChatInputActionsProps {
|
||||
conversationStatus: ConversationStatus | null;
|
||||
disabled: boolean;
|
||||
handleResumeAgent: () => void;
|
||||
}
|
||||
|
||||
export function ChatInputActions({
|
||||
conversationStatus,
|
||||
disabled,
|
||||
handleResumeAgent,
|
||||
}: ChatInputActionsProps) {
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const pauseConversationSandboxMutation = useUnifiedPauseConversationSandbox();
|
||||
const resumeConversationSandboxMutation =
|
||||
useUnifiedResumeConversationSandbox();
|
||||
const v1PauseConversationMutation = useV1PauseConversation();
|
||||
const v1ResumeConversationMutation = useV1ResumeConversation();
|
||||
const { conversationId } = useConversationId();
|
||||
const { providers } = useUserProviders();
|
||||
const { send } = useSendMessage();
|
||||
|
||||
const isV1Conversation = conversation?.conversation_version === "V1";
|
||||
|
||||
const handleStopClick = () => {
|
||||
pauseConversationSandboxMutation.mutate({ conversationId });
|
||||
};
|
||||
|
||||
const handlePauseAgent = () => {
|
||||
if (isV1Conversation) {
|
||||
// V1: Pause the conversation (agent execution)
|
||||
@@ -49,6 +62,10 @@ export function ChatInputActions({
|
||||
handleResumeAgent();
|
||||
};
|
||||
|
||||
const handleStartClick = () => {
|
||||
resumeConversationSandboxMutation.mutate({ conversationId, providers });
|
||||
};
|
||||
|
||||
const isPausing =
|
||||
pauseConversationSandboxMutation.isPending ||
|
||||
v1PauseConversationMutation.isPending;
|
||||
@@ -57,6 +74,12 @@ export function ChatInputActions({
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<Tools />
|
||||
<ServerStatus
|
||||
conversationStatus={conversationStatus}
|
||||
isPausing={isPausing}
|
||||
handleStop={handleStopClick}
|
||||
handleResumeAgent={handleStartClick}
|
||||
/>
|
||||
</div>
|
||||
<AgentStatus
|
||||
className="ml-2 md:ml-3"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from "react";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { DragOver } from "../drag-over";
|
||||
import { UploadedFiles } from "../uploaded-files";
|
||||
import { ChatInputRow } from "./chat-input-row";
|
||||
import { ChatInputActions } from "./chat-input-actions";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ChatInputContainerProps {
|
||||
chatContainerRef: React.RefObject<HTMLDivElement | null>;
|
||||
@@ -12,6 +11,7 @@ interface ChatInputContainerProps {
|
||||
disabled: boolean;
|
||||
showButton: boolean;
|
||||
buttonClassName: string;
|
||||
conversationStatus: ConversationStatus | null;
|
||||
chatInputRef: React.RefObject<HTMLDivElement | null>;
|
||||
handleFileIconClick: (isDisabled: boolean) => void;
|
||||
handleSubmit: () => void;
|
||||
@@ -32,6 +32,7 @@ export function ChatInputContainer({
|
||||
disabled,
|
||||
showButton,
|
||||
buttonClassName,
|
||||
conversationStatus,
|
||||
chatInputRef,
|
||||
handleFileIconClick,
|
||||
handleSubmit,
|
||||
@@ -45,17 +46,10 @@ export function ChatInputContainer({
|
||||
onFocus,
|
||||
onBlur,
|
||||
}: ChatInputContainerProps) {
|
||||
const conversationMode = useConversationStore(
|
||||
(state) => state.conversationMode,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={chatContainerRef}
|
||||
className={cn(
|
||||
"bg-[#25272D] box-border content-stretch flex flex-col items-start justify-center p-4 pt-3 relative rounded-[15px] w-full",
|
||||
conversationMode === "plan" && "border border-[#597FF4]",
|
||||
)}
|
||||
className="bg-[#25272D] box-border content-stretch flex flex-col items-start justify-center p-4 pt-3 relative rounded-[15px] w-full"
|
||||
onDragOver={(e) => onDragOver(e, disabled)}
|
||||
onDragLeave={(e) => onDragLeave(e, disabled)}
|
||||
onDrop={(e) => onDrop(e, disabled)}
|
||||
@@ -80,6 +74,7 @@ export function ChatInputContainer({
|
||||
/>
|
||||
|
||||
<ChatInputActions
|
||||
conversationStatus={conversationStatus}
|
||||
disabled={disabled}
|
||||
handleResumeAgent={handleResumeAgent}
|
||||
/>
|
||||
|
||||
@@ -137,6 +137,7 @@ export function CustomChatInput({
|
||||
disabled={isDisabled}
|
||||
showButton={showButton}
|
||||
buttonClassName={buttonClassName}
|
||||
conversationStatus={conversationStatus}
|
||||
chatInputRef={chatInputRef}
|
||||
handleFileIconClick={handleFileIconClick}
|
||||
handleSubmit={handleSubmit}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import posthog from "posthog-js";
|
||||
import PRIcon from "#/icons/u-pr.svg?react";
|
||||
import { cn, getCreatePRPrompt } from "#/utils/utils";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
|
||||
interface GitControlBarPrButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
@@ -20,7 +20,6 @@ export function GitControlBarPrButton({
|
||||
isConversationReady = true,
|
||||
}: GitControlBarPrButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const { trackCreatePrButtonClick } = useTracking();
|
||||
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
@@ -29,7 +28,7 @@ export function GitControlBarPrButton({
|
||||
providersAreSet && hasRepository && isConversationReady;
|
||||
|
||||
const handlePrClick = () => {
|
||||
trackCreatePrButtonClick();
|
||||
posthog.capture("create_pr_button_clicked");
|
||||
onSuggestionsClick(getCreatePRPrompt(currentGitProvider));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import posthog from "posthog-js";
|
||||
import ArrowDownIcon from "#/icons/u-arrow-down.svg?react";
|
||||
import { cn, getGitPullPrompt } from "#/utils/utils";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
|
||||
interface GitControlBarPullButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
@@ -16,7 +16,6 @@ export function GitControlBarPullButton({
|
||||
isConversationReady = true,
|
||||
}: GitControlBarPullButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const { trackPullButtonClick } = useTracking();
|
||||
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const { providers } = useUserProviders();
|
||||
@@ -27,7 +26,7 @@ export function GitControlBarPullButton({
|
||||
providersAreSet && hasRepository && isConversationReady;
|
||||
|
||||
const handlePullClick = () => {
|
||||
trackPullButtonClick();
|
||||
posthog.capture("pull_button_clicked");
|
||||
onSuggestionsClick(getGitPullPrompt());
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import posthog from "posthog-js";
|
||||
import ArrowUpIcon from "#/icons/u-arrow-up.svg?react";
|
||||
import { cn, getGitPushPrompt } from "#/utils/utils";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
|
||||
interface GitControlBarPushButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
@@ -20,7 +20,6 @@ export function GitControlBarPushButton({
|
||||
isConversationReady = true,
|
||||
}: GitControlBarPushButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
const { trackPushButtonClick } = useTracking();
|
||||
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
@@ -29,7 +28,7 @@ export function GitControlBarPushButton({
|
||||
providersAreSet && hasRepository && isConversationReady;
|
||||
|
||||
const handlePushClick = () => {
|
||||
trackPushButtonClick();
|
||||
posthog.capture("push_button_clicked");
|
||||
onSuggestionsClick(getGitPushPrompt(currentGitProvider));
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useConversationStore } from "#/state/conversation-store";
|
||||
import CircleErrorIcon from "#/icons/circle-error.svg?react";
|
||||
import { useAgentState } from "#/hooks/use-agent-state";
|
||||
import { useUnifiedWebSocketStatus } from "#/hooks/use-unified-websocket-status";
|
||||
import { useTaskPolling } from "#/hooks/query/use-task-polling";
|
||||
|
||||
export interface AgentStatusProps {
|
||||
className?: string;
|
||||
@@ -36,7 +35,6 @@ export function AgentStatus({
|
||||
const { curStatusMessage } = useStatusStore();
|
||||
const webSocketStatus = useUnifiedWebSocketStatus();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const { taskStatus } = useTaskPolling();
|
||||
|
||||
const statusCode = getStatusCode(
|
||||
curStatusMessage,
|
||||
@@ -44,34 +42,25 @@ export function AgentStatus({
|
||||
conversation?.status || null,
|
||||
conversation?.runtime_status || null,
|
||||
curAgentState,
|
||||
taskStatus,
|
||||
);
|
||||
|
||||
const isTaskLoading =
|
||||
taskStatus && taskStatus !== "ERROR" && taskStatus !== "READY";
|
||||
|
||||
const shouldShownAgentLoading =
|
||||
isPausing ||
|
||||
curAgentState === AgentState.INIT ||
|
||||
curAgentState === AgentState.LOADING ||
|
||||
(webSocketStatus === "CONNECTING" && taskStatus !== "ERROR") ||
|
||||
isTaskLoading;
|
||||
webSocketStatus === "CONNECTING";
|
||||
|
||||
const shouldShownAgentError =
|
||||
curAgentState === AgentState.ERROR ||
|
||||
curAgentState === AgentState.RATE_LIMITED ||
|
||||
webSocketStatus === "DISCONNECTED" ||
|
||||
taskStatus === "ERROR";
|
||||
curAgentState === AgentState.RATE_LIMITED;
|
||||
|
||||
const shouldShownAgentStop = curAgentState === AgentState.RUNNING;
|
||||
|
||||
const shouldShownAgentResume =
|
||||
curAgentState === AgentState.STOPPED || curAgentState === AgentState.PAUSED;
|
||||
const shouldShownAgentResume = curAgentState === AgentState.STOPPED;
|
||||
|
||||
// Update global state when agent loading condition changes
|
||||
useEffect(() => {
|
||||
if (shouldShownAgentLoading)
|
||||
setShouldShownAgentLoading(shouldShownAgentLoading);
|
||||
setShouldShownAgentLoading(shouldShownAgentLoading);
|
||||
}, [shouldShownAgentLoading, setShouldShownAgentLoading]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -13,7 +13,7 @@ export function ServerStatusContextMenuIconText({
|
||||
}: ServerStatusContextMenuIconTextProps) {
|
||||
return (
|
||||
<button
|
||||
className="flex items-center justify-between p-2 hover:bg-[#5C5D62] rounded text-sm text-white font-normal leading-5 cursor-pointer w-full"
|
||||
className="flex items-center gap-2 p-2 hover:bg-[#5C5D62] rounded text-sm text-white font-normal leading-5 cursor-pointer w-full"
|
||||
onClick={onClick}
|
||||
data-testid={testId}
|
||||
type="button"
|
||||
|
||||
@@ -6,9 +6,6 @@ import { ConversationStatus } from "#/types/conversation-status";
|
||||
import StopCircleIcon from "#/icons/stop-circle.svg?react";
|
||||
import PlayCircleIcon from "#/icons/play-circle.svg?react";
|
||||
import { ServerStatusContextMenuIconText } from "./server-status-context-menu-icon-text";
|
||||
import { ServerStatus } from "./server-status";
|
||||
import { Divider } from "#/ui/divider";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ServerStatusContextMenuProps {
|
||||
onClose: () => void;
|
||||
@@ -16,8 +13,6 @@ interface ServerStatusContextMenuProps {
|
||||
onStartServer?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
conversationStatus: ConversationStatus | null;
|
||||
position?: "top" | "bottom";
|
||||
className?: string;
|
||||
isPausing?: boolean;
|
||||
}
|
||||
|
||||
export function ServerStatusContextMenu({
|
||||
@@ -26,15 +21,10 @@ export function ServerStatusContextMenu({
|
||||
onStartServer,
|
||||
conversationStatus,
|
||||
position = "top",
|
||||
className = "",
|
||||
isPausing = false,
|
||||
}: ServerStatusContextMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
|
||||
|
||||
const shouldActionShown =
|
||||
conversationStatus === "RUNNING" || conversationStatus === "STOPPED";
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
ref={ref}
|
||||
@@ -42,36 +32,24 @@ export function ServerStatusContextMenu({
|
||||
position={position}
|
||||
alignment="left"
|
||||
size="default"
|
||||
className={cn("left-2 w-fit min-w-42", className)}
|
||||
className="left-2 w-fit min-w-max"
|
||||
>
|
||||
<ServerStatus
|
||||
conversationStatus={conversationStatus}
|
||||
isPausing={isPausing}
|
||||
className="py-1"
|
||||
/>
|
||||
{conversationStatus === "RUNNING" && onStopServer && (
|
||||
<ServerStatusContextMenuIconText
|
||||
icon={<StopCircleIcon width={18} height={18} />}
|
||||
text={t(I18nKey.COMMON$STOP_RUNTIME)}
|
||||
onClick={onStopServer}
|
||||
testId="stop-server-button"
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldActionShown && (
|
||||
<>
|
||||
<Divider />
|
||||
|
||||
{conversationStatus === "RUNNING" && onStopServer && (
|
||||
<ServerStatusContextMenuIconText
|
||||
icon={<StopCircleIcon width={18} height={18} />}
|
||||
text={t(I18nKey.COMMON$STOP_RUNTIME)}
|
||||
onClick={onStopServer}
|
||||
testId="stop-server-button"
|
||||
/>
|
||||
)}
|
||||
|
||||
{conversationStatus === "STOPPED" && onStartServer && (
|
||||
<ServerStatusContextMenuIconText
|
||||
icon={<PlayCircleIcon width={18} height={18} />}
|
||||
text={t(I18nKey.COMMON$START_RUNTIME)}
|
||||
onClick={onStartServer}
|
||||
testId="start-server-button"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{conversationStatus === "STOPPED" && onStartServer && (
|
||||
<ServerStatusContextMenuIconText
|
||||
icon={<PlayCircleIcon width={18} height={18} />}
|
||||
text={t(I18nKey.COMMON$START_RUNTIME)}
|
||||
onClick={onStartServer}
|
||||
testId="start-server-button"
|
||||
/>
|
||||
)}
|
||||
</ContextMenu>
|
||||
);
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import DebugStackframeDot from "#/icons/debug-stackframe-dot.svg?react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { ServerStatusContextMenu } from "./server-status-context-menu";
|
||||
import { useAgentState } from "#/hooks/use-agent-state";
|
||||
import { useTaskPolling } from "#/hooks/query/use-task-polling";
|
||||
import { getStatusColor } from "#/utils/utils";
|
||||
|
||||
export interface ServerStatusProps {
|
||||
className?: string;
|
||||
conversationStatus: ConversationStatus | null;
|
||||
isPausing?: boolean;
|
||||
handleStop: () => void;
|
||||
handleResumeAgent: () => void;
|
||||
}
|
||||
|
||||
export function ServerStatus({
|
||||
className = "",
|
||||
conversationStatus,
|
||||
isPausing = false,
|
||||
handleStop,
|
||||
handleResumeAgent,
|
||||
}: ServerStatusProps) {
|
||||
const [showContextMenu, setShowContextMenu] = useState(false);
|
||||
|
||||
const { curAgentState } = useAgentState();
|
||||
const { t } = useTranslation();
|
||||
const { isTask, taskStatus, taskDetail } = useTaskPolling();
|
||||
@@ -27,15 +34,34 @@ export function ServerStatus({
|
||||
|
||||
const isStopStatus = conversationStatus === "STOPPED";
|
||||
|
||||
const statusColor = getStatusColor({
|
||||
isPausing,
|
||||
isTask,
|
||||
taskStatus,
|
||||
isStartingStatus,
|
||||
isStopStatus,
|
||||
curAgentState,
|
||||
});
|
||||
// Get the appropriate color based on agent status
|
||||
const getStatusColor = (): string => {
|
||||
// Show pausing status
|
||||
if (isPausing) {
|
||||
return "#FFD600";
|
||||
}
|
||||
|
||||
// Show task status if we're polling a task
|
||||
if (isTask && taskStatus) {
|
||||
if (taskStatus === "ERROR") {
|
||||
return "#FF684E";
|
||||
}
|
||||
return "#FFD600";
|
||||
}
|
||||
|
||||
if (isStartingStatus) {
|
||||
return "#FFD600";
|
||||
}
|
||||
if (isStopStatus) {
|
||||
return "#ffffff";
|
||||
}
|
||||
if (curAgentState === AgentState.ERROR) {
|
||||
return "#FF684E";
|
||||
}
|
||||
return "#BCFF8C";
|
||||
};
|
||||
|
||||
// Get the appropriate status text based on agent status
|
||||
const getStatusText = (): string => {
|
||||
// Show pausing status
|
||||
if (isPausing) {
|
||||
@@ -74,14 +100,49 @@ export function ServerStatus({
|
||||
return t(I18nKey.COMMON$RUNNING);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (conversationStatus === "RUNNING" || conversationStatus === "STOPPED") {
|
||||
setShowContextMenu(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseContextMenu = () => {
|
||||
setShowContextMenu(false);
|
||||
};
|
||||
|
||||
const handleStopServer = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
handleStop();
|
||||
setShowContextMenu(false);
|
||||
};
|
||||
|
||||
const handleStartServer = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
handleResumeAgent();
|
||||
setShowContextMenu(false);
|
||||
};
|
||||
|
||||
const statusColor = getStatusColor();
|
||||
const statusText = getStatusText();
|
||||
|
||||
return (
|
||||
<div className={className} data-testid="server-status">
|
||||
<div className="flex items-center">
|
||||
<div className={`relative ${className}`}>
|
||||
<div className="flex items-center cursor-pointer" onClick={handleClick}>
|
||||
<DebugStackframeDot className="w-6 h-6" color={statusColor} />
|
||||
<span className="text-[13px] text-white font-normal">{statusText}</span>
|
||||
<span className="text-[11px] text-white font-normal leading-5">
|
||||
{statusText}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{showContextMenu && (
|
||||
<ServerStatusContextMenu
|
||||
onClose={handleCloseContextMenu}
|
||||
onStopServer={handleStopServer}
|
||||
onStartServer={handleStartServer}
|
||||
conversationStatus={conversationStatus}
|
||||
position="top"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import React from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { useAgentState } from "#/hooks/use-agent-state";
|
||||
import { useTaskPolling } from "#/hooks/query/use-task-polling";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { useUnifiedPauseConversationSandbox } from "#/hooks/mutation/use-unified-stop-conversation";
|
||||
import { useUnifiedResumeConversationSandbox } from "#/hooks/mutation/use-unified-start-conversation";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { getStatusColor } from "#/utils/utils";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import DebugStackframeDot from "#/icons/debug-stackframe-dot.svg?react";
|
||||
import { ServerStatusContextMenu } from "../controls/server-status-context-menu";
|
||||
import { ConversationName } from "./conversation-name";
|
||||
|
||||
export function ConversationNameWithStatus() {
|
||||
const { conversationId } = useParams<{ conversationId: string }>();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const { curAgentState } = useAgentState();
|
||||
const { isTask, taskStatus } = useTaskPolling();
|
||||
const { mutate: pauseConversationSandbox } =
|
||||
useUnifiedPauseConversationSandbox();
|
||||
const { mutate: resumeConversationSandbox } =
|
||||
useUnifiedResumeConversationSandbox();
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const isStartingStatus =
|
||||
curAgentState === AgentState.LOADING || curAgentState === AgentState.INIT;
|
||||
const isStopStatus = conversation?.status === "STOPPED";
|
||||
|
||||
const statusColor = getStatusColor({
|
||||
isPausing: false,
|
||||
isTask,
|
||||
taskStatus,
|
||||
isStartingStatus,
|
||||
isStopStatus,
|
||||
curAgentState,
|
||||
});
|
||||
|
||||
const handleStopServer = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (conversationId) {
|
||||
pauseConversationSandbox({ conversationId });
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartServer = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (conversationId) {
|
||||
resumeConversationSandbox({ conversationId, providers });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="group relative">
|
||||
<DebugStackframeDot
|
||||
className="ml-[3.5px] w-6 h-6 cursor-pointer"
|
||||
color={statusColor}
|
||||
/>
|
||||
<ServerStatusContextMenu
|
||||
onClose={() => {}}
|
||||
onStopServer={
|
||||
conversation?.status === "RUNNING" ? handleStopServer : undefined
|
||||
}
|
||||
onStartServer={
|
||||
conversation?.status === "STOPPED" ? handleStartServer : undefined
|
||||
}
|
||||
conversationStatus={conversation?.status ?? null}
|
||||
position="bottom"
|
||||
className="opacity-0 invisible pointer-events-none group-hover:opacity-100 group-hover:visible group-hover:pointer-events-auto bottom-full left-0 mt-0 min-h-fit"
|
||||
isPausing={false}
|
||||
/>
|
||||
</div>
|
||||
<ConversationName />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -124,7 +124,7 @@ export function ConversationName() {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex items-center gap-2 h-[22px] text-base font-normal text-left pl-0 lg:pl-1"
|
||||
className="flex items-center gap-2 h-[22px] text-base font-normal text-left pl-0 lg:pl-3.5"
|
||||
data-testid="conversation-name"
|
||||
>
|
||||
{titleMode === "edit" ? (
|
||||
|
||||
+1
-18
@@ -8,18 +8,15 @@ import { TabContentArea } from "./tab-content-area";
|
||||
import { ConversationTabTitle } from "../conversation-tab-title";
|
||||
import Terminal from "#/components/features/terminal/terminal";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
|
||||
// Lazy load all tab components
|
||||
const EditorTab = lazy(() => import("#/routes/changes-tab"));
|
||||
const BrowserTab = lazy(() => import("#/routes/browser-tab"));
|
||||
const ServedTab = lazy(() => import("#/routes/served-tab"));
|
||||
const VSCodeTab = lazy(() => import("#/routes/vscode-tab"));
|
||||
const PlannerTab = lazy(() => import("#/routes/planner-tab"));
|
||||
|
||||
export function ConversationTabContent() {
|
||||
const { selectedTab, shouldShownAgentLoading } = useConversationStore();
|
||||
const { conversationId } = useConversationId();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -29,7 +26,6 @@ export function ConversationTabContent() {
|
||||
const isServedActive = selectedTab === "served";
|
||||
const isVSCodeActive = selectedTab === "vscode";
|
||||
const isTerminalActive = selectedTab === "terminal";
|
||||
const isPlannerActive = selectedTab === "planner";
|
||||
|
||||
// Define tab configurations
|
||||
const tabs = [
|
||||
@@ -46,11 +42,6 @@ export function ConversationTabContent() {
|
||||
component: Terminal,
|
||||
isActive: isTerminalActive,
|
||||
},
|
||||
{
|
||||
key: "planner",
|
||||
component: PlannerTab,
|
||||
isActive: isPlannerActive,
|
||||
},
|
||||
];
|
||||
|
||||
const conversationTabTitle = useMemo(() => {
|
||||
@@ -69,9 +60,6 @@ export function ConversationTabContent() {
|
||||
if (isTerminalActive) {
|
||||
return t(I18nKey.COMMON$TERMINAL);
|
||||
}
|
||||
if (isPlannerActive) {
|
||||
return t(I18nKey.COMMON$PLANNER);
|
||||
}
|
||||
return "";
|
||||
}, [
|
||||
isEditorActive,
|
||||
@@ -79,7 +67,6 @@ export function ConversationTabContent() {
|
||||
isServedActive,
|
||||
isVSCodeActive,
|
||||
isTerminalActive,
|
||||
isPlannerActive,
|
||||
]);
|
||||
|
||||
if (shouldShownAgentLoading) {
|
||||
@@ -91,11 +78,7 @@ export function ConversationTabContent() {
|
||||
<ConversationTabTitle title={conversationTabTitle} />
|
||||
<TabContentArea>
|
||||
{tabs.map(({ key, component: Component, isActive }) => (
|
||||
<TabWrapper
|
||||
// Force Terminal tab remount to reset XTerm buffer/state when conversationId changes
|
||||
key={key === "terminal" ? `${key}-${conversationId}` : key}
|
||||
isActive={isActive}
|
||||
>
|
||||
<TabWrapper key={key} isActive={isActive}>
|
||||
<Component />
|
||||
</TabWrapper>
|
||||
))}
|
||||
|
||||
+5
-12
@@ -5,16 +5,12 @@ type ConversationTabNavProps = {
|
||||
icon: ComponentType<{ className: string }>;
|
||||
onClick(): void;
|
||||
isActive?: boolean;
|
||||
label?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ConversationTabNav({
|
||||
icon: Icon,
|
||||
onClick,
|
||||
isActive,
|
||||
label,
|
||||
className,
|
||||
}: ConversationTabNavProps) {
|
||||
return (
|
||||
<button
|
||||
@@ -23,21 +19,18 @@ export function ConversationTabNav({
|
||||
onClick();
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md cursor-pointer",
|
||||
"pl-1.5 pr-2 py-1",
|
||||
"p-1 rounded-md cursor-pointer",
|
||||
"text-[#9299AA] bg-[#0D0F11]",
|
||||
isActive && "bg-[#25272D] text-white",
|
||||
isActive
|
||||
? "hover:text-white hover:bg-tertiary"
|
||||
: "hover:text-white hover:bg-[#0D0F11]",
|
||||
isActive ? "focus-within:text-white" : "focus-within:text-[#9299AA]",
|
||||
className,
|
||||
isActive
|
||||
? "focus-within:text-white focus-within:bg-tertiary"
|
||||
: "focus-within:text-[#9299AA] focus-within:bg-[#0D0F11]",
|
||||
)}
|
||||
>
|
||||
<Icon className={cn("w-5 h-5 text-inherit flex-shrink-0")} />
|
||||
{isActive && label && (
|
||||
<span className="text-sm font-medium whitespace-nowrap">{label}</span>
|
||||
)}
|
||||
<Icon className={cn("w-5 h-5 text-inherit")} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
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 { I18nKey } from "#/i18n/declaration";
|
||||
import TerminalIcon from "#/icons/terminal.svg?react";
|
||||
import GlobeIcon from "#/icons/globe.svg?react";
|
||||
import ServerIcon from "#/icons/server.svg?react";
|
||||
import GitChanges from "#/icons/git_changes.svg?react";
|
||||
import VSCodeIcon from "#/icons/vscode.svg?react";
|
||||
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";
|
||||
|
||||
interface ConversationTabsContextMenuProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ConversationTabsContextMenu({
|
||||
isOpen,
|
||||
onClose,
|
||||
}: ConversationTabsContextMenuProps) {
|
||||
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
|
||||
const { t } = useTranslation();
|
||||
const [unpinnedTabs, setUnpinnedTabs] = useLocalStorage<string[]>(
|
||||
"conversation-unpinned-tabs",
|
||||
[],
|
||||
);
|
||||
|
||||
const shouldUsePlanningAgent = USE_PLANNING_AGENT();
|
||||
|
||||
const tabConfig = [
|
||||
{
|
||||
tab: "editor",
|
||||
icon: GitChanges,
|
||||
i18nKey: I18nKey.COMMON$CHANGES,
|
||||
},
|
||||
{
|
||||
tab: "vscode",
|
||||
icon: VSCodeIcon,
|
||||
i18nKey: I18nKey.COMMON$CODE,
|
||||
},
|
||||
{
|
||||
tab: "terminal",
|
||||
icon: TerminalIcon,
|
||||
i18nKey: I18nKey.COMMON$TERMINAL,
|
||||
},
|
||||
{
|
||||
tab: "served",
|
||||
icon: ServerIcon,
|
||||
i18nKey: I18nKey.COMMON$APP,
|
||||
},
|
||||
{
|
||||
tab: "browser",
|
||||
icon: GlobeIcon,
|
||||
i18nKey: I18nKey.COMMON$BROWSER,
|
||||
},
|
||||
];
|
||||
|
||||
if (shouldUsePlanningAgent) {
|
||||
tabConfig.unshift({
|
||||
tab: "planner",
|
||||
icon: LessonPlanIcon,
|
||||
i18nKey: I18nKey.COMMON$PLANNER,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleTabClick = (tab: string) => {
|
||||
const tabString = tab;
|
||||
if (unpinnedTabs.includes(tabString)) {
|
||||
// Tab is unpinned, pin it (remove from unpinned list)
|
||||
setUnpinnedTabs(
|
||||
unpinnedTabs.filter((unpinnedTab) => unpinnedTab !== tabString),
|
||||
);
|
||||
} else {
|
||||
// Tab is pinned, unpin it (add to unpinned list)
|
||||
setUnpinnedTabs([...unpinnedTabs, tabString]);
|
||||
}
|
||||
};
|
||||
|
||||
const isTabPinned = (tab: string) => !unpinnedTabs.includes(tab as string);
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
testId="conversation-tabs-context-menu"
|
||||
ref={ref}
|
||||
alignment="right"
|
||||
position="bottom"
|
||||
className="mt-2 w-fit z-[9999]"
|
||||
>
|
||||
{tabConfig.map(({ tab, icon: Icon, i18nKey }) => {
|
||||
const pinned = isTabPinned(tab);
|
||||
return (
|
||||
<ContextMenuListItem
|
||||
key={tab}
|
||||
onClick={() => handleTabClick(tab)}
|
||||
className="flex items-center gap-2 p-2 hover:bg-[#5C5D62] rounded h-[30px]"
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span className="text-white text-sm">{t(i18nKey)}</span>
|
||||
{pinned ? (
|
||||
<PillFillIcon className="w-7 h-7 ml-auto flex-shrink-0 text-white -mr-[5px]" />
|
||||
) : (
|
||||
<PillIcon className="w-4.5 h-4.5 ml-auto flex-shrink-0 text-white" />
|
||||
)}
|
||||
</ContextMenuListItem>
|
||||
);
|
||||
})}
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
+3
-71
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
import TerminalIcon from "#/icons/terminal.svg?react";
|
||||
@@ -6,8 +6,6 @@ import GlobeIcon from "#/icons/globe.svg?react";
|
||||
import ServerIcon from "#/icons/server.svg?react";
|
||||
import GitChanges from "#/icons/git_changes.svg?react";
|
||||
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 { ConversationTabNav } from "./conversation-tab-nav";
|
||||
import { ChatActionTooltip } from "../../chat/chat-action-tooltip";
|
||||
@@ -17,8 +15,6 @@ import {
|
||||
useConversationStore,
|
||||
type ConversationTab,
|
||||
} from "#/state/conversation-store";
|
||||
import { ConversationTabsContextMenu } from "./conversation-tabs-context-menu";
|
||||
import { USE_PLANNING_AGENT } from "#/utils/feature-flags";
|
||||
|
||||
export function ConversationTabs() {
|
||||
const {
|
||||
@@ -28,8 +24,6 @@ export function ConversationTabs() {
|
||||
setSelectedTab,
|
||||
} = useConversationStore();
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
// Persist selectedTab and isRightPanelShown in localStorage
|
||||
const [persistedSelectedTab, setPersistedSelectedTab] =
|
||||
useLocalStorage<ConversationTab | null>(
|
||||
@@ -40,13 +34,6 @@ export function ConversationTabs() {
|
||||
const [persistedIsRightPanelShown, setPersistedIsRightPanelShown] =
|
||||
useLocalStorage<boolean>("conversation-right-panel-shown", true);
|
||||
|
||||
const [persistedUnpinnedTabs] = useLocalStorage<string[]>(
|
||||
"conversation-unpinned-tabs",
|
||||
[],
|
||||
);
|
||||
|
||||
const shouldUsePlanningAgent = USE_PLANNING_AGENT();
|
||||
|
||||
const onTabChange = (value: ConversationTab | null) => {
|
||||
setSelectedTab(value);
|
||||
// Persist the selected tab to localStorage
|
||||
@@ -100,70 +87,42 @@ export function ConversationTabs() {
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
tabValue: "editor",
|
||||
isActive: isTabActive("editor"),
|
||||
icon: GitChanges,
|
||||
onClick: () => onTabSelected("editor"),
|
||||
tooltipContent: t(I18nKey.COMMON$CHANGES),
|
||||
tooltipAriaLabel: t(I18nKey.COMMON$CHANGES),
|
||||
label: t(I18nKey.COMMON$CHANGES),
|
||||
},
|
||||
{
|
||||
tabValue: "vscode",
|
||||
isActive: isTabActive("vscode"),
|
||||
icon: VSCodeIcon,
|
||||
onClick: () => onTabSelected("vscode"),
|
||||
tooltipContent: <VSCodeTooltipContent />,
|
||||
tooltipAriaLabel: t(I18nKey.COMMON$CODE),
|
||||
label: t(I18nKey.COMMON$CODE),
|
||||
},
|
||||
{
|
||||
tabValue: "terminal",
|
||||
isActive: isTabActive("terminal"),
|
||||
icon: TerminalIcon,
|
||||
onClick: () => onTabSelected("terminal"),
|
||||
tooltipContent: t(I18nKey.COMMON$TERMINAL),
|
||||
tooltipAriaLabel: t(I18nKey.COMMON$TERMINAL),
|
||||
label: t(I18nKey.COMMON$TERMINAL),
|
||||
className: "pl-2",
|
||||
},
|
||||
{
|
||||
tabValue: "served",
|
||||
isActive: isTabActive("served"),
|
||||
icon: ServerIcon,
|
||||
onClick: () => onTabSelected("served"),
|
||||
tooltipContent: t(I18nKey.COMMON$APP),
|
||||
tooltipAriaLabel: t(I18nKey.COMMON$APP),
|
||||
label: t(I18nKey.COMMON$APP),
|
||||
},
|
||||
{
|
||||
tabValue: "browser",
|
||||
isActive: isTabActive("browser"),
|
||||
icon: GlobeIcon,
|
||||
onClick: () => onTabSelected("browser"),
|
||||
tooltipContent: t(I18nKey.COMMON$BROWSER),
|
||||
tooltipAriaLabel: t(I18nKey.COMMON$BROWSER),
|
||||
label: t(I18nKey.COMMON$BROWSER),
|
||||
},
|
||||
];
|
||||
|
||||
if (shouldUsePlanningAgent) {
|
||||
tabs.unshift({
|
||||
tabValue: "planner",
|
||||
isActive: isTabActive("planner"),
|
||||
icon: LessonPlanIcon,
|
||||
onClick: () => onTabSelected("planner"),
|
||||
tooltipContent: t(I18nKey.COMMON$PLANNER),
|
||||
tooltipAriaLabel: t(I18nKey.COMMON$PLANNER),
|
||||
label: t(I18nKey.COMMON$PLANNER),
|
||||
});
|
||||
}
|
||||
|
||||
// Filter out unpinned tabs
|
||||
const visibleTabs = tabs.filter(
|
||||
(tab) => !persistedUnpinnedTabs.includes(tab.tabValue),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -171,17 +130,9 @@ export function ConversationTabs() {
|
||||
"flex flex-row justify-start lg:justify-end items-center gap-4.5",
|
||||
)}
|
||||
>
|
||||
{visibleTabs.map(
|
||||
{tabs.map(
|
||||
(
|
||||
{
|
||||
icon,
|
||||
onClick,
|
||||
isActive,
|
||||
tooltipContent,
|
||||
tooltipAriaLabel,
|
||||
label,
|
||||
className,
|
||||
},
|
||||
{ icon, onClick, isActive, tooltipContent, tooltipAriaLabel },
|
||||
index,
|
||||
) => (
|
||||
<ChatActionTooltip
|
||||
@@ -193,29 +144,10 @@ export function ConversationTabs() {
|
||||
icon={icon}
|
||||
onClick={onClick}
|
||||
isActive={isActive}
|
||||
label={label}
|
||||
className={className}
|
||||
/>
|
||||
</ChatActionTooltip>
|
||||
),
|
||||
)}
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className={cn(
|
||||
"p-1 pl-0 rounded-md cursor-pointer",
|
||||
"text-[#9299AA] bg-[#0D0F11]",
|
||||
)}
|
||||
aria-label={t(I18nKey.COMMON$MORE_OPTIONS)}
|
||||
>
|
||||
<ThreeDotsVerticalIcon className={cn("w-5 h-5 text-inherit")} />
|
||||
</button>
|
||||
<ConversationTabsContextMenu
|
||||
isOpen={isMenuOpen}
|
||||
onClose={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ export function RepositorySelectionForm({
|
||||
onBranchSelect={handleBranchSelection}
|
||||
defaultBranch={defaultBranch}
|
||||
placeholder="Select branch..."
|
||||
className="max-w-full"
|
||||
className="max-w-[500px]"
|
||||
disabled={!selectedRepository || isLoadingSettings}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -7,5 +7,5 @@ export function paragraph({
|
||||
}: React.ClassAttributes<HTMLParagraphElement> &
|
||||
React.HTMLAttributes<HTMLParagraphElement> &
|
||||
ExtraProps) {
|
||||
return <p className="py-2.5 first:pt-0 last:pb-0">{children}</p>;
|
||||
return <p className="pb-[10px] last:pb-0">{children}</p>;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import BitbucketLogo from "#/assets/branding/bitbucket-logo.svg?react";
|
||||
import { useAuthUrl } from "#/hooks/use-auth-url";
|
||||
import { GetConfigResponse } from "#/api/option-service/option.types";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
|
||||
interface AuthModalProps {
|
||||
githubAuthUrl: string | null;
|
||||
@@ -27,7 +26,6 @@ export function AuthModal({
|
||||
providersConfigured,
|
||||
}: AuthModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { trackLoginButtonClick } = useTracking();
|
||||
|
||||
const gitlabAuthUrl = useAuthUrl({
|
||||
appMode: appMode || null,
|
||||
@@ -49,7 +47,6 @@ export function AuthModal({
|
||||
|
||||
const handleGitHubAuth = () => {
|
||||
if (githubAuthUrl) {
|
||||
trackLoginButtonClick({ provider: "github" });
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
window.location.href = githubAuthUrl;
|
||||
}
|
||||
@@ -57,7 +54,6 @@ export function AuthModal({
|
||||
|
||||
const handleGitLabAuth = () => {
|
||||
if (gitlabAuthUrl) {
|
||||
trackLoginButtonClick({ provider: "gitlab" });
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
window.location.href = gitlabAuthUrl;
|
||||
}
|
||||
@@ -65,7 +61,6 @@ export function AuthModal({
|
||||
|
||||
const handleBitbucketAuth = () => {
|
||||
if (bitbucketAuthUrl) {
|
||||
trackLoginButtonClick({ provider: "bitbucket" });
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
window.location.href = bitbucketAuthUrl;
|
||||
}
|
||||
@@ -73,7 +68,6 @@ export function AuthModal({
|
||||
|
||||
const handleEnterpriseSsoAuth = () => {
|
||||
if (enterpriseSsoUrl) {
|
||||
trackLoginButtonClick({ provider: "enterprise_sso" });
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
window.location.href = enterpriseSsoUrl;
|
||||
}
|
||||
|
||||
@@ -134,16 +134,9 @@ const getObservationEventTitle = (event: OpenHandsEvent): React.ReactNode => {
|
||||
case "BrowserObservation":
|
||||
observationKey = "OBSERVATION_MESSAGE$BROWSE";
|
||||
break;
|
||||
case "TaskTrackerObservation": {
|
||||
const { command } = event.observation;
|
||||
if (command === "plan") {
|
||||
observationKey = "OBSERVATION_MESSAGE$TASK_TRACKING_PLAN";
|
||||
} else {
|
||||
// command === "view"
|
||||
observationKey = "OBSERVATION_MESSAGE$TASK_TRACKING_VIEW";
|
||||
}
|
||||
case "TaskTrackerObservation":
|
||||
observationKey = "OBSERVATION_MESSAGE$TASK_TRACKING";
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// For unknown observations, use the type name
|
||||
return observationType.replace("Observation", "").toUpperCase();
|
||||
|
||||
@@ -7,6 +7,17 @@ import {
|
||||
isConversationStateUpdateEvent,
|
||||
} from "#/types/v1/type-guards";
|
||||
|
||||
// V1 events that should not be rendered
|
||||
const NO_RENDER_ACTION_TYPES = [
|
||||
"ThinkAction",
|
||||
// Add more action types that should not be rendered
|
||||
];
|
||||
|
||||
const NO_RENDER_OBSERVATION_TYPES = [
|
||||
"ThinkObservation",
|
||||
// Add more observation types that should not be rendered
|
||||
];
|
||||
|
||||
export const shouldRenderEvent = (event: OpenHandsEvent) => {
|
||||
// Explicitly exclude system events that should not be rendered in chat
|
||||
if (isConversationStateUpdateEvent(event)) {
|
||||
@@ -23,12 +34,18 @@ export const shouldRenderEvent = (event: OpenHandsEvent) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return !NO_RENDER_ACTION_TYPES.includes(actionType);
|
||||
}
|
||||
|
||||
// Render observation events
|
||||
// Render observation events (with filtering)
|
||||
if (isObservationEvent(event)) {
|
||||
return true;
|
||||
// For V1, observation is an object with kind property
|
||||
const observationType = event.observation.kind;
|
||||
|
||||
// Note: ObservationEvent source is always "environment", not "user"
|
||||
// So no need to check for user source here
|
||||
|
||||
return !NO_RENDER_OBSERVATION_TYPES.includes(observationType);
|
||||
}
|
||||
|
||||
// Render message events (user and assistant messages)
|
||||
|
||||
@@ -3,4 +3,3 @@ export { ObservationPairEventMessage } from "./observation-pair-event-message";
|
||||
export { ErrorEventMessage } from "./error-event-message";
|
||||
export { FinishEventMessage } from "./finish-event-message";
|
||||
export { GenericEventMessageWrapper } from "./generic-event-message-wrapper";
|
||||
export { ThoughtEventMessage } from "./thought-event-message";
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from "react";
|
||||
import { ActionEvent } from "#/types/v1/core";
|
||||
import { ChatMessage } from "../../../features/chat/chat-message";
|
||||
|
||||
interface ThoughtEventMessageProps {
|
||||
event: ActionEvent;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function ThoughtEventMessage({
|
||||
event,
|
||||
actions,
|
||||
}: ThoughtEventMessageProps) {
|
||||
// Extract thought content from the action event
|
||||
const thoughtContent = event.thought
|
||||
.filter((t) => t.type === "text")
|
||||
.map((t) => t.text)
|
||||
.join("\n");
|
||||
|
||||
// If there's no thought content, don't render anything
|
||||
if (!thoughtContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChatMessage type="agent" message={thoughtContent} actions={actions} />
|
||||
);
|
||||
}
|
||||
@@ -14,13 +14,13 @@ import {
|
||||
ErrorEventMessage,
|
||||
UserAssistantEventMessage,
|
||||
FinishEventMessage,
|
||||
ObservationPairEventMessage,
|
||||
GenericEventMessageWrapper,
|
||||
ThoughtEventMessage,
|
||||
} from "./event-message-components";
|
||||
|
||||
interface EventMessageProps {
|
||||
event: OpenHandsEvent;
|
||||
messages: OpenHandsEvent[];
|
||||
hasObservationPair: boolean;
|
||||
isLastMessage: boolean;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
@@ -36,7 +36,7 @@ interface EventMessageProps {
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
export function EventMessage({
|
||||
event,
|
||||
messages,
|
||||
hasObservationPair,
|
||||
isLastMessage,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
@@ -69,6 +69,19 @@ export function EventMessage({
|
||||
return <ErrorEventMessage event={event} {...commonProps} />;
|
||||
}
|
||||
|
||||
// Observation pairs with actions
|
||||
if (hasObservationPair && isActionEvent(event)) {
|
||||
return (
|
||||
<ObservationPairEventMessage
|
||||
event={event}
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Finish actions
|
||||
if (isActionEvent(event) && event.action.kind === "FinishAction") {
|
||||
return (
|
||||
@@ -79,39 +92,6 @@ export function EventMessage({
|
||||
);
|
||||
}
|
||||
|
||||
// Action events - render thought + action (will be replaced by thought + observation)
|
||||
if (isActionEvent(event)) {
|
||||
return (
|
||||
<>
|
||||
<ThoughtEventMessage event={event} actions={actions} />
|
||||
<GenericEventMessageWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Observation events - find the corresponding action and render thought + observation
|
||||
if (isObservationEvent(event)) {
|
||||
// Find the action that this observation is responding to
|
||||
const correspondingAction = messages.find(
|
||||
(msg) => isActionEvent(msg) && msg.id === event.action_id,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{correspondingAction && isActionEvent(correspondingAction) && (
|
||||
<ThoughtEventMessage event={correspondingAction} actions={actions} />
|
||||
)}
|
||||
<GenericEventMessageWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Message events (user and assistant messages)
|
||||
if (!isActionEvent(event) && !isObservationEvent(event)) {
|
||||
// This is a MessageEvent
|
||||
@@ -124,7 +104,7 @@ export function EventMessage({
|
||||
);
|
||||
}
|
||||
|
||||
// Generic fallback for all other events
|
||||
// Generic fallback for all other events (including observation events)
|
||||
return (
|
||||
<GenericEventMessageWrapper event={event} isLastMessage={isLastMessage} />
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { OpenHandsEvent } from "#/types/v1/core";
|
||||
import { isActionEvent, isObservationEvent } from "#/types/v1/type-guards";
|
||||
import { EventMessage } from "./event-message";
|
||||
import { ChatMessage } from "../../features/chat/chat-message";
|
||||
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
|
||||
@@ -8,16 +9,29 @@ import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-
|
||||
// import MemoryIcon from "#/icons/memory_icon.svg?react";
|
||||
|
||||
interface MessagesProps {
|
||||
messages: OpenHandsEvent[]; // UI events (actions replaced by observations)
|
||||
allEvents: OpenHandsEvent[]; // Full event history (for action lookup)
|
||||
messages: OpenHandsEvent[];
|
||||
}
|
||||
|
||||
export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
({ messages, allEvents }) => {
|
||||
({ messages }) => {
|
||||
const { getOptimisticUserMessage } = useOptimisticUserMessageStore();
|
||||
|
||||
const optimisticUserMessage = getOptimisticUserMessage();
|
||||
|
||||
const actionHasObservationPair = React.useCallback(
|
||||
(event: OpenHandsEvent): boolean => {
|
||||
if (isActionEvent(event)) {
|
||||
// Check if there's a corresponding observation event
|
||||
return !!messages.some(
|
||||
(msg) => isObservationEvent(msg) && msg.action_id === event.id,
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
[messages],
|
||||
);
|
||||
|
||||
// TODO: Implement microagent functionality for V1 if needed
|
||||
// For now, we'll skip microagent features
|
||||
|
||||
@@ -27,7 +41,7 @@ export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
<EventMessage
|
||||
key={message.id}
|
||||
event={message}
|
||||
messages={allEvents}
|
||||
hasObservationPair={actionHasObservationPair(message)}
|
||||
isLastMessage={messages.length - 1 === index}
|
||||
isInLast10Actions={messages.length - 1 - index < 10}
|
||||
// Microagent props - not implemented yet for V1
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { handleActionEventCacheInvalidation } from "#/utils/cache-utils";
|
||||
import { buildWebSocketUrl } from "#/utils/websocket-url";
|
||||
import type { V1SendMessageRequest } from "#/api/conversation-service/v1-conversation-service.types";
|
||||
import EventService from "#/api/event-service/event-service.api";
|
||||
import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export type V1_WebSocketConnectionState =
|
||||
@@ -67,7 +67,7 @@ export function ConversationWebSocketProvider({
|
||||
const { addEvent } = useEventStore();
|
||||
const { setErrorMessage, removeErrorMessage } = useErrorMessageStore();
|
||||
const { removeOptimisticUserMessage } = useOptimisticUserMessageStore();
|
||||
const { setExecutionStatus } = useV1ConversationStateStore();
|
||||
const { setAgentStatus } = useV1ConversationStateStore();
|
||||
const { appendInput, appendOutput } = useCommandStore();
|
||||
|
||||
// History loading state
|
||||
@@ -154,10 +154,10 @@ export function ConversationWebSocketProvider({
|
||||
// TODO: Tests
|
||||
if (isConversationStateUpdateEvent(event)) {
|
||||
if (isFullStateConversationStateUpdateEvent(event)) {
|
||||
setExecutionStatus(event.value.execution_status);
|
||||
setAgentStatus(event.value.agent_status);
|
||||
}
|
||||
if (isAgentStatusConversationStateUpdateEvent(event)) {
|
||||
setExecutionStatus(event.value);
|
||||
setAgentStatus(event.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ export function ConversationWebSocketProvider({
|
||||
removeOptimisticUserMessage,
|
||||
queryClient,
|
||||
conversationId,
|
||||
setExecutionStatus,
|
||||
setAgentStatus,
|
||||
appendInput,
|
||||
appendOutput,
|
||||
],
|
||||
@@ -211,7 +211,8 @@ export function ConversationWebSocketProvider({
|
||||
// Fetch expected event count for history loading detection
|
||||
if (conversationId) {
|
||||
try {
|
||||
const count = await EventService.getEventCount(conversationId);
|
||||
const count =
|
||||
await V1ConversationService.getEventCount(conversationId);
|
||||
setExpectedEventCount(count);
|
||||
|
||||
// If no events expected, mark as loaded immediately
|
||||
|
||||
@@ -2,7 +2,6 @@ import { QueryClient } from "@tanstack/react-query";
|
||||
import { Provider } from "#/types/settings";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
|
||||
import { SandboxService } from "#/api/sandbox-service/sandbox-service.api";
|
||||
|
||||
/**
|
||||
* Gets the conversation version from the cache
|
||||
@@ -49,7 +48,7 @@ const fetchV1ConversationData = async (
|
||||
*/
|
||||
export const pauseV1ConversationSandbox = async (conversationId: string) => {
|
||||
const { sandboxId } = await fetchV1ConversationData(conversationId);
|
||||
return SandboxService.pauseSandbox(sandboxId);
|
||||
return V1ConversationService.pauseSandbox(sandboxId);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -76,7 +75,7 @@ export const stopV0Conversation = async (conversationId: string) =>
|
||||
*/
|
||||
export const resumeV1ConversationSandbox = async (conversationId: string) => {
|
||||
const { sandboxId } = await fetchV1ConversationData(conversationId);
|
||||
return SandboxService.resumeSandbox(sandboxId);
|
||||
return V1ConversationService.resumeSandbox(sandboxId);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { SecretsService } from "#/api/secrets-service";
|
||||
import { Provider, ProviderToken } from "#/types/settings";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
|
||||
export const useAddGitProviders = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { trackGitProviderConnected } = useTracking();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
@@ -13,18 +11,7 @@ export const useAddGitProviders = () => {
|
||||
}: {
|
||||
providers: Record<Provider, ProviderToken>;
|
||||
}) => SecretsService.addGitProvider(providers),
|
||||
onSuccess: async (_, { providers }) => {
|
||||
// Track which providers were connected (filter out empty tokens)
|
||||
const connectedProviders = Object.entries(providers)
|
||||
.filter(([, value]) => value.token && value.token.trim() !== "")
|
||||
.map(([key]) => key);
|
||||
|
||||
if (connectedProviders.length > 0) {
|
||||
trackGitProviderConnected({
|
||||
providers: connectedProviders,
|
||||
});
|
||||
}
|
||||
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||
},
|
||||
meta: {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import posthog from "posthog-js";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
|
||||
import { SuggestedTask } from "#/utils/types";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { CreateMicroagent, Conversation } from "#/api/open-hands.types";
|
||||
import { USE_V1_CONVERSATION_API } from "#/utils/feature-flags";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
|
||||
interface CreateConversationVariables {
|
||||
query?: string;
|
||||
@@ -31,7 +31,6 @@ interface CreateConversationResponse extends Partial<Conversation> {
|
||||
|
||||
export const useCreateConversation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { trackConversationCreated } = useTracking();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ["create-conversation"],
|
||||
@@ -87,11 +86,12 @@ export const useCreateConversation = () => {
|
||||
is_v1: false,
|
||||
};
|
||||
},
|
||||
onSuccess: async (_, { repository }) => {
|
||||
trackConversationCreated({
|
||||
hasRepository: !!repository,
|
||||
onSuccess: async (_, { query, repository }) => {
|
||||
posthog.capture("initial_query_submitted", {
|
||||
entry_point: "task_form",
|
||||
query_character_length: query?.length,
|
||||
has_repository: !!repository,
|
||||
});
|
||||
|
||||
queryClient.removeQueries({
|
||||
queryKey: ["user", "conversations"],
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import ConversationService from "#/api/conversation-service/conversation-service
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
|
||||
export interface BatchFeedbackData {
|
||||
exists: boolean;
|
||||
@@ -26,20 +25,13 @@ export const getFeedbackExistsQueryKey = (
|
||||
export const useBatchFeedback = () => {
|
||||
const { conversationId } = useConversationId();
|
||||
const { data: config } = useConfig();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const queryClient = useQueryClient();
|
||||
const runtimeIsReady = useRuntimeIsReady();
|
||||
|
||||
const isV1Conversation = conversation?.conversation_version === "V1";
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: getFeedbackQueryKey(conversationId),
|
||||
queryFn: () => ConversationService.getBatchFeedback(conversationId!),
|
||||
enabled:
|
||||
runtimeIsReady &&
|
||||
!!conversationId &&
|
||||
config?.APP_MODE === "saas" &&
|
||||
!isV1Conversation,
|
||||
enabled: runtimeIsReady && !!conversationId && config?.APP_MODE === "saas",
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { SandboxService } from "#/api/sandbox-service/sandbox-service.api";
|
||||
import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
|
||||
|
||||
export const useBatchSandboxes = (ids: string[]) =>
|
||||
useQuery({
|
||||
queryKey: ["sandboxes", "batch", ids],
|
||||
queryFn: () => SandboxService.batchGetSandboxes(ids),
|
||||
queryFn: () => V1ConversationService.batchGetSandboxes(ids),
|
||||
enabled: ids.length > 0,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { BatchFeedbackData, getFeedbackQueryKey } from "./use-batch-feedback";
|
||||
|
||||
export type FeedbackData = BatchFeedbackData;
|
||||
@@ -10,9 +9,6 @@ export const useFeedbackExists = (eventId?: number) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { conversationId } = useConversationId();
|
||||
const { data: config } = useConfig();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
|
||||
const isV1Conversation = conversation?.conversation_version === "V1";
|
||||
|
||||
return useQuery<FeedbackData>({
|
||||
queryKey: [...getFeedbackQueryKey(conversationId), eventId],
|
||||
@@ -26,11 +22,7 @@ export const useFeedbackExists = (eventId?: number) => {
|
||||
|
||||
return batchData?.[eventId.toString()] ?? { exists: false };
|
||||
},
|
||||
enabled:
|
||||
!!eventId &&
|
||||
!!conversationId &&
|
||||
config?.APP_MODE === "saas" &&
|
||||
!isV1Conversation,
|
||||
enabled: !!eventId && !!conversationId && config?.APP_MODE === "saas",
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@@ -3,30 +3,30 @@ import { useAgentStore } from "#/stores/agent-store";
|
||||
import { useV1ConversationStateStore } from "#/stores/v1-conversation-state-store";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { V1ExecutionStatus } from "#/types/v1/core/base/common";
|
||||
import { V1AgentStatus } from "#/types/v1/core/base/common";
|
||||
|
||||
/**
|
||||
* Maps V1 agent status to V0 AgentState
|
||||
*/
|
||||
function mapV1StatusToV0State(status: V1ExecutionStatus | null): AgentState {
|
||||
function mapV1StatusToV0State(status: V1AgentStatus | null): AgentState {
|
||||
if (!status) {
|
||||
return AgentState.LOADING;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case V1ExecutionStatus.IDLE:
|
||||
case V1AgentStatus.IDLE:
|
||||
return AgentState.AWAITING_USER_INPUT;
|
||||
case V1ExecutionStatus.RUNNING:
|
||||
case V1AgentStatus.RUNNING:
|
||||
return AgentState.RUNNING;
|
||||
case V1ExecutionStatus.PAUSED:
|
||||
case V1AgentStatus.PAUSED:
|
||||
return AgentState.PAUSED;
|
||||
case V1ExecutionStatus.WAITING_FOR_CONFIRMATION:
|
||||
case V1AgentStatus.WAITING_FOR_CONFIRMATION:
|
||||
return AgentState.AWAITING_USER_CONFIRMATION;
|
||||
case V1ExecutionStatus.FINISHED:
|
||||
case V1AgentStatus.FINISHED:
|
||||
return AgentState.FINISHED;
|
||||
case V1ExecutionStatus.ERROR:
|
||||
case V1AgentStatus.ERROR:
|
||||
return AgentState.ERROR;
|
||||
case V1ExecutionStatus.STUCK:
|
||||
case V1AgentStatus.STUCK:
|
||||
return AgentState.ERROR; // Map STUCK to ERROR for now
|
||||
default:
|
||||
return AgentState.LOADING;
|
||||
@@ -41,9 +41,7 @@ function mapV1StatusToV0State(status: V1ExecutionStatus | null): AgentState {
|
||||
export function useAgentState() {
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const v0State = useAgentStore((state) => state.curAgentState);
|
||||
const v1Status = useV1ConversationStateStore(
|
||||
(state) => state.execution_status,
|
||||
);
|
||||
const v1Status = useV1ConversationStateStore((state) => state.agent_status);
|
||||
|
||||
const isV1Conversation = conversation?.conversation_version === "V1";
|
||||
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
RefObject,
|
||||
useState,
|
||||
useCallback,
|
||||
useRef,
|
||||
useLayoutEffect,
|
||||
} from "react";
|
||||
import { RefObject, useEffect, useState, useCallback, useRef } from "react";
|
||||
|
||||
export function useScrollToBottom(scrollRef: RefObject<HTMLDivElement | null>) {
|
||||
// Track whether we should auto-scroll to the bottom when content changes
|
||||
@@ -71,20 +65,20 @@ export function useScrollToBottom(scrollRef: RefObject<HTMLDivElement | null>) {
|
||||
}, [scrollRef]);
|
||||
|
||||
// Auto-scroll effect that runs when content changes
|
||||
// Use useLayoutEffect to scroll after DOM updates but before paint
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
// Only auto-scroll if autoscroll is enabled
|
||||
if (autoscroll) {
|
||||
const dom = scrollRef.current;
|
||||
if (dom) {
|
||||
// Scroll to bottom - this will trigger on any DOM change
|
||||
dom.scrollTo({
|
||||
top: dom.scrollHeight,
|
||||
behavior: "smooth",
|
||||
requestAnimationFrame(() => {
|
||||
dom.scrollTo({
|
||||
top: dom.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}); // No dependency array - runs after every render to follow new content
|
||||
});
|
||||
|
||||
return {
|
||||
scrollRef,
|
||||
|
||||
@@ -91,7 +91,6 @@ export const useTerminal = () => {
|
||||
|
||||
return () => {
|
||||
terminal.current?.dispose();
|
||||
lastCommandIndex.current = 0;
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import posthog from "posthog-js";
|
||||
import { useConfig } from "./query/use-config";
|
||||
import { useSettings } from "./query/use-settings";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
/**
|
||||
* Hook that provides tracking functions with automatic data collection
|
||||
* from available hooks (config, settings, etc.)
|
||||
*/
|
||||
export const useTracking = () => {
|
||||
const { data: config } = useConfig();
|
||||
const { data: settings } = useSettings();
|
||||
|
||||
// Common properties included in all tracking events
|
||||
const commonProperties = {
|
||||
app_surface: config?.APP_MODE || "unknown",
|
||||
plan_tier: null,
|
||||
current_url: window.location.href,
|
||||
user_email: settings?.EMAIL || settings?.GIT_USER_EMAIL || null,
|
||||
};
|
||||
|
||||
const trackLoginButtonClick = ({ provider }: { provider: Provider }) => {
|
||||
posthog.capture("login_button_clicked", {
|
||||
provider,
|
||||
...commonProperties,
|
||||
});
|
||||
};
|
||||
|
||||
const trackConversationCreated = ({
|
||||
hasRepository,
|
||||
}: {
|
||||
hasRepository: boolean;
|
||||
}) => {
|
||||
posthog.capture("conversation_created", {
|
||||
has_repository: hasRepository,
|
||||
...commonProperties,
|
||||
});
|
||||
};
|
||||
|
||||
const trackPushButtonClick = () => {
|
||||
posthog.capture("push_button_clicked", {
|
||||
...commonProperties,
|
||||
});
|
||||
};
|
||||
|
||||
const trackPullButtonClick = () => {
|
||||
posthog.capture("pull_button_clicked", {
|
||||
...commonProperties,
|
||||
});
|
||||
};
|
||||
|
||||
const trackCreatePrButtonClick = () => {
|
||||
posthog.capture("create_pr_button_clicked", {
|
||||
...commonProperties,
|
||||
});
|
||||
};
|
||||
|
||||
const trackGitProviderConnected = ({
|
||||
providers,
|
||||
}: {
|
||||
providers: string[];
|
||||
}) => {
|
||||
posthog.capture("git_provider_connected", {
|
||||
providers,
|
||||
...commonProperties,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
trackLoginButtonClick,
|
||||
trackConversationCreated,
|
||||
trackPushButtonClick,
|
||||
trackPullButtonClick,
|
||||
trackCreatePrButtonClick,
|
||||
trackGitProviderConnected,
|
||||
};
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import { useMemo } from "react";
|
||||
import { useWsClient, V0_WebSocketStatus } from "#/context/ws-client-provider";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { useConversationWebSocket } from "#/contexts/conversation-websocket-context";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
|
||||
/**
|
||||
* Unified hook that returns the current WebSocket status
|
||||
@@ -10,15 +9,11 @@ import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
* - For V1 conversations: Returns status from ConversationWebSocketProvider
|
||||
*/
|
||||
export function useUnifiedWebSocketStatus(): V0_WebSocketStatus {
|
||||
const { conversationId } = useConversationId();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const v0Status = useWsClient();
|
||||
const v1Context = useConversationWebSocket();
|
||||
|
||||
// Check if this is a V1 conversation:
|
||||
const isV1Conversation =
|
||||
conversationId.startsWith("task-") ||
|
||||
conversation?.conversation_version === "V1";
|
||||
const isV1Conversation = conversation?.conversation_version === "V1";
|
||||
|
||||
const webSocketStatus = useMemo(() => {
|
||||
if (isV1Conversation) {
|
||||
@@ -38,13 +33,7 @@ export function useUnifiedWebSocketStatus(): V0_WebSocketStatus {
|
||||
}
|
||||
}
|
||||
return v0Status.webSocketStatus;
|
||||
}, [
|
||||
isV1Conversation,
|
||||
v1Context,
|
||||
v0Status.webSocketStatus,
|
||||
conversationId,
|
||||
conversation,
|
||||
]);
|
||||
}, [isV1Conversation, v1Context, v0Status.webSocketStatus]);
|
||||
|
||||
return webSocketStatus;
|
||||
}
|
||||
|
||||
@@ -471,15 +471,12 @@ export enum I18nKey {
|
||||
PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECT_TO_GITHUB = "PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECT_TO_GITHUB",
|
||||
PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECTED = "PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECTED",
|
||||
PROJECT_MENU_DETAILS$AGO_LABEL = "PROJECT_MENU_DETAILS$AGO_LABEL",
|
||||
STATUS$ERROR = "STATUS$ERROR",
|
||||
STATUS$ERROR_LLM_AUTHENTICATION = "STATUS$ERROR_LLM_AUTHENTICATION",
|
||||
STATUS$ERROR_LLM_SERVICE_UNAVAILABLE = "STATUS$ERROR_LLM_SERVICE_UNAVAILABLE",
|
||||
STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR = "STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR",
|
||||
STATUS$ERROR_LLM_OUT_OF_CREDITS = "STATUS$ERROR_LLM_OUT_OF_CREDITS",
|
||||
STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION = "STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION",
|
||||
STATUS$ERROR_RUNTIME_DISCONNECTED = "STATUS$ERROR_RUNTIME_DISCONNECTED",
|
||||
STATUS$ERROR_MEMORY = "STATUS$ERROR_MEMORY",
|
||||
STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR = "STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR",
|
||||
STATUS$LLM_RETRY = "STATUS$LLM_RETRY",
|
||||
AGENT_ERROR$BAD_ACTION = "AGENT_ERROR$BAD_ACTION",
|
||||
AGENT_ERROR$ACTION_TIMEOUT = "AGENT_ERROR$ACTION_TIMEOUT",
|
||||
@@ -860,7 +857,6 @@ export enum I18nKey {
|
||||
BUTTON$DELETE_CONVERSATION = "BUTTON$DELETE_CONVERSATION",
|
||||
BUTTON$RENAME = "BUTTON$RENAME",
|
||||
COMMON$APP = "COMMON$APP",
|
||||
COMMON$PLANNER = "COMMON$PLANNER",
|
||||
COMMON$APPLICATION_SETTINGS = "COMMON$APPLICATION_SETTINGS",
|
||||
COMMON$BROWSER = "COMMON$BROWSER",
|
||||
COMMON$CHANGES = "COMMON$CHANGES",
|
||||
@@ -935,6 +931,4 @@ export enum I18nKey {
|
||||
TOAST$FAILED_TO_STOP_CONVERSATION = "TOAST$FAILED_TO_STOP_CONVERSATION",
|
||||
TOAST$CONVERSATION_STOPPED = "TOAST$CONVERSATION_STOPPED",
|
||||
AGENT_STATUS$WAITING_FOR_USER_CONFIRMATION = "AGENT_STATUS$WAITING_FOR_USER_CONFIRMATION",
|
||||
COMMON$MORE_OPTIONS = "COMMON$MORE_OPTIONS",
|
||||
COMMON$CREATE_A_PLAN = "COMMON$CREATE_A_PLAN",
|
||||
}
|
||||
|
||||
@@ -6912,20 +6912,20 @@
|
||||
"tr": "Bu alan zorunludur"
|
||||
},
|
||||
"PLANNER$EMPTY_MESSAGE": {
|
||||
"en": "There is currently no plan for this repo",
|
||||
"uk": "Наразі для цього репозиторію немає плану",
|
||||
"ja": "現在このリポジトリには計画がありません",
|
||||
"zh-CN": "当前此仓库没有计划",
|
||||
"zh-TW": "目前此存儲庫沒有計劃",
|
||||
"ko-KR": "이 저장소에 대한 계획이 현재 없습니다",
|
||||
"no": "Det finnes foreløpig ingen plan for dette repoet",
|
||||
"ar": "لا يوجد حالياً خطة لهذا المستودع",
|
||||
"de": "Derzeit gibt es keinen Plan für dieses Repository",
|
||||
"fr": "Il n'y a actuellement aucun plan pour ce dépôt",
|
||||
"it": "Attualmente non c'è un piano per questo repository",
|
||||
"pt": "Atualmente não há plano para este repositório",
|
||||
"es": "Actualmente no hay un plan para este repositorio",
|
||||
"tr": "Şu anda bu depo için bir plan yok"
|
||||
"en": "No plan created.",
|
||||
"zh-CN": "计划未创建",
|
||||
"zh-TW": "未建立任何計劃。",
|
||||
"de": "Kein Plan erstellt.",
|
||||
"ko-KR": "생성된 계획이 없습니다.",
|
||||
"no": "Ingen plan opprettet.",
|
||||
"it": "Nessun piano creato.",
|
||||
"pt": "Nenhum plano criado.",
|
||||
"es": "Ningún plan creado.",
|
||||
"ar": "لم يتم إنشاء أي خطة.",
|
||||
"fr": "Aucun plan créé.",
|
||||
"tr": "Plan oluşturulmadı.",
|
||||
"ja": "プランナーは空です",
|
||||
"uk": "План не створено."
|
||||
},
|
||||
"FEEDBACK$PUBLIC_LABEL": {
|
||||
"en": "Public",
|
||||
@@ -7615,22 +7615,6 @@
|
||||
"tr": "İçerik politikası ihlali. Çıktı, içerik filtreleme politikası tarafından engellendi.",
|
||||
"uk": "Порушення політики щодо вмісту. Вивід було заблоковано політикою фільтрації вмісту."
|
||||
},
|
||||
"STATUS$ERROR": {
|
||||
"en": "An error occurred. Please try again.",
|
||||
"zh-CN": "发生错误,请重试",
|
||||
"zh-TW": "發生錯誤,請重試",
|
||||
"ko-KR": "오류가 발생했습니다. 다시 시도해주세요.",
|
||||
"ja": "エラーが発生しました。もう一度お試しください。",
|
||||
"no": "Det oppstod en feil. Vennligst prøv igjen.",
|
||||
"ar": "حدث خطأ. يرجى المحاولة مرة أخرى.",
|
||||
"de": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.",
|
||||
"fr": "Une erreur s'est produite. Veuillez réessayer.",
|
||||
"it": "Si è verificato un errore. Per favore, riprova.",
|
||||
"pt": "Ocorreu um erro. Por favor, tente novamente.",
|
||||
"es": "Ocurrió un error. Por favor, inténtalo de nuevo.",
|
||||
"tr": "Bir hata oluştu. Lütfen tekrar deneyin.",
|
||||
"uk": "Сталася помилка. Будь ласка, спробуйте ще раз."
|
||||
},
|
||||
"STATUS$ERROR_RUNTIME_DISCONNECTED": {
|
||||
"en": "There was an error while connecting to the runtime. Please refresh the page.",
|
||||
"zh-CN": "运行时已断开连接",
|
||||
@@ -7647,38 +7631,6 @@
|
||||
"tr": "Çalışma zamanına bağlanırken bir hata oluştu. Lütfen sayfayı yenileyin.",
|
||||
"uk": "Під час підключення до середовища виконання сталася помилка. Оновіть сторінку."
|
||||
},
|
||||
"STATUS$ERROR_MEMORY": {
|
||||
"en": "Memory error occurred. Please try reducing the workload or restarting.",
|
||||
"zh-CN": "发生内存错误,请尝试减少工作负载或重新启动",
|
||||
"zh-TW": "發生記憶體錯誤,請嘗試減少工作負載或重新啟動",
|
||||
"ko-KR": "메모리 오류가 발생했습니다. 작업 부하를 줄이거나 다시 시작해주세요.",
|
||||
"ja": "メモリエラーが発生しました。作業負荷を減らすか、再起動してください。",
|
||||
"no": "Minnefeil oppstod. Vennligst prøv å redusere arbeidsmengden eller start på nytt.",
|
||||
"ar": "حدث خطأ في الذاكرة. يرجى محاولة تقليل عبء العمل أو إعادة التشغيل.",
|
||||
"de": "Speicherfehler aufgetreten. Bitte versuchen Sie, die Arbeitslast zu reduzieren oder neu zu starten.",
|
||||
"fr": "Erreur de mémoire. Veuillez essayer de réduire la charge de travail ou de redémarrer.",
|
||||
"it": "Si è verificato un errore di memoria. Prova a ridurre il carico di lavoro o a riavviare.",
|
||||
"pt": "Ocorreu um erro de memória. Tente reduzir a carga de trabalho ou reiniciar.",
|
||||
"es": "Ocurrió un error de memoria. Intenta reducir la carga de trabajo o reiniciar.",
|
||||
"tr": "Bellek hatası oluştu. Lütfen iş yükünü azaltmayı veya yeniden başlatmayı deneyin.",
|
||||
"uk": "Сталася помилка пам'яті. Спробуйте зменшити навантаження або перезапустити."
|
||||
},
|
||||
"STATUS$GIT_PROVIDER_AUTHENTICATION_ERROR": {
|
||||
"en": "Error authenticating with the Git provider. Please check your credentials.",
|
||||
"zh-CN": "Git提供商认证错误,请检查您的凭据",
|
||||
"zh-TW": "Git提供商認證錯誤,請檢查您的憑據",
|
||||
"ko-KR": "Git 공급자 인증 오류. 자격 증명을 확인해주세요.",
|
||||
"ja": "Git プロバイダーの認証エラー。認証情報を確認してください。",
|
||||
"no": "Feil ved autentisering med Git-leverandøren. Vennligst sjekk dine legitimasjoner.",
|
||||
"ar": "خطأ في المصادقة مع مزود Git. يرجى التحقق من بيانات الاعتماد الخاصة بك.",
|
||||
"de": "Fehler bei der Authentifizierung mit dem Git-Anbieter. Bitte überprüfen Sie Ihre Anmeldedaten.",
|
||||
"fr": "Erreur d'authentification auprès du fournisseur Git. Veuillez vérifier vos informations d'identification.",
|
||||
"it": "Errore di autenticazione con il provider Git. Controlla le tue credenziali.",
|
||||
"pt": "Erro ao autenticar com o provedor Git. Por favor, verifique suas credenciais.",
|
||||
"es": "Error al autenticar con el proveedor Git. Por favor, verifica tus credenciales.",
|
||||
"tr": "Git sağlayıcısı ile kimlik doğrulama hatası. Lütfen kimlik bilgilerinizi kontrol edin.",
|
||||
"uk": "Помилка автентифікації у постачальника Git. Перевірте свої облікові дані."
|
||||
},
|
||||
"STATUS$LLM_RETRY": {
|
||||
"en": "Retrying LLM request",
|
||||
"es": "Reintentando solicitud LLM",
|
||||
@@ -8704,20 +8656,20 @@
|
||||
"uk": "Додати платіжну інформацію"
|
||||
},
|
||||
"BILLING$YOURE_IN": {
|
||||
"en": "You're in! You can start using your $10 in free credits now.",
|
||||
"ja": "登録完了!$10分の無料クレジットを今すぐご利用いただけます。",
|
||||
"zh-CN": "您已加入!现在可以开始使用$10的免费额度了。",
|
||||
"zh-TW": "您已加入!現在可以開始使用$10的免費額度了。",
|
||||
"ko-KR": "가입 완료! 지금 바로 $10 상당의 무료 크레딧을 사용하실 수 있습니다.",
|
||||
"no": "Du er med! Du kan begynne å bruke dine $10 i gratis kreditter nå.",
|
||||
"it": "Ci sei! Puoi iniziare a utilizzare i tuoi $10 in crediti gratuiti ora.",
|
||||
"pt": "Você está dentro! Você pode começar a usar seus $10 em créditos gratuitos agora.",
|
||||
"es": "¡Ya estás dentro! Puedes empezar a usar tus $10 en créditos gratuitos ahora.",
|
||||
"ar": "أنت معنا! يمكنك البدء في استخدام رصيدك المجاني البالغ 10 دولارًا الآن.",
|
||||
"fr": "C'est fait ! Vous pouvez commencer à utiliser vos 10 $ de crédits gratuits maintenant.",
|
||||
"tr": "Başardın! Şimdi $10 değerindeki ücretsiz kredilerini kullanmaya başlayabilirsin.",
|
||||
"de": "Du bist dabei! Du kannst jetzt deine $10 an kostenlosen Guthaben nutzen.",
|
||||
"uk": "Готово! Ви можете почати використовувати свої безкоштовні кредити на суму 10 доларів США вже зараз."
|
||||
"en": "You're in! You can start using your $50 in free credits now.",
|
||||
"ja": "登録完了!$50分の無料クレジットを今すぐご利用いただけます。",
|
||||
"zh-CN": "您已加入!现在可以开始使用$50的免费额度了。",
|
||||
"zh-TW": "您已加入!現在可以開始使用$50的免費額度了。",
|
||||
"ko-KR": "가입 완료! 지금 바로 $50 상당의 무료 크레딧을 사용하실 수 있습니다.",
|
||||
"no": "Du er med! Du kan begynne å bruke dine $50 i gratis kreditter nå.",
|
||||
"it": "Ci sei! Puoi iniziare a utilizzare i tuoi $50 in crediti gratuiti ora.",
|
||||
"pt": "Você está dentro! Você pode começar a usar seus $50 em créditos gratuitos agora.",
|
||||
"es": "¡Ya estás dentro! Puedes empezar a usar tus $50 en créditos gratuitos ahora.",
|
||||
"ar": "أنت معنا! يمكنك البدء في استخدام رصيدك المجاني البالغ 50 دولارًا الآن.",
|
||||
"fr": "C'est fait ! Vous pouvez commencer à utiliser vos 50 $ de crédits gratuits maintenant.",
|
||||
"tr": "Başardın! Şimdi $50 değerindeki ücretsiz kredilerini kullanmaya başlayabilirsin.",
|
||||
"de": "Du bist dabei! Du kannst jetzt deine $50 an kostenlosen Guthaben nutzen.",
|
||||
"uk": "Готово! Ви можете почати використовувати свої безкоштовні кредити на суму 50 доларів США вже зараз."
|
||||
},
|
||||
"PAYMENT$ADD_FUNDS": {
|
||||
"en": "Add Funds",
|
||||
@@ -13759,22 +13711,6 @@
|
||||
"de": "App",
|
||||
"uk": "Додаток"
|
||||
},
|
||||
"COMMON$PLANNER": {
|
||||
"en": "Planner",
|
||||
"ja": "プランナー",
|
||||
"zh-CN": "计划器",
|
||||
"zh-TW": "規劃器",
|
||||
"ko-KR": "플래너",
|
||||
"no": "Planlegger",
|
||||
"it": "Pianificatore",
|
||||
"pt": "Planejador",
|
||||
"es": "Planificador",
|
||||
"ar": "المخطط",
|
||||
"fr": "Planificateur",
|
||||
"tr": "Planlayıcı",
|
||||
"de": "Planer",
|
||||
"uk": "Планувальник"
|
||||
},
|
||||
"COMMON$APPLICATION_SETTINGS": {
|
||||
"en": "Application Settings",
|
||||
"ja": "アプリケーション設定",
|
||||
@@ -14958,37 +14894,5 @@
|
||||
"tr": "Kullanıcı onayı bekleniyor",
|
||||
"de": "Warte auf Benutzerbestätigung",
|
||||
"uk": "Очікується підтвердження користувача"
|
||||
},
|
||||
"COMMON$MORE_OPTIONS": {
|
||||
"en": "More options",
|
||||
"ja": "その他のオプション",
|
||||
"zh-CN": "更多选项",
|
||||
"zh-TW": "更多選項",
|
||||
"ko-KR": "추가 옵션",
|
||||
"no": "Flere alternativer",
|
||||
"it": "Altre opzioni",
|
||||
"pt": "Mais opções",
|
||||
"es": "Más opciones",
|
||||
"ar": "خيارات إضافية",
|
||||
"fr": "Plus d'options",
|
||||
"tr": "Daha fazla seçenek",
|
||||
"de": "Weitere Optionen",
|
||||
"uk": "Більше опцій"
|
||||
},
|
||||
"COMMON$CREATE_A_PLAN": {
|
||||
"en": "Create a plan",
|
||||
"ja": "プランを作成する",
|
||||
"zh-CN": "创建计划",
|
||||
"zh-TW": "建立計劃",
|
||||
"ko-KR": "계획 만들기",
|
||||
"no": "Lag en plan",
|
||||
"it": "Crea un piano",
|
||||
"pt": "Criar um plano",
|
||||
"es": "Crear un plan",
|
||||
"ar": "إنشاء خطة",
|
||||
"fr": "Créer un plan",
|
||||
"tr": "Bir plan oluştur",
|
||||
"de": "Einen Plan erstellen",
|
||||
"uk": "Створити план"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="109" height="109" viewBox="0 0 109 109" fill="none">
|
||||
<path d="M40.1979 21.8969L34.7311 17.832L25.2691 30.5574L20.2094 26.784L16.1367 32.2451L26.6653 40.0969L40.1979 21.8969Z" fill="currentColor"/>
|
||||
<path d="M90.8342 35.1983H50.4639V28.3858H90.8342V35.1983Z" fill="currentColor"/>
|
||||
<path d="M90.8342 57.9067H50.4638V51.0942H90.8342V57.9067Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.2508 63.5837C32.2674 63.5837 36.3342 59.517 36.3342 54.5004C36.3342 49.4838 32.2674 45.4171 27.2508 45.4171C22.2342 45.4171 18.1675 49.4838 18.1675 54.5004C18.1675 59.517 22.2342 63.5837 27.2508 63.5837ZM27.2508 59.0421C29.7591 59.0421 31.7925 57.0087 31.7925 54.5004C31.7925 51.9921 29.7591 49.9587 27.2508 49.9587C24.7425 49.9587 22.7092 51.9921 22.7092 54.5004C22.7092 57.0087 24.7425 59.0421 27.2508 59.0421Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.3342 77.2087C36.3342 82.2253 32.2674 86.2921 27.2508 86.2921C22.2342 86.2921 18.1675 82.2253 18.1675 77.2087C18.1675 72.1922 22.2342 68.1254 27.2508 68.1254C32.2674 68.1254 36.3342 72.1922 36.3342 77.2087ZM31.7925 77.2087C31.7925 79.717 29.7591 81.7504 27.2508 81.7504C24.7425 81.7504 22.7092 79.717 22.7092 77.2087C22.7092 74.7005 24.7425 72.6671 27.2508 72.6671C29.7591 72.6671 31.7925 74.7005 31.7925 77.2087Z" fill="currentColor"/>
|
||||
<path d="M50.4637 80.615H90.834V73.8025H50.4637V80.615Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M10.9706 10.1586C10.2819 10.0621 9.56905 10.1784 8.93842 10.5075C8.88672 10.5345 8.83557 10.5629 8.78504 10.5928C8.54529 10.7344 8.3193 10.9082 8.11331 11.1142L9.83702 12.8372L7.00054 15.75L7 17H8.25064L11.1628 14.1632L12.8857 15.8865C13.0917 15.6805 13.2655 15.4545 13.4071 15.2147C13.437 15.1642 13.4654 15.1131 13.4924 15.0614C13.8215 14.4308 13.9378 13.718 13.8413 13.0293L17 10.6946L13.3053 7L10.9706 10.1586Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 552 B |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.13832 8.76476C8.25957 8.17949 9.52697 7.9727 10.7515 8.14436L14.9025 2.52826L21.4717 9.09737L15.8555 13.2484C16.0272 14.4729 15.8204 15.7403 15.2351 16.8616C15.1872 16.9535 15.1366 17.0444 15.0836 17.1343C14.8318 17.5605 14.5228 17.9624 14.1566 18.3286L10.4443 14.6163L4.75207 20.3085H3.69141V19.2478L9.38361 13.5556L5.6713 9.84332C6.03755 9.47708 6.43936 9.16808 6.86562 8.91632C6.95547 8.86326 7.04641 8.81274 7.13832 8.76476ZM15.0735 4.82054L19.1794 8.92641L14.2461 12.5727L14.3701 13.4567C14.492 14.3266 14.3596 15.2233 13.9758 16.0265L7.97338 10.0241C8.77665 9.64035 9.67335 9.50789 10.5432 9.62984L11.4272 9.75376L15.0735 4.82054Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 817 B |
@@ -22,7 +22,7 @@ import { ConversationSubscriptionsProvider } from "#/context/conversation-subscr
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
|
||||
import { ConversationMain } from "#/components/features/conversation/conversation-main/conversation-main";
|
||||
import { ConversationNameWithStatus } from "#/components/features/conversation/conversation-name-with-status";
|
||||
import { ConversationName } from "#/components/features/conversation/conversation-name";
|
||||
|
||||
import { ConversationTabs } from "#/components/features/conversation/conversation-tabs/conversation-tabs";
|
||||
import { WebSocketProviderWrapper } from "#/contexts/websocket-provider-wrapper";
|
||||
@@ -160,7 +160,7 @@ function AppContent() {
|
||||
className="p-3 md:p-0 flex flex-col h-full gap-3"
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4.5 pt-2 lg:pt-0">
|
||||
<ConversationNameWithStatus />
|
||||
<ConversationName />
|
||||
<ConversationTabs />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
function PlannerTab() {
|
||||
const { t } = useTranslation();
|
||||
const setConversationMode = useConversationStore(
|
||||
(state) => state.setConversationMode,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full p-10">
|
||||
<LessonPlanIcon width={109} height={109} color="#A1A1A1" />
|
||||
<span className="text-[#8D95A9] text-[19px] font-normal leading-5 pb-9">
|
||||
{t(I18nKey.PLANNER$EMPTY_MESSAGE)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConversationMode("plan")}
|
||||
className="flex w-[164px] h-[40px] p-2 justify-center items-center shrink-0 rounded-lg bg-white overflow-hidden text-black text-ellipsis font-sans text-[16px] not-italic font-normal leading-[20px] hover:cursor-pointer hover:opacity-80"
|
||||
>
|
||||
{t(I18nKey.COMMON$CREATE_A_PLAN)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlannerTab;
|
||||
@@ -6,10 +6,7 @@ export type ConversationTab =
|
||||
| "browser"
|
||||
| "served"
|
||||
| "vscode"
|
||||
| "terminal"
|
||||
| "planner";
|
||||
|
||||
export type ConversationMode = "code" | "plan";
|
||||
| "terminal";
|
||||
|
||||
export interface IMessageToSend {
|
||||
text: string;
|
||||
@@ -28,7 +25,6 @@ interface ConversationState {
|
||||
submittedMessage: string | null;
|
||||
shouldHideSuggestions: boolean; // New state to hide suggestions when input expands
|
||||
hasRightPanelToggled: boolean;
|
||||
conversationMode: ConversationMode;
|
||||
}
|
||||
|
||||
interface ConversationActions {
|
||||
@@ -52,7 +48,6 @@ interface ConversationActions {
|
||||
setSubmittedMessage: (message: string | null) => void;
|
||||
resetConversationState: () => void;
|
||||
setHasRightPanelToggled: (hasRightPanelToggled: boolean) => void;
|
||||
setConversationMode: (conversationMode: ConversationMode) => void;
|
||||
}
|
||||
|
||||
type ConversationStore = ConversationState & ConversationActions;
|
||||
@@ -78,7 +73,6 @@ export const useConversationStore = create<ConversationStore>()(
|
||||
submittedMessage: null,
|
||||
shouldHideSuggestions: false,
|
||||
hasRightPanelToggled: true,
|
||||
conversationMode: "code",
|
||||
|
||||
// Actions
|
||||
setIsRightPanelShown: (isRightPanelShown) =>
|
||||
@@ -214,9 +208,6 @@ export const useConversationStore = create<ConversationStore>()(
|
||||
|
||||
setHasRightPanelToggled: (hasRightPanelToggled) =>
|
||||
set({ hasRightPanelToggled }, false, "setHasRightPanelToggled"),
|
||||
|
||||
setConversationMode: (conversationMode) =>
|
||||
set({ conversationMode }, false, "setConversationMode"),
|
||||
}),
|
||||
{
|
||||
name: "conversation-store",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { create } from "zustand";
|
||||
import { V1ExecutionStatus } from "#/types/v1/core/base/common";
|
||||
import { V1AgentStatus } from "#/types/v1/core/base/common";
|
||||
|
||||
interface V1ConversationStateStore {
|
||||
execution_status: V1ExecutionStatus | null;
|
||||
agent_status: V1AgentStatus | null;
|
||||
|
||||
/**
|
||||
* Set the agent status
|
||||
*/
|
||||
setExecutionStatus: (execution_status: V1ExecutionStatus) => void;
|
||||
setAgentStatus: (agent_status: V1AgentStatus) => void;
|
||||
|
||||
/**
|
||||
* Reset the store to initial state
|
||||
@@ -17,11 +17,10 @@ interface V1ConversationStateStore {
|
||||
|
||||
export const useV1ConversationStateStore = create<V1ConversationStateStore>(
|
||||
(set) => ({
|
||||
execution_status: null,
|
||||
agent_status: null,
|
||||
|
||||
setExecutionStatus: (execution_status: V1ExecutionStatus) =>
|
||||
set({ execution_status }),
|
||||
setAgentStatus: (agent_status: V1AgentStatus) => set({ agent_status }),
|
||||
|
||||
reset: () => set({ execution_status: null }),
|
||||
reset: () => set({ agent_status: null }),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -64,7 +64,7 @@ export enum SecurityRisk {
|
||||
}
|
||||
|
||||
// Agent status
|
||||
export enum V1ExecutionStatus {
|
||||
export enum V1AgentStatus {
|
||||
IDLE = "idle",
|
||||
RUNNING = "running",
|
||||
PAUSED = "paused",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { BaseEvent } from "../base/event";
|
||||
import { V1ExecutionStatus } from "../base/common";
|
||||
import { V1AgentStatus } from "../base/common";
|
||||
|
||||
/**
|
||||
* Conversation state value types
|
||||
*/
|
||||
export interface ConversationState {
|
||||
execution_status: V1ExecutionStatus;
|
||||
agent_status: V1AgentStatus;
|
||||
// Add other conversation state fields here as needed
|
||||
}
|
||||
|
||||
@@ -19,12 +19,12 @@ interface ConversationStateUpdateEventBase extends BaseEvent {
|
||||
* Unique key for this state update event.
|
||||
* Can be "full_state" for full state snapshots or field names for partial updates.
|
||||
*/
|
||||
key: "full_state" | "execution_status"; // Extend with other keys as needed
|
||||
key: "full_state" | "agent_status"; // Extend with other keys as needed
|
||||
|
||||
/**
|
||||
* Conversation state updates
|
||||
*/
|
||||
value: ConversationState | V1ExecutionStatus;
|
||||
value: ConversationState | V1AgentStatus;
|
||||
}
|
||||
|
||||
// Narrowed interfaces for full state update event
|
||||
@@ -37,8 +37,8 @@ export interface ConversationStateUpdateEventFullState
|
||||
// Narrowed interface for agent status update event
|
||||
export interface ConversationStateUpdateEventAgentStatus
|
||||
extends ConversationStateUpdateEventBase {
|
||||
key: "execution_status";
|
||||
value: V1ExecutionStatus;
|
||||
key: "agent_status";
|
||||
value: V1AgentStatus;
|
||||
}
|
||||
|
||||
// Conversation state update event - contains conversation state updates
|
||||
|
||||
@@ -136,7 +136,7 @@ export const isFullStateConversationStateUpdateEvent = (
|
||||
export const isAgentStatusConversationStateUpdateEvent = (
|
||||
event: ConversationStateUpdateEvent,
|
||||
): event is ConversationStateUpdateEventAgentStatus =>
|
||||
event.key === "execution_status";
|
||||
event.key === "agent_status";
|
||||
|
||||
// =============================================================================
|
||||
// TEMPORARY COMPATIBILITY TYPE GUARDS
|
||||
|
||||
+1
-31
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getStatusCode, getIndicatorColor, IndicatorColor } from "#/utils/status";
|
||||
import { getStatusCode, getIndicatorColor, IndicatorColor } from "../status";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
@@ -87,36 +87,6 @@ describe("getStatusCode", () => {
|
||||
// Should return runtime status since no agent state
|
||||
expect(result).toBe("STATUS$STARTING_RUNTIME");
|
||||
});
|
||||
|
||||
it("should prioritize task ERROR status over websocket CONNECTING state", () => {
|
||||
// Test case: Task has errored but websocket is still trying to connect
|
||||
const result = getStatusCode(
|
||||
{ id: "", message: "", type: "info", status_update: true }, // statusMessage
|
||||
"CONNECTING", // webSocketStatus (stuck connecting)
|
||||
null, // conversationStatus
|
||||
null, // runtimeStatus
|
||||
AgentState.LOADING, // agentState
|
||||
"ERROR", // taskStatus (ERROR)
|
||||
);
|
||||
|
||||
// Should return error message, not "Connecting..."
|
||||
expect(result).toBe(I18nKey.AGENT_STATUS$ERROR_OCCURRED);
|
||||
});
|
||||
|
||||
it("should show Connecting when task is working and websocket is connecting", () => {
|
||||
// Test case: Task is in progress and websocket is connecting normally
|
||||
const result = getStatusCode(
|
||||
{ id: "", message: "", type: "info", status_update: true }, // statusMessage
|
||||
"CONNECTING", // webSocketStatus
|
||||
null, // conversationStatus
|
||||
null, // runtimeStatus
|
||||
AgentState.LOADING, // agentState
|
||||
"WORKING", // taskStatus (in progress)
|
||||
);
|
||||
|
||||
// Should show connecting message since task hasn't errored
|
||||
expect(result).toBe(I18nKey.CHAT_INTERFACE$CONNECTING);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIndicatorColor", () => {
|
||||
@@ -19,4 +19,3 @@ export const ENABLE_TRAJECTORY_REPLAY = () =>
|
||||
loadFeatureFlag("TRAJECTORY_REPLAY");
|
||||
export const USE_V1_CONVERSATION_API = () =>
|
||||
loadFeatureFlag("USE_V1_CONVERSATION_API");
|
||||
export const USE_PLANNING_AGENT = () => loadFeatureFlag("USE_PLANNING_AGENT");
|
||||
|
||||
@@ -2,8 +2,7 @@ import { OpenHandsEvent } from "#/types/v1/core";
|
||||
import { isObservationEvent } from "#/types/v1/type-guards";
|
||||
|
||||
/**
|
||||
* Handles adding an event to the UI events array
|
||||
* Replaces actions with observations when they arrive (so UI shows observation instead of action)
|
||||
* Handles adding an event to the UI events array, with special logic for observation events
|
||||
*/
|
||||
export const handleEventForUI = (
|
||||
event: OpenHandsEvent,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { AgentState } from "#/types/agent-state";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { StatusMessage } from "#/types/message";
|
||||
import { RuntimeStatus } from "#/types/runtime-status";
|
||||
import { V1AppConversationStartTaskStatus } from "#/api/conversation-service/v1-conversation-service.types";
|
||||
|
||||
export enum IndicatorColor {
|
||||
BLUE = "bg-blue-500",
|
||||
@@ -104,15 +103,8 @@ export function getStatusCode(
|
||||
conversationStatus: ConversationStatus | null,
|
||||
runtimeStatus: RuntimeStatus | null,
|
||||
agentState: AgentState | null,
|
||||
taskStatus?: V1AppConversationStartTaskStatus | null,
|
||||
) {
|
||||
// PRIORITY 1: Handle task error state (when start-tasks API returns ERROR)
|
||||
// This must come first to prevent "Connecting..." from showing when task has errored
|
||||
if (taskStatus === "ERROR") {
|
||||
return I18nKey.AGENT_STATUS$ERROR_OCCURRED;
|
||||
}
|
||||
|
||||
// PRIORITY 2: Handle conversation and runtime stopped states
|
||||
// Handle conversation and runtime stopped states
|
||||
if (conversationStatus === "STOPPED" || runtimeStatus === "STATUS$STOPPED") {
|
||||
return I18nKey.CHAT_INTERFACE$STOPPED;
|
||||
}
|
||||
@@ -142,8 +134,7 @@ export function getStatusCode(
|
||||
return runtimeStatus;
|
||||
}
|
||||
|
||||
// PRIORITY 3: Handle WebSocket connection states
|
||||
// Note: WebSocket may be stuck in CONNECTING when task errors, so we check taskStatus first
|
||||
// Handle WebSocket connection states
|
||||
if (webSocketStatus === "DISCONNECTED") {
|
||||
return I18nKey.CHAT_INTERFACE$DISCONNECTED;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { sanitizeQuery } from "#/utils/sanitize-query";
|
||||
import { PRODUCT_URL } from "#/utils/constants";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@@ -610,66 +609,3 @@ export const buildSessionHeaders = (
|
||||
}
|
||||
return headers;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the appropriate color based on agent status
|
||||
* @param options Configuration object for status color calculation
|
||||
* @param options.isPausing Whether the agent is currently pausing
|
||||
* @param options.isTask Whether we're polling a task
|
||||
* @param options.taskStatus The task status string (e.g., "ERROR", "READY")
|
||||
* @param options.isStartingStatus Whether the agent is in a starting state (LOADING or INIT)
|
||||
* @param options.isStopStatus Whether the conversation status is STOPPED
|
||||
* @param options.curAgentState The current agent state
|
||||
* @returns The hex color code for the status
|
||||
*
|
||||
* @example
|
||||
* getStatusColor({
|
||||
* isPausing: false,
|
||||
* isTask: false,
|
||||
* taskStatus: undefined,
|
||||
* isStartingStatus: false,
|
||||
* isStopStatus: false,
|
||||
* curAgentState: AgentState.RUNNING
|
||||
* }) // Returns "#BCFF8C"
|
||||
*/
|
||||
export const getStatusColor = (options: {
|
||||
isPausing: boolean;
|
||||
isTask: boolean;
|
||||
taskStatus?: string | null;
|
||||
isStartingStatus: boolean;
|
||||
isStopStatus: boolean;
|
||||
curAgentState: AgentState;
|
||||
}): string => {
|
||||
const {
|
||||
isPausing,
|
||||
isTask,
|
||||
taskStatus,
|
||||
isStartingStatus,
|
||||
isStopStatus,
|
||||
curAgentState,
|
||||
} = options;
|
||||
|
||||
// Show pausing status
|
||||
if (isPausing) {
|
||||
return "#FFD600";
|
||||
}
|
||||
|
||||
// Show task status if we're polling a task
|
||||
if (isTask && taskStatus) {
|
||||
if (taskStatus === "ERROR") {
|
||||
return "#FF684E";
|
||||
}
|
||||
return "#FFD600";
|
||||
}
|
||||
|
||||
if (isStartingStatus) {
|
||||
return "#FFD600";
|
||||
}
|
||||
if (isStopStatus) {
|
||||
return "#ffffff";
|
||||
}
|
||||
if (curAgentState === AgentState.ERROR) {
|
||||
return "#FF684E";
|
||||
}
|
||||
return "#BCFF8C";
|
||||
};
|
||||
|
||||
@@ -50,7 +50,3 @@ coverage.xml
|
||||
*.manifest
|
||||
# Note: We keep our custom spec file in version control
|
||||
# *.spec
|
||||
|
||||
# Generated artifacts
|
||||
build
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# OpenHands V1 CLI
|
||||
|
||||
A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [OpenHands software-agent-sdk](https://github.com/OpenHands/software-agent-sdk)).
|
||||
A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [agent-sdk](https://github.com/OpenHands/agent-sdk)).
|
||||
|
||||
The [OpenHands V0 CLI (legacy)](https://github.com/OpenHands/OpenHands/tree/main/openhands/cli) is being deprecated.
|
||||
|
||||
---
|
||||
|
||||
@@ -31,4 +33,4 @@ uv run openhands
|
||||
# The binary will be in dist/
|
||||
./dist/openhands # macOS/Linux
|
||||
# dist/openhands.exe # Windows
|
||||
```
|
||||
```
|
||||
+13
-17
@@ -15,11 +15,20 @@ import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from openhands_cli.utils import get_default_cli_agent, get_llm_metadata
|
||||
from openhands_cli.utils import get_llm_metadata, get_default_cli_agent
|
||||
from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
|
||||
|
||||
from openhands.sdk import LLM
|
||||
|
||||
dummy_agent = get_default_cli_agent(
|
||||
llm=LLM(
|
||||
model='dummy-model',
|
||||
api_key='dummy-key',
|
||||
metadata=get_llm_metadata(model_name='dummy-model', llm_type='openhands'),
|
||||
),
|
||||
cli_mode=True,
|
||||
)
|
||||
|
||||
# =================================================
|
||||
# SECTION: Build Binary
|
||||
# =================================================
|
||||
@@ -117,7 +126,7 @@ def _is_welcome(line: str) -> bool:
|
||||
return any(marker in s for marker in WELCOME_MARKERS)
|
||||
|
||||
|
||||
def test_executable(dummy_agent) -> bool:
|
||||
def test_executable() -> bool:
|
||||
"""Test the built executable, measuring boot time and total test time."""
|
||||
print('🧪 Testing the built executable...')
|
||||
|
||||
@@ -265,14 +274,7 @@ def main() -> int:
|
||||
|
||||
# Test the executable
|
||||
if not args.no_test:
|
||||
dummy_agent = get_default_cli_agent(
|
||||
llm=LLM(
|
||||
model='dummy-model',
|
||||
api_key='dummy-key',
|
||||
litellm_extra_body={"metadata": get_llm_metadata(model_name='dummy-model', llm_type='openhands')},
|
||||
)
|
||||
)
|
||||
if not test_executable(dummy_agent):
|
||||
if not test_executable():
|
||||
print('❌ Executable test failed, build process failed')
|
||||
return 1
|
||||
|
||||
@@ -283,10 +285,4 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
sys.exit(main())
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print('❌ Executable test failed')
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(main())
|
||||
|
||||
@@ -53,7 +53,7 @@ a = Analysis(
|
||||
'mcp.client',
|
||||
'mcp.server',
|
||||
'mcp.shared',
|
||||
'openhands.tools.terminal',
|
||||
'openhands.tools.execute_bash',
|
||||
'openhands.tools.str_replace_editor',
|
||||
'openhands.tools.task_tracker',
|
||||
],
|
||||
|
||||
@@ -12,7 +12,7 @@ from openhands.sdk import (
|
||||
Message,
|
||||
TextContent,
|
||||
)
|
||||
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
||||
from openhands.sdk.conversation.state import AgentExecutionStatus
|
||||
from prompt_toolkit import print_formatted_text
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
|
||||
@@ -127,7 +127,7 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
||||
break
|
||||
|
||||
elif command == '/settings':
|
||||
settings_screen = SettingsScreen(runner.conversation if runner else None)
|
||||
settings_screen = SettingsScreen(conversation)
|
||||
settings_screen.display_settings()
|
||||
continue
|
||||
|
||||
@@ -143,9 +143,8 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
||||
elif command == '/new':
|
||||
try:
|
||||
# Start a fresh conversation (no resume ID = new conversation)
|
||||
conversation_id = uuid.uuid4()
|
||||
runner = None
|
||||
conversation = None
|
||||
conversation = setup_conversation(conversation_id)
|
||||
runner = ConversationRunner(conversation)
|
||||
display_welcome(conversation_id, resume=False)
|
||||
print_formatted_text(
|
||||
HTML('<green>✓ Started fresh conversation</green>')
|
||||
@@ -184,9 +183,9 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
||||
|
||||
conversation = runner.conversation
|
||||
if not (
|
||||
conversation.state.execution_status == ConversationExecutionStatus.PAUSED
|
||||
or conversation.state.execution_status
|
||||
== ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
conversation.state.agent_status == AgentExecutionStatus.PAUSED
|
||||
or conversation.state.agent_status
|
||||
== AgentExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
):
|
||||
print_formatted_text(
|
||||
HTML('<red>No paused conversation to resume...</red>')
|
||||
@@ -196,7 +195,7 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
||||
# Resume without new message
|
||||
message = None
|
||||
|
||||
if not runner or not conversation:
|
||||
if not runner:
|
||||
conversation = setup_conversation(conversation_id)
|
||||
runner = ConversationRunner(conversation)
|
||||
runner.process_message(message)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from openhands_cli.listeners.loading_listener import LoadingContext
|
||||
from openhands_cli.listeners.pause_listener import PauseListener
|
||||
|
||||
__all__ = ['PauseListener']
|
||||
__all__ = ['PauseListener', 'LoadingContext']
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Loading animation utilities for OpenHands CLI.
|
||||
Provides animated loading screens during agent initialization.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
def display_initialization_animation(text: str, is_loaded: threading.Event) -> None:
|
||||
"""Display a spinning animation while agent is being initialized.
|
||||
|
||||
Args:
|
||||
text: The text to display alongside the animation
|
||||
is_loaded: Threading event that signals when loading is complete
|
||||
"""
|
||||
ANIMATION_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
||||
|
||||
i = 0
|
||||
while not is_loaded.is_set():
|
||||
sys.stdout.write('\n')
|
||||
sys.stdout.write(
|
||||
f'\033[s\033[J\033[38;2;255;215;0m[{ANIMATION_FRAMES[i % len(ANIMATION_FRAMES)]}] {text}\033[0m\033[u\033[1A'
|
||||
)
|
||||
sys.stdout.flush()
|
||||
time.sleep(0.1)
|
||||
i += 1
|
||||
|
||||
sys.stdout.write('\r' + ' ' * (len(text) + 10) + '\r')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
class LoadingContext:
|
||||
"""Context manager for displaying loading animations in a separate thread."""
|
||||
|
||||
def __init__(self, text: str):
|
||||
"""Initialize the loading context.
|
||||
|
||||
Args:
|
||||
text: The text to display during loading
|
||||
"""
|
||||
self.text = text
|
||||
self.is_loaded = threading.Event()
|
||||
self.loading_thread: threading.Thread | None = None
|
||||
|
||||
def __enter__(self) -> 'LoadingContext':
|
||||
"""Start the loading animation in a separate thread."""
|
||||
self.loading_thread = threading.Thread(
|
||||
target=display_initialization_animation,
|
||||
args=(self.text, self.is_loaded),
|
||||
daemon=True,
|
||||
)
|
||||
self.loading_thread.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
"""Stop the loading animation and clean up the thread."""
|
||||
self.is_loaded.set()
|
||||
if self.loading_thread:
|
||||
self.loading_thread.join(
|
||||
timeout=1.0
|
||||
) # Wait up to 1 second for thread to finish
|
||||
@@ -1,10 +1,7 @@
|
||||
from prompt_toolkit import HTML, print_formatted_text
|
||||
|
||||
from openhands.sdk import BaseConversation, Message
|
||||
from openhands.sdk.conversation.state import (
|
||||
ConversationExecutionStatus,
|
||||
ConversationState,
|
||||
)
|
||||
from openhands.sdk.conversation.state import AgentExecutionStatus, ConversationState
|
||||
from openhands.sdk.security.confirmation_policy import (
|
||||
AlwaysConfirm,
|
||||
ConfirmationPolicyBase,
|
||||
@@ -54,10 +51,7 @@ class ConversationRunner:
|
||||
|
||||
def _print_run_status(self) -> None:
|
||||
print_formatted_text('')
|
||||
if (
|
||||
self.conversation.state.execution_status
|
||||
== ConversationExecutionStatus.PAUSED
|
||||
):
|
||||
if self.conversation.state.agent_status == AgentExecutionStatus.PAUSED:
|
||||
print_formatted_text(
|
||||
HTML(
|
||||
'<yellow>Resuming paused conversation...</yellow><grey> (Press Ctrl-P to pause)</grey>'
|
||||
@@ -97,8 +91,8 @@ class ConversationRunner:
|
||||
def _run_with_confirmation(self) -> None:
|
||||
# If agent was paused, resume with confirmation request
|
||||
if (
|
||||
self.conversation.state.execution_status
|
||||
== ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
self.conversation.state.agent_status
|
||||
== AgentExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
):
|
||||
user_confirmation = self._handle_confirmation_request()
|
||||
if user_confirmation == UserConfirmation.DEFER:
|
||||
@@ -112,15 +106,12 @@ class ConversationRunner:
|
||||
break
|
||||
|
||||
# In confirmation mode, agent either finishes or waits for user confirmation
|
||||
if (
|
||||
self.conversation.state.execution_status
|
||||
== ConversationExecutionStatus.FINISHED
|
||||
):
|
||||
if self.conversation.state.agent_status == AgentExecutionStatus.FINISHED:
|
||||
break
|
||||
|
||||
elif (
|
||||
self.conversation.state.execution_status
|
||||
== ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
self.conversation.state.agent_status
|
||||
== AgentExecutionStatus.WAITING_FOR_CONFIRMATION
|
||||
):
|
||||
user_confirmation = self._handle_confirmation_request()
|
||||
if user_confirmation == UserConfirmation.DEFER:
|
||||
|
||||
@@ -2,7 +2,11 @@ import uuid
|
||||
|
||||
from prompt_toolkit import HTML, print_formatted_text
|
||||
|
||||
from openhands.sdk import Agent, BaseConversation, Conversation, Workspace
|
||||
from openhands.sdk import Agent, BaseConversation, Conversation, Workspace, register_tool
|
||||
from openhands.tools.execute_bash import BashTool
|
||||
from openhands.tools.file_editor import FileEditorTool
|
||||
from openhands.tools.task_tracker import TaskTrackerTool
|
||||
from openhands_cli.listeners import LoadingContext
|
||||
from openhands_cli.locations import CONVERSATIONS_DIR, WORK_DIR
|
||||
from openhands_cli.tui.settings.store import AgentStore
|
||||
from openhands.sdk.security.confirmation_policy import (
|
||||
@@ -11,10 +15,9 @@ from openhands.sdk.security.confirmation_policy import (
|
||||
from openhands_cli.tui.settings.settings_screen import SettingsScreen
|
||||
|
||||
|
||||
# register tools
|
||||
from openhands.tools.terminal import TerminalTool
|
||||
from openhands.tools.file_editor import FileEditorTool
|
||||
from openhands.tools.task_tracker import TaskTrackerTool
|
||||
register_tool('BashTool', BashTool)
|
||||
register_tool('FileEditorTool', FileEditorTool)
|
||||
register_tool('TaskTrackerTool', TaskTrackerTool)
|
||||
|
||||
|
||||
class MissingAgentSpec(Exception):
|
||||
@@ -67,29 +70,26 @@ def setup_conversation(
|
||||
MissingAgentSpec: If agent specification is not found or invalid.
|
||||
"""
|
||||
|
||||
print_formatted_text(
|
||||
HTML(f'<white>Initializing agent...</white>')
|
||||
)
|
||||
with LoadingContext('Initializing OpenHands agent...'):
|
||||
agent = load_agent_specs(str(conversation_id))
|
||||
|
||||
agent = load_agent_specs(str(conversation_id))
|
||||
if not include_security_analyzer:
|
||||
# Remove security analyzer from agent spec
|
||||
agent = agent.model_copy(
|
||||
update={"security_analyzer": None}
|
||||
)
|
||||
|
||||
if not include_security_analyzer:
|
||||
# Remove security analyzer from agent spec
|
||||
agent = agent.model_copy(
|
||||
update={"security_analyzer": None}
|
||||
# Create conversation - agent context is now set in AgentStore.load()
|
||||
conversation: BaseConversation = Conversation(
|
||||
agent=agent,
|
||||
workspace=Workspace(working_dir=WORK_DIR),
|
||||
# Conversation will add /<conversation_id> to this path
|
||||
persistence_dir=CONVERSATIONS_DIR,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
|
||||
# Create conversation - agent context is now set in AgentStore.load()
|
||||
conversation: BaseConversation = Conversation(
|
||||
agent=agent,
|
||||
workspace=Workspace(working_dir=WORK_DIR),
|
||||
# Conversation will add /<conversation_id> to this path
|
||||
persistence_dir=CONVERSATIONS_DIR,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
|
||||
if include_security_analyzer:
|
||||
conversation.set_confirmation_policy(AlwaysConfirm())
|
||||
if include_security_analyzer:
|
||||
conversation.set_confirmation_policy(AlwaysConfirm())
|
||||
|
||||
print_formatted_text(
|
||||
HTML(f'<green>✓ Agent initialized with model: {agent.llm.model}</green>')
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import os
|
||||
|
||||
from openhands.sdk import LLM, BaseConversation, LLMSummarizingCondenser, LocalFileStore
|
||||
from openhands.sdk import LLM, BaseConversation, LocalFileStore
|
||||
from prompt_toolkit import HTML, print_formatted_text
|
||||
from prompt_toolkit.shortcuts import print_container
|
||||
from prompt_toolkit.widgets import Frame, TextArea
|
||||
|
||||
from openhands_cli.utils import get_default_cli_agent, get_llm_metadata
|
||||
from openhands_cli.utils import get_llm_metadata, get_default_cli_agent
|
||||
from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
|
||||
from openhands_cli.pt_style import COLOR_GREY
|
||||
from openhands_cli.tui.settings.store import AgentStore
|
||||
@@ -33,6 +33,9 @@ class SettingsScreen:
|
||||
agent_spec = self.agent_store.load()
|
||||
if not agent_spec:
|
||||
return
|
||||
assert self.conversation is not None, (
|
||||
'Conversation must be set to display settings.'
|
||||
)
|
||||
|
||||
llm = agent_spec.llm
|
||||
advanced_llm_settings = True if llm.base_url else False
|
||||
@@ -59,20 +62,12 @@ class SettingsScreen:
|
||||
labels_and_values.extend(
|
||||
[
|
||||
(' API Key', '********' if llm.api_key else 'Not Set'),
|
||||
]
|
||||
)
|
||||
|
||||
if self.conversation:
|
||||
labels_and_values.extend([
|
||||
(
|
||||
' Confirmation Mode',
|
||||
'Enabled'
|
||||
if self.conversation.is_confirmation_mode_active
|
||||
else 'Disabled',
|
||||
)
|
||||
])
|
||||
|
||||
labels_and_values.extend([
|
||||
),
|
||||
(
|
||||
' Memory Condensation',
|
||||
'Enabled' if agent_spec.condenser else 'Disabled',
|
||||
@@ -158,7 +153,7 @@ class SettingsScreen:
|
||||
api_key = prompt_api_key(
|
||||
step_counter,
|
||||
custom_model.split('/')[0] if len(custom_model.split('/')) > 1 else '',
|
||||
self.conversation.state.agent.llm.api_key if self.conversation else None,
|
||||
self.conversation.agent.llm.api_key if self.conversation else None,
|
||||
escapable=escapable,
|
||||
)
|
||||
memory_condensation = choose_memory_condensation(step_counter)
|
||||
@@ -180,21 +175,14 @@ class SettingsScreen:
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
usage_id='agent',
|
||||
litellm_extra_body={"metadata": get_llm_metadata(model_name=model, llm_type='agent')},
|
||||
metadata=get_llm_metadata(model_name=model, llm_type='agent'),
|
||||
)
|
||||
|
||||
agent = self.agent_store.load()
|
||||
if not agent:
|
||||
agent = get_default_cli_agent(llm=llm)
|
||||
|
||||
# Must update all LLMs
|
||||
agent = agent.model_copy(update={'llm': llm})
|
||||
condenser = LLMSummarizingCondenser(
|
||||
llm=llm.model_copy(
|
||||
update={"usage_id": "condenser"}
|
||||
)
|
||||
)
|
||||
agent = agent.model_copy(update={'condenser': condenser})
|
||||
self.agent_store.save(agent)
|
||||
|
||||
def _save_advanced_settings(
|
||||
|
||||
@@ -5,13 +5,13 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastmcp.mcp_config import MCPConfig
|
||||
from openhands_cli.utils import get_llm_metadata
|
||||
from openhands_cli.locations import (
|
||||
AGENT_SETTINGS_PATH,
|
||||
MCP_CONFIG_FILE,
|
||||
PERSISTENCE_DIR,
|
||||
WORK_DIR,
|
||||
)
|
||||
from openhands_cli.utils import get_llm_metadata
|
||||
from prompt_toolkit import HTML, print_formatted_text
|
||||
|
||||
from openhands.sdk import Agent, AgentContext, LocalFileStore
|
||||
@@ -45,29 +45,28 @@ class AgentStore:
|
||||
system_message_suffix=f'You current working directory is: {WORK_DIR}',
|
||||
)
|
||||
|
||||
mcp_config: dict = self.load_mcp_configuration()
|
||||
additional_mcp_config = self.load_mcp_configuration()
|
||||
mcp_config: dict = agent.mcp_config.copy().get('mcpServers', {})
|
||||
mcp_config.update(additional_mcp_config)
|
||||
|
||||
# Update LLM metadata with current information
|
||||
agent_llm_metadata = get_llm_metadata(
|
||||
model_name=agent.llm.model, llm_type='agent', session_id=session_id
|
||||
)
|
||||
updated_llm = agent.llm.model_copy(update={'litellm_extra_body': {'metadata': agent_llm_metadata}})
|
||||
updated_llm = agent.llm.model_copy(update={'metadata': agent_llm_metadata})
|
||||
|
||||
condenser_updates = {}
|
||||
if agent.condenser and isinstance(agent.condenser, LLMSummarizingCondenser):
|
||||
condenser_updates['llm'] = agent.condenser.llm.model_copy(
|
||||
update={
|
||||
'litellm_extra_body': {
|
||||
'metadata': get_llm_metadata(
|
||||
model_name=agent.condenser.llm.model,
|
||||
llm_type='condenser',
|
||||
session_id=session_id,
|
||||
)
|
||||
}
|
||||
'metadata': get_llm_metadata(
|
||||
model_name=agent.condenser.llm.model,
|
||||
llm_type='condenser',
|
||||
session_id=session_id,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
# Update tools and context
|
||||
agent = agent.model_copy(
|
||||
update={
|
||||
'llm': updated_llm,
|
||||
|
||||
@@ -4,7 +4,7 @@ requires = [ "hatchling>=1.25" ]
|
||||
|
||||
[project]
|
||||
name = "openhands"
|
||||
version = "1.0.6"
|
||||
version = "1.0.4"
|
||||
description = "OpenHands CLI - Terminal User Interface for OpenHands AI Agent"
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
@@ -18,8 +18,8 @@ classifiers = [
|
||||
# Using Git URLs for dependencies so installs from PyPI pull from GitHub
|
||||
# TODO: pin package versions once agent-sdk has published PyPI packages
|
||||
dependencies = [
|
||||
"openhands-sdk==1",
|
||||
"openhands-tools==1",
|
||||
"openhands-sdk==1.0.0a5",
|
||||
"openhands-tools==1.0.0a5",
|
||||
"prompt-toolkit>=3",
|
||||
"typer>=0.17.4",
|
||||
]
|
||||
@@ -102,5 +102,5 @@ ignore_missing_imports = true
|
||||
# UNCOMMENT TO USE EXACT COMMIT FROM AGENT-SDK
|
||||
|
||||
# [tool.uv.sources]
|
||||
# openhands-sdk = { git = "https://github.com/OpenHands/agent-sdk.git", subdirectory = "openhands-sdk", rev = "aaa0066ee078688e015fcad590393fe6771c10a1" }
|
||||
# openhands-tools = { git = "https://github.com/OpenHands/agent-sdk.git", subdirectory = "openhands-tools", rev = "aaa0066ee078688e015fcad590393fe6771c10a1" }
|
||||
# openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands-sdk", rev = "512399d896521aee3131eea4bb59087fb9dfa243" }
|
||||
# openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands-tools", rev = "512399d896521aee3131eea4bb59087fb9dfa243" }
|
||||
|
||||
@@ -2,18 +2,12 @@
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from uuid import UUID
|
||||
|
||||
from prompt_toolkit.input.defaults import create_pipe_input
|
||||
from prompt_toolkit.output.defaults import DummyOutput
|
||||
|
||||
from openhands_cli.setup import (
|
||||
MissingAgentSpec,
|
||||
verify_agent_exists_or_setup_agent,
|
||||
)
|
||||
from openhands_cli.setup import MissingAgentSpec, verify_agent_exists_or_setup_agent, setup_conversation
|
||||
from openhands_cli.user_actions import UserConfirmation
|
||||
|
||||
|
||||
@patch("openhands_cli.setup.load_agent_specs")
|
||||
@patch('openhands_cli.setup.load_agent_specs')
|
||||
def test_verify_agent_exists_or_setup_agent_success(mock_load_agent_specs):
|
||||
"""Test that verify_agent_exists_or_setup_agent returns agent successfully."""
|
||||
# Mock the agent object
|
||||
@@ -28,10 +22,11 @@ def test_verify_agent_exists_or_setup_agent_success(mock_load_agent_specs):
|
||||
mock_load_agent_specs.assert_called_once_with()
|
||||
|
||||
|
||||
@patch("openhands_cli.setup.SettingsScreen")
|
||||
@patch("openhands_cli.setup.load_agent_specs")
|
||||
@patch('openhands_cli.setup.SettingsScreen')
|
||||
@patch('openhands_cli.setup.load_agent_specs')
|
||||
def test_verify_agent_exists_or_setup_agent_missing_agent_spec(
|
||||
mock_load_agent_specs, mock_settings_screen_class
|
||||
mock_load_agent_specs,
|
||||
mock_settings_screen_class
|
||||
):
|
||||
"""Test that verify_agent_exists_or_setup_agent handles MissingAgentSpec exception."""
|
||||
# Mock the SettingsScreen instance
|
||||
@@ -42,7 +37,7 @@ def test_verify_agent_exists_or_setup_agent_missing_agent_spec(
|
||||
mock_agent = MagicMock()
|
||||
mock_load_agent_specs.side_effect = [
|
||||
MissingAgentSpec("Agent spec missing"),
|
||||
mock_agent,
|
||||
mock_agent
|
||||
]
|
||||
|
||||
# Call the function
|
||||
@@ -56,11 +51,14 @@ def test_verify_agent_exists_or_setup_agent_missing_agent_spec(
|
||||
mock_settings_screen.configure_settings.assert_called_once_with(first_time=True)
|
||||
|
||||
|
||||
@patch("openhands_cli.agent_chat.exit_session_confirmation")
|
||||
@patch("openhands_cli.agent_chat.get_session_prompter")
|
||||
@patch("openhands_cli.agent_chat.setup_conversation")
|
||||
@patch("openhands_cli.agent_chat.verify_agent_exists_or_setup_agent")
|
||||
@patch("openhands_cli.agent_chat.ConversationRunner")
|
||||
|
||||
|
||||
|
||||
@patch('openhands_cli.agent_chat.exit_session_confirmation')
|
||||
@patch('openhands_cli.agent_chat.get_session_prompter')
|
||||
@patch('openhands_cli.agent_chat.setup_conversation')
|
||||
@patch('openhands_cli.agent_chat.verify_agent_exists_or_setup_agent')
|
||||
@patch('openhands_cli.agent_chat.ConversationRunner')
|
||||
def test_new_command_resets_confirmation_mode(
|
||||
mock_runner_cls,
|
||||
mock_verify_agent,
|
||||
@@ -76,35 +74,27 @@ def test_new_command_resets_confirmation_mode(
|
||||
mock_verify_agent.return_value = mock_agent
|
||||
|
||||
# Mock conversation - only one is created when /new is called
|
||||
conv1 = MagicMock()
|
||||
conv1.id = UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
|
||||
conv1 = MagicMock(); conv1.id = UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
||||
mock_setup_conversation.return_value = conv1
|
||||
|
||||
# One runner instance for the conversation
|
||||
runner1 = MagicMock()
|
||||
runner1.is_confirmation_mode_active = True
|
||||
runner1 = MagicMock(); runner1.is_confirmation_mode_active = True
|
||||
mock_runner_cls.return_value = runner1
|
||||
|
||||
# Real session fed by a pipe (no interactive confirmation now)
|
||||
from openhands_cli.user_actions.utils import (
|
||||
get_session_prompter as real_get_session_prompter,
|
||||
)
|
||||
|
||||
from openhands_cli.user_actions.utils import get_session_prompter as real_get_session_prompter
|
||||
with create_pipe_input() as pipe:
|
||||
output = DummyOutput()
|
||||
session = real_get_session_prompter(input=pipe, output=output)
|
||||
mock_get_session_prompter.return_value = session
|
||||
|
||||
from openhands_cli.agent_chat import run_cli_entry
|
||||
|
||||
# Trigger /new
|
||||
# First user message should trigger runner creation
|
||||
# Then /exit (exit will be auto-accepted)
|
||||
for ch in "/new\rhello\r/exit\r":
|
||||
# Trigger /new, then /exit (exit will be auto-accepted)
|
||||
for ch in "/new\r/exit\r":
|
||||
pipe.send_text(ch)
|
||||
|
||||
run_cli_entry(None)
|
||||
|
||||
# Assert we created one runner for the conversation when a message was processed after /new
|
||||
# Assert we created one runner for the conversation when /new was called
|
||||
assert mock_runner_cls.call_count == 1
|
||||
assert mock_runner_cls.call_args_list[0].args[0] is conv1
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
from prompt_toolkit.input.defaults import create_pipe_input
|
||||
from prompt_toolkit.output.defaults import DummyOutput
|
||||
|
||||
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
||||
from openhands.sdk.conversation.state import AgentExecutionStatus
|
||||
from openhands_cli.user_actions import UserConfirmation
|
||||
|
||||
|
||||
@@ -39,43 +39,43 @@ def run_resume_command_test(commands, agent_status=None, expect_runner_created=T
|
||||
patch('openhands_cli.agent_chat.setup_conversation') as mock_setup_conversation, \
|
||||
patch('openhands_cli.agent_chat.verify_agent_exists_or_setup_agent') as mock_verify_agent, \
|
||||
patch('openhands_cli.agent_chat.ConversationRunner') as mock_runner_cls:
|
||||
|
||||
|
||||
# Auto-accept the exit prompt to avoid interactive UI
|
||||
mock_exit_confirm.return_value = UserConfirmation.ACCEPT
|
||||
|
||||
|
||||
# Mock agent verification to succeed
|
||||
mock_agent = MagicMock()
|
||||
mock_verify_agent.return_value = mock_agent
|
||||
|
||||
|
||||
# Mock conversation setup
|
||||
conv = MagicMock()
|
||||
conv.id = UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
||||
if agent_status:
|
||||
conv.state.execution_status = agent_status
|
||||
conv.state.agent_status = agent_status
|
||||
mock_setup_conversation.return_value = conv
|
||||
|
||||
|
||||
# Mock runner
|
||||
runner = MagicMock()
|
||||
runner.conversation = conv
|
||||
mock_runner_cls.return_value = runner
|
||||
|
||||
|
||||
# Real session fed by a pipe
|
||||
from openhands_cli.user_actions.utils import get_session_prompter as real_get_session_prompter
|
||||
with create_pipe_input() as pipe:
|
||||
output = DummyOutput()
|
||||
session = real_get_session_prompter(input=pipe, output=output)
|
||||
mock_get_session_prompter.return_value = session
|
||||
|
||||
|
||||
from openhands_cli.agent_chat import run_cli_entry
|
||||
|
||||
|
||||
# Send commands
|
||||
for ch in commands:
|
||||
pipe.send_text(ch)
|
||||
|
||||
|
||||
# Capture printed output
|
||||
with patch('openhands_cli.agent_chat.print_formatted_text') as mock_print:
|
||||
run_cli_entry(None)
|
||||
|
||||
|
||||
return mock_runner_cls, runner, mock_print
|
||||
|
||||
|
||||
@@ -93,17 +93,17 @@ def run_resume_command_test(commands, agent_status=None, expect_runner_created=T
|
||||
def test_resume_command_warnings(commands, expected_warning, expect_runner_created):
|
||||
"""Test /resume command shows appropriate warnings."""
|
||||
# Set agent status to FINISHED for the "conversation exists but not paused" test
|
||||
agent_status = ConversationExecutionStatus.FINISHED if expect_runner_created else None
|
||||
|
||||
agent_status = AgentExecutionStatus.FINISHED if expect_runner_created else None
|
||||
|
||||
mock_runner_cls, runner, mock_print = run_resume_command_test(
|
||||
commands, agent_status=agent_status, expect_runner_created=expect_runner_created
|
||||
)
|
||||
|
||||
|
||||
# Verify warning message was printed
|
||||
warning_calls = [call for call in mock_print.call_args_list
|
||||
warning_calls = [call for call in mock_print.call_args_list
|
||||
if expected_warning in str(call)]
|
||||
assert len(warning_calls) > 0, f"Expected warning about {expected_warning}"
|
||||
|
||||
|
||||
# Verify runner creation expectation
|
||||
if expect_runner_created:
|
||||
assert mock_runner_cls.call_count == 1
|
||||
@@ -117,31 +117,31 @@ def test_resume_command_warnings(commands, expected_warning, expect_runner_creat
|
||||
@pytest.mark.parametrize(
|
||||
"agent_status",
|
||||
[
|
||||
ConversationExecutionStatus.PAUSED,
|
||||
ConversationExecutionStatus.WAITING_FOR_CONFIRMATION,
|
||||
AgentExecutionStatus.PAUSED,
|
||||
AgentExecutionStatus.WAITING_FOR_CONFIRMATION,
|
||||
],
|
||||
)
|
||||
def test_resume_command_successful_resume(agent_status):
|
||||
"""Test /resume command successfully resumes paused/waiting conversations."""
|
||||
commands = "hello\r/resume\r/exit\r"
|
||||
|
||||
|
||||
mock_runner_cls, runner, mock_print = run_resume_command_test(
|
||||
commands, agent_status=agent_status, expect_runner_created=True
|
||||
)
|
||||
|
||||
|
||||
# Verify runner was created and process_message was called
|
||||
assert mock_runner_cls.call_count == 1
|
||||
|
||||
|
||||
# Verify process_message was called twice: once with the initial message, once with None for resume
|
||||
assert runner.process_message.call_count == 2
|
||||
|
||||
|
||||
# Check the calls to process_message
|
||||
calls = runner.process_message.call_args_list
|
||||
|
||||
|
||||
# First call should have a message (the "hello" message)
|
||||
first_call_args = calls[0][0]
|
||||
assert first_call_args[0] is not None, "First call should have a message"
|
||||
|
||||
|
||||
# Second call should have None (the /resume command)
|
||||
second_call_args = calls[1][0]
|
||||
assert second_call_args[0] is None, "Second call should have None message for resume"
|
||||
assert second_call_args[0] is None, "Second call should have None message for resume"
|
||||
@@ -1,57 +0,0 @@
|
||||
"""Test for the /settings command functionality."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from prompt_toolkit.input.defaults import create_pipe_input
|
||||
from prompt_toolkit.output.defaults import DummyOutput
|
||||
|
||||
from openhands_cli.agent_chat import run_cli_entry
|
||||
from openhands_cli.user_actions import UserConfirmation
|
||||
|
||||
|
||||
@patch('openhands_cli.agent_chat.exit_session_confirmation')
|
||||
@patch('openhands_cli.agent_chat.get_session_prompter')
|
||||
@patch('openhands_cli.agent_chat.setup_conversation')
|
||||
@patch('openhands_cli.agent_chat.verify_agent_exists_or_setup_agent')
|
||||
@patch('openhands_cli.agent_chat.ConversationRunner')
|
||||
@patch('openhands_cli.agent_chat.SettingsScreen')
|
||||
def test_settings_command_works_without_conversation(
|
||||
mock_settings_screen_class,
|
||||
mock_runner_cls,
|
||||
mock_verify_agent,
|
||||
mock_setup_conversation,
|
||||
mock_get_session_prompter,
|
||||
mock_exit_confirm,
|
||||
):
|
||||
"""Test that /settings command works when no conversation is active (bug fix scenario)."""
|
||||
# Auto-accept the exit prompt to avoid interactive UI
|
||||
mock_exit_confirm.return_value = UserConfirmation.ACCEPT
|
||||
|
||||
# Mock agent verification to succeed
|
||||
mock_agent = MagicMock()
|
||||
mock_verify_agent.return_value = mock_agent
|
||||
|
||||
# Mock the SettingsScreen instance
|
||||
mock_settings_screen = MagicMock()
|
||||
mock_settings_screen_class.return_value = mock_settings_screen
|
||||
|
||||
# No runner initially (simulates starting CLI without a conversation)
|
||||
mock_runner_cls.return_value = None
|
||||
|
||||
# Real session fed by a pipe
|
||||
from openhands_cli.user_actions.utils import get_session_prompter as real_get_session_prompter
|
||||
with create_pipe_input() as pipe:
|
||||
output = DummyOutput()
|
||||
session = real_get_session_prompter(input=pipe, output=output)
|
||||
mock_get_session_prompter.return_value = session
|
||||
|
||||
# Trigger /settings, then /exit (exit will be auto-accepted)
|
||||
for ch in "/settings\r/exit\r":
|
||||
pipe.send_text(ch)
|
||||
|
||||
run_cli_entry(None)
|
||||
|
||||
# Assert SettingsScreen was created with None conversation (the bug fix)
|
||||
mock_settings_screen_class.assert_called_once_with(None)
|
||||
|
||||
# Assert display_settings was called (settings screen was shown)
|
||||
mock_settings_screen.display_settings.assert_called_once()
|
||||
@@ -1,114 +0,0 @@
|
||||
"""Minimal tests: mcp.json overrides persisted agent MCP servers."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.sdk import Agent, LLM
|
||||
from openhands_cli.locations import MCP_CONFIG_FILE, AGENT_SETTINGS_PATH
|
||||
from openhands_cli.tui.settings.store import AgentStore
|
||||
|
||||
|
||||
# ---------------------- tiny helpers ----------------------
|
||||
|
||||
def write_json(path: Path, obj: dict) -> None:
|
||||
path.write_text(json.dumps(obj))
|
||||
|
||||
|
||||
def write_agent(root: Path, agent: Agent) -> None:
|
||||
(root / AGENT_SETTINGS_PATH).write_text(
|
||||
agent.model_dump_json(context={"expose_secrets": True})
|
||||
)
|
||||
|
||||
|
||||
# ---------------------- fixtures ----------------------
|
||||
|
||||
@pytest.fixture
|
||||
def persistence_dir(tmp_path, monkeypatch) -> Path:
|
||||
# Create root dir and point AgentStore at it
|
||||
root = tmp_path / "openhands"
|
||||
root.mkdir()
|
||||
monkeypatch.setattr("openhands_cli.tui.settings.store.PERSISTENCE_DIR", str(root))
|
||||
return root
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent_store() -> AgentStore:
|
||||
return AgentStore()
|
||||
|
||||
|
||||
# ---------------------- tests ----------------------
|
||||
|
||||
@patch("openhands_cli.tui.settings.store.get_default_tools", return_value=[])
|
||||
@patch("openhands_cli.tui.settings.store.get_llm_metadata", return_value={})
|
||||
def test_load_overrides_persisted_mcp_with_mcp_json_file(
|
||||
mock_meta,
|
||||
mock_tools,
|
||||
persistence_dir,
|
||||
agent_store
|
||||
):
|
||||
"""If agent has MCP servers, mcp.json must replace them entirely."""
|
||||
# Persist an agent that already contains MCP servers
|
||||
persisted_agent = Agent(
|
||||
llm=LLM(model="gpt-4", api_key=SecretStr("k"), usage_id="svc"),
|
||||
tools=[],
|
||||
mcp_config={
|
||||
"mcpServers": {
|
||||
"persistent_server": {"command": "python", "args": ["-m", "old_server"]}
|
||||
}
|
||||
},
|
||||
)
|
||||
write_agent(persistence_dir, persisted_agent)
|
||||
|
||||
# Create mcp.json with different servers (this must fully override)
|
||||
write_json(
|
||||
persistence_dir / MCP_CONFIG_FILE,
|
||||
{
|
||||
"mcpServers": {
|
||||
"file_server": {"command": "uvx", "args": ["mcp-server-fetch"]}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
loaded = agent_store.load()
|
||||
assert loaded is not None
|
||||
# Expect ONLY the MCP json file's config
|
||||
assert loaded.mcp_config == {
|
||||
"mcpServers": {
|
||||
"file_server": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"],
|
||||
"env": {},
|
||||
"transport": "stdio",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@patch("openhands_cli.tui.settings.store.get_default_tools", return_value=[])
|
||||
@patch("openhands_cli.tui.settings.store.get_llm_metadata", return_value={})
|
||||
def test_load_when_mcp_file_missing_ignores_persisted_mcp(
|
||||
mock_meta,
|
||||
mock_tools,
|
||||
persistence_dir,
|
||||
agent_store
|
||||
):
|
||||
"""If mcp.json is absent, loaded agent.mcp_config should be empty (persisted MCP ignored)."""
|
||||
persisted_agent = Agent(
|
||||
llm=LLM(model="gpt-4", api_key=SecretStr("k"), usage_id="svc"),
|
||||
tools=[],
|
||||
mcp_config={
|
||||
"mcpServers": {
|
||||
"persistent_server": {"command": "python", "args": ["-m", "old_server"]}
|
||||
}
|
||||
},
|
||||
)
|
||||
write_agent(persistence_dir, persisted_agent)
|
||||
|
||||
# No mcp.json created
|
||||
|
||||
loaded = agent_store.load()
|
||||
assert loaded is not None
|
||||
assert loaded.mcp_config == {} # persisted MCP is ignored if file is missin
|
||||
@@ -121,38 +121,6 @@ def test_update_existing_settings_workflow(tmp_path: Path):
|
||||
assert True # If we get here, the workflow completed successfully
|
||||
|
||||
|
||||
def test_all_llms_in_agent_are_updated():
|
||||
"""Test that modifying LLM settings creates multiple LLMs with same API key but different usage_ids."""
|
||||
# Create a screen with existing agent settings
|
||||
screen = SettingsScreen(conversation=None)
|
||||
initial_llm = LLM(model='openai/gpt-3.5-turbo', api_key=SecretStr('sk-initial'), usage_id='test-service')
|
||||
initial_agent = get_default_cli_agent(llm=initial_llm)
|
||||
|
||||
# Mock the agent store to return the initial agent and capture the save call
|
||||
with (
|
||||
patch.object(screen.agent_store, 'load', return_value=initial_agent),
|
||||
patch.object(screen.agent_store, 'save') as mock_save
|
||||
):
|
||||
# Modify the LLM settings with new API key
|
||||
screen._save_llm_settings(model='openai/gpt-4o-mini', api_key='sk-updated-123')
|
||||
mock_save.assert_called_once()
|
||||
|
||||
# Get the saved agent from the mock
|
||||
saved_agent = mock_save.call_args[0][0]
|
||||
all_llms = list(saved_agent.get_all_llms())
|
||||
assert len(all_llms) >= 2, f"Expected at least 2 LLMs, got {len(all_llms)}"
|
||||
|
||||
# Verify all LLMs have the same API key
|
||||
api_keys = [llm.api_key.get_secret_value() for llm in all_llms]
|
||||
assert all(api_key == 'sk-updated-123' for api_key in api_keys), \
|
||||
f"Not all LLMs have the same API key: {api_keys}"
|
||||
|
||||
# Verify none of the usage_id attributes match
|
||||
usage_ids = [llm.usage_id for llm in all_llms]
|
||||
assert len(set(usage_ids)) == len(usage_ids), \
|
||||
f"Some usage_ids are duplicated: {usage_ids}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'step_to_cancel',
|
||||
['type', 'provider', 'model', 'apikey', 'save'],
|
||||
|
||||
@@ -73,6 +73,8 @@ class TestConfirmationMode:
|
||||
persistence_dir=ANY,
|
||||
conversation_id=mock_conversation_id,
|
||||
)
|
||||
# Verify print_formatted_text was called
|
||||
mock_print.assert_called_once()
|
||||
|
||||
def test_setup_conversation_raises_missing_agent_spec(self) -> None:
|
||||
"""Test that setup_conversation raises MissingAgentSpec when agent is not found."""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user