From 6c821ab73e6ae077f3cf3f94aa3e6cad9ac78b6b Mon Sep 17 00:00:00 2001
From: Hiep Le <69354317+hieptl@users.noreply.github.com>
Date: Mon, 1 Dec 2025 21:29:18 +0700
Subject: [PATCH] fix(frontend): the content of the FinishObservation event is
not being rendered correctly. (#11846)
---
.../components/features/chat/chat-message.tsx | 21 +----
.../features/chat/error-message.tsx | 19 +----
.../features/chat/expandable-message.tsx | 19 +----
.../features/chat/generic-event-message.tsx | 17 +---
.../features/markdown/markdown-renderer.tsx | 80 +++++++++++++++++++
...ent-management-view-microagent-content.tsx | 21 +----
.../get-observation-content.ts | 17 +++-
.../generic-event-message-wrapper.tsx | 17 ++--
frontend/src/routes/planner-tab.tsx | 35 +-------
.../src/types/v1/core/base/observation.ts | 8 +-
10 files changed, 128 insertions(+), 126 deletions(-)
create mode 100644 frontend/src/components/features/markdown/markdown-renderer.tsx
diff --git a/frontend/src/components/features/chat/chat-message.tsx b/frontend/src/components/features/chat/chat-message.tsx
index a3dc934475..6f2f388682 100644
--- a/frontend/src/components/features/chat/chat-message.tsx
+++ b/frontend/src/components/features/chat/chat-message.tsx
@@ -1,15 +1,9 @@
import React from "react";
-import Markdown from "react-markdown";
-import remarkGfm from "remark-gfm";
-import remarkBreaks from "remark-breaks";
-import { code } from "../markdown/code";
import { cn } from "#/utils/utils";
-import { ul, ol } from "../markdown/list";
import { CopyToClipboardButton } from "#/components/shared/buttons/copy-to-clipboard-button";
-import { anchor } from "../markdown/anchor";
import { OpenHandsSourceType } from "#/types/core/base";
-import { paragraph } from "../markdown/paragraph";
import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
+import { MarkdownRenderer } from "../markdown/markdown-renderer";
interface ChatMessageProps {
type: OpenHandsSourceType;
@@ -116,18 +110,7 @@ export function ChatMessage({
wordBreak: "break-word",
}}
>
-
- {message}
-
+ {message}
{children}
diff --git a/frontend/src/components/features/chat/error-message.tsx b/frontend/src/components/features/chat/error-message.tsx
index 8de367a9a2..da40b3786e 100644
--- a/frontend/src/components/features/chat/error-message.tsx
+++ b/frontend/src/components/features/chat/error-message.tsx
@@ -1,13 +1,9 @@
import React from "react";
-import Markdown from "react-markdown";
-import remarkGfm from "remark-gfm";
-import remarkBreaks from "remark-breaks";
import { useTranslation } from "react-i18next";
-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 i18n from "#/i18n";
+import { MarkdownRenderer } from "../markdown/markdown-renderer";
interface ErrorMessageProps {
errorId?: string;
@@ -40,18 +36,7 @@ export function ErrorMessage({ errorId, defaultMessage }: ErrorMessageProps) {
- {showDetails && (
-
- {defaultMessage}
-
- )}
+ {showDetails && {defaultMessage}}
);
}
diff --git a/frontend/src/components/features/chat/expandable-message.tsx b/frontend/src/components/features/chat/expandable-message.tsx
index 918eafd6b8..cf9ae550d2 100644
--- a/frontend/src/components/features/chat/expandable-message.tsx
+++ b/frontend/src/components/features/chat/expandable-message.tsx
@@ -1,9 +1,6 @@
import { useEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
-import Markdown from "react-markdown";
import { Link } from "react-router";
-import remarkGfm from "remark-gfm";
-import remarkBreaks from "remark-breaks";
import { useConfig } from "#/hooks/query/use-config";
import { I18nKey } from "#/i18n/declaration";
import ArrowDown from "#/icons/angle-down-solid.svg?react";
@@ -13,9 +10,7 @@ import XCircle from "#/icons/x-circle-solid.svg?react";
import { OpenHandsAction } from "#/types/core/actions";
import { OpenHandsObservation } from "#/types/core/observations";
import { cn } from "#/utils/utils";
-import { code } from "../markdown/code";
-import { ol, ul } from "../markdown/list";
-import { paragraph } from "../markdown/paragraph";
+import { MarkdownRenderer } from "../markdown/markdown-renderer";
import { MonoComponent } from "./mono-component";
import { PathComponent } from "./path-component";
@@ -192,17 +187,7 @@ export function ExpandableMessage({
{showDetails && (
-
- {details}
-
+ {details}
)}
diff --git a/frontend/src/components/features/chat/generic-event-message.tsx b/frontend/src/components/features/chat/generic-event-message.tsx
index e5124b69fe..ff2ab633b1 100644
--- a/frontend/src/components/features/chat/generic-event-message.tsx
+++ b/frontend/src/components/features/chat/generic-event-message.tsx
@@ -1,13 +1,9 @@
import React from "react";
-import Markdown from "react-markdown";
-import remarkGfm from "remark-gfm";
-import remarkBreaks from "remark-breaks";
-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";
+import { MarkdownRenderer } from "../markdown/markdown-renderer";
interface GenericEventMessageProps {
title: React.ReactNode;
@@ -49,16 +45,7 @@ export function GenericEventMessage({
{showDetails &&
(typeof details === "string" ? (
-
- {details}
-
+ {details}
) : (
details
))}
diff --git a/frontend/src/components/features/markdown/markdown-renderer.tsx b/frontend/src/components/features/markdown/markdown-renderer.tsx
new file mode 100644
index 0000000000..0cb55498d6
--- /dev/null
+++ b/frontend/src/components/features/markdown/markdown-renderer.tsx
@@ -0,0 +1,80 @@
+import Markdown, { Components } from "react-markdown";
+import remarkGfm from "remark-gfm";
+import remarkBreaks from "remark-breaks";
+import { code } from "./code";
+import { ul, ol } from "./list";
+import { paragraph } from "./paragraph";
+import { anchor } from "./anchor";
+import { h1, h2, h3, h4, h5, h6 } from "./headings";
+
+interface MarkdownRendererProps {
+ /**
+ * The markdown content to render. Can be passed as children (string) or content prop.
+ */
+ children?: string;
+ content?: string;
+ /**
+ * Additional or override components for markdown elements.
+ * Default components (code, ul, ol) are always included unless overridden.
+ */
+ components?: Partial;
+ /**
+ * Whether to include standard components (anchor, paragraph).
+ * Defaults to false.
+ */
+ includeStandard?: boolean;
+ /**
+ * Whether to include heading components (h1-h6).
+ * Defaults to false.
+ */
+ includeHeadings?: boolean;
+}
+
+/**
+ * A reusable Markdown renderer component that provides consistent
+ * markdown rendering across the application.
+ *
+ * By default, includes:
+ * - code, ul, ol components
+ * - remarkGfm and remarkBreaks plugins
+ *
+ * Can be extended with:
+ * - includeStandard: adds anchor and paragraph components
+ * - includeHeadings: adds h1-h6 heading components
+ * - components prop: allows custom overrides or additional components
+ */
+export function MarkdownRenderer({
+ children,
+ content,
+ components: customComponents,
+ includeStandard = false,
+ includeHeadings = false,
+}: MarkdownRendererProps) {
+ // Build the components object with defaults and optional additions
+ const components: Components = {
+ code,
+ ul,
+ ol,
+ ...(includeStandard && {
+ a: anchor,
+ p: paragraph,
+ }),
+ ...(includeHeadings && {
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ }),
+ ...customComponents, // Custom components override defaults
+ };
+
+ const markdownContent = content ?? children ?? "";
+
+ return (
+
+ {markdownContent}
+
+ );
+}
diff --git a/frontend/src/components/features/microagent-management/microagent-management-view-microagent-content.tsx b/frontend/src/components/features/microagent-management/microagent-management-view-microagent-content.tsx
index dc5b5fecaa..2994946731 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-view-microagent-content.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-view-microagent-content.tsx
@@ -1,16 +1,10 @@
import { useTranslation } from "react-i18next";
import { Spinner } from "@heroui/react";
-import Markdown from "react-markdown";
-import remarkGfm from "remark-gfm";
-import remarkBreaks from "remark-breaks";
-import { code } from "../markdown/code";
-import { ul, ol } from "../markdown/list";
-import { paragraph } from "../markdown/paragraph";
-import { anchor } from "../markdown/anchor";
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
import { useRepositoryMicroagentContent } from "#/hooks/query/use-repository-microagent-content";
import { I18nKey } from "#/i18n/declaration";
import { extractRepositoryInfo } from "#/utils/utils";
+import { MarkdownRenderer } from "../markdown/markdown-renderer";
export function MicroagentManagementViewMicroagentContent() {
const { t } = useTranslation();
@@ -49,18 +43,9 @@ export function MicroagentManagementViewMicroagentContent() {
)}
{microagentData && !isLoading && !error && (
-
+
{microagentData.content}
-
+
)}
);
diff --git a/frontend/src/components/v1/chat/event-content-helpers/get-observation-content.ts b/frontend/src/components/v1/chat/event-content-helpers/get-observation-content.ts
index a227e99cfc..35d01b0655 100644
--- a/frontend/src/components/v1/chat/event-content-helpers/get-observation-content.ts
+++ b/frontend/src/components/v1/chat/event-content-helpers/get-observation-content.ts
@@ -184,7 +184,22 @@ const getFinishObservationContent = (
event: ObservationEvent,
): string => {
const { observation } = event;
- return observation.message || "";
+
+ // Extract text content from the observation
+ const textContent = observation.content
+ .filter((c) => c.type === "text")
+ .map((c) => c.text)
+ .join("\n");
+
+ let content = "";
+
+ if (observation.is_error) {
+ content += `**Error:**\n${textContent}`;
+ } else {
+ content += textContent;
+ }
+
+ return content;
};
export const getObservationContent = (event: ObservationEvent): string => {
diff --git a/frontend/src/components/v1/chat/event-message-components/generic-event-message-wrapper.tsx b/frontend/src/components/v1/chat/event-message-components/generic-event-message-wrapper.tsx
index 94f35aec66..95c2652549 100644
--- a/frontend/src/components/v1/chat/event-message-components/generic-event-message-wrapper.tsx
+++ b/frontend/src/components/v1/chat/event-message-components/generic-event-message-wrapper.tsx
@@ -9,6 +9,7 @@ import {
} from "../event-content-helpers/create-skill-ready-event";
import { V1ConfirmationButtons } from "#/components/shared/buttons/v1-confirmation-buttons";
import { ObservationResultStatus } from "../../../features/chat/event-content-helpers/get-observation-result";
+import { MarkdownRenderer } from "#/components/features/markdown/markdown-renderer";
interface GenericEventMessageWrapperProps {
event: OpenHandsEvent | SkillReadyEvent;
@@ -23,11 +24,17 @@ export function GenericEventMessageWrapper({
// SkillReadyEvent is not an observation event, so skip the observation checks
if (!isSkillReadyEvent(event)) {
- if (
- isObservationEvent(event) &&
- event.observation.kind === "TaskTrackerObservation"
- ) {
- return {details}
;
+ if (isObservationEvent(event)) {
+ if (event.observation.kind === "TaskTrackerObservation") {
+ return {details}
;
+ }
+ if (event.observation.kind === "FinishObservation") {
+ return (
+
+ {details as string}
+
+ );
+ }
}
}
diff --git a/frontend/src/routes/planner-tab.tsx b/frontend/src/routes/planner-tab.tsx
index 4fb46f9939..989e85596e 100644
--- a/frontend/src/routes/planner-tab.tsx
+++ b/frontend/src/routes/planner-tab.tsx
@@ -1,22 +1,8 @@
import { useTranslation } from "react-i18next";
-import Markdown from "react-markdown";
-import remarkGfm from "remark-gfm";
-import remarkBreaks from "remark-breaks";
import { I18nKey } from "#/i18n/declaration";
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
import { useConversationStore } from "#/state/conversation-store";
-import { code } from "#/components/features/markdown/code";
-import { ul, ol } from "#/components/features/markdown/list";
-import { paragraph } from "#/components/features/markdown/paragraph";
-import { anchor } from "#/components/features/markdown/anchor";
-import {
- h1,
- h2,
- h3,
- h4,
- h5,
- h6,
-} from "#/components/features/markdown/headings";
+import { MarkdownRenderer } from "#/components/features/markdown/markdown-renderer";
function PlannerTab() {
const { t } = useTranslation();
@@ -26,24 +12,9 @@ function PlannerTab() {
if (planContent !== null && planContent !== undefined) {
return (
-
+
{planContent}
-
+
);
}
diff --git a/frontend/src/types/v1/core/base/observation.ts b/frontend/src/types/v1/core/base/observation.ts
index 062d7ddf6e..7e510888f0 100644
--- a/frontend/src/types/v1/core/base/observation.ts
+++ b/frontend/src/types/v1/core/base/observation.ts
@@ -25,9 +25,13 @@ export interface MCPToolObservation
export interface FinishObservation
extends ObservationBase<"FinishObservation"> {
/**
- * Final message sent to the user
+ * Content returned from the finish action as a list of TextContent/ImageContent objects.
*/
- message: string;
+ content: Array;
+ /**
+ * Whether the finish action resulted in an error
+ */
+ is_error: boolean;
}
export interface ThinkObservation extends ObservationBase<"ThinkObservation"> {