Compare commits

..

9 Commits

Author SHA1 Message Date
Robert Brennan d57bb264de update version to 0.57.2 2025-09-25 08:36:21 -04:00
Ray Myers 5df917008b fix - Set claude sonnet output limit (#11098) 2025-09-25 08:34:33 -04:00
Engel Nyst 3de9f38451 build(deps): pin litellm to avoid build failure (#11054)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-25 08:26:18 -04:00
Rohit Malhotra 135d3aea09 Hotfix: rm model choice override (#11022) 2025-09-18 14:44:24 -04:00
sp.wack 68ddda7e2e Hide Tavily search API key help text in SaaS mode (#11014)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-18 14:44:16 -04:00
sp.wack 5e7ea6da6d Fix SaaS callback URLs and pro pill positioning (#10998)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-17 13:07:40 -04:00
Ray Myers 67d7b00620 Fix Slack resolver failing on AWAITING_USER_INPUT state (#10992)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-17 13:07:34 -04:00
mamoodi 4edd575f93 Remove runtime settings (#10996) 2025-09-17 13:07:29 -04:00
mamoodi d72f31a3b0 Release 0.57.0 2025-09-16 09:49:43 -04:00
53 changed files with 464 additions and 986 deletions
+1 -1
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.56-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.57-nikolaik`
## Develop inside Docker container
+3 -3
View File
@@ -79,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.56-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.56-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.56
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
</details>
+3 -3
View File
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.56-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.56-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.56
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
> **注意**: 如果您在0.44版本之前使用过OpenHands,您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
+3 -3
View File
@@ -42,17 +42,17 @@ OpenHandsはDockerを利用してローカル環境でも実行できます。
> 公共ネットワークで実行していますか?[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.56-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.56-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.56
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
+1 -1
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.56-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:
+1 -1
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.56-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:
+2 -2
View File
@@ -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.56-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.56 \
docker.all-hands.dev/all-hands-ai/openhands:0.57 \
python -m openhands.cli.entry --override-cli-mode true
```
+2 -2
View File
@@ -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.56-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.56 \
docker.all-hands.dev/all-hands-ai/openhands:0.57 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+4 -4
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.56-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.56-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.56
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.56
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
+3 -3
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.56-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.56-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.56
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
</Accordion>
@@ -2,7 +2,6 @@ from experiments.constants import (
ENABLE_EXPERIMENT_MANAGER,
)
from experiments.experiment_versions import (
handle_claude4_vs_gpt5_experiment,
handle_condenser_max_step_experiment,
handle_system_prompt_experiment,
)
@@ -44,9 +43,6 @@ class SaaSExperimentManager(ExperimentManager):
return conversation_settings
# Apply conversation-scoped experiments
conversation_settings = handle_claude4_vs_gpt5_experiment(
user_id, conversation_id, conversation_settings
)
conversation_settings = handle_condenser_max_step_experiment(
user_id, conversation_id, conversation_settings
)
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.56.0",
"version": "0.57.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.56.0",
"version": "0.57.0",
"dependencies": {
"@heroui/react": "^2.8.3",
"@heroui/use-infinite-scroll": "^2.2.11",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.56.0",
"version": "0.57.2",
"private": true,
"type": "module",
"engines": {
@@ -1,35 +0,0 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { Typography } from "#/ui/typography";
import { Pre } from "#/ui/pre";
interface MicroagentContentProps {
content: string;
}
export function MicroagentContent({ content }: MicroagentContentProps) {
const { t } = useTranslation();
return (
<div className="mt-2">
<Typography.Text className="text-sm font-semibold text-gray-300 mb-2">
{t(I18nKey.MICROAGENTS_MODAL$CONTENT)}
</Typography.Text>
<Pre
size="default"
font="mono"
lineHeight="relaxed"
background="dark"
textColor="light"
padding="medium"
borderRadius="medium"
shadow="inner"
maxHeight="small"
overflow="auto"
className="mt-2"
>
{content || t(I18nKey.MICROAGENTS_MODAL$NO_CONTENT)}
</Pre>
</div>
);
}
@@ -1,52 +0,0 @@
import { ChevronDown, ChevronRight } from "lucide-react";
import { Microagent } from "#/api/open-hands.types";
import { Typography } from "#/ui/typography";
import { MicroagentTriggers } from "./microagent-triggers";
import { MicroagentContent } from "./microagent-content";
interface MicroagentItemProps {
agent: Microagent;
isExpanded: boolean;
onToggle: (agentName: string) => void;
}
export function MicroagentItem({
agent,
isExpanded,
onToggle,
}: MicroagentItemProps) {
return (
<div className="rounded-md overflow-hidden">
<button
type="button"
onClick={() => onToggle(agent.name)}
className="w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors"
>
<div className="flex items-center">
<Typography.Text className="font-bold text-gray-100">
{agent.name}
</Typography.Text>
</div>
<div className="flex items-center">
<Typography.Text className="px-2 py-1 text-xs rounded-full bg-gray-800 mr-2">
{agent.type === "repo" ? "Repository" : "Knowledge"}
</Typography.Text>
<Typography.Text className="text-gray-300">
{isExpanded ? (
<ChevronDown size={18} />
) : (
<ChevronRight size={18} />
)}
</Typography.Text>
</div>
</button>
{isExpanded && (
<div className="px-2 pb-3 pt-1">
<MicroagentTriggers triggers={agent.triggers} />
<MicroagentContent content={agent.content} />
</div>
)}
</div>
);
}
@@ -1,33 +0,0 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { Typography } from "#/ui/typography";
interface MicroagentTriggersProps {
triggers: string[];
}
export function MicroagentTriggers({ triggers }: MicroagentTriggersProps) {
const { t } = useTranslation();
if (!triggers || triggers.length === 0) {
return null;
}
return (
<div className="mt-2 mb-3">
<Typography.Text className="text-sm font-semibold text-gray-300 mb-2">
{t(I18nKey.MICROAGENTS_MODAL$TRIGGERS)}
</Typography.Text>
<div className="flex flex-wrap gap-1">
{triggers.map((trigger) => (
<Typography.Text
key={trigger}
className="px-2 py-1 text-xs rounded-full bg-blue-900"
>
{trigger}
</Typography.Text>
))}
</div>
</div>
);
}
@@ -1,21 +0,0 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { Typography } from "#/ui/typography";
interface MicroagentsEmptyStateProps {
isError: boolean;
}
export function MicroagentsEmptyState({ isError }: MicroagentsEmptyStateProps) {
const { t } = useTranslation();
return (
<div className="flex items-center justify-center h-full p-4">
<Typography.Text className="text-gray-400">
{isError
? t(I18nKey.MICROAGENTS_MODAL$FETCH_ERROR)
: t(I18nKey.CONVERSATION$NO_MICROAGENTS)}
</Typography.Text>
</div>
);
}
@@ -1,7 +0,0 @@
export function MicroagentsLoadingState() {
return (
<div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary" />
</div>
);
}
@@ -1,45 +0,0 @@
import { useTranslation } from "react-i18next";
import { RefreshCw } from "lucide-react";
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
import { I18nKey } from "#/i18n/declaration";
import { BrandButton } from "../settings/brand-button";
interface MicroagentsModalHeaderProps {
isAgentReady: boolean;
isLoading: boolean;
isRefetching: boolean;
onRefresh: () => void;
}
export function MicroagentsModalHeader({
isAgentReady,
isLoading,
isRefetching,
onRefresh,
}: MicroagentsModalHeaderProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col gap-6 w-full">
<div className="flex items-center justify-between w-full">
<BaseModalTitle title={t(I18nKey.MICROAGENTS_MODAL$TITLE)} />
{isAgentReady && (
<BrandButton
testId="refresh-microagents"
type="button"
variant="primary"
className="flex items-center gap-2"
onClick={onRefresh}
isDisabled={isLoading || isRefetching}
>
<RefreshCw
size={16}
className={`${isRefetching ? "animate-spin" : ""}`}
/>
{t(I18nKey.BUTTON$REFRESH)}
</BrandButton>
)}
</div>
</div>
);
}
@@ -1,17 +1,15 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { ChevronDown, ChevronRight, RefreshCw } from "lucide-react";
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { I18nKey } from "#/i18n/declaration";
import { useConversationMicroagents } from "#/hooks/query/use-conversation-microagents";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
import { Typography } from "#/ui/typography";
import { MicroagentsModalHeader } from "./microagents-modal-header";
import { MicroagentsLoadingState } from "./microagents-loading-state";
import { MicroagentsEmptyState } from "./microagents-empty-state";
import { MicroagentItem } from "./microagent-item";
import { BrandButton } from "../settings/brand-button";
interface MicroagentsModalProps {
onClose: () => void;
@@ -49,34 +47,57 @@ export function MicroagentsModal({ onClose }: MicroagentsModalProps) {
className="max-h-[80vh] flex flex-col items-start"
testID="microagents-modal"
>
<MicroagentsModalHeader
isAgentReady={isAgentReady}
isLoading={isLoading}
isRefetching={isRefetching}
onRefresh={refetch}
/>
<div className="flex flex-col gap-6 w-full">
<div className="flex items-center justify-between w-full">
<BaseModalTitle title={t(I18nKey.MICROAGENTS_MODAL$TITLE)} />
{isAgentReady && (
<BrandButton
testId="refresh-microagents"
type="button"
variant="primary"
className="flex items-center gap-2"
onClick={refetch}
isDisabled={isLoading || isRefetching}
>
<RefreshCw
size={16}
className={`${isRefetching ? "animate-spin" : ""}`}
/>
{t(I18nKey.BUTTON$REFRESH)}
</BrandButton>
)}
</div>
</div>
{isAgentReady && (
<Typography.Text className="text-sm text-gray-400">
<span className="text-sm text-gray-400">
{t(I18nKey.MICROAGENTS_MODAL$WARNING)}
</Typography.Text>
</span>
)}
<div className="w-full h-[60vh] overflow-auto rounded-md custom-scrollbar-always">
<div className="w-full h-[60vh] overflow-auto rounded-md">
{!isAgentReady && (
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
<Typography.Text>
{t(I18nKey.DIFF_VIEWER$WAITING_FOR_RUNTIME)}
</Typography.Text>
{t(I18nKey.DIFF_VIEWER$WAITING_FOR_RUNTIME)}
</div>
)}
{isLoading && <MicroagentsLoadingState />}
{isLoading && (
<div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary" />
</div>
)}
{!isLoading &&
isAgentReady &&
(isError || !microagents || microagents.length === 0) && (
<MicroagentsEmptyState isError={isError} />
<div className="flex items-center justify-center h-full p-4">
<p className="text-gray-400">
{isError
? t(I18nKey.MICROAGENTS_MODAL$FETCH_ERROR)
: t(I18nKey.CONVERSATION$NO_MICROAGENTS)}
</p>
</div>
)}
{!isLoading &&
@@ -88,12 +109,68 @@ export function MicroagentsModal({ onClose }: MicroagentsModalProps) {
const isExpanded = expandedAgents[agent.name] || false;
return (
<MicroagentItem
<div
key={agent.name}
agent={agent}
isExpanded={isExpanded}
onToggle={toggleAgent}
/>
className="rounded-md overflow-hidden"
>
<button
type="button"
onClick={() => toggleAgent(agent.name)}
className="w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors"
>
<div className="flex items-center">
<h3 className="font-bold text-gray-100">
{agent.name}
</h3>
</div>
<div className="flex items-center">
<span className="px-2 py-1 text-xs rounded-full bg-gray-800 mr-2">
{agent.type === "repo" ? "Repository" : "Knowledge"}
</span>
<span className="text-gray-300">
{isExpanded ? (
<ChevronDown size={18} />
) : (
<ChevronRight size={18} />
)}
</span>
</div>
</button>
{isExpanded && (
<div className="px-2 pb-3 pt-1">
{agent.triggers && agent.triggers.length > 0 && (
<div className="mt-2 mb-3">
<h4 className="text-sm font-semibold text-gray-300 mb-2">
{t(I18nKey.MICROAGENTS_MODAL$TRIGGERS)}
</h4>
<div className="flex flex-wrap gap-1">
{agent.triggers.map((trigger) => (
<span
key={trigger}
className="px-2 py-1 text-xs rounded-full bg-blue-900"
>
{trigger}
</span>
))}
</div>
</div>
)}
<div className="mt-2">
<h4 className="text-sm font-semibold text-gray-300 mb-2">
{t(I18nKey.MICROAGENTS_MODAL$CONTENT)}
</h4>
<div className="text-sm mt-2 p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed">
{agent.content ||
t(I18nKey.MICROAGENTS_MODAL$NO_CONTENT)}
</pre>
</div>
</div>
</div>
)}
</div>
);
})}
</div>
@@ -1,9 +1,12 @@
import { useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { ChevronDown, ChevronRight } from "lucide-react";
import ReactJsonView from "@microlink/react-json-view";
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { SystemMessageHeader } from "./system-message-modal/system-message-header";
import { TabNavigation } from "./system-message-modal/tab-navigation";
import { TabContent } from "./system-message-modal/tab-content";
import { cn } from "#/utils/utils";
import { JSON_VIEW_THEME } from "#/utils/constants";
interface SystemMessageModalProps {
isOpen: boolean;
@@ -16,11 +19,26 @@ interface SystemMessageModalProps {
} | null;
}
interface FunctionData {
name?: string;
description?: string;
parameters?: Record<string, unknown>;
}
interface ToolData {
type?: string;
function?: FunctionData;
name?: string;
description?: string;
parameters?: Record<string, unknown>;
}
export function SystemMessageModal({
isOpen,
onClose,
systemMessage,
}: SystemMessageModalProps) {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<"system" | "tools">("system");
const [expandedTools, setExpandedTools] = useState<Record<number, boolean>>(
{},
@@ -44,27 +62,155 @@ export function SystemMessageModal({
width="medium"
className="max-h-[80vh] flex flex-col items-start"
>
<SystemMessageHeader
agentClass={systemMessage.agent_class}
openhandsVersion={systemMessage.openhands_version}
/>
<div className="flex flex-col gap-6 w-full">
<BaseModalTitle title={t("SYSTEM_MESSAGE_MODAL$TITLE")} />
<div className="flex flex-col gap-2">
{systemMessage.agent_class && (
<div className="text-sm">
<span className="font-semibold text-gray-300">
{t("SYSTEM_MESSAGE_MODAL$AGENT_CLASS")}
</span>{" "}
<span className="font-medium text-gray-100">
{systemMessage.agent_class}
</span>
</div>
)}
{systemMessage.openhands_version && (
<div className="text-sm">
<span className="font-semibold text-gray-300">
{t("SYSTEM_MESSAGE_MODAL$OPENHANDS_VERSION")}
</span>{" "}
<span className="text-gray-100">
{systemMessage.openhands_version}
</span>
</div>
)}
</div>
</div>
<div className="w-full">
<TabNavigation
activeTab={activeTab}
onTabChange={setActiveTab}
hasTools={
!!(systemMessage.tools && systemMessage.tools.length > 0)
}
/>
<div className="flex border-b mb-2">
<button
type="button"
className={cn(
"px-4 py-2 font-medium border-b-2 transition-colors",
activeTab === "system"
? "border-primary text-gray-100"
: "border-transparent hover:text-gray-700 dark:hover:text-gray-300",
)}
onClick={() => setActiveTab("system")}
>
{t("SYSTEM_MESSAGE_MODAL$SYSTEM_MESSAGE_TAB")}
</button>
{systemMessage.tools && systemMessage.tools.length > 0 && (
<button
type="button"
className={cn(
"px-4 py-2 font-medium border-b-2 transition-colors",
activeTab === "tools"
? "border-primary text-gray-100"
: "border-transparent hover:text-gray-700 dark:hover:text-gray-300",
)}
onClick={() => setActiveTab("tools")}
>
{t("SYSTEM_MESSAGE_MODAL$TOOLS_TAB")}
</button>
)}
</div>
<div className="max-h-[51vh] overflow-auto rounded-md custom-scrollbar-always">
<TabContent
activeTab={activeTab}
systemMessage={systemMessage}
expandedTools={expandedTools}
onToggleTool={toggleTool}
/>
<div className="max-h-[51vh] overflow-auto rounded-md">
{activeTab === "system" && (
<div className="p-4 whitespace-pre-wrap font-mono text-sm leading-relaxed text-gray-300 shadow-inner">
{systemMessage.content}
</div>
)}
{activeTab === "tools" &&
systemMessage.tools &&
systemMessage.tools.length > 0 && (
<div className="p-2 space-y-3">
{systemMessage.tools.map((tool, index) => {
// Extract function data from the nested structure
const toolData = tool as ToolData;
const functionData = toolData.function || toolData;
const name =
functionData.name ||
(toolData.type === "function" &&
toolData.function?.name) ||
"";
const description =
functionData.description ||
(toolData.type === "function" &&
toolData.function?.description) ||
"";
const parameters =
functionData.parameters ||
(toolData.type === "function" &&
toolData.function?.parameters) ||
null;
const isExpanded = expandedTools[index] || false;
return (
<div key={index} className="rounded-md overflow-hidden">
<button
type="button"
onClick={() => toggleTool(index)}
className="w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors"
>
<div className="flex items-center">
<h3 className="font-bold text-gray-100">
{String(name)}
</h3>
</div>
<span className="text-gray-300">
{isExpanded ? (
<ChevronDown size={18} />
) : (
<ChevronRight size={18} />
)}
</span>
</button>
{isExpanded && (
<div className="px-2 pb-3 pt-1">
<div className="mt-2 mb-3">
<p className="text-sm whitespace-pre-wrap text-gray-300 leading-relaxed">
{String(description)}
</p>
</div>
{/* Parameters section */}
{parameters && (
<div className="mt-2">
<h4 className="text-sm font-semibold text-gray-300">
{t("SYSTEM_MESSAGE_MODAL$PARAMETERS")}
</h4>
<div className="text-sm mt-2 p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
<ReactJsonView
name={false}
src={parameters}
theme={JSON_VIEW_THEME}
/>
</div>
</div>
)}
</div>
)}
</div>
);
})}
</div>
)}
{activeTab === "tools" &&
(!systemMessage.tools || systemMessage.tools.length === 0) && (
<div className="flex items-center justify-center h-full p-4">
<p className="text-gray-400">
{t("SYSTEM_MESSAGE_MODAL$NO_TOOLS")}
</p>
</div>
)}
</div>
</div>
</ModalBody>
@@ -1,14 +0,0 @@
import { useTranslation } from "react-i18next";
import { Typography } from "#/ui/typography";
export function EmptyToolsState() {
const { t } = useTranslation();
return (
<div className="flex items-center justify-center h-full p-4">
<Typography.Text className="text-gray-400">
{t("SYSTEM_MESSAGE_MODAL$NO_TOOLS")}
</Typography.Text>
</div>
);
}
@@ -1,13 +0,0 @@
import { Typography } from "#/ui/typography";
interface SystemMessageContentProps {
content: string;
}
export function SystemMessageContent({ content }: SystemMessageContentProps) {
return (
<div className="p-4 shadow-inner">
<Typography.CodeBlock>{content}</Typography.CodeBlock>
</div>
);
}
@@ -1,43 +0,0 @@
import { useTranslation } from "react-i18next";
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
import { Typography } from "#/ui/typography";
interface SystemMessageHeaderProps {
agentClass: string | null;
openhandsVersion: string | null;
}
export function SystemMessageHeader({
agentClass,
openhandsVersion,
}: SystemMessageHeaderProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col gap-6 w-full">
<BaseModalTitle title={t("SYSTEM_MESSAGE_MODAL$TITLE")} />
<div className="flex flex-col gap-2">
{agentClass && (
<div className="text-sm">
<Typography.Text className="font-semibold text-gray-300">
{t("SYSTEM_MESSAGE_MODAL$AGENT_CLASS")}
</Typography.Text>{" "}
<Typography.Text className="font-medium text-gray-100">
{agentClass}
</Typography.Text>
</div>
)}
{openhandsVersion && (
<div className="text-sm">
<Typography.Text className="font-semibold text-gray-300">
{t("SYSTEM_MESSAGE_MODAL$OPENHANDS_VERSION")}
</Typography.Text>{" "}
<Typography.Text className="text-gray-100">
{openhandsVersion}
</Typography.Text>
</div>
)}
</div>
</div>
);
}
@@ -1,37 +0,0 @@
import { cn } from "#/utils/utils";
interface TabButtonProps {
isActive: boolean;
children: React.ReactNode;
onClick: () => void;
className?: string;
disabled?: boolean;
}
export function TabButton({
isActive,
children,
onClick,
className,
disabled = false,
}: TabButtonProps) {
return (
<button
type="button"
disabled={disabled}
className={cn(
"px-4 py-2 font-medium border-b-2 transition-colors",
isActive
? "border-primary text-gray-100"
: "border-transparent hover:text-gray-700 dark:hover:text-gray-300",
disabled && "opacity-50 cursor-not-allowed",
className,
)}
onClick={onClick}
aria-selected={isActive}
role="tab"
>
{children}
</button>
);
}
@@ -1,40 +0,0 @@
import { SystemMessageContent } from "./system-message-content";
import { ToolsList } from "./tools-list";
import { EmptyToolsState } from "./empty-tools-state";
interface TabContentProps {
activeTab: "system" | "tools";
systemMessage: {
content: string;
tools: Array<Record<string, unknown>> | null;
};
expandedTools: Record<number, boolean>;
onToggleTool: (index: number) => void;
}
export function TabContent({
activeTab,
systemMessage,
expandedTools,
onToggleTool,
}: TabContentProps) {
if (activeTab === "system") {
return <SystemMessageContent content={systemMessage.content} />;
}
if (activeTab === "tools") {
if (systemMessage.tools && systemMessage.tools.length > 0) {
return (
<ToolsList
tools={systemMessage.tools}
expandedTools={expandedTools}
onToggleTool={onToggleTool}
/>
);
}
return <EmptyToolsState />;
}
return null;
}
@@ -1,35 +0,0 @@
import { useTranslation } from "react-i18next";
import { TabButton } from "./tab-button";
interface TabNavigationProps {
activeTab: "system" | "tools";
onTabChange: (tab: "system" | "tools") => void;
hasTools: boolean;
}
export function TabNavigation({
activeTab,
onTabChange,
hasTools,
}: TabNavigationProps) {
const { t } = useTranslation();
return (
<div className="flex border-b mb-2" role="tablist">
<TabButton
isActive={activeTab === "system"}
onClick={() => onTabChange("system")}
>
{t("SYSTEM_MESSAGE_MODAL$SYSTEM_MESSAGE_TAB")}
</TabButton>
{hasTools && (
<TabButton
isActive={activeTab === "tools"}
onClick={() => onTabChange("tools")}
>
{t("SYSTEM_MESSAGE_MODAL$TOOLS_TAB")}
</TabButton>
)}
</div>
);
}
@@ -1,33 +0,0 @@
import { ChevronDown, ChevronRight } from "lucide-react";
import { Typography } from "#/ui/typography";
interface ToggleButtonProps {
title: string;
isExpanded: boolean;
onClick: () => void;
className?: string;
}
export function ToggleButton({
title,
isExpanded,
onClick,
className,
}: ToggleButtonProps) {
return (
<button
type="button"
onClick={onClick}
className={`w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors ${className || ""}`}
>
<div className="flex items-center">
<Typography.Text className="font-bold text-gray-100">
{title}
</Typography.Text>
</div>
<Typography.Text className="text-gray-300">
{isExpanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
</Typography.Text>
</button>
);
}
@@ -1,65 +0,0 @@
import { Typography } from "#/ui/typography";
import { ToolParameters } from "./tool-parameters";
import { ToggleButton } from "./toggle-button";
interface FunctionData {
name?: string;
description?: string;
parameters?: Record<string, unknown>;
}
interface ToolData {
type?: string;
function?: FunctionData;
name?: string;
description?: string;
parameters?: Record<string, unknown>;
}
interface ToolItemProps {
tool: Record<string, unknown>;
index: number;
isExpanded: boolean;
onToggle: (index: number) => void;
}
export function ToolItem({ tool, index, isExpanded, onToggle }: ToolItemProps) {
// Extract function data from the nested structure
const toolData = tool as ToolData;
const functionData = toolData.function || toolData;
const name =
functionData.name ||
(toolData.type === "function" && toolData.function?.name) ||
"";
const description =
functionData.description ||
(toolData.type === "function" && toolData.function?.description) ||
"";
const parameters =
functionData.parameters ||
(toolData.type === "function" && toolData.function?.parameters) ||
null;
return (
<div className="rounded-md overflow-hidden">
<ToggleButton
title={String(name)}
isExpanded={isExpanded}
onClick={() => onToggle(index)}
/>
{isExpanded && (
<div className="px-2 pb-3 pt-1">
<div className="mt-2 mb-3">
<Typography.Text className="text-sm whitespace-pre-wrap text-gray-300 leading-relaxed">
{String(description)}
</Typography.Text>
</div>
{/* Parameters section */}
{parameters && <ToolParameters parameters={parameters} />}
</div>
)}
</div>
);
}
@@ -1,23 +0,0 @@
import { useTranslation } from "react-i18next";
import ReactJsonView from "@microlink/react-json-view";
import { JSON_VIEW_THEME } from "#/utils/constants";
import { Typography } from "#/ui/typography";
interface ToolParametersProps {
parameters: Record<string, unknown>;
}
export function ToolParameters({ parameters }: ToolParametersProps) {
const { t } = useTranslation();
return (
<div className="mt-2">
<Typography.Text className="text-sm font-semibold text-gray-300">
{t("SYSTEM_MESSAGE_MODAL$PARAMETERS")}
</Typography.Text>
<div className="text-sm mt-2 p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
<ReactJsonView name={false} src={parameters} theme={JSON_VIEW_THEME} />
</div>
</div>
);
}
@@ -1,27 +0,0 @@
import { ToolItem } from "./tool-item";
interface ToolsListProps {
tools: Array<Record<string, unknown>>;
expandedTools: Record<number, boolean>;
onToggleTool: (index: number) => void;
}
export function ToolsList({
tools,
expandedTools,
onToggleTool,
}: ToolsListProps) {
return (
<div className="p-2 space-y-3">
{tools.map((tool, index) => (
<ToolItem
key={index}
tool={tool}
index={index}
isExpanded={expandedTools[index] || false}
onToggle={onToggleTool}
/>
))}
</div>
);
}
@@ -12,7 +12,7 @@ import { SystemMessageModal } from "../conversation-panel/system-message-modal";
import { MicroagentsModal } from "../conversation-panel/microagents-modal";
import { ConfirmDeleteModal } from "../conversation-panel/confirm-delete-modal";
import { ConfirmStopModal } from "../conversation-panel/confirm-stop-modal";
import { MetricsModal } from "./metrics-modal/metrics-modal";
import { MetricsModal } from "./metrics-modal";
export function ConversationName() {
const { t } = useTranslation();
@@ -0,0 +1,130 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { BaseModal } from "../../shared/modals/base-modal/base-modal";
import { BudgetDisplay } from "../conversation-panel/budget-display";
import { RootState } from "#/store";
import { I18nKey } from "#/i18n/declaration";
interface MetricsModalProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}
export function MetricsModal({ isOpen, onOpenChange }: MetricsModalProps) {
const { t } = useTranslation();
const metrics = useSelector((state: RootState) => state.metrics);
return (
<BaseModal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t(I18nKey.CONVERSATION$METRICS_INFO)}
testID="metrics-modal"
>
<div className="space-y-4">
{(metrics?.cost !== null || metrics?.usage !== null) && (
<div className="rounded-md p-3">
<div className="grid gap-3">
{metrics?.cost !== null && (
<div className="flex justify-between items-center pb-2">
<span className="text-lg font-semibold">
{t(I18nKey.CONVERSATION$TOTAL_COST)}
</span>
<span className="font-semibold">
${metrics.cost.toFixed(4)}
</span>
</div>
)}
<BudgetDisplay
cost={metrics?.cost ?? null}
maxBudgetPerTask={metrics?.max_budget_per_task ?? null}
/>
{metrics?.usage !== null && (
<>
<div className="flex justify-between items-center pb-2">
<span>{t(I18nKey.CONVERSATION$INPUT)}</span>
<span className="font-semibold">
{metrics.usage.prompt_tokens.toLocaleString()}
</span>
</div>
<div className="grid grid-cols-2 gap-2 pl-4 text-sm">
<span className="text-neutral-400">
{t(I18nKey.CONVERSATION$CACHE_HIT)}
</span>
<span className="text-right">
{metrics.usage.cache_read_tokens.toLocaleString()}
</span>
<span className="text-neutral-400">
{t(I18nKey.CONVERSATION$CACHE_WRITE)}
</span>
<span className="text-right">
{metrics.usage.cache_write_tokens.toLocaleString()}
</span>
</div>
<div className="flex justify-between items-center border-b border-neutral-700 pb-2">
<span>{t(I18nKey.CONVERSATION$OUTPUT)}</span>
<span className="font-semibold">
{metrics.usage.completion_tokens.toLocaleString()}
</span>
</div>
<div className="flex justify-between items-center border-b border-neutral-700 pb-2">
<span className="font-semibold">
{t(I18nKey.CONVERSATION$TOTAL)}
</span>
<span className="font-bold">
{(
metrics.usage.prompt_tokens +
metrics.usage.completion_tokens
).toLocaleString()}
</span>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="font-semibold">
{t(I18nKey.CONVERSATION$CONTEXT_WINDOW)}
</span>
</div>
<div className="w-full h-1.5 bg-neutral-700 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 transition-all duration-300"
style={{
width: `${Math.min(100, (metrics.usage.per_turn_token / metrics.usage.context_window) * 100)}%`,
}}
/>
</div>
<div className="flex justify-end">
<span className="text-xs text-neutral-400">
{metrics.usage.per_turn_token.toLocaleString()} /{" "}
{metrics.usage.context_window.toLocaleString()} (
{(
(metrics.usage.per_turn_token /
metrics.usage.context_window) *
100
).toFixed(2)}
% {t(I18nKey.CONVERSATION$USED)})
</span>
</div>
</div>
</>
)}
</div>
</div>
)}
{!metrics?.cost && !metrics?.usage && (
<div className="rounded-md p-4 text-center">
<p className="text-neutral-400">
{t(I18nKey.CONVERSATION$NO_METRICS)}
</p>
</div>
)}
</div>
</BaseModal>
);
}
@@ -1,39 +0,0 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface ContextWindowSectionProps {
perTurnToken: number;
contextWindow: number;
}
export function ContextWindowSection({
perTurnToken,
contextWindow,
}: ContextWindowSectionProps) {
const { t } = useTranslation();
const usagePercentage = (perTurnToken / contextWindow) * 100;
const progressWidth = Math.min(100, usagePercentage);
return (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="font-semibold">
{t(I18nKey.CONVERSATION$CONTEXT_WINDOW)}
</span>
</div>
<div className="w-full h-1.5 bg-neutral-700 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 transition-all duration-300"
style={{ width: `${progressWidth}%` }}
/>
</div>
<div className="flex justify-end">
<span className="text-xs text-neutral-400">
{perTurnToken.toLocaleString()} / {contextWindow.toLocaleString()} (
{usagePercentage.toFixed(2)}% {t(I18nKey.CONVERSATION$USED)})
</span>
</div>
</div>
);
}
@@ -1,28 +0,0 @@
import { useTranslation } from "react-i18next";
import { BudgetDisplay } from "../../conversation-panel/budget-display";
import { I18nKey } from "#/i18n/declaration";
interface CostSectionProps {
cost: number | null;
maxBudgetPerTask: number | null;
}
export function CostSection({ cost, maxBudgetPerTask }: CostSectionProps) {
const { t } = useTranslation();
if (cost === null) {
return null;
}
return (
<>
<div className="flex justify-between items-center pb-2">
<span className="text-lg font-semibold">
{t(I18nKey.CONVERSATION$TOTAL_COST)}
</span>
<span className="font-semibold">${cost.toFixed(4)}</span>
</div>
<BudgetDisplay cost={cost} maxBudgetPerTask={maxBudgetPerTask} />
</>
);
}
@@ -1,12 +0,0 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function EmptyState() {
const { t } = useTranslation();
return (
<div className="rounded-md p-4 text-center">
<p className="text-neutral-400">{t(I18nKey.CONVERSATION$NO_METRICS)}</p>
</div>
);
}
@@ -1,22 +0,0 @@
import { ReactNode } from "react";
interface MetricRowProps {
label: ReactNode;
value: ReactNode;
labelClassName?: string;
valueClassName?: string;
}
export function MetricRow({
label,
value,
labelClassName = "",
valueClassName = "font-semibold",
}: MetricRowProps) {
return (
<div className="flex justify-between items-center border-b border-neutral-700 pb-2">
<span className={labelClassName}>{label}</span>
<span className={valueClassName}>{value}</span>
</div>
);
}
@@ -1,53 +0,0 @@
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { BaseModal } from "../../../shared/modals/base-modal/base-modal";
import { RootState } from "#/store";
import { I18nKey } from "#/i18n/declaration";
import { CostSection } from "./cost-section";
import { UsageSection } from "./usage-section";
import { ContextWindowSection } from "./context-window-section";
import { EmptyState } from "./empty-state";
interface MetricsModalProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}
export function MetricsModal({ isOpen, onOpenChange }: MetricsModalProps) {
const { t } = useTranslation();
const metrics = useSelector((state: RootState) => state.metrics);
return (
<BaseModal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t(I18nKey.CONVERSATION$METRICS_INFO)}
testID="metrics-modal"
>
<div className="space-y-4">
{(metrics?.cost !== null || metrics?.usage !== null) && (
<div className="rounded-md p-3">
<div className="grid gap-3">
<CostSection
cost={metrics?.cost ?? null}
maxBudgetPerTask={metrics?.max_budget_per_task ?? null}
/>
{metrics?.usage !== null && (
<>
<UsageSection usage={metrics.usage} />
<ContextWindowSection
perTurnToken={metrics.usage.per_turn_token}
contextWindow={metrics.usage.context_window}
/>
</>
)}
</div>
</div>
)}
{!metrics?.cost && !metrics?.usage && <EmptyState />}
</div>
</BaseModal>
);
}
@@ -1,52 +0,0 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { MetricRow } from "./metric-row";
interface UsageSectionProps {
usage: {
prompt_tokens: number;
completion_tokens: number;
cache_read_tokens: number;
cache_write_tokens: number;
};
}
export function UsageSection({ usage }: UsageSectionProps) {
const { t } = useTranslation();
return (
<>
<MetricRow
label={t(I18nKey.CONVERSATION$INPUT)}
value={usage.prompt_tokens.toLocaleString()}
/>
<div className="grid grid-cols-2 gap-2 pl-4 text-sm">
<span className="text-neutral-400">
{t(I18nKey.CONVERSATION$CACHE_HIT)}
</span>
<span className="text-right">
{usage.cache_read_tokens.toLocaleString()}
</span>
<span className="text-neutral-400">
{t(I18nKey.CONVERSATION$CACHE_WRITE)}
</span>
<span className="text-right">
{usage.cache_write_tokens.toLocaleString()}
</span>
</div>
<MetricRow
label={t(I18nKey.CONVERSATION$OUTPUT)}
value={usage.completion_tokens.toLocaleString()}
/>
<MetricRow
label={t(I18nKey.CONVERSATION$TOTAL)}
value={(usage.prompt_tokens + usage.completion_tokens).toLocaleString()}
labelClassName="font-semibold"
valueClassName="font-bold"
/>
</>
);
}
+8 -6
View File
@@ -526,12 +526,14 @@ function LlmSettingsScreen() {
/>
)}
<HelpLink
testId="search-api-key-help-anchor"
text={t(I18nKey.SETTINGS$SEARCH_API_KEY_OPTIONAL)}
linkText={t(I18nKey.SETTINGS$SEARCH_API_KEY_INSTRUCTIONS)}
href="https://tavily.com/"
/>
{config?.APP_MODE !== "saas" && (
<HelpLink
testId="search-api-key-help-anchor"
text={t(I18nKey.SETTINGS$SEARCH_API_KEY_OPTIONAL)}
linkText={t(I18nKey.SETTINGS$SEARCH_API_KEY_INSTRUCTIONS)}
href="https://tavily.com/"
/>
)}
</div>
)}
-106
View File
@@ -1,106 +0,0 @@
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "#/utils/utils";
const preVariants = cva("whitespace-pre-wrap", {
variants: {
size: {
default: "text-sm",
small: "text-xs",
},
font: {
default: "",
mono: "font-mono",
},
lineHeight: {
default: "",
relaxed: "leading-relaxed",
},
background: {
default: "",
dark: "bg-gray-900",
},
textColor: {
default: "",
light: "text-gray-300",
},
padding: {
default: "",
medium: "p-3",
large: "px-5",
},
borderRadius: {
default: "",
medium: "rounded-md",
},
shadow: {
default: "",
inner: "shadow-inner",
},
maxHeight: {
default: "",
small: "max-h-[400px]",
large: "max-h-[60vh]",
},
overflow: {
default: "",
auto: "overflow-auto",
},
},
defaultVariants: {
size: "default",
font: "default",
lineHeight: "default",
background: "default",
textColor: "default",
padding: "default",
borderRadius: "default",
shadow: "default",
maxHeight: "default",
overflow: "default",
},
});
interface PreProps extends VariantProps<typeof preVariants> {
className?: string;
testId?: string;
children: React.ReactNode;
}
export function Pre({
size,
font,
lineHeight,
background,
textColor,
padding,
borderRadius,
shadow,
maxHeight,
overflow,
className,
testId,
children,
}: PreProps) {
return (
<pre
data-testid={testId}
className={cn(
preVariants({
size,
font,
lineHeight,
background,
textColor,
padding,
borderRadius,
shadow,
maxHeight,
overflow,
}),
className,
)}
>
{children}
</pre>
);
}
-15
View File
@@ -6,8 +6,6 @@ const typographyVariants = cva("", {
variant: {
h1: "text-[32px] text-white font-bold leading-5",
span: "text-sm font-normal text-white leading-5.5",
codeBlock:
"font-mono text-sm leading-relaxed text-gray-300 whitespace-pre-wrap",
},
},
defaultVariants: {
@@ -64,19 +62,6 @@ export function Text({
);
}
export function CodeBlock({
className,
testId,
children,
}: Omit<TypographyProps, "variant">) {
return (
<Typography variant="codeBlock" className={className} testId={testId}>
{children}
</Typography>
);
}
// Attach components to Typography for the expected API
Typography.H1 = H1;
Typography.Text = Text;
Typography.CodeBlock = CodeBlock;
+7 -5
View File
@@ -502,11 +502,13 @@ class LLM(RetryMixin, DebugMixin):
# Set max_output_tokens from model info if not explicitly set
if self.config.max_output_tokens is None:
# Special case for Claude 3.7 Sonnet models
if any(
model in self.config.model
for model in ['claude-3-7-sonnet', 'claude-3.7-sonnet']
):
# Special case for Claude Sonnet models
sonnet_models = [
'claude-3-7-sonnet',
'claude-3.7-sonnet',
'claude-sonnet-4',
]
if any(model in self.config.model for model in sonnet_models):
self.config.max_output_tokens = 64000 # litellm set max to 128k, but that requires a header to be set
# Try to get from model info
elif self.model_info is not None:
@@ -129,15 +129,11 @@ class ActionExecutionClient(Runtime):
return send_request(self.session, method, url, **kwargs)
def check_if_alive(self) -> None:
request_url = f'{self.action_execution_server_url}/alive'
self.log('debug', f'Sending request to: {request_url}')
response = self._send_action_server_request(
'GET',
request_url,
f'{self.action_execution_server_url}/alive',
timeout=5,
)
self.log('debug', f'Response status code: {response.status_code}')
self.log('debug', f'Response text: {response.text}')
assert response.is_closed
def list_files(self, path: str | None = None) -> list[str]:
+1 -1
View File
@@ -40,7 +40,7 @@ Two configuration options are required to use the Kubernetes runtime:
2. **Runtime Container Image**: Specify the container image to use for the runtime environment
```toml
[sandbox]
runtime_container_image = "docker.all-hands.dev/all-hands-ai/runtime:0.56-nikolaik"
runtime_container_image = "docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik"
```
#### Additional Kubernetes Options
@@ -415,19 +415,11 @@ class RemoteRuntime(ActionExecutionClient):
def _wait_until_alive_impl(self) -> None:
self.log('debug', f'Waiting for runtime to be alive at url: {self.runtime_url}')
self.log(
'debug',
f'Sending request to: {self.config.sandbox.remote_runtime_api_url}/runtime/{self.runtime_id}',
)
runtime_info_response = self._send_runtime_api_request(
'GET',
f'{self.config.sandbox.remote_runtime_api_url}/runtime/{self.runtime_id}',
)
runtime_data = runtime_info_response.json()
self.log(
'debug',
f'received response: {runtime_data}',
)
assert 'runtime_id' in runtime_data
assert runtime_data['runtime_id'] == self.runtime_id
assert 'pod_status' in runtime_data
+1 -3
View File
@@ -15,8 +15,6 @@ from openhands.runtime.plugins.requirement import Plugin, PluginRequirement
from openhands.runtime.utils.system import check_port_available
from openhands.utils.shutdown_listener import should_continue
RUNTIME_USERNAME = os.getenv('RUNTIME_USERNAME')
@dataclass
class VSCodeRequirement(PluginRequirement):
@@ -39,7 +37,7 @@ class VSCodePlugin(Plugin):
)
return
if username not in filter(None, [RUNTIME_USERNAME, 'root', 'openhands']):
if username not in ['root', 'openhands']:
self.vscode_port = None
self.vscode_connection_token = None
logger.warning(
+1 -3
View File
@@ -20,8 +20,6 @@ from openhands.events.observation.commands import (
from openhands.runtime.utils.bash_constants import TIMEOUT_MESSAGE_TEMPLATE
from openhands.utils.shutdown_listener import should_continue
RUNTIME_USERNAME = os.getenv('RUNTIME_USERNAME')
def split_bash_commands(commands: str) -> list[str]:
if not commands.strip():
@@ -195,7 +193,7 @@ class BashSession:
def initialize(self) -> None:
self.server = libtmux.Server()
_shell_command = '/bin/bash'
if self.username in list(filter(None, [RUNTIME_USERNAME, 'root', 'openhands'])):
if self.username in ['root', 'openhands']:
# This starts a non-login (new) shell for the given user
_shell_command = f'su {self.username} -'
+4 -17
View File
@@ -1,5 +1,3 @@
import os
from openhands.core.config import OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.runtime.plugins import PluginRequirement
@@ -14,9 +12,6 @@ DEFAULT_PYTHON_PREFIX = [
]
DEFAULT_MAIN_MODULE = 'openhands.runtime.action_execution_server'
RUNTIME_USERNAME = os.getenv('RUNTIME_USERNAME')
RUNTIME_UID = os.getenv('RUNTIME_UID')
def get_action_execution_server_startup_command(
server_port: int,
@@ -31,10 +26,7 @@ def get_action_execution_server_startup_command(
sandbox_config = app_config.sandbox
logger.debug(f'app_config {vars(app_config)}')
logger.debug(f'sandbox_config {vars(sandbox_config)}')
logger.debug(f'RUNTIME_USERNAME {RUNTIME_USERNAME}, RUNTIME_UID {RUNTIME_UID}')
logger.debug(
f'override_username {override_username}, override_user_id {override_user_id}'
)
logger.debug(f'override_user_id {override_user_id}')
# Plugin args
plugin_args = []
@@ -48,15 +40,10 @@ def get_action_execution_server_startup_command(
'--browsergym-eval-env'
] + sandbox_config.browsergym_eval_env.split(' ')
username = (
override_username
or RUNTIME_USERNAME
or ('openhands' if app_config.run_as_openhands else 'root')
username = override_username or (
'openhands' if app_config.run_as_openhands else 'root'
)
user_id = (
override_user_id or RUNTIME_UID or (1000 if app_config.run_as_openhands else 0)
)
logger.debug(f'username {username}, user_id {user_id}')
user_id = override_user_id or (1000 if app_config.run_as_openhands else 0)
base_cmd = [
*python_prefix,
-1
View File
@@ -27,7 +27,6 @@ class HttpSession:
headers = kwargs.get('headers') or {}
headers = {**self.headers, **headers}
kwargs['headers'] = headers
logger.debug(f'HttpSession:request called with args {args} and kwargs {kwargs}')
return CLIENT.request(*args, **kwargs)
def stream(self, *args, **kwargs):
Generated
+2 -2
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "aiofiles"
@@ -11823,4 +11823,4 @@ third-party-runtimes = ["daytona", "e2b-code-interpreter", "modal", "runloop-api
[metadata]
lock-version = "2.1"
python-versions = "^3.12,<3.14"
content-hash = "6c7bc9a39d6875e09966872a5d579e73b5cb739d1bad89d3a7dde829541cec16"
content-hash = "3b77845e0da841c359d325b3a8ee210528b0ed60a2b6e57cd931a7506692c41e"
+2 -2
View File
@@ -6,7 +6,7 @@ requires = [
[tool.poetry]
name = "openhands-ai"
version = "0.56.0"
version = "0.57.2"
description = "OpenHands: Code Less, Make More"
authors = [ "OpenHands" ]
license = "MIT"
@@ -26,7 +26,7 @@ build = "build_vscode.py" # Build VSCode extension during Poetry build
[tool.poetry.dependencies]
python = "^3.12,<3.14"
litellm = "^1.74.3, !=1.64.4, !=1.67.*" # avoid 1.64.4 (known bug) & 1.67.* (known bug #10272)
litellm = ">=1.74.3, <1.77.2, !=1.64.4, !=1.67.*" # avoid 1.64.4 (known bug) & 1.67.* (known bug #10272)
openai = "1.99.9" # Pin due to litellm incompatibility with >=1.100.0 (BerriAI/litellm#13711)
aiohttp = ">=3.9.0,!=3.11.13" # Pin to avoid yanked version 3.11.13
google-genai = "*" # To use litellm with Gemini Pro API
+11 -6
View File
@@ -1053,17 +1053,22 @@ def test_claude_3_7_sonnet_max_output_tokens():
assert llm.config.max_input_tokens is None
def test_claude_sonnet_4_max_output_tokens():
@patch('openhands.llm.llm.litellm.get_model_info')
def test_claude_sonnet_4_max_output_tokens(mock_get_model_info):
"""Test that Claude Sonnet 4 models get the correct max_output_tokens and max_input_tokens values."""
mock_get_model_info.return_value = {
'max_input_tokens': 100000,
'max_output_tokens': 100000,
}
# Create LLM instance with a Claude Sonnet 4 model
config = LLMConfig(model='claude-sonnet-4-20250514', api_key='test_key')
llm = LLM(config, service_id='test-service')
llm.init_model_info()
# Verify max_output_tokens is set to the expected value
assert llm.config.max_output_tokens == 64000
# Verify max_input_tokens is set to the expected value
# For Claude models, we expect a specific value from litellm
assert llm.config.max_input_tokens == 200000
assert llm.config.max_output_tokens == 64000, 'output max should be decreased'
assert llm.config.max_input_tokens == 100000, (
'input max should be the litellm value'
)
def test_sambanova_deepseek_model_max_output_tokens():