,
+): obj is { thought: string } => "thought" in obj && !!obj.thought;
+
+interface EventMessageProps {
+ event: OpenHandsAction | OpenHandsObservation;
+ hasObservationPair: boolean;
+ isFirstMessageWithResolverTrigger: boolean;
+ isAwaitingUserConfirmation: boolean;
+ isLastMessage: boolean;
+}
+
+export function EventMessage({
+ event,
+ hasObservationPair,
+ isFirstMessageWithResolverTrigger,
+ isAwaitingUserConfirmation,
+ isLastMessage,
+}: EventMessageProps) {
+ const shouldShowConfirmationButtons =
+ isLastMessage && event.source === "agent" && isAwaitingUserConfirmation;
+
+ const isFirstUserMessageWithResolverTrigger =
+ isFirstMessageWithResolverTrigger && isUserMessage(event);
+
+ // Special case: First user message with resolver trigger
+ if (isFirstUserMessageWithResolverTrigger) {
+ return (
+
+
+ {event.args.image_urls && event.args.image_urls.length > 0 && (
+
+ )}
+
+ );
+ }
+
+ if (isErrorObservation(event)) {
+ return (
+
+ );
+ }
+
+ if (
+ hasObservationPair &&
+ isOpenHandsAction(event) &&
+ hasThoughtProperty(event.args)
+ ) {
+ return ;
+ }
+
+ if (isFinishAction(event)) {
+ return (
+
+ );
+ }
+
+ if (isUserMessage(event) || isAssistantMessage(event)) {
+ return (
+
+ {event.args.image_urls && event.args.image_urls.length > 0 && (
+
+ )}
+ {shouldShowConfirmationButtons && }
+
+ );
+ }
+
+ if (isRejectObservation(event)) {
+ return ;
+ }
+
+ return (
+
+ {isOpenHandsAction(event) && hasThoughtProperty(event.args) && (
+
+ )}
+
+
+
+ {shouldShowConfirmationButtons && }
+
+ );
+}
diff --git a/frontend/src/components/features/chat/generic-event-message.tsx b/frontend/src/components/features/chat/generic-event-message.tsx
new file mode 100644
index 0000000000..161e6bcce6
--- /dev/null
+++ b/frontend/src/components/features/chat/generic-event-message.tsx
@@ -0,0 +1,61 @@
+import React from "react";
+import Markdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import { code } from "../markdown/code";
+import { ol, ul } from "../markdown/list";
+import ArrowDown from "#/icons/angle-down-solid.svg?react";
+import ArrowUp from "#/icons/angle-up-solid.svg?react";
+import { SuccessIndicator } from "./success-indicator";
+import { ObservationResultStatus } from "./event-content-helpers/get-observation-result";
+
+interface GenericEventMessageProps {
+ title: React.ReactNode;
+ details: string;
+ success?: ObservationResultStatus;
+}
+
+export function GenericEventMessage({
+ title,
+ details,
+ success,
+}: GenericEventMessageProps) {
+ const [showDetails, setShowDetails] = React.useState(false);
+
+ return (
+
+
+
+ {title}
+ {details && (
+
+ )}
+
+
+ {success &&
}
+
+
+ {showDetails && (
+
+ {details}
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/features/chat/messages.tsx b/frontend/src/components/features/chat/messages.tsx
index f98270650c..7ca7e9e47b 100644
--- a/frontend/src/components/features/chat/messages.tsx
+++ b/frontend/src/components/features/chat/messages.tsx
@@ -1,80 +1,82 @@
import React from "react";
-import type { Message } from "#/message";
-import { ChatMessage } from "#/components/features/chat/chat-message";
-import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
-import { ImageCarousel } from "../images/image-carousel";
-import { ExpandableMessage } from "./expandable-message";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
import { useConversation } from "#/context/conversation-context";
-import { I18nKey } from "#/i18n/declaration";
+import { OpenHandsAction } from "#/types/core/actions";
+import { OpenHandsObservation } from "#/types/core/observations";
+import { isOpenHandsAction, isOpenHandsObservation } from "#/types/core/guards";
+import { OpenHandsEventType } from "#/types/core/base";
+import { EventMessage } from "./event-message";
+import { ChatMessage } from "./chat-message";
+import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
+
+const COMMON_NO_RENDER_LIST: OpenHandsEventType[] = [
+ "system",
+ "agent_state_changed",
+ "change_agent_state",
+];
+
+const ACTION_NO_RENDER_LIST: OpenHandsEventType[] = ["recall"];
+
+const shouldRenderEvent = (event: OpenHandsAction | OpenHandsObservation) => {
+ if (isOpenHandsAction(event)) {
+ const noRenderList = COMMON_NO_RENDER_LIST.concat(ACTION_NO_RENDER_LIST);
+ return !noRenderList.includes(event.action);
+ }
+
+ if (isOpenHandsObservation(event)) {
+ return !COMMON_NO_RENDER_LIST.includes(event.observation);
+ }
+
+ return true;
+};
interface MessagesProps {
- messages: Message[];
+ messages: (OpenHandsAction | OpenHandsObservation)[];
isAwaitingUserConfirmation: boolean;
}
export const Messages: React.FC = React.memo(
({ messages, isAwaitingUserConfirmation }) => {
+ const { getOptimisticUserMessage } = useOptimisticUserMessage();
const { conversationId } = useConversation();
const { data: conversation } = useUserConversation(conversationId || null);
+ const optimisticUserMessage = getOptimisticUserMessage();
+
// Check if conversation metadata has trigger=resolver
const isResolverTrigger = conversation?.trigger === "resolver";
- return messages.map((message, index) => {
- const shouldShowConfirmationButtons =
- messages.length - 1 === index &&
- message.sender === "assistant" &&
- isAwaitingUserConfirmation;
+ const actionHasObservationPair = React.useCallback(
+ (event: OpenHandsAction | OpenHandsObservation): boolean => {
+ if (isOpenHandsAction(event)) {
+ return !!messages.some(
+ (msg) => isOpenHandsObservation(msg) && msg.cause === event.id,
+ );
+ }
- const isFirstUserMessageWithResolverTrigger =
- index === 0 && message.sender === "user" && isResolverTrigger;
+ return false;
+ },
+ [messages],
+ );
- // Special case: First user message with resolver trigger
- if (isFirstUserMessageWithResolverTrigger) {
- return (
-
-
- {message.imageUrls && message.imageUrls.length > 0 && (
-
- )}
-
- );
- }
+ return (
+ <>
+ {messages.filter(shouldRenderEvent).map((message, index) => (
+
+ ))}
- if (message.type === "error" || message.type === "action") {
- return (
-
-
- {shouldShowConfirmationButtons && }
-
- );
- }
-
- return (
-
- {message.imageUrls && message.imageUrls.length > 0 && (
-
- )}
- {shouldShowConfirmationButtons && }
-
- );
- });
+ {optimisticUserMessage && (
+
+ )}
+ >
+ );
},
);
diff --git a/frontend/src/components/features/chat/success-indicator.tsx b/frontend/src/components/features/chat/success-indicator.tsx
new file mode 100644
index 0000000000..4e5ac4779a
--- /dev/null
+++ b/frontend/src/components/features/chat/success-indicator.tsx
@@ -0,0 +1,35 @@
+import { FaClock } from "react-icons/fa";
+import CheckCircle from "#/icons/check-circle-solid.svg?react";
+import XCircle from "#/icons/x-circle-solid.svg?react";
+import { ObservationResultStatus } from "./event-content-helpers/get-observation-result";
+
+interface SuccessIndicatorProps {
+ status: ObservationResultStatus;
+}
+
+export function SuccessIndicator({ status }: SuccessIndicatorProps) {
+ return (
+
+ {status === "success" && (
+
+ )}
+
+ {status === "error" && (
+
+ )}
+
+ {status === "timeout" && (
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/features/conversation-panel/conversation-card.tsx b/frontend/src/components/features/conversation-panel/conversation-card.tsx
index b4581ef705..b7ce6e2895 100644
--- a/frontend/src/components/features/conversation-panel/conversation-card.tsx
+++ b/frontend/src/components/features/conversation-panel/conversation-card.tsx
@@ -15,8 +15,9 @@ import { cn } from "#/utils/utils";
import { BaseModal } from "../../shared/modals/base-modal/base-modal";
import { RootState } from "#/store";
import { I18nKey } from "#/i18n/declaration";
-import { selectSystemMessage } from "#/state/chat-slice";
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
+import { useWsClient } from "#/context/ws-client-provider";
+import { isSystemMessage } from "#/types/core/guards";
interface ConversationCardProps {
onClick?: () => void;
@@ -52,15 +53,17 @@ export function ConversationCard({
conversationId,
}: ConversationCardProps) {
const { t } = useTranslation();
+ const { parsedEvents } = useWsClient();
const [contextMenuVisible, setContextMenuVisible] = React.useState(false);
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
const [metricsModalVisible, setMetricsModalVisible] = React.useState(false);
const [systemModalVisible, setSystemModalVisible] = React.useState(false);
const inputRef = React.useRef(null);
+ const systemMessage = parsedEvents.find(isSystemMessage);
+
// Subscribe to metrics data from Redux store
const metrics = useSelector((state: RootState) => state.metrics);
- const systemMessage = useSelector(selectSystemMessage);
const handleBlur = () => {
if (inputRef.current?.value) {
@@ -365,7 +368,7 @@ export function ConversationCard({
setSystemModalVisible(false)}
- systemMessage={systemMessage}
+ systemMessage={systemMessage ? systemMessage.args : null}
/>
>
);
diff --git a/frontend/src/components/features/home/tasks/task-card.tsx b/frontend/src/components/features/home/tasks/task-card.tsx
index cddc6764f2..cb1f4c9a23 100644
--- a/frontend/src/components/features/home/tasks/task-card.tsx
+++ b/frontend/src/components/features/home/tasks/task-card.tsx
@@ -6,6 +6,7 @@ import { cn } from "#/utils/utils";
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
import { TaskIssueNumber } from "./task-issue-number";
import { Provider } from "#/types/settings";
+import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
const getTaskTypeMap = (
t: (key: string) => string,
@@ -21,6 +22,7 @@ interface TaskCardProps {
}
export function TaskCard({ task }: TaskCardProps) {
+ const { setOptimisticUserMessage } = useOptimisticUserMessage();
const { data: repositories } = useUserRepositories();
const { mutate: createConversation, isPending } = useCreateConversation();
const isCreatingConversation = useIsCreatingConversation();
@@ -38,6 +40,7 @@ export function TaskCard({ task }: TaskCardProps) {
const handleLaunchConversation = () => {
const repo = getRepo(task.repo, task.git_provider);
+ setOptimisticUserMessage("Addressing task...");
return createConversation({
selectedRepository: repo,
diff --git a/frontend/src/context/ws-client-provider.tsx b/frontend/src/context/ws-client-provider.tsx
index e11a267981..c7c7ff1a54 100644
--- a/frontend/src/context/ws-client-provider.tsx
+++ b/frontend/src/context/ws-client-provider.tsx
@@ -3,7 +3,7 @@ import { io, Socket } from "socket.io-client";
import { useQueryClient } from "@tanstack/react-query";
import EventLogger from "#/utils/event-logger";
import { handleAssistantMessage } from "#/services/actions";
-import { showChatError } from "#/utils/error-handler";
+import { showChatError, trackError } from "#/utils/error-handler";
import { useRate } from "#/hooks/use-rate";
import { OpenHandsParsedEvent } from "#/types/core";
import {
@@ -11,10 +11,26 @@ import {
CommandAction,
FileEditAction,
FileWriteAction,
+ OpenHandsAction,
UserMessageAction,
} from "#/types/core/actions";
import { Conversation } from "#/api/open-hands.types";
import { useUserProviders } from "#/hooks/use-user-providers";
+import { OpenHandsObservation } from "#/types/core/observations";
+import {
+ isErrorObservation,
+ isOpenHandsAction,
+ isOpenHandsObservation,
+ isUserMessage,
+} from "#/types/core/guards";
+import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
+import { useWSErrorMessage } from "#/hooks/use-ws-error-message";
+
+const hasValidMessageProperty = (obj: unknown): obj is { message: string } =>
+ typeof obj === "object" &&
+ obj !== null &&
+ "message" in obj &&
+ typeof obj.message === "string";
const isOpenHandsEvent = (event: unknown): event is OpenHandsParsedEvent =>
typeof event === "object" &&
@@ -35,14 +51,6 @@ const isFileEditAction = (
const isCommandAction = (event: OpenHandsParsedEvent): event is CommandAction =>
"action" in event && event.action === "run";
-const isUserMessage = (
- event: OpenHandsParsedEvent,
-): event is UserMessageAction =>
- "source" in event &&
- "type" in event &&
- event.source === "user" &&
- event.type === "message";
-
const isAssistantMessage = (
event: OpenHandsParsedEvent,
): event is AssistantMessageAction =>
@@ -65,6 +73,7 @@ interface UseWsClient {
status: WsClientProviderStatus;
isLoadingMessages: boolean;
events: Record[];
+ parsedEvents: (OpenHandsAction | OpenHandsObservation)[];
send: (event: Record) => void;
}
@@ -72,6 +81,7 @@ const WsClientContext = React.createContext({
status: WsClientProviderStatus.DISCONNECTED,
isLoadingMessages: true,
events: [],
+ parsedEvents: [],
send: () => {
throw new Error("not connected");
},
@@ -121,12 +131,17 @@ export function WsClientProvider({
conversationId,
children,
}: React.PropsWithChildren) {
+ const { removeOptimisticUserMessage } = useOptimisticUserMessage();
+ const { setErrorMessage, removeErrorMessage } = useWSErrorMessage();
const queryClient = useQueryClient();
const sioRef = React.useRef(null);
const [status, setStatus] = React.useState(
WsClientProviderStatus.DISCONNECTED,
);
const [events, setEvents] = React.useState[]>([]);
+ const [parsedEvents, setParsedEvents] = React.useState<
+ (OpenHandsAction | OpenHandsObservation)[]
+ >([]);
const lastEventRef = React.useRef | null>(null);
const { providers } = useUserProviders();
@@ -146,6 +161,24 @@ export function WsClientProvider({
function handleMessage(event: Record) {
if (isOpenHandsEvent(event)) {
+ if (isOpenHandsAction(event) || isOpenHandsObservation(event)) {
+ setParsedEvents((prevEvents) => [...prevEvents, event]);
+ }
+
+ if (isErrorObservation(event)) {
+ trackError({
+ message: event.message,
+ source: "chat",
+ metadata: { msgId: event.id },
+ });
+ } else {
+ removeErrorMessage();
+ }
+
+ if (isUserMessage(event)) {
+ removeOptimisticUserMessage();
+ }
+
if (isMessageAction(event)) {
messageRateHandler.record(new Date().getTime());
}
@@ -202,11 +235,23 @@ export function WsClientProvider({
sio.io.opts.query = sio.io.opts.query || {};
sio.io.opts.query.latest_event_id = lastEventRef.current?.id;
updateStatusWhenErrorMessagePresent(data);
+
+ setErrorMessage(
+ hasValidMessageProperty(data)
+ ? data.message
+ : "The WebSocket connection was closed.",
+ );
}
function handleError(data: unknown) {
setStatus(WsClientProviderStatus.DISCONNECTED);
updateStatusWhenErrorMessagePresent(data);
+
+ setErrorMessage(
+ hasValidMessageProperty(data)
+ ? data.message
+ : "An unknown error occurred on the WebSocket connection.",
+ );
}
React.useEffect(() => {
@@ -267,9 +312,10 @@ export function WsClientProvider({
status,
isLoadingMessages: messageRateHandler.isUnderThreshold,
events,
+ parsedEvents,
send,
}),
- [status, messageRateHandler.isUnderThreshold, events],
+ [status, messageRateHandler.isUnderThreshold, events, parsedEvents],
);
return {children};
diff --git a/frontend/src/hooks/use-handle-ws-events.ts b/frontend/src/hooks/use-handle-ws-events.ts
index 13a12cb02e..b665aed461 100644
--- a/frontend/src/hooks/use-handle-ws-events.ts
+++ b/frontend/src/hooks/use-handle-ws-events.ts
@@ -1,10 +1,7 @@
import React from "react";
-import { useDispatch } from "react-redux";
import { useWsClient } from "#/context/ws-client-provider";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
-import { addErrorMessage } from "#/state/chat-slice";
import { AgentState } from "#/types/agent-state";
-import { ErrorObservation } from "#/types/core/observations";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
interface ServerError {
@@ -15,12 +12,8 @@ interface ServerError {
const isServerError = (data: object): data is ServerError => "error" in data;
-const isErrorObservation = (data: object): data is ErrorObservation =>
- "observation" in data && data.observation === "error";
-
export const useHandleWSEvents = () => {
const { events, send } = useWsClient();
- const dispatch = useDispatch();
React.useEffect(() => {
if (!events.length) {
@@ -49,14 +42,5 @@ export const useHandleWSEvents = () => {
send(generateAgentStateChangeEvent(AgentState.PAUSED));
}
}
-
- if (isErrorObservation(event)) {
- dispatch(
- addErrorMessage({
- id: event.extras?.error_id,
- message: event.message,
- }),
- );
- }
}, [events.length]);
};
diff --git a/frontend/src/hooks/use-optimistic-user-message.ts b/frontend/src/hooks/use-optimistic-user-message.ts
new file mode 100644
index 0000000000..33cbad7d93
--- /dev/null
+++ b/frontend/src/hooks/use-optimistic-user-message.ts
@@ -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(queryKey, message);
+ };
+
+ const getOptimisticUserMessage = () =>
+ queryClient.getQueryData(queryKey);
+
+ const removeOptimisticUserMessage = () => {
+ queryClient.removeQueries({ queryKey });
+ };
+
+ return {
+ setOptimisticUserMessage,
+ getOptimisticUserMessage,
+ removeOptimisticUserMessage,
+ };
+};
diff --git a/frontend/src/hooks/use-ws-error-message.ts b/frontend/src/hooks/use-ws-error-message.ts
new file mode 100644
index 0000000000..370804b7b0
--- /dev/null
+++ b/frontend/src/hooks/use-ws-error-message.ts
@@ -0,0 +1,22 @@
+import { useQueryClient } from "@tanstack/react-query";
+
+export const useWSErrorMessage = () => {
+ const queryClient = useQueryClient();
+
+ const setErrorMessage = (message: string) => {
+ queryClient.setQueryData(["error_message"], message);
+ };
+
+ const getErrorMessage = () =>
+ queryClient.getQueryData(["error_message"]);
+
+ const removeErrorMessage = () => {
+ queryClient.removeQueries({ queryKey: ["error_message"] });
+ };
+
+ return {
+ setErrorMessage,
+ getErrorMessage,
+ removeErrorMessage,
+ };
+};
diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json
index e90305d073..d3556e35bb 100644
--- a/frontend/src/i18n/translation.json
+++ b/frontend/src/i18n/translation.json
@@ -6384,20 +6384,20 @@
"uk": "Завантажити файл"
},
"ACTION_MESSAGE$RUN": {
- "en": "Running {{action.payload.args.command}}",
- "zh-CN": "运行 {{action.payload.args.command}}",
- "zh-TW": "執行 {{action.payload.args.command}}",
- "ko-KR": "실행 {{action.payload.args.command}}",
- "ja": "実行 {{action.payload.args.command}}",
- "no": "Kjører {{action.payload.args.command}}",
- "ar": "تشغيل {{action.payload.args.command}}",
- "de": "Führt {{action.payload.args.command}} aus",
- "fr": "Exécution de {{action.payload.args.command}}",
- "it": "Esecuzione di {{action.payload.args.command}}",
- "pt": "Executando {{action.payload.args.command}}",
- "es": "Ejecutando {{action.payload.args.command}}",
- "tr": "{{action.payload.args.command}} çalıştırılıyor",
- "uk": "Виконую {{action.payload.args.command}}"
+ "en": "Running {{command}}",
+ "zh-CN": "运行 {{command}}",
+ "zh-TW": "執行 {{command}}",
+ "ko-KR": "실행 {{command}}",
+ "ja": "実行 {{command}}",
+ "no": "Kjører {{command}}",
+ "ar": "تشغيل {{command}}",
+ "de": "Führt {{command}} aus",
+ "fr": "Exécution de {{command}}",
+ "it": "Esecuzione di {{command}}",
+ "pt": "Executando {{command}}",
+ "es": "Ejecutando {{command}}",
+ "tr": "{{command}} çalıştırılıyor",
+ "uk": "Виконую {{command}}"
},
"ACTION_MESSAGE$RUN_IPYTHON": {
"en": "Running a Python command",
@@ -6432,52 +6432,52 @@
"uk": "Викликаю інструмент MCP: {{action.payload.args.name}}"
},
"ACTION_MESSAGE$READ": {
- "en": "Reading {{action.payload.args.path}}",
- "zh-CN": "读取 {{action.payload.args.path}}",
- "zh-TW": "讀取 {{action.payload.args.path}}",
- "ko-KR": "읽기 {{action.payload.args.path}}",
- "ja": "読み取り {{action.payload.args.path}}",
- "no": "Leser {{action.payload.args.path}}",
- "ar": "قراءة {{action.payload.args.path}}",
- "de": "Liest {{action.payload.args.path}}",
- "fr": "Lecture de {{action.payload.args.path}}",
- "it": "Lettura di {{action.payload.args.path}}",
- "pt": "Lendo {{action.payload.args.path}}",
- "es": "Leyendo {{action.payload.args.path}}",
- "tr": "{{action.payload.args.path}} okunuyor",
- "uk": "Читаю {{action.payload.args.path}}"
+ "en": "Reading {{path}}",
+ "zh-CN": "读取 {{path}}",
+ "zh-TW": "讀取 {{path}}",
+ "ko-KR": "읽기 {{path}}",
+ "ja": "読み取り {{path}}",
+ "no": "Leser {{path}}",
+ "ar": "قراءة {{path}}",
+ "de": "Liest {{path}}",
+ "fr": "Lecture de {{path}}",
+ "it": "Lettura di {{path}}",
+ "pt": "Lendo {{path}}",
+ "es": "Leyendo {{path}}",
+ "tr": "{{path}} okunuyor",
+ "uk": "Читаю {{path}}"
},
"ACTION_MESSAGE$EDIT": {
- "en": "Editing {{action.payload.args.path}}",
- "zh-CN": "编辑 {{action.payload.args.path}}",
- "zh-TW": "編輯 {{action.payload.args.path}}",
- "ko-KR": "편집 {{action.payload.args.path}}",
- "ja": "編集 {{action.payload.args.path}}",
- "no": "Redigerer {{action.payload.args.path}}",
- "ar": "تحرير {{action.payload.args.path}}",
- "de": "Bearbeitet {{action.payload.args.path}}",
- "fr": "Modification de {{action.payload.args.path}}",
- "it": "Modifica di {{action.payload.args.path}}",
- "pt": "Editando {{action.payload.args.path}}",
- "es": "Editando {{action.payload.args.path}}",
- "tr": "{{action.payload.args.path}} düzenleniyor",
- "uk": "Редагую {{action.payload.args.path}}"
+ "en": "Editing {{path}}",
+ "zh-CN": "编辑 {{path}}",
+ "zh-TW": "編輯 {{path}}",
+ "ko-KR": "편집 {{path}}",
+ "ja": "編集 {{path}}",
+ "no": "Redigerer {{path}}",
+ "ar": "تحرير {{path}}",
+ "de": "Bearbeitet {{path}}",
+ "fr": "Modification de {{path}}",
+ "it": "Modifica di {{path}}",
+ "pt": "Editando {{path}}",
+ "es": "Editando {{path}}",
+ "tr": "{{path}} düzenleniyor",
+ "uk": "Редагую {{path}}"
},
"ACTION_MESSAGE$WRITE": {
- "en": "Writing to {{action.payload.args.path}}",
- "zh-CN": "写入 {{action.payload.args.path}}",
- "zh-TW": "寫入 {{action.payload.args.path}}",
- "ko-KR": "쓰기 {{action.payload.args.path}}",
- "ja": "書き込み {{action.payload.args.path}}",
- "no": "Skriver til {{action.payload.args.path}}",
- "ar": "الكتابة إلى {{action.payload.args.path}}",
- "de": "Schreibt in {{action.payload.args.path}}",
- "fr": "Écriture dans {{action.payload.args.path}}",
- "it": "Scrittura su {{action.payload.args.path}}",
- "pt": "Escrevendo em {{action.payload.args.path}}",
- "es": "Escribiendo en {{action.payload.args.path}}",
- "tr": "{{action.payload.args.path}} dosyasına yazılıyor",
- "uk": "Записую в {{action.payload.args.path}}"
+ "en": "Writing to {{path}}",
+ "zh-CN": "写入 {{path}}",
+ "zh-TW": "寫入 {{path}}",
+ "ko-KR": "쓰기 {{path}}",
+ "ja": "書き込み {{path}}",
+ "no": "Skriver til {{path}}",
+ "ar": "الكتابة إلى {{path}}",
+ "de": "Schreibt in {{path}}",
+ "fr": "Écriture dans {{path}}",
+ "it": "Scrittura su {{path}}",
+ "pt": "Escrevendo em {{path}}",
+ "es": "Escribiendo en {{path}}",
+ "tr": "{{path}} dosyasına yazılıyor",
+ "uk": "Записую в {{path}}"
},
"ACTION_MESSAGE$BROWSE": {
"en": "Browsing the web",
@@ -6544,20 +6544,20 @@
"uk": "Системне повідомлення"
},
"OBSERVATION_MESSAGE$RUN": {
- "en": "Ran {{observation.payload.extras.command}}",
- "zh-CN": "运行 {{observation.payload.extras.command}}",
- "zh-TW": "執行 {{observation.payload.extras.command}}",
- "ko-KR": "실행 {{observation.payload.extras.command}}",
- "ja": "実行 {{observation.payload.extras.command}}",
- "no": "Kjørte {{observation.payload.extras.command}}",
- "ar": "تم تشغيل {{observation.payload.extras.command}}",
- "de": "Führte {{observation.payload.extras.command}} aus",
- "fr": "A exécuté {{observation.payload.extras.command}}",
- "it": "Ha eseguito {{observation.payload.extras.command}}",
- "pt": "Executou {{observation.payload.extras.command}}",
- "es": "Ejecutó {{observation.payload.extras.command}}",
- "tr": "{{observation.payload.extras.command}} çalıştırıldı",
- "uk": "Запустив {{observation.payload.extras.command}}"
+ "en": "Ran {{command}}",
+ "zh-CN": "运行 {{command}}",
+ "zh-TW": "執行 {{command}}",
+ "ko-KR": "실행 {{command}}",
+ "ja": "実行 {{command}}",
+ "no": "Kjørte {{command}}",
+ "ar": "تم تشغيل {{command}}",
+ "de": "Führte {{command}} aus",
+ "fr": "A exécuté {{command}}",
+ "it": "Ha eseguito {{command}}",
+ "pt": "Executou {{command}}",
+ "es": "Ejecutó {{command}}",
+ "tr": "{{command}} çalıştırıldı",
+ "uk": "Запустив {{command}}"
},
"OBSERVATION_MESSAGE$RUN_IPYTHON": {
"en": "Ran a Python command",
@@ -6576,52 +6576,52 @@
"uk": "Виконав команду Python"
},
"OBSERVATION_MESSAGE$READ": {
- "en": "Read {{observation.payload.extras.path}}",
- "zh-CN": "读取 {{observation.payload.extras.path}}",
- "zh-TW": "讀取 {{observation.payload.extras.path}}",
- "ko-KR": "읽기 {{observation.payload.extras.path}}",
- "ja": "読み取り {{observation.payload.extras.path}}",
- "no": "Leste {{observation.payload.extras.path}}",
- "ar": "تمت قراءة {{observation.payload.extras.path}}",
- "de": "Las {{observation.payload.extras.path}}",
- "fr": "A lu {{observation.payload.extras.path}}",
- "it": "Ha letto {{observation.payload.extras.path}}",
- "pt": "Leu {{observation.payload.extras.path}}",
- "es": "Leyó {{observation.payload.extras.path}}",
- "tr": "{{observation.payload.extras.path}} okundu",
- "uk": "Прочитав {{observation.payload.extras.path}}"
+ "en": "Read {{path}}",
+ "zh-CN": "读取 {{path}}",
+ "zh-TW": "讀取 {{path}}",
+ "ko-KR": "읽기 {{path}}",
+ "ja": "読み取り {{path}}",
+ "no": "Leste {{path}}",
+ "ar": "تمت قراءة {{path}}",
+ "de": "Las {{path}}",
+ "fr": "A lu {{path}}",
+ "it": "Ha letto {{path}}",
+ "pt": "Leu {{path}}",
+ "es": "Leyó {{path}}",
+ "tr": "{{path}} okundu",
+ "uk": "Прочитав {{path}}"
},
"OBSERVATION_MESSAGE$EDIT": {
- "en": "Edited {{observation.payload.extras.path}}",
- "zh-CN": "编辑 {{observation.payload.extras.path}}",
- "zh-TW": "編輯 {{observation.payload.extras.path}}",
- "ko-KR": "편집 {{observation.payload.extras.path}}",
- "ja": "編集 {{observation.payload.extras.path}}",
- "no": "Redigerte {{observation.payload.extras.path}}",
- "ar": "تم تحرير {{observation.payload.extras.path}}",
- "de": "Hat {{observation.payload.extras.path}} bearbeitet",
- "fr": "A modifié {{observation.payload.extras.path}}",
- "it": "Ha modificato {{observation.payload.extras.path}}",
- "pt": "Editou {{observation.payload.extras.path}}",
- "es": "Editó {{observation.payload.extras.path}}",
- "tr": "{{observation.payload.extras.path}} düzenlendi",
- "uk": "Відредагував {{observation.payload.extras.path}}"
+ "en": "Edited {{path}}",
+ "zh-CN": "编辑 {{path}}",
+ "zh-TW": "編輯 {{path}}",
+ "ko-KR": "편집 {{path}}",
+ "ja": "編集 {{path}}",
+ "no": "Redigerte {{path}}",
+ "ar": "تم تحرير {{path}}",
+ "de": "Hat {{path}} bearbeitet",
+ "fr": "A modifié {{path}}",
+ "it": "Ha modificato {{path}}",
+ "pt": "Editou {{path}}",
+ "es": "Editó {{path}}",
+ "tr": "{{path}} düzenlendi",
+ "uk": "Відредагував {{path}}"
},
"OBSERVATION_MESSAGE$WRITE": {
- "en": "Wrote to {{observation.payload.extras.path}}",
- "zh-CN": "写入 {{observation.payload.extras.path}}",
- "zh-TW": "寫入 {{observation.payload.extras.path}}",
- "ko-KR": "쓰기 {{observation.payload.extras.path}}",
- "ja": "書き込み {{observation.payload.extras.path}}",
- "no": "Skrev til {{observation.payload.extras.path}}",
- "ar": "تمت الكتابة إلى {{observation.payload.extras.path}}",
- "de": "Hat in {{observation.payload.extras.path}} geschrieben",
- "fr": "A écrit dans {{observation.payload.extras.path}}",
- "it": "Ha scritto su {{observation.payload.extras.path}}",
- "pt": "Escreveu em {{observation.payload.extras.path}}",
- "es": "Escribió en {{observation.payload.extras.path}}",
- "tr": "{{observation.payload.extras.path}} dosyasına yazıldı",
- "uk": "Записав на {{observation.payload.extras.path}}"
+ "en": "Wrote to {{path}}",
+ "zh-CN": "写入 {{path}}",
+ "zh-TW": "寫入 {{path}}",
+ "ko-KR": "쓰기 {{path}}",
+ "ja": "書き込み {{path}}",
+ "no": "Skrev til {{path}}",
+ "ar": "تمت الكتابة إلى {{path}}",
+ "de": "Hat in {{path}} geschrieben",
+ "fr": "A écrit dans {{path}}",
+ "it": "Ha scritto su {{path}}",
+ "pt": "Escreveu em {{path}}",
+ "es": "Escribió en {{path}}",
+ "tr": "{{path}} dosyasına yazıldı",
+ "uk": "Записав на {{path}}"
},
"OBSERVATION_MESSAGE$BROWSE": {
"en": "Browsing completed",
diff --git a/frontend/src/routes/conversation.tsx b/frontend/src/routes/conversation.tsx
index f75b891034..d3f55511ed 100644
--- a/frontend/src/routes/conversation.tsx
+++ b/frontend/src/routes/conversation.tsx
@@ -13,7 +13,6 @@ import {
useConversation,
} from "#/context/conversation-context";
import { Controls } from "#/components/features/controls/controls";
-import { clearMessages, addUserMessage } from "#/state/chat-slice";
import { clearTerminal } from "#/state/command-slice";
import { useEffectOnce } from "#/hooks/use-effect-once";
import GlobeIcon from "#/icons/globe.svg?react";
@@ -34,7 +33,6 @@ import Security from "#/components/shared/modals/security/security";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
import { ServedAppLabel } from "#/components/layout/served-app-label";
import { useSettings } from "#/hooks/query/use-settings";
-import { clearFiles, clearInitialPrompt } from "#/state/initial-query-slice";
import { RootState } from "#/store";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state";
@@ -49,9 +47,7 @@ function AppContent() {
const { data: conversation, isFetched } = useUserConversation(
conversationId || null,
);
- const { initialPrompt, files } = useSelector(
- (state: RootState) => state.initialQuery,
- );
+
const { curAgentState } = useSelector((state: RootState) => state.agent);
const dispatch = useDispatch();
const navigate = useNavigate();
@@ -71,25 +67,11 @@ function AppContent() {
}, [conversation, isFetched]);
React.useEffect(() => {
- dispatch(clearMessages());
dispatch(clearTerminal());
dispatch(clearJupyter());
- if (conversationId && (initialPrompt || files.length > 0)) {
- dispatch(
- addUserMessage({
- content: initialPrompt || "",
- imageUrls: files || [],
- timestamp: new Date().toISOString(),
- pending: true,
- }),
- );
- dispatch(clearInitialPrompt());
- dispatch(clearFiles());
- }
}, [conversationId]);
useEffectOnce(() => {
- dispatch(clearMessages());
dispatch(clearTerminal());
dispatch(clearJupyter());
});
diff --git a/frontend/src/services/__tests__/actions.test.ts b/frontend/src/services/__tests__/actions.test.ts
index fc071ce911..20a834a5d4 100644
--- a/frontend/src/services/__tests__/actions.test.ts
+++ b/frontend/src/services/__tests__/actions.test.ts
@@ -4,7 +4,6 @@ import { StatusMessage } from "#/types/message";
import { queryClient } from "#/query-client-config";
import store from "#/store";
import { setCurStatusMessage } from "#/state/status-slice";
-import { addErrorMessage } from "#/state/chat-slice";
import { trackError } from "#/utils/error-handler";
// Mock dependencies
@@ -101,9 +100,6 @@ describe("handleStatusMessage", () => {
metadata: { msgId: "ERROR_ID" },
});
- // Verify that store.dispatch was called with addErrorMessage
- expect(store.dispatch).toHaveBeenCalledWith(addErrorMessage(statusMessage));
-
// Verify that queryClient.invalidateQueries was not called
expect(queryClient.invalidateQueries).not.toHaveBeenCalled();
});
diff --git a/frontend/src/services/actions.ts b/frontend/src/services/actions.ts
index 18850e6f41..126b4dac40 100644
--- a/frontend/src/services/actions.ts
+++ b/frontend/src/services/actions.ts
@@ -1,13 +1,5 @@
-import {
- addAssistantMessage,
- addAssistantAction,
- addUserMessage,
- addErrorMessage,
-} from "#/state/chat-slice";
import { trackError } from "#/utils/error-handler";
import { appendSecurityAnalyzerInput } from "#/state/security-analyzer-slice";
-import { setCode, setActiveFilepath } from "#/state/code-slice";
-import { appendJupyterInput } from "#/state/jupyter-slice";
import { setCurStatusMessage } from "#/state/status-slice";
import { setMetrics } from "#/state/metrics-slice";
import store from "#/store";
@@ -21,67 +13,6 @@ import { handleObservationMessage } from "./observations";
import { appendInput } from "#/state/command-slice";
import { queryClient } from "#/query-client-config";
-const messageActions = {
- [ActionType.BROWSE]: (message: ActionMessage) => {
- if (!message.args.thought && message.message) {
- store.dispatch(addAssistantMessage(message.message));
- }
- },
- [ActionType.BROWSE_INTERACTIVE]: (message: ActionMessage) => {
- if (!message.args.thought && message.message) {
- store.dispatch(addAssistantMessage(message.message));
- }
- },
- [ActionType.WRITE]: (message: ActionMessage) => {
- const { path, content } = message.args;
- store.dispatch(setActiveFilepath(path));
- store.dispatch(setCode(content));
- },
- [ActionType.MESSAGE]: (message: ActionMessage) => {
- if (message.source === "user") {
- store.dispatch(
- addUserMessage({
- content: message.args.content,
- imageUrls:
- typeof message.args.image_urls === "string"
- ? [message.args.image_urls]
- : message.args.image_urls,
- timestamp: message.timestamp,
- pending: false,
- }),
- );
- } else {
- store.dispatch(addAssistantMessage(message.args.content));
- }
- },
- [ActionType.RUN_IPYTHON]: (message: ActionMessage) => {
- if (message.args.confirmation_state !== "rejected") {
- store.dispatch(appendJupyterInput(message.args.code));
- }
- },
- [ActionType.FINISH]: (message: ActionMessage) => {
- store.dispatch(addAssistantMessage(message.args.final_thought));
- let successPrediction = "";
- if (message.args.task_completed === "partial") {
- successPrediction =
- "I believe that the task was **completed partially**.";
- } else if (message.args.task_completed === "false") {
- successPrediction = "I believe that the task was **not completed**.";
- } else if (message.args.task_completed === "true") {
- successPrediction =
- "I believe that the task was **completed successfully**.";
- }
- if (successPrediction) {
- // if final_thought is not empty, add a new line before the success prediction
- if (message.args.final_thought) {
- store.dispatch(addAssistantMessage(`\n${successPrediction}`));
- } else {
- store.dispatch(addAssistantMessage(successPrediction));
- }
- }
- },
-};
-
export function handleActionMessage(message: ActionMessage) {
if (message.args?.hidden) {
return;
@@ -103,26 +34,6 @@ export function handleActionMessage(message: ActionMessage) {
if ("args" in message && "security_risk" in message.args) {
store.dispatch(appendSecurityAnalyzerInput(message));
}
-
- if (message.source === "agent") {
- // Only add thought as a message if it's not a "think" action
- if (
- message.args &&
- message.args.thought &&
- message.action !== ActionType.THINK
- ) {
- store.dispatch(addAssistantMessage(message.args.thought));
- }
- // Need to convert ActionMessage to RejectAction
- // @ts-expect-error TODO: fix
- store.dispatch(addAssistantAction(message));
- }
-
- if (message.action in messageActions) {
- const actionFn =
- messageActions[message.action as keyof typeof messageActions];
- actionFn(message);
- }
}
export function handleStatusMessage(message: StatusMessage) {
@@ -146,11 +57,6 @@ export function handleStatusMessage(message: StatusMessage) {
source: "chat",
metadata: { msgId: message.id },
});
- store.dispatch(
- addErrorMessage({
- ...message,
- }),
- );
}
}
@@ -161,33 +67,5 @@ export function handleAssistantMessage(message: Record) {
handleObservationMessage(message as unknown as ObservationMessage);
} else if (message.status_update) {
handleStatusMessage(message as unknown as StatusMessage);
- } else if (message.error) {
- // Handle error messages from the server
- const errorMessage =
- typeof message.message === "string"
- ? message.message
- : String(message.message || "Unknown error");
- trackError({
- message: errorMessage,
- source: "websocket",
- metadata: { raw_message: message },
- });
- store.dispatch(
- addErrorMessage({
- message: errorMessage,
- }),
- );
- } else {
- const errorMsg = "Unknown message type received";
- trackError({
- message: errorMsg,
- source: "chat",
- metadata: { raw_message: message },
- });
- store.dispatch(
- addErrorMessage({
- message: errorMsg,
- }),
- );
}
}
diff --git a/frontend/src/services/observations.ts b/frontend/src/services/observations.ts
index 2951915109..ab5776e939 100644
--- a/frontend/src/services/observations.ts
+++ b/frontend/src/services/observations.ts
@@ -2,14 +2,9 @@ import { setCurrentAgentState } from "#/state/agent-slice";
import { setUrl, setScreenshotSrc } from "#/state/browser-slice";
import store from "#/store";
import { ObservationMessage } from "#/types/message";
-import { AgentState } from "#/types/agent-state";
import { appendOutput } from "#/state/command-slice";
import { appendJupyterOutput } from "#/state/jupyter-slice";
import ObservationType from "#/types/observation-type";
-import {
- addAssistantMessage,
- addAssistantObservation,
-} from "#/state/chat-slice";
export function handleObservationMessage(message: ObservationMessage) {
switch (message.observation) {
@@ -48,11 +43,6 @@ export function handleObservationMessage(message: ObservationMessage) {
store.dispatch(setCurrentAgentState(message.extras.agent_state));
break;
case ObservationType.DELEGATE:
- // TODO: better UI for delegation result (#2309)
- if (message.content) {
- store.dispatch(addAssistantMessage(message.content));
- }
- break;
case ObservationType.READ:
case ObservationType.EDIT:
case ObservationType.THINK:
@@ -62,110 +52,13 @@ export function handleObservationMessage(message: ObservationMessage) {
case ObservationType.MCP:
break; // We don't display the default message for these observations
default:
- store.dispatch(addAssistantMessage(message.message));
break;
}
if (!message.extras?.hidden) {
// Convert the message to the appropriate observation type
const { observation } = message;
- const baseObservation = {
- ...message,
- source: "agent" as const,
- };
switch (observation) {
- case "agent_state_changed":
- store.dispatch(
- addAssistantObservation({
- ...baseObservation,
- observation: "agent_state_changed" as const,
- extras: {
- agent_state: (message.extras.agent_state as AgentState) || "idle",
- },
- }),
- );
- break;
- case "recall":
- store.dispatch(
- addAssistantObservation({
- ...baseObservation,
- observation: "recall" as const,
- extras: {
- ...(message.extras || {}),
- recall_type:
- (message.extras?.recall_type as
- | "workspace_context"
- | "knowledge") || "knowledge",
- },
- }),
- );
- break;
- case "run":
- store.dispatch(
- addAssistantObservation({
- ...baseObservation,
- observation: "run" as const,
- extras: {
- command: String(message.extras.command || ""),
- metadata: message.extras.metadata,
- hidden: Boolean(message.extras.hidden),
- },
- }),
- );
- break;
- case "read":
- store.dispatch(
- addAssistantObservation({
- ...baseObservation,
- observation,
- extras: {
- path: String(message.extras.path || ""),
- impl_source: String(message.extras.impl_source || ""),
- },
- }),
- );
- break;
- case "edit":
- store.dispatch(
- addAssistantObservation({
- ...baseObservation,
- observation,
- extras: {
- path: String(message.extras.path || ""),
- diff: String(message.extras.diff || ""),
- impl_source: String(message.extras.impl_source || ""),
- },
- }),
- );
- break;
- case "run_ipython":
- store.dispatch(
- addAssistantObservation({
- ...baseObservation,
- observation: "run_ipython" as const,
- extras: {
- code: String(message.extras.code || ""),
- image_urls: Array.isArray(message.extras.image_urls)
- ? message.extras.image_urls
- : [],
- },
- }),
- );
- break;
- case "delegate":
- store.dispatch(
- addAssistantObservation({
- ...baseObservation,
- observation: "delegate" as const,
- extras: {
- outputs:
- typeof message.extras.outputs === "object"
- ? (message.extras.outputs as Record)
- : {},
- },
- }),
- );
- break;
case "browse":
if (message.extras?.screenshot) {
store.dispatch(setScreenshotSrc(message.extras.screenshot));
@@ -173,45 +66,6 @@ export function handleObservationMessage(message: ObservationMessage) {
if (message.extras?.url) {
store.dispatch(setUrl(message.extras.url));
}
-
- store.dispatch(
- addAssistantObservation({
- ...baseObservation,
- observation: "browse" as const,
- extras: {
- url: String(message.extras.url || ""),
- screenshot: String(message.extras.screenshot || ""),
- error: Boolean(message.extras.error),
- open_page_urls: Array.isArray(message.extras.open_page_urls)
- ? message.extras.open_page_urls
- : [],
- active_page_index: Number(message.extras.active_page_index || 0),
- dom_object:
- typeof message.extras.dom_object === "object"
- ? (message.extras.dom_object as Record)
- : {},
- axtree_object:
- typeof message.extras.axtree_object === "object"
- ? (message.extras.axtree_object as Record)
- : {},
- extra_element_properties:
- typeof message.extras.extra_element_properties === "object"
- ? (message.extras.extra_element_properties as Record<
- string,
- unknown
- >)
- : {},
- last_browser_action: String(
- message.extras.last_browser_action || "",
- ),
- last_browser_action_error:
- message.extras.last_browser_action_error,
- focused_element_bid: String(
- message.extras.focused_element_bid || "",
- ),
- },
- }),
- );
break;
case "browse_interactive":
if (message.extras?.screenshot) {
@@ -220,65 +74,6 @@ export function handleObservationMessage(message: ObservationMessage) {
if (message.extras?.url) {
store.dispatch(setUrl(message.extras.url));
}
-
- store.dispatch(
- addAssistantObservation({
- ...baseObservation,
- observation: "browse_interactive" as const,
- extras: {
- url: String(message.extras.url || ""),
- screenshot: String(message.extras.screenshot || ""),
- error: Boolean(message.extras.error),
- open_page_urls: Array.isArray(message.extras.open_page_urls)
- ? message.extras.open_page_urls
- : [],
- active_page_index: Number(message.extras.active_page_index || 0),
- dom_object:
- typeof message.extras.dom_object === "object"
- ? (message.extras.dom_object as Record)
- : {},
- axtree_object:
- typeof message.extras.axtree_object === "object"
- ? (message.extras.axtree_object as Record)
- : {},
- extra_element_properties:
- typeof message.extras.extra_element_properties === "object"
- ? (message.extras.extra_element_properties as Record<
- string,
- unknown
- >)
- : {},
- last_browser_action: String(
- message.extras.last_browser_action || "",
- ),
- last_browser_action_error:
- message.extras.last_browser_action_error,
- focused_element_bid: String(
- message.extras.focused_element_bid || "",
- ),
- },
- }),
- );
- break;
- case "error":
- store.dispatch(
- addAssistantObservation({
- ...baseObservation,
- observation: "error" as const,
- source: "user" as const,
- extras: {
- error_id: message.extras.error_id,
- },
- }),
- );
- break;
- case "mcp":
- store.dispatch(
- addAssistantObservation({
- ...baseObservation,
- observation: "mcp" as const,
- }),
- );
break;
default:
// For any unhandled observation types, just ignore them
diff --git a/frontend/src/state/chat-slice.ts b/frontend/src/state/chat-slice.ts
deleted file mode 100644
index 8b0e19a433..0000000000
--- a/frontend/src/state/chat-slice.ts
+++ /dev/null
@@ -1,380 +0,0 @@
-import { createSlice, PayloadAction } from "@reduxjs/toolkit";
-import type { Message } from "#/message";
-
-import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
-import { OpenHandsAction } from "#/types/core/actions";
-import { OpenHandsEventType } from "#/types/core/base";
-import {
- CommandObservation,
- IPythonObservation,
- OpenHandsObservation,
- RecallObservation,
-} from "#/types/core/observations";
-
-type SliceState = {
- messages: Message[];
- systemMessage: {
- content: string;
- tools: Array> | null;
- openhands_version: string | null;
- agent_class: string | null;
- } | null;
-};
-
-const MAX_CONTENT_LENGTH = 1000;
-
-const HANDLED_ACTIONS: OpenHandsEventType[] = [
- "run",
- "run_ipython",
- "write",
- "read",
- "browse",
- "browse_interactive",
- "edit",
- "recall",
- "think",
- "system",
- "call_tool_mcp",
- "mcp",
-];
-
-function getRiskText(risk: ActionSecurityRisk) {
- switch (risk) {
- case ActionSecurityRisk.LOW:
- return "Low Risk";
- case ActionSecurityRisk.MEDIUM:
- return "Medium Risk";
- case ActionSecurityRisk.HIGH:
- return "High Risk";
- case ActionSecurityRisk.UNKNOWN:
- default:
- return "Unknown Risk";
- }
-}
-
-const initialState: SliceState = {
- messages: [],
- systemMessage: null,
-};
-
-export const chatSlice = createSlice({
- name: "chat",
- initialState,
- reducers: {
- addUserMessage(
- state,
- action: PayloadAction<{
- content: string;
- imageUrls: string[];
- timestamp: string;
- pending?: boolean;
- }>,
- ) {
- const message: Message = {
- type: "thought",
- sender: "user",
- content: action.payload.content,
- imageUrls: action.payload.imageUrls,
- timestamp: action.payload.timestamp || new Date().toISOString(),
- pending: !!action.payload.pending,
- };
- // Remove any pending messages
- let i = state.messages.length;
- while (i) {
- i -= 1;
- const m = state.messages[i] as Message;
- if (m.pending) {
- state.messages.splice(i, 1);
- }
- }
- state.messages.push(message);
- },
-
- addAssistantMessage(state: SliceState, action: PayloadAction) {
- const message: Message = {
- type: "thought",
- sender: "assistant",
- content: action.payload,
- imageUrls: [],
- timestamp: new Date().toISOString(),
- pending: false,
- };
- state.messages.push(message);
- },
-
- addAssistantAction(
- state: SliceState,
- action: PayloadAction,
- ) {
- const actionID = action.payload.action;
- if (!HANDLED_ACTIONS.includes(actionID)) {
- return;
- }
- const translationID = `ACTION_MESSAGE$${actionID.toUpperCase()}`;
- let text = "";
-
- if (actionID === "system") {
- // Store the system message in the state
- state.systemMessage = {
- content: action.payload.args.content,
- tools: action.payload.args.tools,
- openhands_version: action.payload.args.openhands_version,
- agent_class: action.payload.args.agent_class,
- };
- // Don't add a message for system actions
- return;
- }
- if (actionID === "run") {
- text = `Command:\n\`${action.payload.args.command}\``;
- } else if (actionID === "run_ipython") {
- text = `\`\`\`\n${action.payload.args.code}\n\`\`\``;
- } else if (actionID === "write") {
- let { content } = action.payload.args;
- if (content.length > MAX_CONTENT_LENGTH) {
- content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
- }
- text = `${action.payload.args.path}\n${content}`;
- } else if (actionID === "browse") {
- text = `Browsing ${action.payload.args.url}`;
- } else if (actionID === "browse_interactive") {
- // Include the browser_actions in the content
- text = `**Action:**\n\n\`\`\`python\n${action.payload.args.browser_actions}\n\`\`\``;
- } else if (actionID === "recall") {
- // skip recall actions
- return;
- } else if (actionID === "call_tool_mcp") {
- // Format MCP action with name and arguments
- const name = action.payload.args.name || "";
- const args = action.payload.args.arguments || {};
- text = `**MCP Tool Call:** ${name}\n\n`;
- // Include thought if available
- if (action.payload.args.thought) {
- text += `\n\n**Thought:**\n${action.payload.args.thought}`;
- }
- text += `\n\n**Arguments:**\n\`\`\`json\n${JSON.stringify(args, null, 2)}\n\`\`\``;
- }
- if (actionID === "run" || actionID === "run_ipython") {
- if (
- action.payload.args.confirmation_state === "awaiting_confirmation"
- ) {
- text += `\n\n${getRiskText(action.payload.args.security_risk as unknown as ActionSecurityRisk)}`;
- }
- } else if (actionID === "think") {
- text = action.payload.args.thought;
- }
- const message: Message = {
- type: "action",
- sender: "assistant",
- translationID,
- eventID: action.payload.id,
- content: text,
- imageUrls: [],
- timestamp: new Date().toISOString(),
- action,
- };
-
- state.messages.push(message);
- },
-
- addAssistantObservation(
- state: SliceState,
- observation: PayloadAction,
- ) {
- const observationID = observation.payload.observation;
- if (!HANDLED_ACTIONS.includes(observationID)) {
- return;
- }
-
- // Special handling for RecallObservation - create a new message instead of updating an existing one
- if (observationID === "recall") {
- const recallObs = observation.payload as RecallObservation;
- let content = ``;
-
- // Handle workspace context
- if (recallObs.extras.recall_type === "workspace_context") {
- if (recallObs.extras.repo_name) {
- content += `\n\n**Repository:** ${recallObs.extras.repo_name}`;
- }
- if (recallObs.extras.repo_directory) {
- content += `\n\n**Directory:** ${recallObs.extras.repo_directory}`;
- }
- if (recallObs.extras.date) {
- content += `\n\n**Date:** ${recallObs.extras.date}`;
- }
- if (
- recallObs.extras.runtime_hosts &&
- Object.keys(recallObs.extras.runtime_hosts).length > 0
- ) {
- content += `\n\n**Available Hosts**`;
- for (const [host, port] of Object.entries(
- recallObs.extras.runtime_hosts,
- )) {
- content += `\n\n- ${host} (port ${port})`;
- }
- }
- if (
- recallObs.extras.custom_secrets_descriptions &&
- Object.keys(recallObs.extras.custom_secrets_descriptions).length > 0
- ) {
- content += `\n\n**Custom Secrets**`;
- for (const [name, description] of Object.entries(
- recallObs.extras.custom_secrets_descriptions,
- )) {
- content += `\n\n- $${name}: ${description}`;
- }
- }
- if (recallObs.extras.repo_instructions) {
- content += `\n\n**Repository Instructions:**\n\n${recallObs.extras.repo_instructions}`;
- }
- if (recallObs.extras.additional_agent_instructions) {
- content += `\n\n**Additional Instructions:**\n\n${recallObs.extras.additional_agent_instructions}`;
- }
- }
-
- // Create a new message for the observation
- // Use the correct translation ID format that matches what's in the i18n file
- const translationID = `OBSERVATION_MESSAGE$${observationID.toUpperCase()}`;
-
- // Handle microagent knowledge
- if (
- recallObs.extras.microagent_knowledge &&
- recallObs.extras.microagent_knowledge.length > 0
- ) {
- content += `\n\n**Triggered Microagent Knowledge:**`;
- for (const knowledge of recallObs.extras.microagent_knowledge) {
- content += `\n\n- **${knowledge.name}** (triggered by keyword: ${knowledge.trigger})\n\n\`\`\`\n${knowledge.content}\n\`\`\``;
- }
- }
-
- const message: Message = {
- type: "action",
- sender: "assistant",
- translationID,
- eventID: observation.payload.id,
- content,
- imageUrls: [],
- timestamp: new Date().toISOString(),
- success: true,
- };
-
- state.messages.push(message);
- return; // Skip the normal observation handling below
- }
-
- // Normal handling for other observation types
- const translationID = `OBSERVATION_MESSAGE$${observationID.toUpperCase()}`;
- const causeID = observation.payload.cause;
- const causeMessage = state.messages.find(
- (message) => message.eventID === causeID,
- );
- if (!causeMessage) {
- return;
- }
- causeMessage.translationID = translationID;
- causeMessage.observation = observation;
- // Set success property based on observation type
- if (observationID === "run") {
- const commandObs = observation.payload as CommandObservation;
- // If exit_code is -1, it means the command timed out, so we set success to undefined
- // to not show any status indicator
- if (commandObs.extras.metadata.exit_code === -1) {
- causeMessage.success = undefined;
- } else {
- causeMessage.success = commandObs.extras.metadata.exit_code === 0;
- }
- } else if (observationID === "run_ipython") {
- // For IPython, we consider it successful if there's no error message
- const ipythonObs = observation.payload as IPythonObservation;
- causeMessage.success = !ipythonObs.content
- .toLowerCase()
- .includes("error:");
- } else if (observationID === "read" || observationID === "edit") {
- // For read/edit operations, we consider it successful if there's content and no error
-
- if (observation.payload.extras.impl_source === "oh_aci") {
- causeMessage.success =
- observation.payload.content.length > 0 &&
- !observation.payload.content.startsWith("ERROR:\n");
- } else {
- causeMessage.success =
- observation.payload.content.length > 0 &&
- !observation.payload.content.toLowerCase().includes("error:");
- }
- }
-
- if (observationID === "run" || observationID === "run_ipython") {
- let { content } = observation.payload;
- if (content.length > MAX_CONTENT_LENGTH) {
- content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
- }
- content = `${causeMessage.content}\n\nOutput:\n\`\`\`\n${content.trim() || "[Command finished execution with no output]"}\n\`\`\``;
- causeMessage.content = content; // Observation content includes the action
- } else if (observationID === "read") {
- causeMessage.content = `\`\`\`\n${observation.payload.content}\n\`\`\``; // Content is already truncated by the ACI
- } else if (observationID === "edit") {
- if (causeMessage.success) {
- causeMessage.content = `\`\`\`diff\n${observation.payload.extras.diff}\n\`\`\``; // Content is already truncated by the ACI
- } else {
- causeMessage.content = observation.payload.content;
- }
- } else if (observationID === "browse") {
- let content = `**URL:** ${observation.payload.extras.url}\n`;
- if (observation.payload.extras.error) {
- content += `\n\n**Error:**\n${observation.payload.extras.error}\n`;
- }
- content += `\n\n**Output:**\n${observation.payload.content}`;
- if (content.length > MAX_CONTENT_LENGTH) {
- content = `${content.slice(0, MAX_CONTENT_LENGTH)}...(truncated)`;
- }
- causeMessage.content = content;
- } else if (observationID === "mcp") {
- // For MCP observations, we want to show the content as formatted output
- // similar to how run/run_ipython actions are handled
- let { content } = observation.payload;
- if (content.length > MAX_CONTENT_LENGTH) {
- content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
- }
- content = `${causeMessage.content}\n\n**Output:**\n\`\`\`\n${content.trim() || "[MCP Tool finished execution with no output]"}\n\`\`\``;
- causeMessage.content = content; // Observation content includes the action
- // Set success based on whether there's an error message
- causeMessage.success = !observation.payload.content
- .toLowerCase()
- .includes("error:");
- }
- },
-
- addErrorMessage(
- state: SliceState,
- action: PayloadAction<{ id?: string; message: string }>,
- ) {
- const { id, message } = action.payload;
- state.messages.push({
- translationID: id,
- content: message,
- type: "error",
- sender: "assistant",
- timestamp: new Date().toISOString(),
- });
- },
-
- clearMessages(state: SliceState) {
- state.messages = [];
- state.systemMessage = null;
- },
- },
-});
-
-export const {
- addUserMessage,
- addAssistantMessage,
- addAssistantAction,
- addAssistantObservation,
- addErrorMessage,
- clearMessages,
-} = chatSlice.actions;
-
-// Selectors
-export const selectSystemMessage = (state: { chat: SliceState }) =>
- state.chat.systemMessage;
-
-export default chatSlice.reducer;
diff --git a/frontend/src/store.ts b/frontend/src/store.ts
index b9fbd322fa..93b92bee04 100644
--- a/frontend/src/store.ts
+++ b/frontend/src/store.ts
@@ -1,7 +1,6 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import agentReducer from "./state/agent-slice";
import browserReducer from "./state/browser-slice";
-import chatReducer from "./state/chat-slice";
import codeReducer from "./state/code-slice";
import fileStateReducer from "./state/file-state-slice";
import initialQueryReducer from "./state/initial-query-slice";
@@ -15,7 +14,6 @@ export const rootReducer = combineReducers({
fileState: fileStateReducer,
initialQuery: initialQueryReducer,
browser: browserReducer,
- chat: chatReducer,
code: codeReducer,
cmd: commandReducer,
agent: agentReducer,
diff --git a/frontend/src/types/core/base.ts b/frontend/src/types/core/base.ts
index b089411664..6322bcc288 100644
--- a/frontend/src/types/core/base.ts
+++ b/frontend/src/types/core/base.ts
@@ -2,6 +2,7 @@ export type OpenHandsEventType =
| "message"
| "system"
| "agent_state_changed"
+ | "change_agent_state"
| "run"
| "read"
| "write"
@@ -16,11 +17,14 @@ export type OpenHandsEventType =
| "error"
| "recall"
| "mcp"
- | "call_tool_mcp";
+ | "call_tool_mcp"
+ | "user_rejected";
+
+export type OpenHandsSourceType = "agent" | "user" | "environment";
interface OpenHandsBaseEvent {
id: number;
- source: "agent" | "user";
+ source: OpenHandsSourceType;
message: string;
timestamp: string; // ISO 8601
}
diff --git a/frontend/src/types/core/guards.ts b/frontend/src/types/core/guards.ts
new file mode 100644
index 0000000000..70dc5c6aa1
--- /dev/null
+++ b/frontend/src/types/core/guards.ts
@@ -0,0 +1,59 @@
+import { OpenHandsParsedEvent } from ".";
+import {
+ UserMessageAction,
+ AssistantMessageAction,
+ OpenHandsAction,
+ SystemMessageAction,
+} from "./actions";
+import {
+ CommandObservation,
+ ErrorObservation,
+ OpenHandsObservation,
+} from "./observations";
+
+export const isOpenHandsAction = (
+ event: OpenHandsParsedEvent,
+): event is OpenHandsAction => "action" in event;
+
+export const isOpenHandsObservation = (
+ event: OpenHandsParsedEvent,
+): event is OpenHandsObservation => "observation" in event;
+
+export const isUserMessage = (
+ event: OpenHandsParsedEvent,
+): event is UserMessageAction =>
+ isOpenHandsAction(event) &&
+ event.source === "user" &&
+ event.action === "message";
+
+export const isAssistantMessage = (
+ event: OpenHandsParsedEvent,
+): event is AssistantMessageAction =>
+ isOpenHandsAction(event) &&
+ event.source === "agent" &&
+ (event.action === "message" || event.action === "finish");
+
+export const isErrorObservation = (
+ event: OpenHandsParsedEvent,
+): event is ErrorObservation =>
+ isOpenHandsObservation(event) && event.observation === "error";
+
+export const isCommandObservation = (
+ event: OpenHandsParsedEvent,
+): event is CommandObservation =>
+ isOpenHandsObservation(event) && event.observation === "run";
+
+export const isFinishAction = (
+ event: OpenHandsParsedEvent,
+): event is AssistantMessageAction =>
+ isOpenHandsAction(event) && event.action === "finish";
+
+export const isSystemMessage = (
+ event: OpenHandsParsedEvent,
+): event is SystemMessageAction =>
+ isOpenHandsAction(event) && event.action === "system";
+
+export const isRejectObservation = (
+ event: OpenHandsParsedEvent,
+): event is OpenHandsObservation =>
+ isOpenHandsObservation(event) && event.observation === "user_rejected";
diff --git a/frontend/src/types/core/observations.ts b/frontend/src/types/core/observations.ts
index 92e6a5673d..8962ff0371 100644
--- a/frontend/src/types/core/observations.ts
+++ b/frontend/src/types/core/observations.ts
@@ -138,6 +138,14 @@ export interface MCPObservation extends OpenHandsObservationEvent<"mcp"> {
};
}
+export interface UserRejectedObservation
+ extends OpenHandsObservationEvent<"user_rejected"> {
+ source: "agent";
+ extras: {
+ // Add any specific fields for MCP observations
+ };
+}
+
export type OpenHandsObservation =
| AgentStateChangeObservation
| AgentThinkObservation
@@ -151,4 +159,5 @@ export type OpenHandsObservation =
| EditObservation
| ErrorObservation
| RecallObservation
- | MCPObservation;
+ | MCPObservation
+ | UserRejectedObservation;