Compare commits

..

1 Commits

Author SHA1 Message Date
amanape
c4d9da03f8 Import get_subscription_access outside block 2025-09-26 17:12:46 +04:00
205 changed files with 1648 additions and 13856 deletions

View File

@@ -18,7 +18,7 @@ DOCKER_RUN_COMMAND="docker run -it --rm \
docker.all-hands.dev/all-hands-ai/openhands:${SHORT_SHA}"
# Define the uvx command
UVX_RUN_COMMAND="uvx --python 3.12 --from git+https://github.com/All-Hands-AI/OpenHands@${BRANCH_NAME}#subdirectory=openhands-cli openhands"
UVX_RUN_COMMAND="uvx --python 3.12 --from git+https://github.com/All-Hands-AI/OpenHands@${BRANCH_NAME} openhands"
# Get the current PR body
PR_BODY=$(gh pr view "$PR_NUMBER" --json body --jq .body)

View File

@@ -1,58 +0,0 @@
# Workflow that builds and tests the CLI binary executable
name: CLI - Build and Test Binary
# Run on pushes to main branch and all pull requests, but only when CLI files change
on:
push:
branches:
- main
paths:
- "openhands-cli/**"
pull_request:
paths:
- "openhands-cli/**"
# Cancel previous runs if a new commit is pushed
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
build-and-test-binary:
name: Build and test binary executable
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Install dependencies
working-directory: openhands-cli
run: |
uv sync
- name: Build binary executable
working-directory: openhands-cli
run: |
./build.sh --install-pyinstaller | tee output.log
echo "Full output:"
cat output.log
if grep -q "❌" output.log; then
echo "❌ Found failure marker in output"
exit 1
fi
echo "✅ Build & test finished without ❌ markers"

View File

@@ -46,7 +46,6 @@ jobs:
else
json=$(jq -n -c '[
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
{ image: "ghcr.io/all-hands-ai/python-nodejs:python3.13-nodejs22-trixie", tag: "trixie" },
{ image: "ubuntu:24.04", tag: "ubuntu" }
]')
fi
@@ -137,7 +136,6 @@ jobs:
if: github.event.pull_request.head.repo.fork != true
shell: bash
run: |
./containers/build.sh -i runtime -o ${{ env.REPO_OWNER }} -t ${{ matrix.base_image.tag }} --dry
DOCKER_BUILD_JSON=$(jq -c . < docker-build-dry.json)
@@ -213,8 +211,6 @@ jobs:
latest=auto
prefix=
suffix=
env:
DOCKER_METADATA_PR_HEAD_SHA: true
- name: Determine app image tag
shell: bash
run: |

View File

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

View File

@@ -127,58 +127,11 @@ jobs:
name: coverage-enterprise
path: ".coverage.enterprise.${{ matrix.python_version }}"
include-hidden-files: true
# Run CLI unit tests
test-cli-python:
name: CLI Unit Tests
runs-on: blacksmith-4vcpu-ubuntu-2404
strategy:
matrix:
python-version: ["3.12"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"
- name: Install dependencies
working-directory: ./openhands-cli
run: |
uv sync --group dev
- name: Run CLI unit tests
working-directory: ./openhands-cli
env:
# write coverage to repo root so the merge step finds it
COVERAGE_FILE: "${{ github.workspace }}/.coverage.openhands-cli.${{ matrix.python-version }}"
run: |
uv run pytest --forked -n auto -s \
-p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark \
tests --cov=openhands_cli --cov-branch
- name: Store coverage file
uses: actions/upload-artifact@v4
with:
name: coverage-openhands-cli
path: ".coverage.openhands-cli.${{ matrix.python-version }}"
include-hidden-files: true
coverage-comment:
name: Coverage Comment
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: [test-on-linux, test-enterprise, test-cli-python]
needs: [test-on-linux, test-enterprise]
permissions:
pull-requests: write
@@ -192,9 +145,6 @@ jobs:
pattern: coverage-*
merge-multiple: true
- name: Create symlink for CLI source files
run: ln -sf openhands-cli/openhands_cli openhands_cli
- name: Coverage comment
id: coverage_comment
uses: py-cov-action/python-coverage-comment-action@v3

View File

@@ -19,4 +19,4 @@ jobs:
close-issue-message: 'This issue was automatically closed due to 50 days of inactivity. We do this to help keep the issues somewhat manageable and focus on active issues.'
close-pr-message: 'This PR was closed because it had no activity for 50 days. If you feel this was closed in error, and you would like to continue the PR, please resubmit or let us know.'
days-before-close: 10
operations-per-run: 300
operations-per-run: 150

View File

@@ -45,7 +45,7 @@ jobs:
"This issue has been labeled as **good first issue**, which means it's a great place to get started with the OpenHands project.\n\n" +
"If you're interested in working on it, feel free to! No need to ask for permission.\n\n" +
"Be sure to check out our [development setup guide](" + repoUrl + "/blob/main/Development.md) to get your environment set up, and follow our [contribution guidelines](" + repoUrl + "/blob/main/CONTRIBUTING.md) when you're ready to submit a fix.\n\n" +
"Feel free to join our developer community on [Slack](https://all-hands.dev/joinslack). You can ask for [help](https://openhands-ai.slack.com/archives/C078L0FUGUX), [feedback](https://openhands-ai.slack.com/archives/C086ARSNMGA), and even ask for a [PR review](https://openhands-ai.slack.com/archives/C08D8FJ5771).\n\n" +
"Feel free to join our developer community on [Slack](dub.sh/openhands). You can ask for [help](https://openhands-ai.slack.com/archives/C078L0FUGUX), [feedback](https://openhands-ai.slack.com/archives/C086ARSNMGA), and even ask for a [PR review](https://openhands-ai.slack.com/archives/C08D8FJ5771).\n\n" +
"🙌 Happy hacking! 🙌\n\n" +
"<!-- auto-comment:good-first-issue -->"
});

View File

@@ -113,19 +113,19 @@ individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
### Slack Etiquettes
### Slack and Discord Etiquettes
These Slack etiquette guidelines are designed to foster an inclusive, respectful, and productive environment for all community members. By following these best practices, we ensure effective communication and collaboration while minimizing disruptions. Lets work together to build a supportive and welcoming community!
These Slack and Discord etiquette guidelines are designed to foster an inclusive, respectful, and productive environment for all community members. By following these best practices, we ensure effective communication and collaboration while minimizing disruptions. Lets work together to build a supportive and welcoming community!
- Communicate respectfully and professionally, avoiding sarcasm or harsh language, and remember that tone can be difficult to interpret in text.
- Use threads for specific discussions to keep channels organized and easier to follow.
- Tag others only when their input is critical or urgent, and use @here, @channel or @everyone sparingly to minimize disruptions.
- Be patient, as open-source contributors and maintainers often have other commitments and may need time to respond.
- Post questions or discussions in the most relevant channel (e.g., for [slack - #general](https://openhands-ai.slack.com/archives/C06P5NCGSFP) for general topics, [slack - #questions](https://openhands-ai.slack.com/archives/C06U8UTKSAD) for queries/questions.
- Post questions or discussions in the most relevant channel (e.g., for [slack - #general](https://openhands-ai.slack.com/archives/C06P5NCGSFP) for general topics, [slack - #questions](https://openhands-ai.slack.com/archives/C06U8UTKSAD) for queries/questions, [discord - #general](https://discord.com/channels/1222935860639563850/1222935861386018885)).
- When asking for help or raising issues, include necessary details like links, screenshots, or clear explanations to provide context.
- Keep discussions in public channels whenever possible to allow others to benefit from the conversation, unless the matter is sensitive or private.
- Always adhere to [our standards](https://github.com/All-Hands-AI/OpenHands/blob/main/CODE_OF_CONDUCT.md#our-standards) to ensure a welcoming and collaborative environment.
- If you choose to mute a channel, consider setting up alerts for topics that still interest you to stay engaged. For Slack, Go to Settings → Notifications → My Keywords to add specific keywords that will notify you when mentioned. For example, if you're here for discussions about LLMs, mute the channel if its too busy, but set notifications to alert you only when “LLMs” appears in messages.
- If you choose to mute a channel, consider setting up alerts for topics that still interest you to stay engaged. For Slack, Go to Settings → Notifications → My Keywords to add specific keywords that will notify you when mentioned. For example, if you're here for discussions about LLMs, mute the channel if its too busy, but set notifications to alert you only when “LLMs” appears in messages. Also for Discord, go to the channel notifications and choose the option that best describes your need.
## Attribution

View File

@@ -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/all-hands-ai/runtime:0.58-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.57-nikolaik`
## Develop inside Docker container

View File

@@ -11,7 +11,8 @@
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://all-hands.dev/joinslack"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://dub.sh/openhands"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits"></a>
<br/>
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation"></a>
@@ -43,6 +44,8 @@ Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or [sign up for
> [this short form](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> to join our Design Partner program, where you'll get early access to commercial features and the opportunity to provide input on our product roadmap.
![App screenshot](./docs/static/img/screenshot.png)
## ☁️ OpenHands Cloud
The easiest way to get started with OpenHands is on [OpenHands Cloud](https://app.all-hands.dev),
which comes with $20 in free credits for new users.
@@ -76,17 +79,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.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-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.all-hands.dev/all-hands-ai/openhands:0.58
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
</details>
@@ -100,7 +103,7 @@ docker run -it --rm --pull=always \
### Getting Started
When you open the application, you'll be asked to choose an LLM provider and add an API key.
[Anthropic's Claude Sonnet 4.5](https://www.anthropic.com/api) (`anthropic/claude-sonnet-4-5-20250929`)
[Anthropic's Claude Sonnet 4](https://www.anthropic.com/api) (`anthropic/claude-sonnet-4-20250514`)
works best, but you have [many options](https://docs.all-hands.dev/usage/llms).
See the [Running OpenHands](https://docs.all-hands.dev/usage/installation) guide for
@@ -137,9 +140,10 @@ troubleshooting resources, and advanced configuration options.
## 🤝 How to Join the Community
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
through Slack, so this is the best place to start, but we also are happy to have you contact us on Github:
through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github:
- [Join our Slack workspace](https://all-hands.dev/joinslack) - Here we talk about research, architecture, and future development.
- [Join our Slack workspace](https://dub.sh/openhands) - Here we talk about research, architecture, and future development.
- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
- [Read or post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas.
See more about the community in [COMMUNITY.md](./COMMUNITY.md) or find details on contributing in [CONTRIBUTING.md](./CONTRIBUTING.md).

148
README_CN.md Normal file
View File

@@ -0,0 +1,148 @@
<a name="readme-top"></a>
<div align="center">
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
<h1 align="center">OpenHands: 少写代码,多做事</h1>
</div>
<div align="center">
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://dub.sh/openhands"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="加入我们的Slack社区"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="加入我们的Discord社区"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="致谢"></a>
<br/>
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="查看文档"></a>
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Arxiv论文"></a>
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="评估基准分数"></a>
<hr>
</div>
欢迎使用OpenHands前身为OpenDevin这是一个由AI驱动的软件开发代理平台。
OpenHands代理可以完成人类开发者能做的任何事情修改代码、运行命令、浏览网页、调用API甚至从StackOverflow复制代码片段。
在[docs.all-hands.dev](https://docs.all-hands.dev)了解更多信息,或[注册OpenHands Cloud](https://app.all-hands.dev)开始使用。
> [!IMPORTANT]
> 在工作中使用OpenHands我们很想与您交流填写
> [这份简短表格](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> 加入我们的设计合作伙伴计划,您将获得商业功能的早期访问权限,并有机会对我们的产品路线图提供意见。
![应用截图](./docs/static/img/screenshot.png)
## ☁️ OpenHands Cloud
开始使用OpenHands的最简单方式是在[OpenHands Cloud](https://app.all-hands.dev)上,
新用户可获得$50的免费额度。
## 💻 在本地运行OpenHands
OpenHands也可以使用Docker在本地系统上运行。
查看[运行OpenHands](https://docs.all-hands.dev/usage/installation)指南了解
系统要求和更多信息。
> [!WARNING]
> 在公共网络上?请参阅我们的[强化Docker安装指南](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)
> 通过限制网络绑定和实施其他安全措施来保护您的部署。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-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.all-hands.dev/all-hands-ai/openhands:0.57
```
> **注意**: 如果您在0.44版本之前使用过OpenHands您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands
打开应用程序时您将被要求选择一个LLM提供商并添加API密钥。
[Anthropic的Claude Sonnet 4](https://www.anthropic.com/api)`anthropic/claude-sonnet-4-20250514`
效果最佳,但您还有[许多选择](https://docs.all-hands.dev/usage/llms)。
## 💡 运行OpenHands的其他方式
> [!CAUTION]
> OpenHands旨在由单个用户在其本地工作站上运行。
> 它不适合多租户部署,即多个用户共享同一实例。没有内置的身份验证、隔离或可扩展性。
>
> 如果您有兴趣在多租户环境中运行OpenHands
> [与我们联系](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> 了解高级部署选项。
您还可以[将OpenHands连接到本地文件系统](https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem)
以可编程的[无头模式](https://docs.all-hands.dev/usage/how-to/headless-mode)运行OpenHands
通过[友好的CLI](https://docs.all-hands.dev/usage/how-to/cli-mode)与其交互,
或使用[GitHub Action](https://docs.all-hands.dev/usage/how-to/github-action)在标记的问题上运行它。
访问[运行OpenHands](https://docs.all-hands.dev/usage/installation)获取更多信息和设置说明。
如果您想修改OpenHands源代码请查看[Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md)。
遇到问题?[故障排除指南](https://docs.all-hands.dev/usage/troubleshooting)可以提供帮助。
## 📖 文档
<a href="https://deepwiki.com/All-Hands-AI/OpenHands"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki" title="DeepWiki自动生成文档"></a>
要了解有关项目的更多信息以及使用OpenHands的技巧
请查看我们的[文档](https://docs.all-hands.dev/usage/getting-started)。
在那里您将找到有关如何使用不同LLM提供商、
故障排除资源和高级配置选项的资源。
## 🤝 如何加入社区
OpenHands是一个社区驱动的项目我们欢迎每个人的贡献。我们大部分沟通
通过Slack进行因此这是开始的最佳场所但我们也很乐意您通过Discord或Github与我们联系
- [加入我们的Slack工作空间](https://dub.sh/openhands) - 这里我们讨论研究、架构和未来发展。
- [加入我们的Discord服务器](https://discord.gg/ESHStjSjD4) - 这是一个社区运营的服务器,用于一般讨论、问题和反馈。
- [阅读或发布Github问题](https://github.com/All-Hands-AI/OpenHands/issues) - 查看我们正在处理的问题,或添加您自己的想法。
在[COMMUNITY.md](./COMMUNITY.md)中了解更多关于社区的信息,或在[CONTRIBUTING.md](./CONTRIBUTING.md)中找到有关贡献的详细信息。
## 📈 进展
在[这里](https://github.com/orgs/All-Hands-AI/projects/1)查看OpenHands月度路线图每月月底在维护者会议上更新
<p align="center">
<a href="https://star-history.com/#All-Hands-AI/OpenHands&Date">
<img src="https://api.star-history.com/svg?repos=All-Hands-AI/OpenHands&type=Date" width="500" alt="Star History Chart">
</a>
</p>
## 📜 许可证
根据MIT许可证分发。有关更多信息请参阅[`LICENSE`](./LICENSE)。
## 🙏 致谢
OpenHands由大量贡献者构建每一份贡献都备受感谢我们还借鉴了其他开源项目对他们的工作深表感谢。
有关OpenHands中使用的开源项目和许可证列表请参阅我们的[CREDITS.md](./CREDITS.md)文件。
## 📚 引用
```
@misc{openhands,
title={{OpenHands: An Open Platform for AI Software Developers as Generalist Agents}},
author={Xingyao Wang and Boxuan Li and Yufan Song and Frank F. Xu and Xiangru Tang and Mingchen Zhuge and Jiayi Pan and Yueqi Song and Bowen Li and Jaskirat Singh and Hoang H. Tran and Fuqiang Li and Ren Ma and Mingzhang Zheng and Bill Qian and Yanjun Shao and Niklas Muennighoff and Yizhe Zhang and Binyuan Hui and Junyang Lin and Robert Brennan and Hao Peng and Heng Ji and Graham Neubig},
year={2024},
eprint={2407.16741},
archivePrefix={arXiv},
primaryClass={cs.SE},
url={https://arxiv.org/abs/2407.16741},
}
```

60
README_JA.md Normal file
View File

@@ -0,0 +1,60 @@
<a name="readme-top"></a>
<div align="center">
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
<h1 align="center">OpenHands: コードを減らして、もっと作ろう</h1>
</div>
<div align="center">
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://dub.sh/openhands"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Slackコミュニティに参加"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Discordコミュニティに参加"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="クレジット"></a>
<br/>
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="ドキュメントを見る"></a>
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Arxiv論文"></a>
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="評価ベンチマークスコア"></a>
<hr>
</div>
OpenHands旧OpenDevinへようこそ。これはAIが駆動するソフトウェア開発エージェントのプラットフォームです。
OpenHandsのエージェントは人間の開発者ができることは何でもこなします。コードを修正し、コマンドを実行し、ウェブを閲覧し、APIを呼び出し、StackOverflowからコードスニペットをコピーすることさえできます。
詳細は[docs.all-hands.dev](https://docs.all-hands.dev)をご覧いただくか、[OpenHands Cloud](https://app.all-hands.dev)に登録して始めましょう。
> [!IMPORTANT]
> 仕事でOpenHandsを使っていますかぜひお話を聞かせてください。[こちらの短いフォーム](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)にご記入いただき、Design Partnerプログラムにご参加ください。商用機能の早期アクセスや製品ロードマップへのフィードバックの機会を提供します。
![アプリのスクリーンショット](./docs/static/img/screenshot.png)
## ☁️ OpenHands Cloud
OpenHandsを始める最も簡単な方法は[OpenHands Cloud](https://app.all-hands.dev)を利用することです。新規ユーザーには50ドル分の無料クレジットが付与されます。
## 💻 OpenHandsをローカルで実行する
OpenHandsはDockerを利用してローカル環境でも実行できます。システム要件や詳細については[Running OpenHands](https://docs.all-hands.dev/usage/installation)ガイドをご覧ください。
> [!WARNING]
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-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.all-hands.dev/all-hands-ai/openhands:0.57
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
OpenHandsは[http://localhost:3000](http://localhost:3000)で起動します!

View File

@@ -1,5 +1,5 @@
ARG OPENHANDS_BUILD_VERSION=dev
FROM node:24.8-trixie-slim AS frontend-builder
FROM node:24.3.0-bookworm-slim AS frontend-builder
WORKDIR /app
@@ -9,7 +9,7 @@ RUN npm ci
COPY frontend ./
RUN npm run build
FROM python:3.13.7-slim-trixie AS base
FROM python:3.12.10-slim AS base
FROM base AS backend-builder
WORKDIR /app

View File

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

View File

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

View File

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

View File

@@ -3,16 +3,18 @@
"theme": "mint",
"name": "All Hands Docs",
"colors": {
"primary": "#99873c"
"primary": "#99873c",
"light": "#ffe165",
"dark": "#ffe165"
},
"background": {
"color": {
"light": "#f7f3ee"
"light": "#f7f3ee",
"dark": "#0B0D0E"
}
},
"appearance": {
"default": "light",
"strict": true
"default": "light"
},
"favicon": "/logo-square.png",
"navigation": {
@@ -212,8 +214,9 @@
},
"footer": {
"socials": {
"slack": "https://all-hands.dev/joinslack",
"github": "https://github.com/All-Hands-AI/OpenHands"
"slack": "https://dub.sh/openhands",
"github": "https://github.com/All-Hands-AI/OpenHands",
"discord": "https://discord.gg/ESHStjSjD4"
}
},
"contextual": {

View File

@@ -4,8 +4,7 @@ description: OpenHands - Code Less, Make More
icon: book-open
mode: wide
---
Use AI to tackle the toil in your backlog. Our agents have all the same tools as a human developer:
they can modify code, run commands, browse the web, call APIs, and yes-even copy code snippets from StackOverflow.
Use AI to tackle the toil in your backlog. Our agents have all the same tools as a human developer: they can modify code, run commands, browse the web, call APIs, and yes-even copy code snippets from StackOverflow.
<iframe
className="w-full aspect-video"

View File

@@ -1,14 +0,0 @@
// Reo.dev tracking initialization
(function() {
var e, t, n;
e = "6bac7145b4ee6ec";
t = function() {
Reo.init({clientID: "6bac7145b4ee6ec"});
};
n = document.createElement("script");
n.src = "https://static.reo.dev/" + e + "/reo.js";
n.defer = true;
n.onload = t;
document.head.appendChild(n);
})();

View File

@@ -17,87 +17,96 @@ To use the OpenHands Cloud API, you'll need to generate an API key:
4. Give your key a descriptive name (Example: "Development" or "Production") and select `Create`.
5. Copy the generated API key and store it securely. It will only be shown once.
## API Usage Example
## API Usage
### Starting a New Conversation
To start a new conversation with OpenHands to perform a task,
[you'll need to make a POST request to the conversation endpoint](/api-reference/new-conversation).
To start a new conversation with OpenHands to perform a task, you'll need to make a POST request to the conversation endpoint.
<Tabs>
<Tab title="cURL">
```bash
curl -X POST "https://app.all-hands.dev/api/conversations" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"initial_user_msg": "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
"repository": "yourusername/your-repo"
}'
```
</Tab>
<Tab title="Python (with requests)">
```python
import requests
#### Request Parameters
api_key = "YOUR_API_KEY"
url = "https://app.all-hands.dev/api/conversations"
| Parameter | Type | Required | Description |
|--------------------|----------|----------|------------------------------------------------------------------------------------------------------|
| `initial_user_msg` | string | Yes | The initial message to start the conversation. |
| `repository` | string | No | Git repository name to provide context in the format `owner/repo`. You must have access to the repo. |
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
#### Examples
data = {
"initial_user_msg": "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
"repository": "yourusername/your-repo"
}
response = requests.post(url, headers=headers, json=data)
conversation = response.json()
<Accordion title="cURL">
```bash
curl -X POST "https://app.all-hands.dev/api/conversations" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"initial_user_msg": "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
"repository": "yourusername/your-repo"
}'
```
</Accordion>
print(f"Conversation Link: https://app.all-hands.dev/conversations/{conversation['conversation_id']}")
print(f"Status: {conversation['status']}")
```
</Tab>
<Tab title="TypeScript/JavaScript (with fetch)">
```typescript
const apiKey = "YOUR_API_KEY";
const url = "https://app.all-hands.dev/api/conversations";
<Accordion title="Python (with requests)">
```python
import requests
const headers = {
"Authorization": `Bearer ${apiKey}`,
api_key = "YOUR_API_KEY"
url = "https://app.all-hands.dev/api/conversations"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
};
}
const data = {
initial_user_msg: "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
repository: "yourusername/your-repo"
};
data = {
"initial_user_msg": "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
"repository": "yourusername/your-repo"
}
async function startConversation() {
try {
const response = await fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify(data)
});
response = requests.post(url, headers=headers, json=data)
conversation = response.json()
const conversation = await response.json();
print(f"Conversation Link: https://app.all-hands.dev/conversations/{conversation['conversation_id']}")
print(f"Status: {conversation['status']}")
```
</Accordion>
console.log(`Conversation Link: https://app.all-hands.dev/conversations/${conversation.id}`);
console.log(`Status: ${conversation.status}`);
<Accordion title="TypeScript/JavaScript (with fetch)">
```typescript
const apiKey = "YOUR_API_KEY";
const url = "https://app.all-hands.dev/api/conversations";
return conversation;
} catch (error) {
console.error("Error starting conversation:", error);
}
const headers = {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
};
const data = {
initial_user_msg: "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
repository: "yourusername/your-repo"
};
async function startConversation() {
try {
const response = await fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify(data)
});
const conversation = await response.json();
console.log(`Conversation Link: https://app.all-hands.dev/conversations/${conversation.id}`);
console.log(`Status: ${conversation.status}`);
return conversation;
} catch (error) {
console.error("Error starting conversation:", error);
}
}
startConversation();
```
</Tab>
</Tabs>
startConversation();
```
</Accordion>
#### Response
@@ -116,6 +125,42 @@ You may receive an `AuthenticationError` if:
- You provided the wrong repository name.
- You don't have access to the repository.
### Retrieving Conversation Status
You can check the status of a conversation by making a GET request to the conversation endpoint.
#### Endpoint
```
GET https://app.all-hands.dev/api/conversations/{conversation_id}
```
#### Example
<Accordion title="cURL">
```bash
curl -X GET "https://app.all-hands.dev/api/conversations/{conversation_id}" \
-H "Authorization: Bearer YOUR_API_KEY"
```
</Accordion>
#### Response
The response is formatted as follows:
```json
{
"conversation_id":"abc1234",
"title":"Update README.md",
"created_at":"2025-04-29T15:13:51.370706Z",
"last_updated_at":"2025-04-29T15:13:57.199210Z",
"status":"RUNNING",
"selected_repository":"yourusername/your-repo",
"trigger":"gui"
}
```
## Rate Limits
If you have too many conversations running at once, older conversations will be paused to limit the number of concurrent conversations.

View File

@@ -30,13 +30,13 @@ Settings are divided across tabs, with each tab focusing on a specific area of c
- Toggle Solvability Analysis.
- Set a maximum budget per conversation.
- Configure the username and email that OpenHands uses for commits.
- `LLM` (Available for [Pro subscription users](/usage/cloud/pro-subscription))
- `LLM` (Available for `Pro Users`)
- Choose to use another LLM or use different models from the OpenHands provider.
- `Billing`
- Add credits for using the OpenHands provider.
- Cancel your `Pro subscription`.
- Cancel your `Pro` subscription.
- `Secrets`
- [Manage secrets](/usage/settings/secrets-settings).
- [Generate custom secrets](/usage/settings/secrets-settings).
- `API Keys`
- [Create API keys to work with OpenHands programmatically](/usage/cloud/cloud-api).
- `MCP`

View File

@@ -1,31 +1,26 @@
---
title: "Pro Subscription"
description: "Learn about OpenHands Cloud Pro Subscription features and pricing."
description: "Learn about OpenHands Cloud Pro Subscription features and pricing"
---
## Overview
The OpenHands Pro Subscription unlocks additional features and better pricing when you run OpenHands conversations in
OpenHands Cloud.
The OpenHands Pro Subscription unlocks additional features and better pricing when you run OpenHands conversations in OpenHands Cloud.
## Base Features
All users start on the Pay-as-you-go plan and have access to these base features when they sign up:
* **Run multiple OpenHands conversations on OpenHands Cloud runtimes.**
* **API keys to the OpenHands LLM provider for use in OpenHands CLI or when running OpenHands on your own.**
* **$20 in initial OpenHands Cloud credits to get started.**
* **Support for GitHub, GitLab, Bitbucket, Slack, and more.**
* **Run multiple OpenHands conversations on OpenHands Cloud runtimes**
* **API keys to the OpenHands LLM provider for use in tools like OpenHands CLI or OpenHands Local GUI**
* **$20 in initial OpenHands Cloud credits to get started**
* **Support for GitHub, GitLab, Bitbucket, Slack, and more**
## What you get with a Pro Subscription
The $20/month Pro subscription covers the cost of runtime compute in OpenHands Cloud, plus enables the following
features:
The $20/month Pro Subscription covers the cost of runtime compute in OpenHands Cloud, plus enables the following features:
* **Bring Your Own LLM Keys:** Bring your own API keys from OpenAI, Anthropic, Mistral, and other providers.
* **Model Choice:** Unlocks access to OpenHands LLM provider models for use within OpenHands Cloud.
* **No Markup Pricing on LLM usage:** When you use the OpenHands LLM provider in OpenHands Cloud, you pay for
LLM usage at-cost (zero markup) based on API prices.
* **No Markup Pricing on LLM usage:** When you use the OpenHands LLM provider in OpenHands Cloud, you pay for LLM usage at-cost (zero markup) based on API prices.
## Plan Comparison
@@ -38,7 +33,7 @@ Here are the key differences between Pay-as-you-go and Pro subscriptions:
| Can I bring my own LLM key? | No | ✅ Yes |
| Do I pay for LLM usage? | ✅ Yes | ✅ Yes |
| Can I select from different LLMs without bringing my own LLM key? | No \- defaults to Claude Sonnet 4 | ✅ Yes \- via OpenHands LLM provider <br/><br/>[*See models and pricing*](https://docs.all-hands.dev/usage/llms/openhands-llms#pricing) |
| How much am I charged for LLM usage? | **Marked up pricing** \- 2x Claude Sonnet 4 API prices. *This markup helps cover the cost of runtime compute.* | **No markup** \- 1x API prices. *The $20 monthly subscription covers the cost of runtime compute.* |
| How much am I charged for LLM usage? | **Marked up pricing** \- 2x Claude Sonnet 4 API prices *This markup helps cover the cost of runtime compute.* | **No markup** \- 1x API prices *The $20 monthly subscription covers the cost of runtime compute.* |
### When using the OpenHands LLM Provider outside of OpenHands Cloud
@@ -47,4 +42,4 @@ The following applies to **both** the Pay-as-you-go and Pro subscription:
| :---- | :---- |
| Do I have access to multiple models via the OpenHands LLM provider? | ✅ Yes <br/><br/> [*See models and pricing*](https://docs.all-hands.dev/usage/llms/openhands-llms#pricing) |
| Can I generate and refresh OpenHands LLM API keys? | ✅ Yes |
| How much am I charged for LLM usage when I use the OpenHands LLM provider in other AI coding tools? | **No markup** \- pay 1x API prices <br/> [*See models and pricing*](https://docs.all-hands.dev/usage/llms/openhands-llms#pricing) <br/><br/> *Usage is deducted from your OpenHands Cloud credit balance.* <br/><br/> *The OpenHands LLM provider is available to all OpenHands Cloud users, and LLM usage is billed at-cost (zero markup). Use these models with OpenHands CLI, running OpenHands on your own, or even other AI coding agents\! [Learn more.](https://www.all-hands.dev/blog/access-state-of-the-art-llm-models-at-cost-via-openhands-gui-and-cli)* |
| How much am I charged for LLM usage when I use the OpenHands LLM provider in other AI coding tools? | **No markup** \- pay 1x API prices <br/> [*See models and pricing*](https://docs.all-hands.dev/usage/llms/openhands-llms#pricing) <br/><br/> *Usage is deducted from your OpenHands Cloud credit balance.* <br/><br/> *The OpenHands LLM provider is available to all OpenHands Cloud users, and LLM usage is billed at-cost (zero markup). Use these models with OpenHands CLI, OpenHands Local GUI, or even other AI coding agents\! [Learn more.](https://www.all-hands.dev/blog/access-state-of-the-art-llm-models-at-cost-via-openhands-gui-and-cli)* |

View File

@@ -90,6 +90,7 @@ If you would like to set things up more systematically, you can:
others have encountered the same problem.
2. **Join our community**: Get help from other users and developers:
- [Slack community](https://dub.sh/openhands)
- [Discord server](https://discord.gg/ESHStjSjD4)
3. **Check our troubleshooting guide**: Common issues and solutions are documented in
[Troubleshooting](/usage/troubleshooting/troubleshooting).
4. **Report bugs**: If you've found a bug, please [create an issue](https://github.com/All-Hands-AI/OpenHands/issues/new)

View File

@@ -105,7 +105,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
1. Set the following environment variables in your terminal:
- `SANDBOX_VOLUMES` to specify the directory you want OpenHands to access ([See using SANDBOX_VOLUMES for more info](../runtimes/docker#using-sandbox_volumes))
- `LLM_MODEL` - the LLM model to use (e.g. `export LLM_MODEL="anthropic/claude-sonnet-4-20250514"` or `export LLM_MODEL="anthropic/claude-sonnet-4-5-20250929"`)
- `LLM_MODEL` - the LLM model to use (e.g. `export LLM_MODEL="anthropic/claude-sonnet-4-20250514"`)
- `LLM_API_KEY` - your API key (e.g. `export LLM_API_KEY="sk_test_12345"`)
2. Run the following command:
@@ -113,7 +113,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -122,7 +122,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.58 \
docker.all-hands.dev/all-hands-ai/openhands:0.57 \
python -m openhands.cli.entry --override-cli-mode true
```

View File

@@ -53,7 +53,7 @@ Set environment variables and run the Docker command:
```bash
# Set required environment variables
export SANDBOX_VOLUMES="/path/to/workspace:/workspace:rw" # Format: host_path:container_path:mode
export LLM_MODEL="anthropic/claude-sonnet-4-20250514" # or "anthropic/claude-sonnet-4-5-20250929"
export LLM_MODEL="anthropic/claude-sonnet-4-20250514"
export LLM_API_KEY="your-api-key"
export SANDBOX_SELECTED_REPO="owner/repo-name" # Optional: requires GITHUB_TOKEN
export GITHUB_TOKEN="your-token" # Required for repository operations
@@ -61,7 +61,7 @@ export GITHUB_TOKEN="your-token" # Required for repository operations
# Run OpenHands
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -73,7 +73,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.58 \
docker.all-hands.dev/all-hands-ai/openhands:0.57 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -4,17 +4,16 @@ description: Running OpenHands Cloud or running on your own.
icon: rocket
---
<Tabs>
<Tab title="OpenHands Cloud">
The easiest way to get started with OpenHands is on OpenHands Cloud, which comes with $20 in free credits for new users.
## OpenHands Cloud
To get started with OpenHands Cloud, visit [app.all-hands.dev](https://app.all-hands.dev).
The easiest way to get started with OpenHands is on OpenHands Cloud, which comes with $20 in free credits for new users.
For more information see [getting started with OpenHands Cloud.](/usage/cloud/openhands-cloud)
</Tab>
<Tab title="Running OpenHands on Your Own">
Run OpenHands on your local system and bring your own LLM and API key.
To get started with OpenHands Cloud, visit [app.all-hands.dev](https://app.all-hands.dev).
For more information see [running OpenHands on your own.](/usage/local-setup)
</Tab>
</Tabs>
For more information see [getting started with OpenHands Cloud.](/usage/cloud/openhands-cloud)
## Running OpenHands on Your Own
Run OpenHands on your local system and bring your own LLM and API key.
For more information see [running OpenHands on your own.](/usage/local-setup)

View File

@@ -18,7 +18,6 @@ Based on these findings and community feedback, these are the latest models that
### Cloud / API-Based Models
- [anthropic/claude-sonnet-4-20250514](https://www.anthropic.com/api) (recommended)
- [anthropic/claude-sonnet-4-5-20250929](https://www.anthropic.com/api) (recommended)
- [openai/gpt-5-2025-08-07](https://openai.com/api/) (recommended)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)

View File

@@ -68,23 +68,23 @@ Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstud
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-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.all-hands.dev/all-hands-ai/openhands:0.58
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
2. Wait until the server is running (see log below):
```
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.58
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.57
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
@@ -119,7 +119,7 @@ When started for the first time, OpenHands will prompt you to set up the LLM pro
That's it! You can now start using OpenHands with the local LLM server.
If you encounter any issues, let us know on [Slack](https://dub.sh/openhands).
If you encounter any issues, let us know on [Slack](https://dub.sh/openhands) or [Discord](https://discord.gg/ESHStjSjD4).
## Advanced: Alternative LLM Backends

View File

@@ -15,7 +15,7 @@ description: OpenHands LLM provider with access to state-of-the-art (SOTA) agent
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
- `LLM Provider` to `OpenHands`
- `LLM Model` to the model you will be using (e.g. claude-sonnet-4-20250514 or claude-sonnet-4-5-20250929)
- `LLM Model` to the model you will be using (e.g. claude-sonnet-4-20250514)
- `API Key` to your OpenHands LLM API key copied from above
## Using OpenHands LLM Provider in the CLI
@@ -36,7 +36,6 @@ Pricing follows official API provider rates. Below are the current pricing detai
|-------|----------------------------|-----------------------------------|------------------------------|------------------|-------------------|
| claude-opus-4-20250514 | $15.00 | $1.50 | $75.00 | 200,000 | 32,000 |
| claude-sonnet-4-20250514 | $3.00 | $0.30 | $15.00 | 200,000 | 64,000 |
| claude-sonnet-4-5-20250929 | $3.00 | $0.30 | $15.00 | 200,000 | 64,000 |
| devstral-medium-2507 | $0.40 | N/A | $2.00 | 128,000 | 128,000 |
| devstral-small-2505 | $0.10 | N/A | $0.30 | 128,000 | 128,000 |
| devstral-small-2507 | $0.10 | N/A | $0.30 | 128,000 | 128,000 |

View File

@@ -116,17 +116,17 @@ Note that you'll still need `uv` installed for the default MCP servers to work p
<Accordion title="Docker Command (Click to expand)">
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-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.all-hands.dev/all-hands-ai/openhands:0.58
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
</Accordion>

View File

@@ -41,71 +41,71 @@ MCP configuration can be defined in:
### Configuration Options
<Tabs>
<Tab title="SSE Servers">
SSE servers are configured using either a string URL or an object with the following properties:
#### SSE Servers
- `url` (required)
- Type: `str`
- Description: The URL of the SSE server.
SSE servers are configured using either a string URL or an object with the following properties:
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication.
</Tab>
<Tab title="SHTTP Servers">
SHTTP (Streamable HTTP) servers are configured using either a string URL or an object with the following properties:
- `url` (required)
- Type: `str`
- Description: The URL of the SSE server.
- `url` (required)
- Type: `str`
- Description: The URL of the SHTTP server.
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication.
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication.
#### SHTTP Servers
- `timeout` (optional)
- Type: `int`
- Default: `60`
- Range: `1-3600` seconds (1 hour maximum)
- Description: Timeout in seconds for tool execution. This prevents tool calls from hanging indefinitely.
- **Use Cases:**
- **Short timeout (1-30s)**: For lightweight operations like status checks or simple queries.
- **Medium timeout (30-300s)**: For standard processing tasks like data analysis or API calls.
- **Long timeout (300-3600s)**: For heavy operations like file processing, complex calculations, or batch operations.
<Note>
This timeout only applies to individual tool calls, not server connection establishment.
</Note>
</Tab>
<Tab title="Stdio Servers">
<Note>
While stdio servers are supported, [we recommend using MCP proxies](/usage/settings/mcp-settings#configuration-examples) for
better reliability and performance.
</Note>
SHTTP (Streamable HTTP) servers are configured using either a string URL or an object with the following properties:
Stdio servers are configured using an object with the following properties:
- `url` (required)
- Type: `str`
- Description: The URL of the SHTTP server.
- `name` (required)
- Type: `str`
- Description: A unique name for the server.
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication.
- `command` (required)
- Type: `str`
- Description: The command to run the server.
- `timeout` (optional)
- Type: `int`
- Default: `60`
- Range: `1-3600` seconds (1 hour maximum)
- Description: Timeout in seconds for tool execution. This prevents tool calls from hanging indefinitely.
- **Use Cases:**
- **Short timeout (1-30s)**: For lightweight operations like status checks or simple queries.
- **Medium timeout (30-300s)**: For standard processing tasks like data analysis or API calls.
- **Long timeout (300-3600s)**: For heavy operations like file processing, complex calculations, or batch operations.
<Note>
This timeout only applies to individual tool calls, not server connection establishment.
</Note>
- `args` (optional)
- Type: `list of str`
- Default: `[]`
- Description: Command-line arguments to pass to the server.
#### Stdio Servers
- `env` (optional)
- Type: `dict of str to str`
- Default: `{}`
- Description: Environment variables to set for the server process.
</Tab>
</Tabs>
<Note>
While stdio servers are supported, [we recommend using MCP proxies](/usage/settings/mcp-settings#configuration-examples) for
better reliability and performance.
</Note>
#### When to Use Direct Stdio
Stdio servers are configured using an object with the following properties:
- `name` (required)
- Type: `str`
- Description: A unique name for the server.
- `command` (required)
- Type: `str`
- Description: The command to run the server.
- `args` (optional)
- Type: `list of str`
- Default: `[]`
- Description: Command-line arguments to pass to the server.
- `env` (optional)
- Type: `dict of str to str`
- Default: `{}`
- Description: Environment variables to set for the server process.
##### When to Use Direct Stdio
Direct stdio connections may still be appropriate in these scenarios:
- **Development and testing**: Quick prototyping of MCP servers.
@@ -114,78 +114,76 @@ Direct stdio connections may still be appropriate in these scenarios:
### Configuration Examples
<Tabs>
<Tab title="Proxy Servers (SSE/HTTP) - Recommended">
For stdio-based MCP servers, we recommend using MCP proxy tools like
[`supergateway`](https://github.com/supercorp-ai/supergateway) instead of direct stdio connections.
[SuperGateway](https://github.com/supercorp-ai/supergateway) is a popular MCP proxy that converts stdio MCP servers to
HTTP/SSE endpoints.
#### Recommended: Using Proxy Servers (SSE/HTTP)
Start the proxy servers separately:
```bash
# Terminal 1: Filesystem server proxy
supergateway --stdio "npx @modelcontextprotocol/server-filesystem /" --port 8080
For stdio-based MCP servers, we recommend using MCP proxy tools like
[`supergateway`](https://github.com/supercorp-ai/supergateway) instead of direct stdio connections.
[SuperGateway](https://github.com/supercorp-ai/supergateway) is a popular MCP proxy that converts stdio MCP servers to
HTTP/SSE endpoints.
# Terminal 2: Fetch server proxy
supergateway --stdio "uvx mcp-server-fetch" --port 8081
```
Start the proxy servers separately:
```bash
# Terminal 1: Filesystem server proxy
supergateway --stdio "npx @modelcontextprotocol/server-filesystem /" --port 8080
Then configure OpenHands to use the HTTP endpoint:
# Terminal 2: Fetch server proxy
supergateway --stdio "uvx mcp-server-fetch" --port 8081
```
```toml
[mcp]
# SSE Servers - Recommended approach using proxy tools
sse_servers = [
# Basic SSE server with just a URL
"http://example.com:8080/mcp",
Then configure OpenHands to use the HTTP endpoint:
# SuperGateway proxy for fetch server
"http://localhost:8081/sse",
```toml
[mcp]
# SSE Servers - Recommended approach using proxy tools
sse_servers = [
# Basic SSE server with just a URL
"http://example.com:8080/mcp",
# External MCP service with authentication
{url="https://api.example.com/mcp/sse", api_key="your-api-key"}
]
# SuperGateway proxy for fetch server
"http://localhost:8081/sse",
# SHTTP Servers - Modern streamable HTTP transport (recommended)
shttp_servers = [
# Basic SHTTP server with default 60s timeout
"https://api.example.com/mcp/shttp",
# External MCP service with authentication
{url="https://api.example.com/mcp/sse", api_key="your-api-key"}
]
# Server with custom timeout for heavy operations
{
url = "https://files.example.com/mcp/shttp",
api_key = "your-api-key",
timeout = 1800 # 30 minutes for large file processing
# SHTTP Servers - Modern streamable HTTP transport (recommended)
shttp_servers = [
# Basic SHTTP server with default 60s timeout
"https://api.example.com/mcp/shttp",
# Server with custom timeout for heavy operations
{
url = "https://files.example.com/mcp/shttp",
api_key = "your-api-key",
timeout = 1800 # 30 minutes for large file processing
}
]
```
#### Alternative: Direct Stdio Servers (Not Recommended for Production)
```toml
[mcp]
# Direct stdio servers - use only for development/testing
stdio_servers = [
# Basic stdio server
{name="fetch", command="uvx", args=["mcp-server-fetch"]},
# Stdio server with environment variables
{
name="filesystem",
command="npx",
args=["@modelcontextprotocol/server-filesystem", "/"],
env={
"DEBUG": "true"
}
]
```
</Tab>
<Tab title="Direct Stdio Servers">
<Note>
This setup is not Recommended for production.
</Note>
```toml
[mcp]
# Direct stdio servers - use only for development/testing
stdio_servers = [
# Basic stdio server
{name="fetch", command="uvx", args=["mcp-server-fetch"]},
}
]
```
# Stdio server with environment variables
{
name="filesystem",
command="npx",
args=["@modelcontextprotocol/server-filesystem", "/"],
env={
"DEBUG": "true"
}
}
]
```
For production use, we recommend using proxy tools like SuperGateway.
For production use, we recommend using proxy tools like SuperGateway.
</Tab>
</Tabs>
### Other Proxy Tools
Other options include:

View File

@@ -24,7 +24,7 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/*
# Install Python packages with security fixes
RUN pip install alembic psycopg2-binary cloud-sql-python-connector pg8000 gspread stripe python-keycloak asyncpg sqlalchemy[asyncio] resend tenacity slack-sdk ddtrace "posthog>=6.0.0" "limits==5.2.0" coredis prometheus-client shap scikit-learn pandas numpy && \
RUN pip install alembic psycopg2-binary cloud-sql-python-connector pg8000 gspread stripe python-keycloak asyncpg sqlalchemy[asyncio] resend tenacity slack-sdk ddtrace posthog "limits==5.2.0" coredis prometheus-client shap scikit-learn pandas numpy && \
# Update packages with known CVE fixes
pip install --upgrade \
"mcp>=1.10.0" \

15
enterprise/poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
[[package]]
name = "aiofiles"
@@ -1061,7 +1061,7 @@ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\"", dev = "os_name == \"nt\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
markers = {main = "platform_system == \"Windows\" or os_name == \"nt\" or sys_platform == \"win32\"", dev = "os_name == \"nt\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
[[package]]
name = "comm"
@@ -1990,7 +1990,6 @@ files = [
{file = "fastuuid-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b31dd488d0778c36f8279b306dc92a42f16904cba54acca71e107d65b60b0c"},
{file = "fastuuid-0.12.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:b19361ee649365eefc717ec08005972d3d1eb9ee39908022d98e3bfa9da59e37"},
{file = "fastuuid-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:8fc66b11423e6f3e1937385f655bedd67aebe56a3dcec0cb835351cfe7d358c9"},
{file = "fastuuid-0.12.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:2925f67b88d47cb16aa3eb1ab20fdcf21b94d74490e0818c91ea41434b987493"},
{file = "fastuuid-0.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7b15c54d300279ab20a9cc0579ada9c9f80d1bc92997fc61fb7bf3103d7cb26b"},
{file = "fastuuid-0.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:458f1bc3ebbd76fdb89ad83e6b81ccd3b2a99fa6707cd3650b27606745cfb170"},
{file = "fastuuid-0.12.0-cp38-cp38-manylinux_2_34_x86_64.whl", hash = "sha256:a8f0f83fbba6dc44271a11b22e15838641b8c45612cdf541b4822a5930f6893c"},
@@ -6113,14 +6112,14 @@ files = [
[[package]]
name = "posthog"
version = "6.7.6"
version = "4.10.0"
description = "Integrate PostHog into any python application."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "posthog-6.7.6-py3-none-any.whl", hash = "sha256:b09a7e65a042ec416c28874b397d3accae412a80a8b0ef3fa686fbffc99e4d4b"},
{file = "posthog-6.7.6.tar.gz", hash = "sha256:ee5c5ad04b857d96d9b7a4f715e23916a2f206bfcf25e5a9d328a3d27664b0d3"},
{file = "posthog-4.10.0-py3-none-any.whl", hash = "sha256:b693d3d8209d000d8c5f4d6ea19096bfdfb83047fa8a14c937ae50a3394809a1"},
{file = "posthog-4.10.0.tar.gz", hash = "sha256:513bfbb21344013294abc046b1142173189c5422a3906cf2280d1389b0c2e28b"},
]
[package.dependencies]
@@ -6129,11 +6128,11 @@ distro = ">=1.5.0"
python-dateutil = ">=2.2"
requests = ">=2.7,<3.0"
six = ">=1.5"
typing-extensions = ">=4.2.0"
[package.extras]
dev = ["django-stubs", "lxml", "mypy", "mypy-baseline", "packaging", "pre-commit", "pydantic", "ruff", "setuptools", "tomli", "tomli_w", "twine", "types-mock", "types-python-dateutil", "types-requests", "types-setuptools", "types-six", "wheel"]
langchain = ["langchain (>=0.2.0)"]
sentry = ["django", "sentry-sdk"]
test = ["anthropic", "coverage", "django", "freezegun (==1.5.1)", "google-genai", "langchain-anthropic (>=0.3.15)", "langchain-community (>=0.3.25)", "langchain-core (>=0.3.65)", "langchain-openai (>=0.3.22)", "langgraph (>=0.4.8)", "mock (>=2.0.0)", "openai", "parameterized (>=0.8.1)", "pydantic", "pytest", "pytest-asyncio", "pytest-timeout"]
[[package]]
@@ -10103,4 +10102,4 @@ cffi = ["cffi (>=1.17) ; python_version >= \"3.13\" and platform_python_implemen
[metadata]
lock-version = "2.1"
python-versions = "^3.12,<3.14"
content-hash = "fac67a8991a3e2c840a23702dc90f99e98d381f3537ad50b4c4739cdbde941ca"
content-hash = "8c460070dce6bdec5ee0ee7bc0c2246fcf2602d1e64a0867b4f5e3a0e334fe93"

View File

@@ -38,7 +38,7 @@ resend = "^2.7.0"
tenacity = "^9.1.2"
slack-sdk = "^3.35.0"
ddtrace = "3.13.0" #pin to avoid yanked version 3.12.4
posthog = "^6.0.0"
posthog = "^4.2.0"
limits = "^5.2.0"
coredis = "^4.22.0"
httpx = "*"

View File

@@ -21,7 +21,6 @@ from openhands.events.event_store_abc import EventStoreABC
from openhands.events.observation import AgentStateChangedObservation
from openhands.events.stream import EventStreamSubscriber
from openhands.llm.llm_registry import LLMRegistry
from openhands.runtime.runtime_status import RuntimeStatus
from openhands.server.config.server_config import ServerConfig
from openhands.server.conversation_manager.conversation_manager import (
ConversationManager,
@@ -687,7 +686,6 @@ class ClusteredConversationManager(StandaloneConversationManager):
url=self._get_conversation_url(conversation_id),
session_api_key=None,
event_store=EventStore(conversation_id, self.file_store, uid),
runtime_status=RuntimeStatus.READY,
)
)
return results

View File

@@ -174,17 +174,19 @@ async def keycloak_callback(
posthog_user_id = f'FEATURE_{user_id}' if IS_FEATURE_ENV else user_id
try:
posthog.set(
distinct_id=posthog_user_id,
properties={
'user_id': posthog_user_id,
'original_user_id': user_id,
'is_feature_env': IS_FEATURE_ENV,
posthog.identify(
posthog_user_id,
{
'$set': {
'user_id': posthog_user_id, # Explicitly set as property
'original_user_id': user_id, # Store the original user_id
'is_feature_env': IS_FEATURE_ENV, # Track if this is a feature environment
}
},
)
except Exception as e:
logger.error(
'auth:posthog_set:failed',
'auth:posthog_identify:failed',
extra={
'user_id': user_id,
'error': str(e),

View File

@@ -138,7 +138,6 @@ async def saas_search_repositories(
per_page: int = 5,
sort: str = 'stars',
order: str = 'desc',
selected_provider: ProviderType | None = None,
provider_tokens: PROVIDER_TOKEN_TYPE | None = Depends(get_provider_tokens),
access_token: SecretStr | None = Depends(get_access_token),
user_id: str | None = Depends(get_user_id),
@@ -156,7 +155,6 @@ async def saas_search_repositories(
per_page=per_page,
sort=sort,
order=order,
selected_provider=selected_provider,
provider_tokens=provider_tokens,
access_token=access_token,
user_id=user_id,

View File

@@ -60,14 +60,9 @@ from openhands.utils.utils import create_registry_and_conversation_stats
RUNTIME_URL_PATTERN = os.getenv(
'RUNTIME_URL_PATTERN', 'https://{runtime_id}.prod-runtime.all-hands.dev'
)
RUNTIME_ROUTING_MODE = os.getenv('RUNTIME_ROUTING_MODE', 'subdomain').lower()
# Pattern for base URL for the runtime
RUNTIME_CONVERSATION_URL = RUNTIME_URL_PATTERN + (
'/runtime/api/conversations/{conversation_id}'
if RUNTIME_ROUTING_MODE == 'path'
else '/api/conversations/{conversation_id}'
)
RUNTIME_CONVERSATION_URL = RUNTIME_URL_PATTERN + '/api/conversations/{conversation_id}'
# Time in seconds before a Redis entry is considered expired if not refreshed
_REDIS_ENTRY_TIMEOUT_SECONDS = 300
@@ -349,48 +344,18 @@ class SaasNestedConversationManager(ConversationManager):
api_url: str,
custom_secrets: MappingProxyType[str, Any] | None,
):
"""Setup custom secrets for the nested conversation.
Note: When resuming conversations, secrets may already exist in the runtime.
We check for specific duplicate error messages to handle this case gracefully.
"""
"""Setup custom secrets for the nested conversation."""
if custom_secrets:
for key, secret in custom_secrets.items():
try:
response = await client.post(
f'{api_url}/api/secrets',
json={
'name': key,
'description': secret.description,
'value': secret.secret.get_secret_value(),
},
)
response.raise_for_status()
logger.debug(f'Successfully created secret: {key}')
except httpx.HTTPStatusError as e:
if e.response.status_code == 400:
# Only ignore if it's actually a duplicate error
try:
error_data = e.response.json()
error_msg = error_data.get('message', '')
# The API returns: "Secret {secret_name} already exists"
if 'already exists' in error_msg:
logger.info(
f'Secret "{key}" already exists, continuing - ignoring duplicate',
extra={'api_url': api_url},
)
continue
except (KeyError, ValueError, TypeError):
pass # If we can't parse JSON, fall through to re-raise
# Re-raise all other errors (including non-duplicate 400s)
logger.error(
f'Failed to setup secret "{key}": HTTP {e.response.status_code}',
extra={
'api_url': api_url,
'response_text': e.response.text[:200],
},
)
raise
response = await client.post(
f'{api_url}/api/secrets',
json={
'name': key,
'description': secret.description,
'value': secret.secret.get_secret_value(),
},
)
response.raise_for_status()
def _get_mcp_config(self, user_id: str) -> MCPConfig | None:
api_key_store = ApiKeyStore.get_instance()

View File

@@ -211,7 +211,7 @@ async def test_keycloak_callback_success_with_valid_offline_token(mock_request):
secure=False,
accepted_tos=True,
)
mock_posthog.set.assert_called_once()
mock_posthog.identify.assert_called_once()
@pytest.mark.asyncio
@@ -278,7 +278,7 @@ async def test_keycloak_callback_success_without_offline_token(mock_request):
secure=False,
accepted_tos=True,
)
mock_posthog.set.assert_called_once()
mock_posthog.identify.assert_called_once()
@pytest.mark.asyncio

View File

@@ -1,176 +0,0 @@
"""Tests for SaasNestedConversationManager custom secrets handling during resume."""
from types import MappingProxyType
from unittest.mock import AsyncMock, MagicMock
import httpx
import pytest
from pydantic import SecretStr
from server.saas_nested_conversation_manager import SaasNestedConversationManager
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.integrations.provider import CustomSecret
from openhands.server.config.server_config import ServerConfig
from openhands.storage.memory import InMemoryFileStore
class MockHTTPXResponse:
"""Mock httpx.Response that behaves realistically."""
def __init__(self, status_code: int, json_data: dict | None = None):
self.status_code = status_code
self._json_data = json_data or {}
self.text = str(json_data) if json_data else ''
def json(self):
"""Return JSON data."""
if self._json_data:
return self._json_data
raise ValueError('No JSON data')
def raise_for_status(self):
"""Raise an exception for 4xx/5xx status codes."""
if self.status_code >= 400:
# Create a proper mock response for the exception
mock_response = MagicMock()
mock_response.status_code = self.status_code
mock_response.json = self.json
mock_response.text = self.text
error = httpx.HTTPStatusError(
f"Client error '{self.status_code}' for url 'test'",
request=MagicMock(),
response=mock_response,
)
raise error
@pytest.fixture
def saas_manager():
"""Create a SaasNestedConversationManager instance for testing."""
manager = SaasNestedConversationManager(
sio=MagicMock(),
config=MagicMock(spec=OpenHandsConfig),
server_config=MagicMock(spec=ServerConfig),
file_store=MagicMock(spec=InMemoryFileStore),
event_retrieval=MagicMock(),
)
return manager
@pytest.mark.asyncio
async def test_duplicate_secrets_dont_crash_resume(saas_manager):
"""Test that duplicate secrets during resume are handled gracefully."""
mock_client = AsyncMock(spec=httpx.AsyncClient)
# Simulate resume scenario: secret already exists (400)
mock_response = MockHTTPXResponse(
400, {'message': 'Secret MY_API_KEY already exists'}
)
async def mock_post(*args, **kwargs):
return mock_response
mock_client.post = AsyncMock(side_effect=mock_post)
custom_secrets = MappingProxyType(
{
'MY_API_KEY': CustomSecret(
secret=SecretStr('api_key_value'),
description='API Key that already exists on resume',
),
}
)
# Should not raise despite 400 "already exists" error
await saas_manager._setup_custom_secrets(
client=mock_client,
api_url='https://runtime.example.com',
custom_secrets=custom_secrets,
)
assert mock_client.post.call_count == 1
@pytest.mark.asyncio
async def test_other_400_errors_still_fail(saas_manager):
"""Test that non-duplicate 400 errors are still raised."""
mock_client = AsyncMock(spec=httpx.AsyncClient)
# 400 error but NOT a duplicate
mock_response = MockHTTPXResponse(400, {'message': 'Invalid secret name format'})
async def mock_post(*args, **kwargs):
return mock_response
mock_client.post = AsyncMock(side_effect=mock_post)
custom_secrets = MappingProxyType(
{
'INVALID!NAME': CustomSecret(
secret=SecretStr('value'), description='Secret with invalid name'
),
}
)
with pytest.raises(httpx.HTTPStatusError) as exc_info:
await saas_manager._setup_custom_secrets(
client=mock_client,
api_url='https://runtime.example.com',
custom_secrets=custom_secrets,
)
assert exc_info.value.response.status_code == 400
@pytest.mark.asyncio
async def test_normal_secret_creation_still_works(saas_manager):
"""Test that normal secret creation works correctly."""
mock_client = AsyncMock(spec=httpx.AsyncClient)
# Successful creation
mock_response = MockHTTPXResponse(200, {'message': 'Secret created'})
async def mock_post(*args, **kwargs):
return mock_response
mock_client.post = AsyncMock(side_effect=mock_post)
custom_secrets = MappingProxyType(
{
'NEW_SECRET': CustomSecret(
secret=SecretStr('new_value'), description='A new secret'
),
}
)
await saas_manager._setup_custom_secrets(
client=mock_client,
api_url='https://runtime.example.com',
custom_secrets=custom_secrets,
)
assert mock_client.post.call_count == 1
call_args = mock_client.post.call_args_list[0]
assert call_args[1]['json']['name'] == 'NEW_SECRET'
assert call_args[1]['json']['value'] == 'new_value'
@pytest.mark.asyncio
async def test_handles_empty_secrets_gracefully(saas_manager):
"""Test that empty or missing secrets are handled correctly."""
mock_client = AsyncMock(spec=httpx.AsyncClient)
# Test with None
await saas_manager._setup_custom_secrets(
client=mock_client, api_url='https://runtime.example.com', custom_secrets=None
)
assert mock_client.post.call_count == 0
# Test with empty dict
await saas_manager._setup_custom_secrets(
client=mock_client,
api_url='https://runtime.example.com',
custom_secrets=MappingProxyType({}),
)
assert mock_client.post.call_count == 0

View File

@@ -17,8 +17,8 @@ import type { Message } from "#/message";
import { SUGGESTIONS } from "#/utils/suggestions";
import { ChatInterface } from "#/components/features/chat/chat-interface";
import { useWsClient } from "#/context/ws-client-provider";
import { useErrorMessageStore } from "#/stores/error-message-store";
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
import { useWSErrorMessage } from "#/hooks/use-ws-error-message";
import { useConfig } from "#/hooks/query/use-config";
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
@@ -26,8 +26,8 @@ import { OpenHandsAction } from "#/types/core/actions";
// Mock the hooks
vi.mock("#/context/ws-client-provider");
vi.mock("#/stores/error-message-store");
vi.mock("#/stores/optimistic-user-message-store");
vi.mock("#/hooks/use-optimistic-user-message");
vi.mock("#/hooks/use-ws-error-message");
vi.mock("#/hooks/query/use-config");
vi.mock("#/hooks/mutation/use-get-trajectory");
vi.mock("#/hooks/mutation/use-upload-files");
@@ -61,6 +61,7 @@ vi.mock("#/hooks/use-conversation-name-context-menu", () => ({
}),
}));
// Helper function to render with Router context
const renderChatInterfaceWithRouter = () =>
renderWithProviders(
@@ -108,14 +109,13 @@ describe("ChatInterface - Chat Suggestions", () => {
parsedEvents: [],
});
(
useOptimisticUserMessageStore as unknown as ReturnType<typeof vi.fn>
useOptimisticUserMessage as unknown as ReturnType<typeof vi.fn>
).mockReturnValue({
setOptimisticUserMessage: vi.fn(),
getOptimisticUserMessage: vi.fn(() => null),
});
(
useErrorMessageStore as unknown as ReturnType<typeof vi.fn>
).mockReturnValue({
(useWSErrorMessage as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
getErrorMessage: vi.fn(() => null),
setErrorMessage: vi.fn(),
removeErrorMessage: vi.fn(),
});
@@ -203,7 +203,7 @@ describe("ChatInterface - Chat Suggestions", () => {
test("should hide chat suggestions when there is an optimistic user message", () => {
(
useOptimisticUserMessageStore as unknown as ReturnType<typeof vi.fn>
useOptimisticUserMessage as unknown as ReturnType<typeof vi.fn>
).mockReturnValue({
setOptimisticUserMessage: vi.fn(),
getOptimisticUserMessage: vi.fn(() => "Optimistic message"),
@@ -246,14 +246,13 @@ describe("ChatInterface - Empty state", () => {
parsedEvents: [],
});
(
useOptimisticUserMessageStore as unknown as ReturnType<typeof vi.fn>
useOptimisticUserMessage as unknown as ReturnType<typeof vi.fn>
).mockReturnValue({
setOptimisticUserMessage: vi.fn(),
getOptimisticUserMessage: vi.fn(() => null),
});
(
useErrorMessageStore as unknown as ReturnType<typeof vi.fn>
).mockReturnValue({
(useWSErrorMessage as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
getErrorMessage: vi.fn(() => null),
setErrorMessage: vi.fn(),
removeErrorMessage: vi.fn(),
});

View File

@@ -157,52 +157,8 @@ describe("MicroagentManagement", () => {
owner_type: "organization",
pushed_at: "2021-10-06T12:00:00Z",
},
{
id: "7",
full_name: "user/gitlab-repo/openhands-config",
git_provider: "gitlab",
is_public: true,
owner_type: "user",
pushed_at: "2021-10-07T12:00:00Z",
},
{
id: "8",
full_name: "org/gitlab-org-repo/openhands-config",
git_provider: "gitlab",
is_public: true,
owner_type: "organization",
pushed_at: "2021-10-08T12:00:00Z",
},
];
// Helper function to filter repositories with OpenHands suffixes
const getRepositoriesWithOpenHandsSuffix = (
repositories: GitRepository[],
) => {
return repositories.filter(
(repo) =>
repo.full_name.endsWith("/.openhands") ||
repo.full_name.endsWith("/openhands-config"),
);
};
// Helper functions for mocking search repositories
const mockSearchRepositoriesWithData = (data: GitRepository[]) => {
mockUseSearchRepositories.mockReturnValue({
data,
isLoading: false,
isError: false,
});
};
const mockSearchRepositoriesEmpty = () => {
mockUseSearchRepositories.mockReturnValue({
data: [],
isLoading: false,
isError: false,
});
};
const mockMicroagents: RepositoryMicroagent[] = [
{
name: "test-microagent-1",
@@ -309,11 +265,11 @@ describe("MicroagentManagement", () => {
isError: false,
});
// Mock the search repositories hook to return repositories with OpenHands suffixes
const mockSearchResults =
getRepositoriesWithOpenHandsSuffix(mockRepositories);
mockSearchRepositoriesWithData(mockSearchResults);
mockUseSearchRepositories.mockReturnValue({
data: [],
isLoading: false,
isError: false,
});
// Setup default mock for retrieveUserGitRepositories
vi.spyOn(GitService, "retrieveUserGitRepositories").mockResolvedValue({
@@ -638,9 +594,6 @@ describe("MicroagentManagement", () => {
onLoadMore: vi.fn(),
});
// Mock empty search results
mockSearchRepositoriesEmpty();
renderMicroagentManagement();
// Wait for repositories to be loaded
@@ -829,10 +782,6 @@ describe("MicroagentManagement", () => {
it("should handle empty search results", async () => {
const user = userEvent.setup();
// Mock empty search results for this test
mockSearchRepositoriesEmpty();
renderMicroagentManagement();
// Wait for repositories to be loaded

View File

@@ -133,7 +133,7 @@ describe("Settings Screen", () => {
"user",
"integrations",
"application",
"billing", // The nav item shows "Billing" text and routes to /billing
"billing", // The nav item shows "billing" text and routes to /billing
"secrets",
"api keys",
];

14
frontend/global.d.ts vendored
View File

@@ -1,18 +1,4 @@
interface Window {
__APP_MODE__?: "saas" | "oss";
__GITHUB_CLIENT_ID__?: string | null;
Reo?: {
init: (config: { clientID: string }) => void;
identify: (identity: {
username: string;
type: "github" |"email";
other_identities?: Array<{
username: string;
type: "github" | "email";
}>;
firstname?: string;
lastname?: string;
company?: string;
}) => void;
};
}

View File

@@ -1,19 +1,19 @@
{
"name": "openhands-frontend",
"version": "0.58.0",
"version": "0.57.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.58.0",
"version": "0.57.0",
"dependencies": {
"@heroui/react": "^2.8.4",
"@heroui/use-infinite-scroll": "^2.2.11",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.9.3",
"@react-router/serve": "^7.9.3",
"@react-router/node": "^7.9.1",
"@react-router/serve": "^7.9.1",
"@react-types/shared": "^3.32.0",
"@stripe/react-stripe-js": "^4.0.2",
"@stripe/stripe-js": "^7.9.0",
@@ -21,7 +21,7 @@
"@tailwindcss/vite": "^4.1.13",
"@tanstack/react-query": "^5.90.2",
"@uidotdev/usehooks": "^2.4.1",
"@vitejs/plugin-react": "^5.0.4",
"@vitejs/plugin-react": "^5.0.3",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.12.2",
@@ -30,23 +30,23 @@
"date-fns": "^4.1.0",
"downshift": "^9.0.10",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.23.22",
"framer-motion": "^12.23.19",
"i18next": "^25.5.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.31",
"isbot": "^5.1.30",
"jose": "^6.1.0",
"lucide-react": "^0.544.0",
"monaco-editor": "^0.53.0",
"posthog-js": "^1.268.8",
"posthog-js": "^1.268.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.6.0",
"react-i18next": "^16.0.0",
"react-i18next": "^15.7.2",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-router": "^7.9.3",
"react-router": "^7.9.1",
"react-syntax-highlighter": "^15.6.6",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
@@ -65,15 +65,15 @@
"@babel/types": "^7.28.2",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.55.1",
"@react-router/dev": "^7.9.3",
"@tailwindcss/typography": "^0.5.19",
"@react-router/dev": "^7.9.1",
"@tailwindcss/typography": "^0.5.18",
"@tanstack/eslint-plugin-query": "^5.90.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.5.2",
"@types/react": "^19.1.15",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
@@ -96,7 +96,7 @@
"eslint-plugin-unused-imports": "^4.2.0",
"husky": "^9.1.7",
"jsdom": "^27.0.0",
"lint-staged": "^16.2.3",
"lint-staged": "^16.2.0",
"msw": "^2.6.6",
"prettier": "^3.6.2",
"stripe": "^18.5.0",
@@ -3511,9 +3511,9 @@
"license": "MIT"
},
"node_modules/@posthog/core": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.2.2.tgz",
"integrity": "sha512-f16Ozx6LIigRG+HsJdt+7kgSxZTHeX5f1JlCGKI1lXcvlZgfsCR338FuMI2QRYXGl+jg/vYFzGOTQBxl90lnBg=="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.1.0.tgz",
"integrity": "sha512-igElrcnRPJh2nWYACschjH4OwGwzSa6xVFzRDVzpnjirUivdJ8nv4hE+H31nvwE56MFhvvglfHuotnWLMcRW7w=="
},
"node_modules/@react-aria/breadcrumbs": {
"version": "3.5.28",
@@ -4201,9 +4201,9 @@
}
},
"node_modules/@react-router/dev": {
"version": "7.9.3",
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.9.3.tgz",
"integrity": "sha512-oPaO+OpvCo/rNTJrRipHSp31/K4It19PE5A24x21FlYlemPTe3fbGX/kyC2+8au/abXbvzNHfRbuIBD/rfojmA==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.9.1.tgz",
"integrity": "sha512-fW/qubsdHq1nsufHPLpXa6hiNvXXV9JBtWqRlJ02OOhFeaWERZw4rGoHjG1DCg8/QTTadgbzplmP97ZnzWPkcA==",
"dev": true,
"dependencies": {
"@babel/core": "^7.27.7",
@@ -4214,8 +4214,7 @@
"@babel/traverse": "^7.27.7",
"@babel/types": "^7.27.7",
"@npmcli/package-json": "^4.0.1",
"@react-router/node": "7.9.3",
"@remix-run/node-fetch-server": "^0.9.0",
"@react-router/node": "7.9.1",
"arg": "^5.0.1",
"babel-dead-code-elimination": "^1.0.6",
"chokidar": "^4.0.0",
@@ -4230,6 +4229,7 @@
"prettier": "^3.6.2",
"react-refresh": "^0.14.0",
"semver": "^7.3.7",
"set-cookie-parser": "^2.6.0",
"tinyglobby": "^0.2.14",
"valibot": "^0.41.0",
"vite-node": "^3.2.2"
@@ -4241,9 +4241,9 @@
"node": ">=20.0.0"
},
"peerDependencies": {
"@react-router/serve": "^7.9.3",
"@react-router/serve": "^7.9.1",
"@vitejs/plugin-rsc": "*",
"react-router": "^7.9.3",
"react-router": "^7.9.1",
"typescript": "^5.1.0",
"vite": "^5.1.0 || ^6.0.0 || ^7.0.0",
"wrangler": "^3.28.2 || ^4.0.0"
@@ -4277,9 +4277,9 @@
}
},
"node_modules/@react-router/node": {
"version": "7.9.3",
"resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.9.3.tgz",
"integrity": "sha512-+OvWxPPUgouOshw85QlG0J6yFJM0GMCCpXqPj38IcveeFLlP7ppOAEkOi7RBFrDvg7vSUtCEBDnsbuDCvxUPJg==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.9.1.tgz",
"integrity": "sha512-XfyVLM+sDUDB1frGNr7iqaKNglrPwBiUp8+sFdL4///bngy49pUb2RuEtn2J2Cy5yjL+IlKbjJFrsmfimLBmeg==",
"dependencies": {
"@mjackson/node-fetch-server": "^0.2.0"
},
@@ -4287,7 +4287,7 @@
"node": ">=20.0.0"
},
"peerDependencies": {
"react-router": "7.9.3",
"react-router": "7.9.1",
"typescript": "^5.1.0"
},
"peerDependenciesMeta": {
@@ -4297,13 +4297,12 @@
}
},
"node_modules/@react-router/serve": {
"version": "7.9.3",
"resolved": "https://registry.npmjs.org/@react-router/serve/-/serve-7.9.3.tgz",
"integrity": "sha512-wtiDLo4sY3ouADXPm1xa4eg79zRXP517E0QcuBKPfoKh/40IcANTqN11VeEKNA9QgNxLeCm4CSY3dPbqePuwkA==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@react-router/serve/-/serve-7.9.1.tgz",
"integrity": "sha512-yVBSb5KsNCdkSoOk186/M5GJtcIvKE32Ax9LhXySVpM+suCSjucI+p2TXDOJIYsBqr2aKcBl/bNBm5sIJxG/HA==",
"dependencies": {
"@mjackson/node-fetch-server": "^0.2.0",
"@react-router/express": "7.9.3",
"@react-router/node": "7.9.3",
"@react-router/express": "7.9.1",
"@react-router/node": "7.9.1",
"compression": "^1.7.4",
"express": "^4.19.2",
"get-port": "5.1.1",
@@ -4317,22 +4316,22 @@
"node": ">=20.0.0"
},
"peerDependencies": {
"react-router": "7.9.3"
"react-router": "7.9.1"
}
},
"node_modules/@react-router/serve/node_modules/@react-router/express": {
"version": "7.9.3",
"resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.9.3.tgz",
"integrity": "sha512-XNVj/8AfecE1n61bXD41LqpXAixyWBpmBWkrzVA2iG+SrQOb+J6TjqZYEmZmoqJHuHmkOjt6/Iz1f81p93peGQ==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.9.1.tgz",
"integrity": "sha512-d1sfsD3AJXZj+C5k3jAmxAD3vJXGfoh3lNmtSwxp0NdZFHI54zPC5S9o80cy3P8p6Gc7XzSEQJYk9k7fAM/AIw==",
"dependencies": {
"@react-router/node": "7.9.3"
"@react-router/node": "7.9.1"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"express": "^4.17.1 || ^5",
"react-router": "7.9.3",
"react-router": "7.9.1",
"typescript": "^5.1.0"
},
"peerDependenciesMeta": {
@@ -4976,16 +4975,10 @@
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@remix-run/node-fetch-server": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@remix-run/node-fetch-server/-/node-fetch-server-0.9.0.tgz",
"integrity": "sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA==",
"dev": true
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
"integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw=="
"version": "1.0.0-beta.35",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz",
"integrity": "sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg=="
},
"node_modules/@rollup/pluginutils": {
"version": "5.3.0",
@@ -5928,9 +5921,9 @@
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
"version": "0.5.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.18.tgz",
"integrity": "sha512-dDIgwZOlf+tVkZ7A029VvQ1+ngKATENDjMEx2N35s2yPjfTS05RWSM8ilhEWSa5DMJ6ci2Ha9WNZEd2GQjrdQg==",
"dev": true,
"dependencies": {
"postcss-selector-parser": "6.0.10"
@@ -6262,9 +6255,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.1.15",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.15.tgz",
"integrity": "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA==",
"version": "19.1.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
"integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
"dependencies": {
"csstype": "^3.0.2"
}
@@ -6735,14 +6728,14 @@
"license": "ISC"
},
"node_modules/@vitejs/plugin-react": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz",
"integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==",
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.3.tgz",
"integrity": "sha512-PFVHhosKkofGH0Yzrw1BipSedTH68BFF8ZWy1kfUpCtJcouXXY0+racG8sExw7hw0HoX36813ga5o3LTWZ4FUg==",
"dependencies": {
"@babel/core": "^7.28.4",
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
"@rolldown/pluginutils": "1.0.0-beta.38",
"@rolldown/pluginutils": "1.0.0-beta.35",
"@types/babel__core": "^7.20.5",
"react-refresh": "^0.17.0"
},
@@ -10036,11 +10029,11 @@
}
},
"node_modules/framer-motion": {
"version": "12.23.22",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.22.tgz",
"integrity": "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==",
"version": "12.23.19",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.19.tgz",
"integrity": "sha512-AaWAohgTs2+wUoDdpJaaqMgV6vkm1uzzDlZUItem45linLrFiFqi4iw7bryhcVqu4loaaSLtSjAojfCAB3qczw==",
"dependencies": {
"motion-dom": "^12.23.21",
"motion-dom": "^12.23.19",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
@@ -10786,6 +10779,18 @@
"node": ">= 4"
}
},
"node_modules/immer": {
"version": "10.1.3",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
"license": "MIT",
"optional": true,
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -11392,9 +11397,10 @@
"license": "MIT"
},
"node_modules/isbot": {
"version": "5.1.31",
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.31.tgz",
"integrity": "sha512-DPgQshehErHAqSCKDb3rNW03pa2wS/v5evvUqtxt6TTnHRqAG8FdzcSSJs9656pK6Y+NT7K9R4acEYXLHYfpUQ==",
"version": "5.1.30",
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.30.tgz",
"integrity": "sha512-3wVJEonAns1OETX83uWsk5IAne2S5zfDcntD2hbtU23LelSqNXzXs9zKjMPOLMzroCgIjCfjYAEHrd2D6FOkiA==",
"license": "Unlicense",
"engines": {
"node": ">=18"
}
@@ -11930,18 +11936,18 @@
"license": "MIT"
},
"node_modules/lint-staged": {
"version": "16.2.3",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.3.tgz",
"integrity": "sha512-1OnJEESB9zZqsp61XHH2fvpS1es3hRCxMplF/AJUDa8Ho8VrscYDIuxGrj3m8KPXbcWZ8fT9XTMUhEQmOVKpKw==",
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.0.tgz",
"integrity": "sha512-spdYSOCQ2MdZ9CM1/bu/kDmaYGsrpNOeu1InFFV8uhv14x6YIubGxbCpSmGILFoxkiheNQPDXSg5Sbb5ZuVnug==",
"dev": true,
"dependencies": {
"commander": "^14.0.1",
"listr2": "^9.0.4",
"micromatch": "^4.0.8",
"nano-spawn": "^1.0.3",
"pidtree": "^0.6.0",
"string-argv": "^0.3.2",
"yaml": "^2.8.1"
"commander": "14.0.1",
"listr2": "9.0.4",
"micromatch": "4.0.8",
"nano-spawn": "1.0.3",
"pidtree": "0.6.0",
"string-argv": "0.3.2",
"yaml": "2.8.1"
},
"bin": {
"lint-staged": "bin/lint-staged.js"
@@ -13418,9 +13424,9 @@
}
},
"node_modules/motion-dom": {
"version": "12.23.21",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.21.tgz",
"integrity": "sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==",
"version": "12.23.19",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.19.tgz",
"integrity": "sha512-ivUCJ0zVZt7S++D8+ONeefkJj/8JlpCRYzGegLdXr8Z9aWg64KyljdaCGVa54Vv0K8hNE7vRQSaQve7V5l3rMw==",
"dependencies": {
"motion-utils": "^12.23.6"
}
@@ -14264,11 +14270,11 @@
"license": "MIT"
},
"node_modules/posthog-js": {
"version": "1.268.8",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.268.8.tgz",
"integrity": "sha512-BJiKK4MlUvs7ybnQcy1KkwAz+SZkE/wRLotetIoank5kbqZs8FLbeyozFvmmgx4aoMmaVymYBSmYphYjYQeidw==",
"version": "1.268.1",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.268.1.tgz",
"integrity": "sha512-vkV8vFHUWtPsFeHZCCszGdnLxKJn93UVw7a7SZGTJyyQ3JBC1Sydy4DvolnDt2IhqIUZCs9ljwqaUXcITqLoEg==",
"dependencies": {
"@posthog/core": "1.2.2",
"@posthog/core": "1.1.0",
"core-js": "^3.38.1",
"fflate": "^0.4.8",
"preact": "^10.19.3",
@@ -14631,15 +14637,16 @@
}
},
"node_modules/react-i18next": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.0.0.tgz",
"integrity": "sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q==",
"version": "15.7.3",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.3.tgz",
"integrity": "sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 25.5.2",
"i18next": ">= 25.4.1",
"react": ">= 16.8.0",
"typescript": "^5"
},
@@ -14714,9 +14721,9 @@
}
},
"node_modules/react-router": {
"version": "7.9.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz",
"integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==",
"version": "7.9.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.1.tgz",
"integrity": "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g==",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.58.0",
"version": "0.57.0",
"private": true,
"type": "module",
"engines": {
@@ -11,8 +11,8 @@
"@heroui/use-infinite-scroll": "^2.2.11",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.9.3",
"@react-router/serve": "^7.9.3",
"@react-router/node": "^7.9.1",
"@react-router/serve": "^7.9.1",
"@react-types/shared": "^3.32.0",
"@stripe/react-stripe-js": "^4.0.2",
"@stripe/stripe-js": "^7.9.0",
@@ -20,7 +20,7 @@
"@tailwindcss/vite": "^4.1.13",
"@tanstack/react-query": "^5.90.2",
"@uidotdev/usehooks": "^2.4.1",
"@vitejs/plugin-react": "^5.0.4",
"@vitejs/plugin-react": "^5.0.3",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.12.2",
@@ -29,23 +29,23 @@
"date-fns": "^4.1.0",
"downshift": "^9.0.10",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.23.22",
"framer-motion": "^12.23.19",
"i18next": "^25.5.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.31",
"isbot": "^5.1.30",
"jose": "^6.1.0",
"lucide-react": "^0.544.0",
"monaco-editor": "^0.53.0",
"posthog-js": "^1.268.8",
"posthog-js": "^1.268.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.6.0",
"react-i18next": "^16.0.0",
"react-i18next": "^15.7.2",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-router": "^7.9.3",
"react-router": "^7.9.1",
"react-syntax-highlighter": "^15.6.6",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
@@ -96,15 +96,15 @@
"@babel/types": "^7.28.2",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.55.1",
"@react-router/dev": "^7.9.3",
"@tailwindcss/typography": "^0.5.19",
"@react-router/dev": "^7.9.1",
"@tailwindcss/typography": "^0.5.18",
"@tanstack/eslint-plugin-query": "^5.90.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.5.2",
"@types/react": "^19.1.15",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
@@ -127,7 +127,7 @@
"eslint-plugin-unused-imports": "^4.2.0",
"husky": "^9.1.7",
"jsdom": "^27.0.0",
"lint-staged": "^16.2.3",
"lint-staged": "^16.2.0",
"msw": "^2.6.6",
"prettier": "^3.6.2",
"stripe": "^18.5.0",
@@ -139,7 +139,7 @@
},
"packageManager": "npm@10.5.0",
"volta": {
"node": "22.0.0"
"node": "18.20.1"
},
"msw": {
"workerDirectory": [

View File

@@ -23,7 +23,7 @@ class GitService {
*/
static async searchGitRepositories(
query: string,
per_page = 100,
per_page = 5,
selected_provider?: Provider,
): Promise<GitRepository[]> {
const response = await openHands.get<GitRepository[]>(

View File

@@ -22,8 +22,8 @@ import { useAgentStore } from "#/stores/agent-store";
import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { useErrorMessageStore } from "#/stores/error-message-store";
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
import { useWSErrorMessage } from "#/hooks/use-ws-error-message";
import { ErrorMessageBanner } from "./error-message-banner";
import {
hasUserEvent,
@@ -46,10 +46,10 @@ function getEntryPoint(
export function ChatInterface() {
const { setMessageToSend } = useConversationStore();
const { errorMessage } = useErrorMessageStore();
const { getErrorMessage } = useWSErrorMessage();
const { send, isLoadingMessages, parsedEvents } = useWsClient();
const { setOptimisticUserMessage, getOptimisticUserMessage } =
useOptimisticUserMessageStore();
useOptimisticUserMessage();
const { t } = useTranslation();
const scrollRef = React.useRef<HTMLDivElement>(null);
const {
@@ -73,6 +73,7 @@ export function ChatInterface() {
const { mutateAsync: uploadFiles } = useUploadFiles();
const optimisticUserMessage = getOptimisticUserMessage();
const errorMessage = getErrorMessage();
const events = parsedEvents.filter(shouldRenderEvent);

View File

@@ -58,7 +58,7 @@ export function ChatMessage({
"rounded-xl relative w-fit max-w-full last:mb-4",
"flex flex-col gap-2",
type === "user" && " p-4 bg-tertiary self-end",
type === "agent" && "mt-6 w-full max-w-full bg-transparent",
type === "agent" && "mt-6 max-w-full bg-transparent",
)}
>
<div

View File

@@ -6,12 +6,7 @@ export interface ChatStopButtonProps {
export function ChatStopButton({ handleStop }: ChatStopButtonProps) {
return (
<button
type="button"
onClick={handleStop}
data-testid="stop-button"
className="cursor-pointer"
>
<button type="button" onClick={handleStop} data-testid="stop-button">
<PauseIcon className="block max-w-none w-4 h-4" />
</button>
);

View File

@@ -25,7 +25,6 @@ export function ChatInputActions({
<ServerStatus conversationStatus={conversationStatus} />
</div>
<AgentStatus
className="ml-2 md:ml-3"
handleStop={() => handleStop(onStop)}
handleResumeAgent={handleResumeAgent}
disabled={disabled}

View File

@@ -82,7 +82,6 @@ export function CustomChatInput({
handleGripMouseDown,
handleGripTouchStart,
increaseHeightForEmptyContent,
resetManualResize,
} = useGripResize(
chatInputRef as React.RefObject<HTMLDivElement | null>,
messageToSend,
@@ -93,7 +92,6 @@ export function CustomChatInput({
fileInputRef as React.RefObject<HTMLInputElement | null>,
smartResize,
onSubmit,
resetManualResize,
);
const { handleInput, handlePaste, handleKeyDown, handleBlur, handleFocus } =
@@ -115,6 +113,7 @@ export function CustomChatInput({
},
[setShouldHideSuggestions, clearAllFiles],
);
return (
<div className={`w-full ${className}`}>
{/* Hidden file input */}

View File

@@ -12,7 +12,7 @@ import {
} from "#/types/core/guards";
import { EventMessage } from "./event-message";
import { ChatMessage } from "./chat-message";
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
import { LaunchMicroagentModal } from "./microagent/launch-microagent-modal";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
import { useConversationId } from "#/hooks/use-conversation-id";
@@ -48,7 +48,7 @@ export const Messages: React.FC<MessagesProps> = React.memo(
isPending,
unsubscribeFromConversation,
} = useCreateConversationAndSubscribeMultiple();
const { getOptimisticUserMessage } = useOptimisticUserMessageStore();
const { getOptimisticUserMessage } = useOptimisticUserMessage();
const { conversationId } = useConversationId();
const { data: conversation } = useUserConversation(conversationId);

View File

@@ -1,4 +1,3 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { ContextMenu } from "#/ui/context-menu";
@@ -7,14 +6,91 @@ import { Divider } from "#/ui/divider";
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
import { useConfig } from "#/hooks/query/use-config";
import { I18nKey } from "#/i18n/declaration";
import CreditCardIcon from "#/icons/credit-card.svg?react";
import KeyIcon from "#/icons/key.svg?react";
import LogOutIcon from "#/icons/log-out.svg?react";
import { SAAS_NAV_ITEMS, OSS_NAV_ITEMS } from "#/constants/settings-nav";
import ServerProcessIcon from "#/icons/server-process.svg?react";
import SettingsGearIcon from "#/icons/settings-gear.svg?react";
import CircuitIcon from "#/icons/u-circuit.svg?react";
import PuzzlePieceIcon from "#/icons/u-puzzle-piece.svg?react";
import UserIcon from "#/icons/user.svg?react";
interface AccountSettingsContextMenuProps {
onLogout: () => void;
onClose: () => void;
}
const SAAS_NAV_ITEMS = [
{
icon: <UserIcon width={16} height={16} />,
to: "/settings/user",
text: "COMMON$USER_SETTINGS",
},
{
icon: <PuzzlePieceIcon width={16} height={16} />,
to: "/settings/integrations",
text: "SETTINGS$NAV_INTEGRATIONS",
},
{
icon: <SettingsGearIcon width={16} height={16} />,
to: "/settings/app",
text: "COMMON$APPLICATION_SETTINGS",
},
{
icon: <CircuitIcon width={16} height={16} />,
to: "/settings",
text: "COMMON$LANGUAGE_MODEL_LLM",
},
{
icon: <CreditCardIcon width={16} height={16} />,
to: "/settings/billing",
text: "SETTINGS$NAV_BILLING",
},
{
icon: <KeyIcon width={16} height={16} />,
to: "/settings/secrets",
text: "SETTINGS$NAV_SECRETS",
},
{
icon: <KeyIcon width={16} height={16} />,
to: "/settings/api-keys",
text: "SETTINGS$NAV_API_KEYS",
},
{
icon: <ServerProcessIcon width={16} height={16} />,
to: "/settings/mcp",
text: "SETTINGS$NAV_MCP",
},
];
const OSS_NAV_ITEMS = [
{
icon: <CircuitIcon width={16} height={16} />,
to: "/settings",
text: "COMMON$LANGUAGE_MODEL_LLM",
},
{
icon: <ServerProcessIcon width={16} height={16} />,
to: "/settings/mcp",
text: "COMMON$MODEL_CONTEXT_PROTOCOL_MCP",
},
{
icon: <PuzzlePieceIcon width={16} height={16} />,
to: "/settings/integrations",
text: "SETTINGS$NAV_INTEGRATIONS",
},
{
icon: <SettingsGearIcon width={16} height={16} />,
to: "/settings/app",
text: "COMMON$APPLICATION_SETTINGS",
},
{
icon: <KeyIcon width={16} height={16} />,
to: "/settings/secrets",
text: "SETTINGS$NAV_SECRETS",
},
];
export function AccountSettingsContextMenu({
onLogout,
onClose,
@@ -24,13 +100,7 @@ export function AccountSettingsContextMenu({
const { data: config } = useConfig();
const isSaas = config?.APP_MODE === "saas";
const navItems = (isSaas ? SAAS_NAV_ITEMS : OSS_NAV_ITEMS).map((item) => ({
...item,
icon: React.cloneElement(item.icon, {
width: 16,
height: 16,
} as React.SVGProps<SVGSVGElement>),
}));
const navItems = isSaas ? SAAS_NAV_ITEMS : OSS_NAV_ITEMS;
const handleNavigationClick = () => {
onClose();
@@ -42,7 +112,7 @@ export function AccountSettingsContextMenu({
testId="account-settings-context-menu"
ref={ref}
alignment="right"
className="mt-0 md:right-full md:left-full md:bottom-0 ml-0 w-fit z-[9999]"
className="mt-0 md:right-full md:left-full md:bottom-0 ml-0 z-10 w-fit z-[9999]"
>
{navItems.map(({ to, text, icon }) => (
<Link key={to} to={to} className="text-decoration-none">

View File

@@ -61,11 +61,8 @@ export function AgentStatus({
}, [shouldShownAgentLoading, setShouldShownAgentLoading]);
return (
<div className={cn("flex items-center gap-1 min-w-0", className)}>
<span
className="text-[11px] text-white font-normal leading-5 flex-1 min-w-0 max-w-full whitespace-normal break-words"
title={t(statusCode)}
>
<div className={`flex items-center gap-1 ${className}`}>
<span className="text-[11px] text-white font-normal leading-5">
{t(statusCode)}
</span>
<div

View File

@@ -34,7 +34,8 @@ export function ServerStatus({
const isStartingStatus =
curAgentState === AgentState.LOADING || curAgentState === AgentState.INIT;
const isStopStatus = conversationStatus === "STOPPED";
const isStopStatus =
curAgentState === AgentState.STOPPED || conversationStatus === "STOPPED";
// Get the appropriate color based on agent status
const getStatusColor = (): string => {

View File

@@ -47,6 +47,7 @@ export function BranchDropdownMenu({
key={branch.name}
item={branch}
index={index}
isHighlighted={currentHighlightedIndex === index}
isSelected={currentSelectedItem?.name === branch.name}
getItemProps={currentGetItemProps}
getDisplayText={(branchItem) => branchItem.name}

View File

@@ -134,6 +134,7 @@ export function GitProviderDropdown({
key={item}
item={item}
index={index}
isHighlighted={index === currentHighlightedIndex}
isSelected={item === currentSelectedItem}
getItemProps={currentGetItemProps}
getDisplayText={formatProviderName}

View File

@@ -23,8 +23,6 @@ import { GenericDropdownMenu } from "../shared/generic-dropdown-menu";
import { useConfig } from "#/hooks/query/use-config";
import { I18nKey } from "#/i18n/declaration";
import RepoIcon from "#/icons/repo.svg?react";
import { useHomeStore } from "#/stores/home-store";
import { Typography } from "#/ui/typography";
export interface GitRepoDropdownProps {
provider: Provider;
@@ -47,7 +45,6 @@ export function GitRepoDropdown({
}: GitRepoDropdownProps) {
const { t } = useTranslation();
const { data: config } = useConfig();
const { recentRepositories: storedRecentRepositories } = useHomeStore();
const [inputValue, setInputValue] = useState("");
const [localSelectedItem, setLocalSelectedItem] =
useState<GitRepository | null>(null);
@@ -91,78 +88,37 @@ export function GitRepoDropdown({
repositoryName,
);
// Get recent repositories filtered by provider and input keyword
const recentRepositories = useMemo(() => {
const allRecentRepos = storedRecentRepositories;
const providerFilteredRepos = allRecentRepos.filter(
(repo) => repo.git_provider === provider,
);
// If no input value, return all recent repos for this provider
if (!inputValue || !inputValue.trim()) {
return providerFilteredRepos;
// Filter repositories based on input value
const filteredRepositories = useMemo(() => {
// If we have URL search results, show them directly (no filtering needed)
if (urlSearchResults.length > 0) {
return repositories;
}
// Filter by input keyword
// If we have a selected repository and the input matches it exactly, show all repositories
if (selectedRepository && inputValue === selectedRepository.full_name) {
return repositories;
}
// If no input value, show all repositories
if (!inputValue || !inputValue.trim()) {
return repositories;
}
// For URL inputs, use the processed search input for filtering
const filterText = inputValue.startsWith("https://")
? processedSearchInput
: inputValue;
return providerFilteredRepos.filter((repo) =>
return repositories.filter((repo) =>
repo.full_name.toLowerCase().includes(filterText.toLowerCase()),
);
}, [storedRecentRepositories, provider, inputValue, processedSearchInput]);
// Helper function to prioritize recent repositories at the top
const prioritizeRecentRepositories = useCallback(
(repoList: GitRepository[]) => {
const recentRepoIds = new Set(recentRepositories.map((repo) => repo.id));
const recentRepos = repoList.filter((repo) => recentRepoIds.has(repo.id));
const otherRepos = repoList.filter((repo) => !recentRepoIds.has(repo.id));
return [...recentRepos, ...otherRepos];
},
[recentRepositories],
);
// Filter repositories based on input value
const filteredRepositories = useMemo(() => {
let baseRepositories: GitRepository[];
// If we have URL search results, show them directly (no filtering needed)
if (urlSearchResults.length > 0) {
baseRepositories = repositories;
}
// If we have a selected repository and the input matches it exactly, show all repositories
else if (
selectedRepository &&
inputValue === selectedRepository.full_name
) {
baseRepositories = repositories;
}
// If no input value, show all repositories
else if (!inputValue || !inputValue.trim()) {
baseRepositories = repositories;
}
// For URL inputs, use the processed search input for filtering
else {
const filterText = inputValue.startsWith("https://")
? processedSearchInput
: inputValue;
baseRepositories = repositories.filter((repo) =>
repo.full_name.toLowerCase().includes(filterText.toLowerCase()),
);
}
// Prioritize recent repositories at the top
return prioritizeRecentRepositories(baseRepositories);
}, [
repositories,
inputValue,
selectedRepository,
urlSearchResults,
processedSearchInput,
prioritizeRecentRepositories,
]);
// Handle selection
@@ -284,6 +240,7 @@ export function GitRepoDropdown({
key={item.id}
item={item}
index={index}
isHighlighted={itemHighlightedIndex === index}
isSelected={itemSelectedItem?.id === item.id}
getItemProps={itemGetItemProps}
getDisplayText={(repo) => repo.full_name}
@@ -294,27 +251,12 @@ export function GitRepoDropdown({
const renderEmptyState = (emptyInputValue: string) => (
<EmptyState
inputValue={emptyInputValue}
searchMessage={t(I18nKey.HOME$NO_REPOSITORY_FOUND)}
searchMessage={t(I18nKey.MICROAGENT$NO_REPOSITORY_FOUND)}
emptyMessage={t(I18nKey.COMMON$NO_REPOSITORY)}
testId="git-repo-dropdown-empty"
/>
);
// Create sticky top item for recent repositories
const stickyTopItem = useMemo(() => {
if (recentRepositories.length === 0) {
return null;
}
return (
<div>
<Typography.Text className="text-xs text-[#FAFAFA] font-semibold leading-4 pl-2">
{t(I18nKey.COMMON$MOST_RECENT)}
</Typography.Text>
</div>
);
}, [recentRepositories, localSelectedItem, getItemProps, t]);
return (
<div className={cn("relative", className)}>
<div className="relative">
@@ -367,10 +309,8 @@ export function GitRepoDropdown({
menuRef={menuRef}
renderItem={renderItem}
renderEmptyState={renderEmptyState}
stickyTopItem={stickyTopItem}
stickyFooterItem={stickyFooterItem}
testId="git-repo-dropdown-menu"
numberOfRecentItems={recentRepositories.length}
/>
<ErrorMessage isError={isError} />

View File

@@ -13,7 +13,6 @@ import RepoForkedIcon from "#/icons/repo-forked.svg?react";
import { GitProviderDropdown } from "./git-provider-dropdown";
import { GitBranchDropdown } from "./git-branch-dropdown";
import { GitRepoDropdown } from "./git-repo-dropdown";
import { useHomeStore } from "#/stores/home-store";
interface RepositorySelectionFormProps {
onRepoSelection: (repo: GitRepository | null) => void;
@@ -35,7 +34,6 @@ export function RepositorySelectionForm({
React.useState<Provider | null>(null);
const { providers } = useUserProviders();
const { addRecentRepository } = useHomeStore();
const {
mutate: createConversation,
isPending,
@@ -170,12 +168,7 @@ export function RepositorySelectionForm({
(providers.length > 1 && !selectedProvider) ||
isLoadingSettings
}
onClick={() => {
// Persist the repository to recent repositories when launching
if (selectedRepository) {
addRecentRepository(selectedRepository);
}
onClick={() =>
createConversation(
{
repository: {
@@ -188,8 +181,8 @@ export function RepositorySelectionForm({
onSuccess: (data) =>
navigate(`/conversations/${data.conversation_id}`),
},
);
}}
)
}
className="w-full font-semibold"
>
{!isCreatingConversation && "Launch"}

View File

@@ -4,6 +4,7 @@ import { cn } from "#/utils/utils";
interface DropdownItemProps<T> {
item: T;
index: number;
isHighlighted: boolean;
isSelected: boolean;
getItemProps: <Options>(options: any & Options) => any; // eslint-disable-line @typescript-eslint/no-explicit-any
getDisplayText: (item: T) => string;
@@ -16,6 +17,7 @@ interface DropdownItemProps<T> {
export function DropdownItem<T>({
item,
index,
isHighlighted,
isSelected,
getItemProps,
getDisplayText,
@@ -33,6 +35,7 @@ export function DropdownItem<T>({
: "px-2 py-2 cursor-pointer text-sm rounded-md mx-0 my-0.5",
"text-white focus:outline-none font-normal",
{
"bg-[#5C5D62]": isHighlighted && !isSelected,
"bg-[#C9B974] text-black": isSelected,
"hover:bg-[#5C5D62]": !isSelected,
"hover:bg-[#C9B974] hover:text-black": isSelected,

View File

@@ -29,10 +29,8 @@ export interface GenericDropdownMenuProps<T> {
) => any, // eslint-disable-line @typescript-eslint/no-explicit-any
) => React.ReactNode;
renderEmptyState: (inputValue: string) => React.ReactNode;
stickyTopItem?: React.ReactNode;
stickyFooterItem?: React.ReactNode;
testId?: string;
numberOfRecentItems?: number;
}
export function GenericDropdownMenu<T>({
@@ -47,15 +45,13 @@ export function GenericDropdownMenu<T>({
menuRef,
renderItem,
renderEmptyState,
stickyTopItem,
stickyFooterItem,
testId,
numberOfRecentItems = 0,
}: GenericDropdownMenuProps<T>) {
if (!isOpen) return null;
const hasItems = filteredItems.length > 0;
const showEmptyState = !hasItems && !stickyTopItem && !stickyFooterItem;
const showEmptyState = !hasItems && !stickyFooterItem;
return (
<div className="relative">
@@ -63,7 +59,7 @@ export function GenericDropdownMenu<T>({
className={cn(
"absolute z-10 w-full bg-[#454545] border border-[#727987] rounded-lg shadow-none",
"focus:outline-none mt-1 z-[9999]",
stickyTopItem || stickyFooterItem ? "max-h-60" : "max-h-60",
stickyFooterItem ? "max-h-60" : "max-h-60",
)}
>
<ul
@@ -72,36 +68,23 @@ export function GenericDropdownMenu<T>({
ref: menuRef,
className: cn(
"w-full overflow-auto p-1",
stickyTopItem || stickyFooterItem
? "max-h-[calc(15rem-3rem)]"
: "max-h-60", // Reserve space for sticky items
stickyFooterItem ? "max-h-[calc(15rem-3rem)]" : "max-h-60", // Reserve space for sticky footer
),
onScroll,
"data-testid": testId,
})}
>
{showEmptyState ? (
renderEmptyState(inputValue)
) : (
<>
{stickyTopItem}
{filteredItems.map((item, index) => (
<>
{renderItem(
item,
index,
highlightedIndex,
selectedItem,
getItemProps,
)}
{numberOfRecentItems > 0 &&
index === numberOfRecentItems - 1 && (
<div className="border-b border-[#727987] bg-[#454545] pb-1 mb-1 h-[1px]" />
)}
</>
))}
</>
)}
{showEmptyState
? renderEmptyState(inputValue)
: filteredItems.map((item, index) =>
renderItem(
item,
index,
highlightedIndex,
selectedItem,
getItemProps,
),
)}
</ul>
{stickyFooterItem && (
<div className="border-t border-[#727987] bg-[#454545] p-1 rounded-b-lg">

View File

@@ -4,7 +4,7 @@ import { SuggestedTask } from "#/utils/types";
import { useIsCreatingConversation } from "#/hooks/use-is-creating-conversation";
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
import { TaskIssueNumber } from "./task-issue-number";
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
import { cn } from "#/utils/utils";
const getTaskTypeMap = (
@@ -21,7 +21,7 @@ interface TaskCardProps {
}
export function TaskCard({ task }: TaskCardProps) {
const { setOptimisticUserMessage } = useOptimisticUserMessageStore();
const { setOptimisticUserMessage } = useOptimisticUserMessage();
const { mutate: createConversation } = useCreateConversation();
const isCreatingConversation = useIsCreatingConversation();
const { t } = useTranslation();

View File

@@ -9,12 +9,7 @@ import { GitProviderDropdown } from "#/components/features/home/git-provider-dro
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
import { GitRepository } from "#/types/git";
import { Provider } from "#/types/settings";
import {
cn,
shouldIncludeRepository,
getOpenHandsQuery,
hasOpenHandsSuffix,
} from "#/utils/utils";
import { cn } from "#/utils/utils";
import { sanitizeQuery } from "#/utils/sanitize-query";
import { I18nKey } from "#/i18n/declaration";
import { useDebounce } from "#/hooks/use-debounce";
@@ -60,16 +55,6 @@ export function MicroagentManagementSidebar({
const { data: searchResults, isLoading: isSearchLoading } =
useSearchRepositories(debouncedSearchQuery, selectedProvider, false, 500); // Increase page size to 500 to to retrieve all search results. This should be optimized in the future.
const {
data: userAndOrgLevelRepositorySearchResults,
isLoading: isUserAndOrgLevelRepositoryLoading,
} = useSearchRepositories(
getOpenHandsQuery(selectedProvider),
selectedProvider,
false,
500,
);
// Auto-select provider if there's only one
useEffect(() => {
if (providers.length > 0 && !selectedProvider) {
@@ -82,27 +67,11 @@ export function MicroagentManagementSidebar({
setSearchQuery("");
};
// Helper function to filter repositories by search query
const filterRepositoriesByQuery = (
inputRepositories: GitRepository[],
query: string,
): GitRepository[] => {
if (!query.trim()) {
return inputRepositories;
}
const sanitizedQuery = sanitizeQuery(query);
return inputRepositories.filter((repository: GitRepository) => {
const sanitizedRepoName = sanitizeQuery(repository.full_name);
return sanitizedRepoName.includes(sanitizedQuery);
});
};
// Filter repositories based on search query and available data
const filteredRepositories = useMemo(() => {
// If we have search results, apply client-side filtering for exact matches
// If we have search results, use them directly (no filtering needed)
if (debouncedSearchQuery && searchResults && searchResults.length > 0) {
return filterRepositoriesByQuery(searchResults, debouncedSearchQuery);
return searchResults;
}
// If no search query or no search results, use paginated repositories
@@ -111,65 +80,56 @@ export function MicroagentManagementSidebar({
// Flatten all pages to get all repositories
const allRepositories = repositories.pages.flatMap((page) => page.data);
// Apply filtering to paginated repositories
return filterRepositoriesByQuery(allRepositories, debouncedSearchQuery);
// If no search query, return all repositories
if (!debouncedSearchQuery.trim()) {
return allRepositories;
}
// Fallback to client-side filtering if search didn't return results
const sanitizedQuery = sanitizeQuery(debouncedSearchQuery);
return allRepositories.filter((repository: GitRepository) => {
const sanitizedRepoName = sanitizeQuery(repository.full_name);
return sanitizedRepoName.includes(sanitizedQuery);
});
}, [repositories, debouncedSearchQuery, searchResults]);
// Process personal and organization repositories from search results
useEffect(() => {
if (!userAndOrgLevelRepositorySearchResults?.length) {
if (!filteredRepositories?.length) {
setPersonalRepositories([]);
setOrganizationRepositories([]);
setRepositories([]);
return;
}
const personalRepos: GitRepository[] = [];
const organizationRepos: GitRepository[] = [];
// Process personal repositories with exact match filtering
if (userAndOrgLevelRepositorySearchResults?.length) {
userAndOrgLevelRepositorySearchResults.forEach((repo: GitRepository) => {
if (
hasOpenHandsSuffix(repo, selectedProvider) &&
shouldIncludeRepository(repo, debouncedSearchQuery)
) {
if (repo.owner_type === "user") {
personalRepos.push(repo);
} else if (repo.owner_type === "organization") {
organizationRepos.push(repo);
}
}
});
}
setPersonalRepositories(personalRepos);
setOrganizationRepositories(organizationRepos);
}, [
userAndOrgLevelRepositorySearchResults,
selectedProvider,
debouncedSearchQuery,
setPersonalRepositories,
setOrganizationRepositories,
]);
// Process other repositories (non-OpenHands repositories) from filteredRepositories
useEffect(() => {
if (!filteredRepositories?.length) {
setRepositories([]);
return;
}
const otherRepos: GitRepository[] = [];
filteredRepositories.forEach((repo: GitRepository) => {
// Only include repositories that don't have the OpenHands suffix
if (!hasOpenHandsSuffix(repo, selectedProvider)) {
const hasOpenHandsSuffix =
selectedProvider === "gitlab"
? repo.full_name.endsWith("/openhands-config")
: repo.full_name.endsWith("/.openhands");
if (repo.owner_type === "user" && hasOpenHandsSuffix) {
personalRepos.push(repo);
} else if (repo.owner_type === "organization" && hasOpenHandsSuffix) {
organizationRepos.push(repo);
} else {
otherRepos.push(repo);
}
});
setPersonalRepositories(personalRepos);
setOrganizationRepositories(organizationRepos);
setRepositories(otherRepos);
}, [filteredRepositories, selectedProvider, setRepositories]);
}, [
filteredRepositories,
selectedProvider,
setPersonalRepositories,
setOrganizationRepositories,
setRepositories,
]);
// Handle scroll to bottom for pagination
const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
@@ -239,7 +199,7 @@ export function MicroagentManagementSidebar({
</div>
</div>
{isLoading || isUserAndOrgLevelRepositoryLoading ? (
{isLoading ? (
<div className="flex flex-col items-center justify-center gap-4 flex-1">
<Spinner size="sm" />
<span className="text-sm text-white">

View File

@@ -51,7 +51,7 @@ export function PaymentForm() {
<form
action={billingFormAction}
data-testid="billing-settings"
className="flex flex-col gap-6"
className="flex flex-col gap-6 px-11 py-9"
>
<div
className={cn(

View File

@@ -1,6 +1,3 @@
export { UpgradeBanner } from "./upgrade-banner";
export { UpgradeButton } from "./upgrade-button";
export { BannerMessage } from "./banner-message";
export { MobileHeader } from "./mobile-header";
export { SettingsNavigation } from "./settings-navigation";
export { SettingsLayout } from "./settings-layout";

View File

@@ -1,55 +0,0 @@
import { useTranslation } from "react-i18next";
import SettingsIcon from "#/icons/settings-gear.svg?react";
import { Typography } from "#/ui/typography";
import { I18nKey } from "#/i18n/declaration";
interface MobileHeaderProps {
isMobileMenuOpen: boolean;
onToggleMenu: () => void;
}
export function MobileHeader({
isMobileMenuOpen,
onToggleMenu,
}: MobileHeaderProps) {
const { t } = useTranslation();
return (
<div className="flex items-center justify-between mb-4 md:hidden">
<div className="flex items-center gap-2">
<SettingsIcon width={16} height={16} />
<Typography.H2>{t(I18nKey.SETTINGS$TITLE)}</Typography.H2>
</div>
<button
type="button"
onClick={onToggleMenu}
className="p-2 rounded-md bg-tertiary hover:bg-[#454545] transition-colors"
aria-label="Toggle settings menu"
>
<svg
width={20}
height={20}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
{isMobileMenuOpen ? (
<>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</>
) : (
<>
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</>
)}
</svg>
</button>
</div>
);
}

View File

@@ -1,55 +0,0 @@
import { useState } from "react";
import { MobileHeader } from "./mobile-header";
import { SettingsNavigation } from "./settings-navigation";
interface NavigationItem {
to: string;
icon: React.ReactNode;
text: string;
}
interface SettingsLayoutProps {
children: React.ReactNode;
navigationItems: NavigationItem[];
isSaas: boolean;
}
export function SettingsLayout({
children,
navigationItems,
isSaas,
}: SettingsLayoutProps) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const toggleMobileMenu = () => {
setIsMobileMenuOpen(!isMobileMenuOpen);
};
const closeMobileMenu = () => {
setIsMobileMenuOpen(false);
};
return (
<div className="flex flex-col h-full px-[14px] pt-8">
{/* Mobile header */}
<MobileHeader
isMobileMenuOpen={isMobileMenuOpen}
onToggleMenu={toggleMobileMenu}
/>
{/* Desktop layout with navigation and main content */}
<div className="flex flex-1 overflow-hidden gap-10">
{/* Navigation */}
<SettingsNavigation
isMobileMenuOpen={isMobileMenuOpen}
onCloseMobileMenu={closeMobileMenu}
navigationItems={navigationItems}
isSaas={isSaas}
/>
{/* Main content */}
<main className="flex-1 overflow-auto">{children}</main>
</div>
</div>
);
}

View File

@@ -1,96 +0,0 @@
import { useTranslation } from "react-i18next";
import { NavLink } from "react-router";
import { cn } from "#/utils/utils";
import { Typography } from "#/ui/typography";
import { I18nKey } from "#/i18n/declaration";
import SettingsIcon from "#/icons/settings-gear.svg?react";
import CloseIcon from "#/icons/close.svg?react";
import { ProPill } from "./pro-pill";
interface NavigationItem {
to: string;
icon: React.ReactNode;
text: string;
}
interface SettingsNavigationProps {
isMobileMenuOpen: boolean;
onCloseMobileMenu: () => void;
navigationItems: NavigationItem[];
isSaas: boolean;
}
export function SettingsNavigation({
isMobileMenuOpen,
onCloseMobileMenu,
navigationItems,
isSaas,
}: SettingsNavigationProps) {
const { t } = useTranslation();
return (
<>
{/* Mobile backdrop */}
{isMobileMenuOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden"
onClick={onCloseMobileMenu}
/>
)}
{/* Navigation sidebar */}
<nav
data-testid="settings-navbar"
className={cn(
"flex flex-col gap-6 transition-transform duration-300 ease-in-out",
// Mobile: full screen overlay
"fixed inset-0 z-50 w-full bg-base-secondary p-4 transform md:transform-none",
isMobileMenuOpen ? "translate-x-0" : "-translate-x-full",
// Desktop: static sidebar
"md:relative md:translate-x-0 md:w-64 md:p-0 md:bg-transparent",
)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 ml-1 sm:ml-4.5">
<SettingsIcon width={16} height={16} />
<Typography.H2>{t(I18nKey.SETTINGS$TITLE)}</Typography.H2>
</div>
{/* Close button - only visible on mobile */}
<button
type="button"
onClick={onCloseMobileMenu}
className="md:hidden p-0.5 hover:bg-[#454545] rounded-md transition-colors"
aria-label="Close navigation menu"
>
<CloseIcon width={32} height={32} />
</button>
</div>
<div className="flex flex-col gap-2">
{navigationItems.map(({ to, icon, text }) => (
<NavLink
end
key={to}
to={to}
onClick={onCloseMobileMenu}
className={({ isActive }) =>
cn(
"flex items-center gap-3 p-1 sm:px-[14px] sm:py-2 rounded-md transition-colors",
isActive ? "bg-[#454545]" : "hover:bg-[#454545]",
)
}
>
{icon}
<div className="flex items-center gap-1.5 min-w-0 flex-1">
<Typography.Text className="text-[#A3A3A3] whitespace-nowrap">
{t(text as I18nKey)}
</Typography.Text>
{isSaas && to === "/settings" && <ProPill />}
</div>
</NavLink>
))}
</div>
</nav>
</>
);
}

View File

@@ -1,3 +1,4 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { UpgradeBanner } from "#/components/features/settings";
@@ -17,7 +18,7 @@ export function UpgradeBannerWithBackdrop({
<UpgradeBanner
message={t("SETTINGS$UPGRADE_BANNER_MESSAGE")}
onUpgradeClick={onUpgradeClick}
className="sticky top-0 z-30 mb-6"
className="sticky top-0 z-30"
isDisabled={isDisabled}
/>
<div

View File

@@ -18,7 +18,7 @@ export function UpgradeBanner({
return (
<div
className={cn(
"bg-primary text-base flex items-center justify-center gap-3 p-2 w-full rounded",
"bg-primary text-base flex items-center justify-center gap-3 p-2 w-full",
className,
)}
data-testid="upgrade-banner"

View File

@@ -1,84 +0,0 @@
import CreditCardIcon from "#/icons/credit-card.svg?react";
import KeyIcon from "#/icons/key.svg?react";
import ServerProcessIcon from "#/icons/server-process.svg?react";
import SettingsGearIcon from "#/icons/settings-gear.svg?react";
import CircuitIcon from "#/icons/u-circuit.svg?react";
import PuzzlePieceIcon from "#/icons/u-puzzle-piece.svg?react";
import UserIcon from "#/icons/user.svg?react";
export interface SettingsNavItem {
icon: React.ReactElement;
to: string;
text: string;
}
export const SAAS_NAV_ITEMS: SettingsNavItem[] = [
{
icon: <UserIcon width={22} height={22} />,
to: "/settings/user",
text: "SETTINGS$NAV_USER",
},
{
icon: <PuzzlePieceIcon width={22} height={22} />,
to: "/settings/integrations",
text: "SETTINGS$NAV_INTEGRATIONS",
},
{
icon: <SettingsGearIcon width={22} height={22} />,
to: "/settings/app",
text: "SETTINGS$NAV_APPLICATION",
},
{
icon: <CircuitIcon width={22} height={22} />,
to: "/settings",
text: "COMMON$LANGUAGE_MODEL_LLM",
},
{
icon: <CreditCardIcon width={22} height={22} />,
to: "/settings/billing",
text: "SETTINGS$NAV_BILLING",
},
{
icon: <KeyIcon width={22} height={22} />,
to: "/settings/secrets",
text: "SETTINGS$NAV_SECRETS",
},
{
icon: <KeyIcon width={22} height={22} />,
to: "/settings/api-keys",
text: "SETTINGS$NAV_API_KEYS",
},
{
icon: <ServerProcessIcon width={22} height={22} />,
to: "/settings/mcp",
text: "SETTINGS$NAV_MCP",
},
];
export const OSS_NAV_ITEMS: SettingsNavItem[] = [
{
icon: <CircuitIcon width={22} height={22} />,
to: "/settings",
text: "SETTINGS$NAV_LLM",
},
{
icon: <ServerProcessIcon width={22} height={22} />,
to: "/settings/mcp",
text: "SETTINGS$NAV_MCP",
},
{
icon: <PuzzlePieceIcon width={22} height={22} />,
to: "/settings/integrations",
text: "SETTINGS$NAV_INTEGRATIONS",
},
{
icon: <SettingsGearIcon width={22} height={22} />,
to: "/settings/app",
text: "SETTINGS$NAV_APPLICATION",
},
{
icon: <KeyIcon width={22} height={22} />,
to: "/settings/secrets",
text: "SETTINGS$NAV_SECRETS",
},
];

View File

@@ -26,8 +26,8 @@ import {
isStatusUpdate,
isUserMessage,
} from "#/types/core/guards";
import { useErrorMessageStore } from "#/stores/error-message-store";
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
import { useWSErrorMessage } from "#/hooks/use-ws-error-message";
export type WebSocketStatus = "CONNECTING" | "CONNECTED" | "DISCONNECTED";
@@ -131,8 +131,8 @@ export function WsClientProvider({
conversationId,
children,
}: React.PropsWithChildren<WsClientProviderProps>) {
const { setErrorMessage, removeErrorMessage } = useErrorMessageStore();
const { removeOptimisticUserMessage } = useOptimisticUserMessageStore();
const { removeOptimisticUserMessage } = useOptimisticUserMessage();
const { setErrorMessage, removeErrorMessage } = useWSErrorMessage();
const queryClient = useQueryClient();
const sioRef = React.useRef<Socket | null>(null);
const [webSocketStatus, setWebSocketStatus] =

View File

@@ -12,7 +12,6 @@ export const useChatSubmission = (
fileInputRef: React.RefObject<HTMLInputElement | null>,
smartResize: () => void,
onSubmit: (message: string) => void,
resetManualResize?: () => void,
) => {
// Send button click handler
const handleSubmit = useCallback(() => {
@@ -31,10 +30,7 @@ export const useChatSubmission = (
// Reset height and show suggestions again
smartResize();
// Reset manual resize state for next message
resetManualResize?.();
}, [chatInputRef, fileInputRef, smartResize, onSubmit, resetManualResize]);
}, [chatInputRef, fileInputRef, smartResize, onSubmit]);
// Resume agent button click handler
const handleResumeAgent = useCallback(() => {
@@ -48,10 +44,7 @@ export const useChatSubmission = (
// Reset height and show suggestions again
smartResize();
// Reset manual resize state for next message
resetManualResize?.();
}, [chatInputRef, fileInputRef, smartResize, onSubmit, resetManualResize]);
}, [chatInputRef, fileInputRef, smartResize, onSubmit]);
// Handle stop button click
const handleStop = useCallback((onStop?: () => void) => {

View File

@@ -58,7 +58,6 @@ export const useGripResize = (
handleGripMouseDown,
handleGripTouchStart,
increaseHeightForEmptyContent,
resetManualResize,
} = useAutoResize(chatInputRef as React.RefObject<HTMLElement | null>, {
minHeight: 20,
maxHeight: 400,
@@ -77,6 +76,5 @@ export const useGripResize = (
handleGripMouseDown,
handleGripTouchStart,
increaseHeightForEmptyContent,
resetManualResize,
};
};

View File

@@ -6,7 +6,7 @@ export function useSearchRepositories(
query: string,
selectedProvider?: Provider | null,
disabled?: boolean,
pageSize: number = 100,
pageSize: number = 3,
) {
return useQuery({
queryKey: ["repositories", "search", query, selectedProvider, pageSize],

View File

@@ -1,7 +1,5 @@
import { useCallback, useEffect, RefObject, useRef } from "react";
import { useCallback, useEffect, RefObject } from "react";
import { IMessageToSend } from "#/state/conversation-store";
import { EPS } from "#/utils/constants";
import { getStyleHeightPx, setStyleHeightPx } from "#/utils/utils";
import { useDragResize } from "./use-drag-resize";
// Constants
@@ -10,13 +8,6 @@ const DEFAULT_MAX_HEIGHT = 120;
const HEIGHT_INCREMENT = 20;
const MANUAL_OVERSIZE_THRESHOLD = 50;
// Manual height tracking utilities
const useManualHeight = () => {
const hasUserResizedRef = useRef(false);
const manualHeightRef = useRef<number | null>(null);
return { hasUserResizedRef, manualHeightRef };
};
interface UseAutoResizeOptions {
minHeight?: number;
maxHeight?: number;
@@ -28,11 +19,11 @@ interface UseAutoResizeOptions {
}
interface UseAutoResizeReturn {
autoResize: () => void;
smartResize: () => void;
handleGripMouseDown: (e: React.MouseEvent) => void;
handleGripTouchStart: (e: React.TouchEvent) => void;
increaseHeightForEmptyContent: () => void;
resetManualResize: () => void;
}
// Height management utilities
@@ -69,6 +60,27 @@ const applyHeightToElement = (
return finalHeight;
};
const calculateOptimalHeight = (
element: HTMLElement,
constraints: HeightConstraints,
): number => {
const { minHeight, maxHeight, scrollHeight } = {
...constraints,
scrollHeight: element.scrollHeight,
};
if (scrollHeight <= maxHeight) {
return Math.max(scrollHeight, minHeight);
}
return maxHeight;
};
const getCurrentElementHeight = (
element: HTMLElement,
minHeight: number,
): number =>
element.offsetHeight || parseInt(element.style.height || `${minHeight}`, 10);
const isManuallyOversized = (
currentHeight: number,
contentHeight: number,
@@ -79,16 +91,18 @@ const measureElementHeights = (
element: HTMLElement,
minHeight: number,
): HeightMeasurements => {
// Use the previous explicit style height as the "current" for restore, not offsetHeight
const currentStyleHeight = getStyleHeightPx(element, minHeight);
const currentHeight = currentStyleHeight;
const currentHeight = getCurrentElementHeight(element, minHeight);
const currentStyleHeight = parseInt(
element.style.height || `${minHeight}`,
10,
);
// Temporarily reset to measure content
element.style.setProperty("height", "auto");
const contentHeight = element.scrollHeight;
// Restore height
setStyleHeightPx(element, currentStyleHeight);
element.style.setProperty("height", `${currentStyleHeight}px`);
return {
currentHeight,
@@ -97,6 +111,44 @@ const measureElementHeights = (
};
};
const determineResizeStrategy = (
measurements: HeightMeasurements,
minHeight: number,
maxHeight: number,
): ResizeStrategy => {
const { currentHeight, contentHeight } = measurements;
// If content fits in current height, just manage overflow
if (contentHeight <= currentHeight) {
return {
finalHeight: currentHeight,
overflowY: "hidden",
};
}
// If content exceeds current height but is within normal auto-resize range
if (contentHeight <= maxHeight) {
// Only grow if the current height is close to the content height (not manually resized much larger)
if (!isManuallyOversized(currentHeight, contentHeight)) {
return {
finalHeight: Math.max(contentHeight, minHeight),
overflowY: "hidden",
};
}
// Keep manual height but show scrollbar since content exceeds visible area
return {
finalHeight: currentHeight,
overflowY: "auto",
};
}
// Content exceeds max height
return {
finalHeight: maxHeight,
overflowY: "auto",
};
};
const applyResizeStrategy = (
element: HTMLElement,
strategy: ResizeStrategy,
@@ -117,12 +169,15 @@ const executeHeightCallback = (
};
// DOM manipulation utilities
const resetElementHeight = (element: HTMLElement): void => {
element.style.setProperty("height", "auto");
element.style.setProperty("overflow-y", "hidden");
};
export const useAutoResize = (
elementRef: RefObject<HTMLElement | null>,
options: UseAutoResizeOptions = {},
): UseAutoResizeReturn => {
const pendingSmartRef = useRef<number | null>(null);
const {
minHeight = DEFAULT_MIN_HEIGHT,
maxHeight = DEFAULT_MAX_HEIGHT,
@@ -134,184 +189,65 @@ export const useAutoResize = (
} = options;
const constraints: HeightConstraints = { minHeight, maxHeight };
const { hasUserResizedRef, manualHeightRef } = useManualHeight();
const resetManualResize = () => {
hasUserResizedRef.current = false;
manualHeightRef.current = null;
};
// Wrap onHeightChange to track manual height during drag
const handleExternalHeightChange = useCallback(
(elementHeight: number) => {
onHeightChange?.(elementHeight);
if (hasUserResizedRef.current) {
manualHeightRef.current = elementHeight;
}
},
[onHeightChange],
);
// Handle drag start - set manual mode flag
const handleDragStart = useCallback(() => {
hasUserResizedRef.current = true;
onGripDragStart?.();
}, [onGripDragStart]);
// Handle drag end - clear manual mode if at minimum height
const handleDragEnd = useCallback(() => {
const textareaElement = elementRef.current;
if (textareaElement) {
const currentHeight = getStyleHeightPx(textareaElement, minHeight);
if (Math.abs(currentHeight - minHeight) <= EPS) {
hasUserResizedRef.current = false;
manualHeightRef.current = null;
}
}
onGripDragEnd?.();
}, [minHeight, onGripDragEnd]);
// Use the drag resize hook for manual resizing functionality
const { handleGripMouseDown, handleGripTouchStart } = useDragResize({
elementRef,
minHeight,
maxHeight,
onGripDragStart: enableManualResize ? handleDragStart : undefined,
onGripDragEnd: enableManualResize ? handleDragEnd : undefined,
onHeightChange: handleExternalHeightChange,
onGripDragStart: enableManualResize ? onGripDragStart : undefined,
onGripDragEnd: enableManualResize ? onGripDragEnd : undefined,
onHeightChange,
});
// Handle content that fits within current height
const handleContentFitsInCurrentHeight = useCallback(
(
element: HTMLElement,
currentHeight: number,
contentHeight: number,
): void => {
// If user manually resized and we're above min height, preserve their chosen height
if (hasUserResizedRef.current && currentHeight > minHeight) {
applyResizeStrategy(element, {
finalHeight: currentHeight,
overflowY: "hidden",
});
executeHeightCallback(currentHeight, onHeightChange);
return;
}
// Otherwise allow shrinking towards content (respect minHeight)
const finalHeight = Math.max(contentHeight, minHeight);
applyResizeStrategy(element, {
finalHeight,
overflowY: "hidden",
});
executeHeightCallback(finalHeight, onHeightChange);
},
[minHeight, onHeightChange],
);
// Handle content that exceeds current height but within max height
const handleContentExceedsCurrentHeight = useCallback(
(
element: HTMLElement,
currentHeight: number,
contentHeight: number,
): void => {
// Grow unless the element is manually oversized beyond content significantly
if (!isManuallyOversized(currentHeight, contentHeight)) {
const finalHeight = Math.max(contentHeight, minHeight);
applyResizeStrategy(element, {
finalHeight,
overflowY: "hidden",
});
executeHeightCallback(finalHeight, onHeightChange);
return;
}
// Keep manual height and allow scrolling as needed
applyResizeStrategy(element, {
finalHeight: currentHeight,
overflowY: "auto",
});
executeHeightCallback(currentHeight, onHeightChange);
},
[minHeight, onHeightChange],
);
// Handle content that exceeds max height
const handleContentExceedsMaxHeight = useCallback(
(element: HTMLElement) => {
applyResizeStrategy(element, {
finalHeight: maxHeight,
overflowY: "auto",
});
executeHeightCallback(maxHeight, onHeightChange);
},
[maxHeight, onHeightChange],
);
// Debounced smartResize body
const smartResizeBody = useCallback(() => {
// Auto-resize functionality for contenteditable div
const autoResize = () => {
const element = elementRef.current;
if (!element) return;
const textIsEmpty = (element.textContent ?? "").trim().length === 0;
// Reset height to auto to get the actual content height
resetElementHeight(element);
// If empty content and we have a manual height above min, preserve it
if (
textIsEmpty &&
hasUserResizedRef.current &&
manualHeightRef.current &&
manualHeightRef.current > minHeight + EPS
) {
setStyleHeightPx(element, manualHeightRef.current);
element.style.overflowY = "hidden";
executeHeightCallback(manualHeightRef.current, onHeightChange);
return;
}
// Calculate and apply optimal height
const optimalHeight = calculateOptimalHeight(element, constraints);
const finalHeight = applyHeightToElement(
element,
optimalHeight,
constraints,
);
// Execute height change callback
executeHeightCallback(finalHeight, onHeightChange);
};
// Smart resize that respects manual height
const smartResize = useCallback(() => {
const element = elementRef.current;
if (!element) return;
// Measure element heights
const measurements = measureElementHeights(element, minHeight);
const { currentHeight, contentHeight } = measurements;
// If content fits within current height
if (contentHeight <= currentHeight) {
handleContentFitsInCurrentHeight(element, currentHeight, contentHeight);
return;
}
// Determine the best resize strategy
const strategy = determineResizeStrategy(
measurements,
minHeight,
maxHeight,
);
// If content exceeds current height but within max
if (contentHeight <= maxHeight) {
handleContentExceedsCurrentHeight(element, currentHeight, contentHeight);
return;
}
// Apply the resize strategy
applyResizeStrategy(element, strategy);
// Content exceeds max height
handleContentExceedsMaxHeight(element);
}, [
elementRef,
minHeight,
maxHeight,
onHeightChange,
handleContentFitsInCurrentHeight,
handleContentExceedsCurrentHeight,
handleContentExceedsMaxHeight,
]);
// rAF-debounced smartResize wrapper to collapse bursts
const smartResize = useCallback(() => {
if (pendingSmartRef.current) cancelAnimationFrame(pendingSmartRef.current);
pendingSmartRef.current = requestAnimationFrame(() => {
pendingSmartRef.current = null;
smartResizeBody();
});
}, [smartResizeBody]);
// Execute height change callback
executeHeightCallback(strategy.finalHeight, onHeightChange);
}, [elementRef, minHeight, maxHeight, onHeightChange]);
// Function to increase height when content is empty
const increaseHeightForEmptyContent = () => {
const element = elementRef.current;
if (!element) return;
const currentHeight = getStyleHeightPx(element, minHeight);
const currentHeight = element.offsetHeight;
const newHeight = Math.min(currentHeight + HEIGHT_INCREMENT, maxHeight);
if (newHeight > currentHeight) {
@@ -319,10 +255,6 @@ export const useAutoResize = (
// Execute height change callback
executeHeightCallback(finalHeight, onHeightChange);
// Set manual mode for Shift+Enter height increases
hasUserResizedRef.current = true;
manualHeightRef.current = finalHeight;
}
};
@@ -341,10 +273,10 @@ export const useAutoResize = (
}, [smartResize]);
return {
autoResize,
smartResize,
handleGripMouseDown,
handleGripTouchStart,
increaseHeightForEmptyContent,
resetManualResize,
};
};

View File

@@ -1,5 +1,4 @@
import { RefObject } from "react";
import { EPS } from "#/utils/constants";
import { isMobileDevice } from "#/utils/utils";
// Drag handling hook
@@ -10,7 +9,6 @@ interface UseDragResizeOptions {
onGripDragStart?: () => void;
onGripDragEnd?: () => void;
onHeightChange?: (height: number) => void;
onReachedMinHeight?: () => void; // Notify when user drags to minimum height
}
export const useDragResize = ({
@@ -20,7 +18,6 @@ export const useDragResize = ({
onGripDragStart,
onGripDragEnd,
onHeightChange,
onReachedMinHeight,
}: UseDragResizeOptions) => {
const getClientY = (event: MouseEvent | TouchEvent): number => {
if ("touches" in event && event.touches.length > 0) {
@@ -59,11 +56,6 @@ export const useDragResize = ({
if (onHeightChange) {
onHeightChange(newHeight);
}
// Notify when dragged to minimum height to clear manual mode
if (onReachedMinHeight && Math.abs(newHeight - minHeight) <= EPS) {
onReachedMinHeight();
}
};
return handleDragMove;
};

View File

@@ -0,0 +1,23 @@
import { useQueryClient } from "@tanstack/react-query";
export const useOptimisticUserMessage = () => {
const queryKey = ["optimistic_user_message"] as const;
const queryClient = useQueryClient();
const setOptimisticUserMessage = (message: string) => {
queryClient.setQueryData<string>(queryKey, message);
};
const getOptimisticUserMessage = () =>
queryClient.getQueryData<string>(queryKey);
const removeOptimisticUserMessage = () => {
queryClient.removeQueries({ queryKey });
};
return {
setOptimisticUserMessage,
getOptimisticUserMessage,
removeOptimisticUserMessage,
};
};

View File

@@ -1,129 +0,0 @@
import React from "react";
import { useConfig } from "./query/use-config";
import { useGitUser } from "./query/use-git-user";
import { getLoginMethod, LoginMethod } from "#/utils/local-storage";
import reoService, { ReoIdentity } from "#/utils/reo";
/**
* Maps login method to Reo identity type
*/
const mapLoginMethodToReoType = (method: LoginMethod): ReoIdentity["type"] => {
// Reo is not supporting gitlab and bitbucket.
switch (method) {
case LoginMethod.GITHUB:
return "github";
case LoginMethod.ENTERPRISE_SSO:
return "email";
default:
return "email";
}
};
/**
* Creates email identity object if email is available
*/
const buildEmailIdentity = (
email?: string | null,
): ReoIdentity["other_identities"] => {
if (!email) {
return undefined;
}
return [
{
username: email,
type: "email",
},
];
};
/**
* Parses full name into firstname and lastname
* Handles cases where name might be empty or only have one part
*/
const parseNameFields = (
fullName?: string | null,
): { firstname?: string; lastname?: string } => {
if (!fullName) {
return {};
}
const [firstname, ...rest] = fullName.split(" ");
if (!firstname) {
return {};
}
return {
firstname,
lastname: rest.length > 0 ? rest.join(" ") : undefined,
};
};
/**
* Builds complete Reo identity from user data and login method
*/
const buildReoIdentity = (
user: {
login: string;
email?: string | null;
name?: string | null;
company?: string | null;
},
loginMethod: LoginMethod,
): ReoIdentity => {
const { firstname, lastname } = parseNameFields(user.name);
return {
username: user.login,
type: mapLoginMethodToReoType(loginMethod),
other_identities: buildEmailIdentity(user.email),
firstname,
lastname,
company: user.company || undefined,
};
};
/**
* Hook to handle Reo.dev tracking integration
* Only active in SaaS mode
*/
export const useReoTracking = () => {
const { data: config } = useConfig();
const { data: user } = useGitUser();
const [hasIdentified, setHasIdentified] = React.useState(false);
// Initialize Reo.dev when in SaaS mode
React.useEffect(() => {
const initReo = async () => {
if (config?.APP_MODE === "saas" && !reoService.isInitialized()) {
await reoService.init();
}
};
initReo();
}, [config?.APP_MODE]);
// Identify user when user data is available and we're in SaaS mode
React.useEffect(() => {
if (
config?.APP_MODE !== "saas" ||
!user ||
hasIdentified ||
!reoService.isInitialized()
) {
return;
}
const loginMethod = getLoginMethod();
if (!loginMethod) {
return;
}
// Build identity payload from user data
const identity = buildReoIdentity(user, loginMethod);
// Identify user in Reo
reoService.identify(identity);
setHasIdentified(true);
}, [config?.APP_MODE, user, hasIdentified]);
};

View File

@@ -0,0 +1,22 @@
import { useQueryClient } from "@tanstack/react-query";
export const useWSErrorMessage = () => {
const queryClient = useQueryClient();
const setErrorMessage = (message: string) => {
queryClient.setQueryData<string>(["error_message"], message);
};
const getErrorMessage = () =>
queryClient.getQueryData<string>(["error_message"]);
const removeErrorMessage = () => {
queryClient.removeQueries({ queryKey: ["error_message"] });
};
return {
setErrorMessage,
getErrorMessage,
removeErrorMessage,
};
};

View File

@@ -916,6 +916,4 @@ export enum I18nKey {
COMMON$START_RUNTIME = "COMMON$START_RUNTIME",
COMMON$JUPYTER_EMPTY_MESSAGE = "COMMON$JUPYTER_EMPTY_MESSAGE",
COMMON$CONFIRMATION_MODE_ENABLED = "COMMON$CONFIRMATION_MODE_ENABLED",
COMMON$MOST_RECENT = "COMMON$MOST_RECENT",
HOME$NO_REPOSITORY_FOUND = "HOME$NO_REPOSITORY_FOUND",
}

View File

@@ -9728,20 +9728,20 @@
"uk": "або перегляньте"
},
"AGENT_ERROR$ERROR_ACTION_NOT_EXECUTED_STOPPED": {
"en": "Pause button pressed. Agent is stopped. The action has not been executed.",
"ja": "一時停止ボタンが押されました。エージェントは停止しています。アクションは実行されていません。",
"zh-CN": "按下暂停按钮。代理已停止。该操作尚未执行。",
"zh-TW": "按下暫停按鈕。代理已停止。該操作尚未執行。",
"ko-KR": "일시정지 버튼이 눌렸습니다. 에이전트가 중지되었습니다. 작업이 실행되지 않았습니다.",
"no": "Pauseknappen ble trykket. Agenten er stoppet. Handlingen har ikke blitt utført.",
"it": "Pulsante di pausa premuto. L'agente è fermo. L'azione non è stata eseguita.",
"pt": "Botão de pausa pressionado. O agente está parado. A ação não foi executada.",
"es": "Se presionó el botón de pausa. El agente está detenido. La acción no se ha ejecutado.",
"ar": "تم الضغط على زر الإيقاف المؤقت. تم إيقاف الوكيل. لم يتم تنفيذ الإجراء.",
"fr": "Bouton pause enfoncé. L'agent est arrêté. L'action n'a pas été exécutée.",
"tr": "Duraklat düğmesine basıldı. Ajan durduruldu. Eylem gerçekleştirilmedi.",
"de": "Pausentaste gedrückt. Agent ist gestoppt. Die Aktion wurde nicht ausgeführt.",
"uk": "Натиснуто кнопку паузи. Агент зупинений. Дію не виконано."
"en": "Stop button pressed. The action has not been executed.",
"ja": "停止ボタンが押されました。アクションは実行されていません。",
"zh-CN": "按下了停止按钮。操作尚未执行。",
"zh-TW": "按下了停止按鈕。操作尚未執行。",
"ko-KR": "지 버튼이 눌렸습니다. 작업이 실행되지 않았습니다.",
"no": "Stoppknappen ble trykket. Handlingen har ikke blitt utført.",
"it": "Pulsante di arresto premuto. L'azione non è stata eseguita.",
"pt": "Botão de parar pressionado. A ação não foi executada.",
"es": "Botón de detener presionado. La acción no se ha ejecutado.",
"ar": "تم الضغط على زر التوقف. لم يتم تنفيذ الإجراء.",
"fr": "Bouton d'arrêt appuyé. L'action n'a pas été exécutée.",
"tr": "Durdurma düğmesine basıldı. Eylem yürütülmedi.",
"de": "Stopp-Taste gedrückt. Die Aktion wurde nicht ausgeführt.",
"uk": "Натиснуто кнопку зупинки. Дію не виконано."
},
"AGENT_ERROR$ERROR_ACTION_NOT_EXECUTED_ERROR": {
"en": "The action has not been executed due to a runtime error. The runtime system may have crashed and restarted due to resource constraints. Any previously established system state, dependencies, or environment variables may have been lost.",
@@ -14654,37 +14654,5 @@
"tr": "Onay modu etkinleştirildi",
"de": "Bestätigungsmodus aktiviert",
"uk": "Режим підтвердження увімкнено"
},
"COMMON$MOST_RECENT": {
"en": "Most Recent",
"ja": "最新",
"zh-CN": "最新",
"zh-TW": "最新",
"ko-KR": "최신",
"no": "Nyeste",
"it": "Più recente",
"pt": "Mais recente",
"es": "Más reciente",
"ar": "الأحدث",
"fr": "Le plus récent",
"tr": "En Son",
"de": "Neueste",
"uk": "Найновіше"
},
"HOME$NO_REPOSITORY_FOUND": {
"en": "No repository found to launch conversation",
"ja": "会話を開始するためのリポジトリが見つかりません",
"zh-CN": "未找到用于启动会话的存储库",
"zh-TW": "未找到用於啟動對話的存儲庫",
"ko-KR": "대화를 시작할 저장소를 찾을 수 없습니다",
"no": "Ingen repository funnet for å starte samtale",
"it": "Nessun repository trovato per avviare la conversazione",
"pt": "Nenhum repositório encontrado para iniciar a conversa",
"es": "No se encontró ningún repositorio para iniciar la conversación",
"ar": "لم يتم العثور على مستودع لبدء المحادثة",
"fr": "Aucun dépôt trouvé pour lancer la conversation",
"tr": "Konuşma başlatmak için depo bulunamadı",
"de": "Kein Repository gefunden, um das Gespräch zu starten",
"uk": "Не знайдено репозиторій для запуску розмови"
}
}

View File

@@ -115,9 +115,7 @@ const openHandsHandlers = [
"gpt-4o-mini",
"anthropic/claude-3.5",
"anthropic/claude-sonnet-4-20250514",
"anthropic/claude-sonnet-4-5-20250929",
"openhands/claude-sonnet-4-20250514",
"openhands/claude-sonnet-4-5-20250929",
"sambanova/Meta-Llama-3.1-8B-Instruct",
]),
),

View File

@@ -3,7 +3,7 @@ import { ApiKeysManager } from "#/components/features/settings/api-keys-manager"
function ApiKeysScreen() {
return (
<div className="flex flex-col grow overflow-auto">
<div className="flex flex-col grow overflow-auto p-9">
<ApiKeysManager />
</div>
);

View File

@@ -187,7 +187,7 @@ function AppSettingsScreen() {
>
{shouldBeLoading && <AppSettingsInputsSkeleton />}
{!shouldBeLoading && (
<div className="flex flex-col gap-6">
<div className="p-9 flex flex-col gap-6">
<LanguageInput
name="language-input"
defaultKey={settings.LANGUAGE}
@@ -282,7 +282,7 @@ function AppSettingsScreen() {
</div>
)}
<div className="flex gap-6 p-6 justify-end">
<div className="flex gap-6 p-6 justify-end border-t border-t-tertiary">
<BrandButton
testId="submit-button"
variant="primary"

View File

@@ -88,15 +88,13 @@ function GitChanges() {
</div>
</div>
) : (
gitChanges
.slice(0, 100)
.map((change) => (
<FileDiffViewer
key={change.path}
path={change.path}
type={change.status}
/>
))
gitChanges.map((change) => (
<FileDiffViewer
key={change.path}
path={change.path}
type={change.status}
/>
))
)}
</main>
);

View File

@@ -123,7 +123,7 @@ function GitSettingsScreen() {
className="flex flex-col h-full justify-between"
>
{!isLoading && (
<div className="flex flex-col">
<div className="p-9 flex flex-col">
{shouldRenderExternalConfigureButtons && !isLoading && (
<>
<div className="pb-1 flex flex-col">
@@ -202,7 +202,7 @@ function GitSettingsScreen() {
{isLoading && <GitSettingInputsSkeleton />}
<div className="flex gap-6 p-6 justify-end">
<div className="flex gap-6 p-6 justify-end border-t border-t-tertiary">
{!shouldRenderExternalConfigureButtons && (
<>
<BrandButton

View File

@@ -476,7 +476,7 @@ function LlmSettingsScreen() {
)}
inert={shouldShowUpgradeBanner}
>
<div className="flex flex-col gap-6">
<div className="p-9 flex flex-col gap-6">
<SettingsSwitch
testId="advanced-settings-switch"
defaultIsToggled={view === "advanced"}
@@ -751,7 +751,7 @@ function LlmSettingsScreen() {
)}
</div>
<div className="flex gap-6 p-6 justify-end">
<div className="flex gap-6 p-6 justify-end border-t border-t-tertiary">
<BrandButton
testId="submit-button"
type="submit"

View File

@@ -126,7 +126,7 @@ function MCPSettingsScreen() {
if (isLoading) {
return (
<div className="flex flex-col gap-5">
<div className="px-11 py-9 flex flex-col gap-5">
<div className="animate-pulse">
<div className="h-6 bg-gray-300 rounded w-1/4 mb-4" />
<div className="h-4 bg-gray-300 rounded w-1/2 mb-8" />
@@ -137,7 +137,7 @@ function MCPSettingsScreen() {
}
return (
<div className="flex flex-col gap-5">
<div className="px-11 py-9 flex flex-col gap-5">
{view === "list" && (
<>
<BrandButton

View File

@@ -24,7 +24,6 @@ import { displaySuccessToast } from "#/utils/custom-toast-handlers";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
import { useAutoLogin } from "#/hooks/use-auto-login";
import { useAuthCallback } from "#/hooks/use-auth-callback";
import { useReoTracking } from "#/hooks/use-reo-tracking";
import { LOCAL_STORAGE_KEYS } from "#/utils/local-storage";
import { EmailVerificationGuard } from "#/components/features/guards/email-verification-guard";
import { MaintenanceBanner } from "#/components/features/maintenance/maintenance-banner";
@@ -97,9 +96,6 @@ export default function MainApp() {
// Handle authentication callback and set login method after successful authentication
useAuthCallback();
// Initialize Reo.dev tracking in SaaS mode
useReoTracking();
React.useEffect(() => {
// Don't change language when on TOS page
if (!isOnTosPage && settings?.LANGUAGE) {

View File

@@ -62,7 +62,10 @@ function SecretsSettingsScreen() {
};
return (
<div data-testid="secrets-settings-screen" className="flex flex-col gap-5">
<div
data-testid="secrets-settings-screen"
className="px-11 py-9 flex flex-col gap-5"
>
{isLoadingSecrets && view === "list" && (
<ul>
<SecretListItemSkeleton />

View File

@@ -1,15 +1,14 @@
import { useMemo } from "react";
import { Outlet, redirect, useLocation } from "react-router";
import { NavLink, Outlet, redirect } from "react-router";
import { useTranslation } from "react-i18next";
import SettingsIcon from "#/icons/settings.svg?react";
import { cn } from "#/utils/utils";
import { useConfig } from "#/hooks/query/use-config";
import { I18nKey } from "#/i18n/declaration";
import { Route } from "./+types/settings";
import OptionService from "#/api/option-service/option-service.api";
import { queryClient } from "#/query-client-config";
import { ProPill } from "#/components/features/settings/pro-pill";
import { GetConfigResponse } from "#/api/option-service/option.types";
import { useSubscriptionAccess } from "#/hooks/query/use-subscription-access";
import { SAAS_NAV_ITEMS, OSS_NAV_ITEMS } from "#/constants/settings-nav";
import { Typography } from "#/ui/typography";
import { SettingsLayout } from "#/components/features/settings/settings-layout";
const SAAS_ONLY_PATHS = [
"/settings/user",
@@ -18,6 +17,25 @@ const SAAS_ONLY_PATHS = [
"/settings/api-keys",
];
const SAAS_NAV_ITEMS = [
{ to: "/settings/user", text: "SETTINGS$NAV_USER" },
{ to: "/settings/integrations", text: "SETTINGS$NAV_INTEGRATIONS" },
{ to: "/settings/app", text: "SETTINGS$NAV_APPLICATION" },
{ to: "/settings", text: "SETTINGS$NAV_LLM" },
{ to: "/settings/billing", text: "SETTINGS$NAV_BILLING" },
{ to: "/settings/secrets", text: "SETTINGS$NAV_SECRETS" },
{ to: "/settings/api-keys", text: "SETTINGS$NAV_API_KEYS" },
{ to: "/settings/mcp", text: "SETTINGS$NAV_MCP" },
];
const OSS_NAV_ITEMS = [
{ to: "/settings", text: "SETTINGS$NAV_LLM" },
{ to: "/settings/mcp", text: "SETTINGS$NAV_MCP" },
{ to: "/settings/integrations", text: "SETTINGS$NAV_INTEGRATIONS" },
{ to: "/settings/app", text: "SETTINGS$NAV_APPLICATION" },
{ to: "/settings/secrets", text: "SETTINGS$NAV_SECRETS" },
];
export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => {
const url = new URL(request.url);
const { pathname } = url;
@@ -41,38 +59,46 @@ export const clientLoader = async ({ request }: Route.ClientLoaderArgs) => {
function SettingsScreen() {
const { t } = useTranslation();
const { data: config } = useConfig();
const { data: subscriptionAccess } = useSubscriptionAccess();
const location = useLocation();
const isSaas = config?.APP_MODE === "saas";
// Navigation items configuration
const navItems = useMemo(() => {
const items = [];
if (isSaas) {
items.push(...SAAS_NAV_ITEMS);
} else {
items.push(...OSS_NAV_ITEMS);
}
return items;
}, [isSaas, !!subscriptionAccess]);
// Current section title for the main content area
const currentSectionTitle = useMemo(() => {
const currentItem = navItems.find((item) => item.to === location.pathname);
return currentItem ? currentItem.text : "SETTINGS$NAV_LLM";
}, [navItems, location.pathname]);
const navItems = isSaas ? SAAS_NAV_ITEMS : OSS_NAV_ITEMS;
return (
<main data-testid="settings-screen" className="h-full">
<SettingsLayout navigationItems={navItems} isSaas={isSaas}>
<div className="flex flex-col gap-6 h-full">
<Typography.H2>{t(currentSectionTitle)}</Typography.H2>
<div className="flex-1 overflow-auto">
<Outlet />
</div>
</div>
</SettingsLayout>
<main
data-testid="settings-screen"
className="bg-base-secondary border border-tertiary h-full rounded-xl flex flex-col"
>
<header className="px-3 py-1.5 border-b border-b-tertiary flex items-center gap-2">
<SettingsIcon width={16} height={16} />
<h1 className="text-sm leading-6">{t(I18nKey.SETTINGS$TITLE)}</h1>
</header>
<nav
data-testid="settings-navbar"
className="flex items-end gap-6 px-3 md:px-9 border-b border-tertiary"
>
{navItems.map(({ to, text }) => (
<NavLink
end
key={to}
to={to}
className={({ isActive }) =>
cn(
"border-b-2 border-transparent py-2.5 px-4 min-w-[40px] flex items-center justify-center relative",
isActive && "border-primary",
)
}
>
<span className="text-[#F9FBFE] text-sm">{t(text)}</span>
{isSaas && to === "/settings" && <ProPill className="ml-2" />}
</NavLink>
))}
</nav>
<div className="flex flex-col grow overflow-auto">
<Outlet />
</div>
</main>
);
}

View File

@@ -203,7 +203,7 @@ function UserSettingsScreen() {
return (
<div data-testid="user-settings-screen" className="flex flex-col h-full">
<div className="flex flex-col gap-6">
<div className="p-9 flex flex-col gap-6">
{isLoading ? (
<div className="animate-pulse h-8 w-64 bg-tertiary rounded-sm" />
) : (

View File

@@ -1,30 +0,0 @@
import { create } from "zustand";
interface ErrorMessageState {
errorMessage: string | null;
}
interface ErrorMessageActions {
setErrorMessage: (message: string) => void;
removeErrorMessage: () => void;
}
type ErrorMessageStore = ErrorMessageState & ErrorMessageActions;
const initialState: ErrorMessageState = {
errorMessage: null,
};
export const useErrorMessageStore = create<ErrorMessageStore>((set) => ({
...initialState,
setErrorMessage: (message: string) =>
set(() => ({
errorMessage: message,
})),
removeErrorMessage: () =>
set(() => ({
errorMessage: null,
})),
}));

View File

@@ -1,53 +0,0 @@
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { GitRepository } from "#/types/git";
interface HomeState {
recentRepositories: GitRepository[];
}
interface HomeActions {
addRecentRepository: (repository: GitRepository) => void;
clearRecentRepositories: () => void;
getRecentRepositories: () => GitRepository[];
}
type HomeStore = HomeState & HomeActions;
const initialState: HomeState = {
recentRepositories: [],
};
export const useHomeStore = create<HomeStore>()(
persist(
(set, get) => ({
...initialState,
addRecentRepository: (repository: GitRepository) =>
set((state) => {
// Remove the repository if it already exists to avoid duplicates
const filteredRepos = state.recentRepositories.filter(
(repo) => repo.id !== repository.id,
);
// Add the new repository to the beginning and keep only top 3
const updatedRepos = [repository, ...filteredRepos].slice(0, 3);
return {
recentRepositories: updatedRepos,
};
}),
clearRecentRepositories: () =>
set(() => ({
recentRepositories: [],
})),
getRecentRepositories: () => get().recentRepositories,
}),
{
name: "home-store", // unique name for localStorage
storage: createJSONStorage(() => localStorage),
},
),
);

View File

@@ -1,36 +0,0 @@
import { create } from "zustand";
interface OptimisticUserMessageState {
optimisticUserMessage: string | null;
}
interface OptimisticUserMessageActions {
setOptimisticUserMessage: (message: string) => void;
getOptimisticUserMessage: () => string | null;
removeOptimisticUserMessage: () => void;
}
type OptimisticUserMessageStore = OptimisticUserMessageState &
OptimisticUserMessageActions;
const initialState: OptimisticUserMessageState = {
optimisticUserMessage: null,
};
export const useOptimisticUserMessageStore = create<OptimisticUserMessageStore>(
(set, get) => ({
...initialState,
setOptimisticUserMessage: (message: string) =>
set(() => ({
optimisticUserMessage: message,
})),
getOptimisticUserMessage: () => get().optimisticUserMessage,
removeOptimisticUserMessage: () =>
set(() => ({
optimisticUserMessage: null,
})),
}),
);

View File

@@ -1,183 +0,0 @@
import { ActionBase } from "./base";
import { TaskItem } from "./common";
interface MCPToolAction extends ActionBase<"MCPToolAction"> {
/**
* Dynamic data fields from the tool call
*/
data: Record<string, unknown>;
}
interface FinishAction extends ActionBase<"FinishAction"> {
/**
* Final message to send to the user
*/
message: string;
}
interface ThinkAction extends ActionBase<"ThinkAction"> {
/**
* The thought to log
*/
thought: string;
}
interface ExecuteBashAction extends ActionBase<"ExecuteBashAction"> {
/**
* The bash command to execute. Can be empty string to view additional logs when previous exit code is `-1`. Can be `C-c` (Ctrl+C) to interrupt the currently running process.
*/
command: string;
/**
* If True, the command is an input to the running process. If False, the command is a bash command to be executed in the terminal. Default is False.
*/
is_input: boolean;
/**
* Optional. Sets a maximum time limit (in seconds) for running the command. If the command takes longer than this limit, youll be asked whether to continue or stop it.
*/
timeout: number | null;
/**
* If True, reset the terminal by creating a new session. Used only when the terminal becomes unresponsive. Note that all previously set environment variables and session state will be lost after reset. Cannot be used with is_input=True.
*/
reset: boolean;
}
interface StrReplaceEditorAction extends ActionBase<"StrReplaceEditorAction"> {
/**
* The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.
*/
command: "view" | "create" | "str_replace" | "insert" | "undo_edit";
/**
* Absolute path to file or directory.
*/
path: string;
/**
* Required parameter of `create` command, with the content of the file to be created.
*/
file_text: string | null;
/**
* Required parameter of `str_replace` command containing the string in `path` to replace.
*/
old_str: string | null;
/**
* Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.
*/
new_str: string | null;
/**
* Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. Must be >= 1.
*/
insert_line: number | null;
/**
* Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.
*/
view_range: [number, number] | null;
}
interface TaskTrackerAction extends ActionBase<"TaskTrackerAction"> {
/**
* The command to execute. `view` shows the current task list. `plan` creates or updates the task list based on provided requirements and progress. Always `view` the current list before making changes.
*/
command: "view" | "plan";
/**
* The full task list. Required parameter of `plan` command.
*/
task_list: TaskItem[];
}
interface BrowserNavigateAction extends ActionBase<"BrowserNavigateAction"> {
/**
* The URL to navigate to
*/
url: string;
/**
* Whether to open in a new tab. Default: False
*/
new_tab: boolean;
}
interface BrowserClickAction extends ActionBase<"BrowserClickAction"> {
/**
* The index of the element to click (from browser_get_state)
*/
index: number;
/**
* Whether to open any resulting navigation in a new tab. Default: False
*/
new_tab: boolean;
}
interface BrowserTypeAction extends ActionBase<"BrowserTypeAction"> {
/**
* The index of the input element (from browser_get_state)
*/
index: number;
/**
* The text to type
*/
text: string;
}
interface BrowserGetStateAction extends ActionBase<"BrowserGetStateAction"> {
/**
* Whether to include a screenshot of the current page. Default: False
*/
include_screenshot: boolean;
}
interface BrowserGetContentAction
extends ActionBase<"BrowserGetContentAction"> {
/**
* Whether to include links in the content (default: False)
*/
extract_links: boolean;
/**
* Character index to start from in the page content (default: 0)
*/
start_from_char: number;
}
interface BrowserScrollAction extends ActionBase<"BrowserScrollAction"> {
/**
* Direction to scroll. Options: 'up', 'down'. Default: 'down'
*/
direction: "up" | "down";
}
interface BrowserGoBackAction extends ActionBase<"BrowserGoBackAction"> {
// No additional properties - this action has no parameters
}
interface BrowserListTabsAction extends ActionBase<"BrowserListTabsAction"> {
// No additional properties - this action has no parameters
}
interface BrowserSwitchTabAction extends ActionBase<"BrowserSwitchTabAction"> {
/**
* 4 Character Tab ID of the tab to switch to (from browser_list_tabs)
*/
tab_id: string;
}
interface BrowserCloseTabAction extends ActionBase<"BrowserCloseTabAction"> {
/**
* 4 Character Tab ID of the tab to close (from browser_list_tabs)
*/
tab_id: string;
}
export type Action =
| MCPToolAction
| FinishAction
| ThinkAction
| ExecuteBashAction
| StrReplaceEditorAction
| TaskTrackerAction
| BrowserNavigateAction
| BrowserClickAction
| BrowserTypeAction
| BrowserGetStateAction
| BrowserGetContentAction
| BrowserScrollAction
| BrowserGoBackAction
| BrowserListTabsAction
| BrowserSwitchTabAction
| BrowserCloseTabAction;

View File

@@ -1,36 +0,0 @@
type EventType =
| "MCPTool"
| "Finish"
| "Think"
| "ExecuteBash"
| "StrReplaceEditor"
| "TaskTracker";
type ActionOnlyType =
| "BrowserNavigate"
| "BrowserClick"
| "BrowserType"
| "BrowserGetState"
| "BrowserGetContent"
| "BrowserScroll"
| "BrowserGoBack"
| "BrowserListTabs"
| "BrowserSwitchTab"
| "BrowserCloseTab";
type ObservationOnlyType = "Browser";
type ActionEventType = `${ActionOnlyType}Action` | `${EventType}Action`;
type ObservationEventType =
| `${ObservationOnlyType}Observation`
| `${EventType}Observation`;
export interface ActionBase<T extends ActionEventType = ActionEventType> {
kind: T;
}
export interface ObservationBase<
T extends ObservationEventType = ObservationEventType,
> {
kind: T;
}

View File

@@ -1,77 +0,0 @@
export interface TaskItem {
/**
* A brief title for the task.
*/
title: string;
/**
* Additional details or notes about the task.
*/
notes: string;
/**
* The current status of the task. One of 'todo', 'in_progress', or 'done'.
*/
status: "todo" | "in_progress" | "done";
}
export interface CmdOutputMetadata {
/**
* The exit code of the last executed command
*/
exit_code: number;
/**
* The process ID of the last executed command
*/
pid: number;
/**
* The username of the current user
*/
username: string | null;
/**
* The hostname of the machine
*/
hostname: string | null;
/**
* The current working directory
*/
working_dir: string | null;
/**
* The path to the current Python interpreter, if any
*/
py_interpreter_path: string | null;
/**
* Prefix to add to command output
*/
prefix: string;
/**
* Suffix to add to command output
*/
suffix: string;
}
// Type aliases for event and tool call IDs
export type EventID = string;
export type ToolCallID = string;
// Source type for events
export type SourceType = "agent" | "user" | "environment";
// Security risk levels
export enum SecurityRisk {
UNKNOWN = "UNKNOWN",
LOW = "LOW",
MEDIUM = "MEDIUM",
HIGH = "HIGH",
}
// Content types for LLM messages
export interface TextContent {
type: "text";
text: string;
cache_prompt?: boolean;
}
export interface ImageContent {
type: "image";
image_urls: string[];
cache_prompt?: boolean;
}

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