fix(frontend): the content of the FinishObservation event is not being rendered correctly. (#11846)

This commit is contained in:
Hiep Le 2025-12-01 21:29:18 +07:00 committed by GitHub
parent 96f13b15e7
commit 6c821ab73e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 128 additions and 126 deletions

View File

@ -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",
}}
>
<Markdown
components={{
code,
ul,
ol,
a: anchor,
p: paragraph,
}}
remarkPlugins={[remarkGfm, remarkBreaks]}
>
{message}
</Markdown>
<MarkdownRenderer includeStandard>{message}</MarkdownRenderer>
</div>
{children}
</article>

View File

@ -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) {
</button>
</div>
{showDetails && (
<Markdown
components={{
code,
ul,
ol,
}}
remarkPlugins={[remarkGfm, remarkBreaks]}
>
{defaultMessage}
</Markdown>
)}
{showDetails && <MarkdownRenderer>{defaultMessage}</MarkdownRenderer>}
</div>
);
}

View File

@ -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({
</div>
{showDetails && (
<div className="text-sm">
<Markdown
components={{
code,
ul,
ol,
p: paragraph,
}}
remarkPlugins={[remarkGfm, remarkBreaks]}
>
{details}
</Markdown>
<MarkdownRenderer includeStandard>{details}</MarkdownRenderer>
</div>
)}
</div>

View File

@ -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" ? (
<Markdown
components={{
code,
ul,
ol,
}}
remarkPlugins={[remarkGfm, remarkBreaks]}
>
{details}
</Markdown>
<MarkdownRenderer>{details}</MarkdownRenderer>
) : (
details
))}

View File

@ -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<Components>;
/**
* 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 (
<Markdown components={components} remarkPlugins={[remarkGfm, remarkBreaks]}>
{markdownContent}
</Markdown>
);
}

View File

@ -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() {
</div>
)}
{microagentData && !isLoading && !error && (
<Markdown
components={{
code,
ul,
ol,
a: anchor,
p: paragraph,
}}
remarkPlugins={[remarkGfm, remarkBreaks]}
>
<MarkdownRenderer includeStandard>
{microagentData.content}
</Markdown>
</MarkdownRenderer>
)}
</div>
);

View File

@ -184,7 +184,22 @@ const getFinishObservationContent = (
event: ObservationEvent<FinishObservation>,
): 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 => {

View File

@ -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 <div>{details}</div>;
if (isObservationEvent(event)) {
if (event.observation.kind === "TaskTrackerObservation") {
return <div>{details}</div>;
}
if (event.observation.kind === "FinishObservation") {
return (
<MarkdownRenderer includeStandard includeHeadings>
{details as string}
</MarkdownRenderer>
);
}
}
}

View File

@ -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 (
<div className="flex flex-col w-full h-full p-4 overflow-auto">
<Markdown
components={{
code,
ul,
ol,
a: anchor,
p: paragraph,
h1,
h2,
h3,
h4,
h5,
h6,
}}
remarkPlugins={[remarkGfm, remarkBreaks]}
>
<MarkdownRenderer includeStandard includeHeadings>
{planContent}
</Markdown>
</MarkdownRenderer>
</div>
);
}

View File

@ -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<TextContent | ImageContent>;
/**
* Whether the finish action resulted in an error
*/
is_error: boolean;
}
export interface ThinkObservation extends ObservationBase<"ThinkObservation"> {