mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e27efce042 | |||
| 95bf98f21a |
@@ -14,7 +14,10 @@ import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
|||||||
import { FeedbackModal } from "../feedback/feedback-modal";
|
import { FeedbackModal } from "../feedback/feedback-modal";
|
||||||
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
|
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
|
||||||
import { TypingIndicator } from "./typing-indicator";
|
import { TypingIndicator } from "./typing-indicator";
|
||||||
import { useWsClient } from "#/context/ws-client-provider";
|
import {
|
||||||
|
useWsClient,
|
||||||
|
WsClientProviderStatus,
|
||||||
|
} from "#/context/ws-client-provider";
|
||||||
import { Messages } from "./messages";
|
import { Messages } from "./messages";
|
||||||
import { ChatSuggestions } from "./chat-suggestions";
|
import { ChatSuggestions } from "./chat-suggestions";
|
||||||
import { ActionSuggestions } from "./action-suggestions";
|
import { ActionSuggestions } from "./action-suggestions";
|
||||||
@@ -34,7 +37,7 @@ function getEntryPoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInterface() {
|
export function ChatInterface() {
|
||||||
const { send, isLoadingMessages } = useWsClient();
|
const { send, isLoadingMessages, status, pendingMessages } = useWsClient();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||||
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
|
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
|
||||||
@@ -54,6 +57,9 @@ export function ChatInterface() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { mutate: getTrajectory } = useGetTrajectory();
|
const { mutate: getTrajectory } = useGetTrajectory();
|
||||||
|
|
||||||
|
const isClientDisconnected = status === WsClientProviderStatus.DISCONNECTED;
|
||||||
|
const hasPendingMessages = pendingMessages.length > 0;
|
||||||
|
|
||||||
const handleSendMessage = async (content: string, files: File[]) => {
|
const handleSendMessage = async (content: string, files: File[]) => {
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
posthog.capture("initial_query_submitted", {
|
posthog.capture("initial_query_submitted", {
|
||||||
@@ -76,7 +82,16 @@ export function ChatInterface() {
|
|||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
const pending = true;
|
const pending = true;
|
||||||
dispatch(addUserMessage({ content, imageUrls, timestamp, pending }));
|
dispatch(addUserMessage({ content, imageUrls, timestamp, pending }));
|
||||||
send(createChatMessage(content, imageUrls, timestamp));
|
|
||||||
|
// Create the chat message
|
||||||
|
const chatMessage = createChatMessage(content, imageUrls, timestamp);
|
||||||
|
|
||||||
|
// Send or queue the message depending on connection status
|
||||||
|
send(chatMessage);
|
||||||
|
|
||||||
|
// Set agent state to RUNNING when a message is sent
|
||||||
|
send(generateAgentStateChangeEvent(AgentState.RUNNING));
|
||||||
|
|
||||||
setMessageToSend(null);
|
setMessageToSend(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,8 +146,20 @@ export function ChatInterface() {
|
|||||||
className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2"
|
className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2"
|
||||||
>
|
>
|
||||||
{isLoadingMessages && (
|
{isLoadingMessages && (
|
||||||
<div className="flex justify-center">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<LoadingSpinner size="small" />
|
<LoadingSpinner size="small" />
|
||||||
|
{isClientDisconnected && (
|
||||||
|
<div className="text-sm text-neutral-400">
|
||||||
|
Waiting for client to become ready...
|
||||||
|
{hasPendingMessages && (
|
||||||
|
<div className="text-xs text-neutral-500 mt-1">
|
||||||
|
{pendingMessages.length} message
|
||||||
|
{pendingMessages.length !== 1 ? "s" : ""} will be sent when
|
||||||
|
connected
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -179,7 +206,7 @@ export function ChatInterface() {
|
|||||||
onSubmit={handleSendMessage}
|
onSubmit={handleSendMessage}
|
||||||
onStop={handleStop}
|
onStop={handleStop}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
curAgentState === AgentState.LOADING ||
|
// Allow input even when loading, but not during confirmation
|
||||||
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
|
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
|
||||||
}
|
}
|
||||||
mode={curAgentState === AgentState.RUNNING ? "stop" : "submit"}
|
mode={curAgentState === AgentState.RUNNING ? "stop" : "submit"}
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ export function AgentStatusBar() {
|
|||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||||
const { curStatusMessage } = useSelector((state: RootState) => state.status);
|
const { curStatusMessage } = useSelector((state: RootState) => state.status);
|
||||||
const { status } = useWsClient();
|
const { status, pendingMessages } = useWsClient();
|
||||||
const { notify } = useNotification();
|
const { notify } = useNotification();
|
||||||
|
|
||||||
const [statusMessage, setStatusMessage] = React.useState<string>("");
|
const [statusMessage, setStatusMessage] = React.useState<string>("");
|
||||||
|
const hasPendingMessages = pendingMessages.length > 0;
|
||||||
|
|
||||||
const updateStatusMessage = () => {
|
const updateStatusMessage = () => {
|
||||||
let message = curStatusMessage.message || "";
|
let message = curStatusMessage.message || "";
|
||||||
@@ -71,7 +72,13 @@ export function AgentStatusBar() {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (status === WsClientProviderStatus.DISCONNECTED) {
|
if (status === WsClientProviderStatus.DISCONNECTED) {
|
||||||
setStatusMessage("Connecting...");
|
if (hasPendingMessages) {
|
||||||
|
setStatusMessage(
|
||||||
|
`Connecting... (${pendingMessages.length} pending message${pendingMessages.length !== 1 ? "s" : ""})`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setStatusMessage("Connecting...");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
|
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
|
||||||
if (notificationStates.includes(curAgentState)) {
|
if (notificationStates.includes(curAgentState)) {
|
||||||
@@ -87,7 +94,7 @@ export function AgentStatusBar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [curAgentState, notify, t]);
|
}, [curAgentState, status, pendingMessages.length, notify, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ const isMessageAction = (
|
|||||||
): event is UserMessageAction | AssistantMessageAction =>
|
): event is UserMessageAction | AssistantMessageAction =>
|
||||||
isUserMessage(event) || isAssistantMessage(event);
|
isUserMessage(event) || isAssistantMessage(event);
|
||||||
|
|
||||||
|
// Check if an event is an agent state changed observation
|
||||||
|
const isAgentStateEvent = (event: Record<string, unknown>): boolean =>
|
||||||
|
isOpenHandsEvent(event) &&
|
||||||
|
"type" in event &&
|
||||||
|
event.type === "observation" &&
|
||||||
|
"observation_id" in event &&
|
||||||
|
event.observation_id === "agent_state_changed";
|
||||||
|
|
||||||
export enum WsClientProviderStatus {
|
export enum WsClientProviderStatus {
|
||||||
CONNECTED,
|
CONNECTED,
|
||||||
DISCONNECTED,
|
DISCONNECTED,
|
||||||
@@ -49,15 +57,21 @@ interface UseWsClient {
|
|||||||
isLoadingMessages: boolean;
|
isLoadingMessages: boolean;
|
||||||
events: Record<string, unknown>[];
|
events: Record<string, unknown>[];
|
||||||
send: (event: Record<string, unknown>) => void;
|
send: (event: Record<string, unknown>) => void;
|
||||||
|
queueMessage: (event: Record<string, unknown>) => void;
|
||||||
|
pendingMessages: Record<string, unknown>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const WsClientContext = React.createContext<UseWsClient>({
|
const WsClientContext = React.createContext<UseWsClient>({
|
||||||
status: WsClientProviderStatus.DISCONNECTED,
|
status: WsClientProviderStatus.DISCONNECTED,
|
||||||
isLoadingMessages: true,
|
isLoadingMessages: true,
|
||||||
events: [],
|
events: [],
|
||||||
|
pendingMessages: [],
|
||||||
send: () => {
|
send: () => {
|
||||||
throw new Error("not connected");
|
throw new Error("not connected");
|
||||||
},
|
},
|
||||||
|
queueMessage: () => {
|
||||||
|
throw new Error("not connected");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface WsClientProviderProps {
|
interface WsClientProviderProps {
|
||||||
@@ -109,26 +123,50 @@ export function WsClientProvider({
|
|||||||
WsClientProviderStatus.DISCONNECTED,
|
WsClientProviderStatus.DISCONNECTED,
|
||||||
);
|
);
|
||||||
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
|
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
|
||||||
|
const [pendingMessages, setPendingMessages] = React.useState<
|
||||||
|
Record<string, unknown>[]
|
||||||
|
>([]);
|
||||||
|
const [backendReady, setBackendReady] = React.useState(false);
|
||||||
const lastEventRef = React.useRef<Record<string, unknown> | null>(null);
|
const lastEventRef = React.useRef<Record<string, unknown> | null>(null);
|
||||||
|
|
||||||
const messageRateHandler = useRate({ threshold: 250 });
|
const messageRateHandler = useRate({ threshold: 250 });
|
||||||
|
|
||||||
|
function queueMessage(event: Record<string, unknown>) {
|
||||||
|
setPendingMessages((prev) => [...prev, event]);
|
||||||
|
}
|
||||||
|
|
||||||
function send(event: Record<string, unknown>) {
|
function send(event: Record<string, unknown>) {
|
||||||
if (!sioRef.current) {
|
if (!sioRef.current) {
|
||||||
EventLogger.error("WebSocket is not connected.");
|
EventLogger.error("WebSocket is not connected.");
|
||||||
|
queueMessage(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!backendReady) {
|
||||||
|
// If backend is not ready yet, queue the message
|
||||||
|
EventLogger.info("Backend not ready, queueing message");
|
||||||
|
queueMessage(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
sioRef.current.emit("oh_action", event);
|
sioRef.current.emit("oh_action", event);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConnect() {
|
function handleConnect() {
|
||||||
setStatus(WsClientProviderStatus.CONNECTED);
|
setStatus(WsClientProviderStatus.CONNECTED);
|
||||||
|
// Don't send queued messages yet - wait for backend ready signal
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMessage(event: Record<string, unknown>) {
|
function handleMessage(event: Record<string, unknown>) {
|
||||||
if (isOpenHandsEvent(event) && isMessageAction(event)) {
|
if (isOpenHandsEvent(event) && isMessageAction(event)) {
|
||||||
messageRateHandler.record(new Date().getTime());
|
messageRateHandler.record(new Date().getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a state change event indicating backend is ready
|
||||||
|
if (isAgentStateEvent(event)) {
|
||||||
|
setBackendReady(true);
|
||||||
|
}
|
||||||
|
|
||||||
setEvents((prevEvents) => [...prevEvents, event]);
|
setEvents((prevEvents) => [...prevEvents, event]);
|
||||||
if (!Number.isNaN(parseInt(event.id as string, 10))) {
|
if (!Number.isNaN(parseInt(event.id as string, 10))) {
|
||||||
lastEventRef.current = event;
|
lastEventRef.current = event;
|
||||||
@@ -139,6 +177,7 @@ export function WsClientProvider({
|
|||||||
|
|
||||||
function handleDisconnect(data: unknown) {
|
function handleDisconnect(data: unknown) {
|
||||||
setStatus(WsClientProviderStatus.DISCONNECTED);
|
setStatus(WsClientProviderStatus.DISCONNECTED);
|
||||||
|
setBackendReady(false);
|
||||||
const sio = sioRef.current;
|
const sio = sioRef.current;
|
||||||
if (!sio) {
|
if (!sio) {
|
||||||
return;
|
return;
|
||||||
@@ -150,11 +189,34 @@ export function WsClientProvider({
|
|||||||
|
|
||||||
function handleError(data: unknown) {
|
function handleError(data: unknown) {
|
||||||
setStatus(WsClientProviderStatus.DISCONNECTED);
|
setStatus(WsClientProviderStatus.DISCONNECTED);
|
||||||
|
setBackendReady(false);
|
||||||
updateStatusWhenErrorMessagePresent(data);
|
updateStatusWhenErrorMessagePresent(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watch for backend ready state and send queued messages when ready
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (backendReady && pendingMessages.length > 0 && sioRef.current) {
|
||||||
|
// Backend is ready and we have pending messages
|
||||||
|
EventLogger.info(`Sending ${pendingMessages.length} queued messages`);
|
||||||
|
|
||||||
|
pendingMessages.forEach((event) => {
|
||||||
|
sioRef.current?.emit("oh_action", event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also set the agent state to RUNNING if needed
|
||||||
|
const agentStateEvent = {
|
||||||
|
action: "change_agent_state",
|
||||||
|
args: { agent_state: "running" },
|
||||||
|
};
|
||||||
|
sioRef.current.emit("oh_action", agentStateEvent);
|
||||||
|
|
||||||
|
setPendingMessages([]);
|
||||||
|
}
|
||||||
|
}, [backendReady, pendingMessages.length]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
lastEventRef.current = null;
|
lastEventRef.current = null;
|
||||||
|
setBackendReady(false);
|
||||||
}, [conversationId]);
|
}, [conversationId]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -210,9 +272,11 @@ export function WsClientProvider({
|
|||||||
status,
|
status,
|
||||||
isLoadingMessages: messageRateHandler.isUnderThreshold,
|
isLoadingMessages: messageRateHandler.isUnderThreshold,
|
||||||
events,
|
events,
|
||||||
|
pendingMessages,
|
||||||
send,
|
send,
|
||||||
|
queueMessage,
|
||||||
}),
|
}),
|
||||||
[status, messageRateHandler.isUnderThreshold, events],
|
[status, messageRateHandler.isUnderThreshold, events, pendingMessages],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <WsClientContext value={value}>{children}</WsClientContext>;
|
return <WsClientContext value={value}>{children}</WsClientContext>;
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ class EventLogger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an info message
|
||||||
|
* @param info The info message
|
||||||
|
*/
|
||||||
|
static info(info: string) {
|
||||||
|
if (this.isDevMode) {
|
||||||
|
console.info(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log an error message
|
* Log an error message
|
||||||
* @param error The error message
|
* @param error The error message
|
||||||
|
|||||||
Reference in New Issue
Block a user