mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
8 Commits
test-user
...
chuck-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08096db29f | ||
|
|
b2b6ddf90c | ||
|
|
87fe36d811 | ||
|
|
39d255d313 | ||
|
|
e334b67f21 | ||
|
|
d5c02bf87b | ||
|
|
14a4664fe8 | ||
|
|
774caf0607 |
@@ -2,8 +2,6 @@ import React, { useRef, useCallback, useState, useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { ServerStatus } from "#/components/features/controls/server-status";
|
||||
import { AgentStatus } from "#/components/features/controls/agent-status";
|
||||
import { ChatSendButton } from "./chat-send-button";
|
||||
import { ChatAddFileButton } from "./chat-add-file-button";
|
||||
import { cn, isMobileDevice } from "#/utils/utils";
|
||||
@@ -20,6 +18,8 @@ import {
|
||||
} from "#/state/conversation-slice";
|
||||
import { CHAT_INPUT } from "#/utils/constants";
|
||||
import { RootState } from "#/store";
|
||||
import { ServerStatus } from "../controls/server-status";
|
||||
import { AgentStatus } from "../controls/agent-status";
|
||||
|
||||
export interface CustomChatInputProps {
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -263,8 +263,8 @@ export function WsClientProvider({
|
||||
}
|
||||
sio.io.opts.query = sio.io.opts.query || {};
|
||||
sio.io.opts.query.latest_event_id = lastEventRef.current?.id;
|
||||
updateStatusWhenErrorMessagePresent(data);
|
||||
|
||||
updateStatusWhenErrorMessagePresent(data);
|
||||
setErrorMessage(hasValidMessageProperty(data) ? data.message : "");
|
||||
}
|
||||
|
||||
@@ -296,8 +296,29 @@ export function WsClientProvider({
|
||||
if (!conversationId) {
|
||||
throw new Error("No conversation ID provided");
|
||||
}
|
||||
if (conversation?.status !== "RUNNING" && !conversation?.runtime_status) {
|
||||
return () => undefined; // conversation not yet loaded
|
||||
|
||||
// Clear error messages when conversation is intentionally stopped
|
||||
if (conversation && conversation.status === "STOPPED") {
|
||||
removeErrorMessage();
|
||||
setWebSocketStatus("DISCONNECTED");
|
||||
return () => undefined; // conversation intentionally stopped
|
||||
}
|
||||
|
||||
// Set connecting status when conversation is starting
|
||||
if (conversation && conversation.status === "STARTING") {
|
||||
removeErrorMessage();
|
||||
setWebSocketStatus("CONNECTING");
|
||||
return () => undefined; // conversation is starting, will connect when ready
|
||||
}
|
||||
|
||||
// Only connect when conversation is fully loaded and running
|
||||
if (
|
||||
!conversation ||
|
||||
conversation.status !== "RUNNING" ||
|
||||
!conversation.runtime_status ||
|
||||
conversation.runtime_status === "STATUS$STOPPED"
|
||||
) {
|
||||
return () => undefined; // conversation not ready for WebSocket connection
|
||||
}
|
||||
|
||||
let sio = sioRef.current;
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
|
||||
export const useStopConversation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const { conversationId: currentConversationId } = useParams<{
|
||||
conversationId: string;
|
||||
}>();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (variables: { conversationId: string }) =>
|
||||
@@ -32,5 +37,14 @@ export const useStopConversation = () => {
|
||||
// Also invalidate the conversations list for consistency
|
||||
queryClient.invalidateQueries({ queryKey: ["user", "conversations"] });
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
// Only redirect if we're stopping the conversation we're currently viewing
|
||||
if (
|
||||
currentConversationId &&
|
||||
variables.conversationId === currentConversationId
|
||||
) {
|
||||
navigate("/");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { clearTerminal } from "#/state/command-slice";
|
||||
@@ -19,7 +20,6 @@ import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
import { useIsAuthed } from "#/hooks/query/use-is-authed";
|
||||
import { ConversationSubscriptionsProvider } from "#/context/conversation-subscriptions-provider";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
@@ -28,16 +28,19 @@ import { ConversationMain } from "#/components/features/conversation/conversatio
|
||||
import { ConversationName } from "#/components/features/conversation/conversation-name";
|
||||
|
||||
import { ConversationTabs } from "#/components/features/conversation/conversation-tabs/conversation-tabs";
|
||||
import { useStartConversation } from "#/hooks/mutation/use-start-conversation";
|
||||
|
||||
function AppContent() {
|
||||
useConversationConfig();
|
||||
|
||||
const { conversationId } = useConversationId();
|
||||
const { data: conversation, isFetched, refetch } = useActiveConversation();
|
||||
const { mutate: startConversation } = useStartConversation();
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
const { providers } = useUserProviders();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Fetch batch feedback data when conversation is loaded
|
||||
useBatchFeedback();
|
||||
@@ -45,6 +48,13 @@ function AppContent() {
|
||||
// Set the document title to the conversation title when available
|
||||
useDocumentTitleFromState();
|
||||
|
||||
// Force fresh conversation data when navigating to prevent stale cache issues
|
||||
React.useEffect(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["user", "conversation", conversationId],
|
||||
});
|
||||
}, [conversationId, queryClient]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isFetched && !conversation && isAuthed) {
|
||||
displayErrorToast(
|
||||
@@ -52,13 +62,25 @@ function AppContent() {
|
||||
);
|
||||
navigate("/");
|
||||
} else if (conversation?.status === "STOPPED") {
|
||||
// start the conversation if the state is stopped on initial load
|
||||
ConversationService.startConversation(
|
||||
conversation.conversation_id,
|
||||
providers,
|
||||
).then(() => refetch());
|
||||
// If conversation is STOPPED, attempt to start it
|
||||
startConversation(
|
||||
{ conversationId: conversation.conversation_id, providers },
|
||||
{
|
||||
onError: (error) => {
|
||||
displayErrorToast(`Failed to start conversation: ${error.message}`);
|
||||
// Refetch the conversation to ensure UI consistency
|
||||
refetch();
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}, [conversation?.conversation_id, isFetched, isAuthed, providers]);
|
||||
}, [
|
||||
conversation?.conversation_id,
|
||||
conversation?.status,
|
||||
isFetched,
|
||||
isAuthed,
|
||||
providers,
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(clearTerminal());
|
||||
|
||||
@@ -161,6 +161,7 @@ class EventStream(EventStore):
|
||||
self._clean_up_subscriber(subscriber_id, callback_id)
|
||||
|
||||
def add_event(self, event: Event, source: EventSource) -> None:
|
||||
logger.info(f'Adding event with ID {event.id}')
|
||||
if event.id != Event.INVALID_ID:
|
||||
raise ValueError(
|
||||
f'Event already has an ID:{event.id}. It was probably added back to the EventStream from inside a handler, triggering a loop.'
|
||||
@@ -183,6 +184,7 @@ class EventStream(EventStore):
|
||||
if len(current_write_page) == self.cache_size:
|
||||
self._write_page_cache = []
|
||||
|
||||
logger.info(f'Event now has ID {event.id}')
|
||||
if event.id is not None:
|
||||
# Write the event to the store - this can take some time
|
||||
event_json = json.dumps(data)
|
||||
@@ -204,12 +206,18 @@ class EventStream(EventStore):
|
||||
|
||||
def _store_cache_page(self, current_write_page: list[dict]):
|
||||
"""Store a page in the cache. Reading individual events is slow when there are a lot of them, so we use pages."""
|
||||
logger.info(
|
||||
f'Writing event cache if page len {len(current_write_page)} is greater than {self.cache_size}'
|
||||
)
|
||||
if len(current_write_page) < self.cache_size:
|
||||
return
|
||||
start = current_write_page[0]['id']
|
||||
end = start + self.cache_size
|
||||
contents = json.dumps(current_write_page)
|
||||
cache_filename = self._get_filename_for_cache(start, end)
|
||||
logger.info(
|
||||
f'writing event cache to {cache_filename} in file store of type {type(self.file_store)}'
|
||||
)
|
||||
self.file_store.write(cache_filename, contents)
|
||||
|
||||
def set_secrets(self, secrets: dict[str, str]) -> None:
|
||||
|
||||
@@ -13,6 +13,15 @@ from openhands.runtime.plugins.requirement import Plugin, PluginRequirement
|
||||
from openhands.runtime.utils import find_available_tcp_port
|
||||
from openhands.utils.shutdown_listener import should_continue
|
||||
|
||||
SU_TO_USER = os.getenv('SU_TO_USER', 'true').lower() in (
|
||||
'1',
|
||||
'true',
|
||||
't',
|
||||
'yes',
|
||||
'y',
|
||||
'on',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class JupyterRequirement(PluginRequirement):
|
||||
@@ -36,7 +45,7 @@ class JupyterPlugin(Plugin):
|
||||
|
||||
if not is_local_runtime:
|
||||
# Non-LocalRuntime
|
||||
prefix = f'su - {username} -s '
|
||||
prefix = f'su - {username} -s ' if SU_TO_USER else ''
|
||||
# cd to code repo, setup all env vars and run micromamba
|
||||
poetry_prefix = (
|
||||
'cd /openhands/code\n'
|
||||
|
||||
@@ -16,6 +16,14 @@ from openhands.runtime.utils.system import check_port_available
|
||||
from openhands.utils.shutdown_listener import should_continue
|
||||
|
||||
RUNTIME_USERNAME = os.getenv('RUNTIME_USERNAME')
|
||||
SU_TO_USER = os.getenv('SU_TO_USER', 'true').lower() in (
|
||||
'1',
|
||||
'true',
|
||||
't',
|
||||
'yes',
|
||||
'y',
|
||||
'on',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -85,13 +93,19 @@ class VSCodePlugin(Plugin):
|
||||
if path_mode:
|
||||
base_path_flag = f' --server-base-path /{runtime_id}/vscode'
|
||||
|
||||
cmd = (
|
||||
f"su - {username} -s /bin/bash << 'EOF'\n"
|
||||
f'sudo chown -R {username}:{username} /openhands/.openvscode-server\n'
|
||||
f'cd {workspace_path}\n'
|
||||
f'exec /openhands/.openvscode-server/bin/openvscode-server --host 0.0.0.0 --connection-token {self.vscode_connection_token} --port {self.vscode_port} --disable-workspace-trust{base_path_flag}\n'
|
||||
'EOF'
|
||||
)
|
||||
cmd = (
|
||||
(
|
||||
f"su - {username} -s /bin/bash << 'EOF'\n"
|
||||
if SU_TO_USER
|
||||
else "/bin/bash << 'EOF'\n"
|
||||
)
|
||||
+ f'sudo chown -R {username}:{username} /openhands/.openvscode-server\n'
|
||||
+ f'cd {workspace_path}\n'
|
||||
+ 'exec /openhands/.openvscode-server/bin/openvscode-server '
|
||||
+ f'--host 0.0.0.0 --connection-token {self.vscode_connection_token} '
|
||||
+ f'--port {self.vscode_port} --disable-workspace-trust{base_path_flag}\n'
|
||||
+ 'EOF'
|
||||
)
|
||||
|
||||
# Using asyncio.create_subprocess_shell instead of subprocess.Popen
|
||||
# to avoid ASYNC101 linting error
|
||||
|
||||
@@ -21,6 +21,14 @@ from openhands.runtime.utils.bash_constants import TIMEOUT_MESSAGE_TEMPLATE
|
||||
from openhands.utils.shutdown_listener import should_continue
|
||||
|
||||
RUNTIME_USERNAME = os.getenv('RUNTIME_USERNAME')
|
||||
SU_TO_USER = os.getenv('SU_TO_USER', 'true').lower() in (
|
||||
'1',
|
||||
'true',
|
||||
't',
|
||||
'yes',
|
||||
'y',
|
||||
'on',
|
||||
)
|
||||
|
||||
|
||||
def split_bash_commands(commands: str) -> list[str]:
|
||||
@@ -195,7 +203,9 @@ 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 SU_TO_USER and self.username in list(
|
||||
filter(None, [RUNTIME_USERNAME, 'root', 'openhands'])
|
||||
):
|
||||
# This starts a non-login (new) shell for the given user
|
||||
_shell_command = f'su {self.username} -'
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import threading
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
@@ -62,6 +63,11 @@ class BatchedWebHookFileStore(FileStore):
|
||||
batch_size_limit_bytes: Size limit in bytes after which a batch is sent.
|
||||
If None, uses the default constant WEBHOOK_BATCH_SIZE_LIMIT_BYTES.
|
||||
"""
|
||||
logger.info(
|
||||
f'BatchedWebHookFileStore __init__ called with filestore type {type(file_store)}'
|
||||
)
|
||||
stack = '\n'.join(traceback.format_stack())
|
||||
logger.info('BatchedWebHookFileStore __init__ stack trace:\n%s', stack)
|
||||
self.file_store = file_store
|
||||
self.base_url = base_url
|
||||
if client is None:
|
||||
@@ -89,6 +95,9 @@ class BatchedWebHookFileStore(FileStore):
|
||||
path: The path to write to
|
||||
contents: The contents to write
|
||||
"""
|
||||
logger.info(
|
||||
f'BatchedWebHookFileStore write to {path} in filestore of type {type(self.file_store)}'
|
||||
)
|
||||
self.file_store.write(path, contents)
|
||||
self._queue_update(path, 'write', contents)
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import os
|
||||
import traceback
|
||||
from typing import Any, TypedDict
|
||||
|
||||
import boto3
|
||||
import botocore
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.storage.files import FileStore
|
||||
|
||||
|
||||
@@ -37,6 +39,8 @@ class S3FileStore(FileStore):
|
||||
)
|
||||
|
||||
def write(self, path: str, contents: str | bytes) -> None:
|
||||
stack = '\n'.join(traceback.format_stack())
|
||||
logger.info('S3FileStore write stack trace:\n%s', stack)
|
||||
try:
|
||||
as_bytes = (
|
||||
contents.encode('utf-8') if isinstance(contents, str) else contents
|
||||
|
||||
Reference in New Issue
Block a user