diff --git a/frontend/__tests__/components/features/chat/task-tracking-observation-content.test.tsx b/frontend/__tests__/components/features/chat/task-tracking-observation-content.test.tsx index e44c33ca7d..5db3942aa2 100644 --- a/frontend/__tests__/components/features/chat/task-tracking-observation-content.test.tsx +++ b/frontend/__tests__/components/features/chat/task-tracking-observation-content.test.tsx @@ -8,10 +8,11 @@ vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => { const translations: Record = { - "TASK_TRACKING_OBSERVATION$TASK_LIST": "Task List", - "TASK_TRACKING_OBSERVATION$TASK_ID": "ID", - "TASK_TRACKING_OBSERVATION$TASK_NOTES": "Notes", - "TASK_TRACKING_OBSERVATION$RESULT": "Result", + TASK_TRACKING_OBSERVATION$TASK_LIST: "Task List", + TASK_TRACKING_OBSERVATION$TASK_ID: "ID", + TASK_TRACKING_OBSERVATION$TASK_NOTES: "Notes", + TASK_TRACKING_OBSERVATION$RESULT: "Result", + COMMON$TASKS: "Tasks", }; return translations[key] || key; }, @@ -61,19 +62,26 @@ describe("TaskTrackingObservationContent", () => { it("renders task list when command is 'plan' and tasks exist", () => { render(); - expect(screen.getByText("Task List (3 items)")).toBeInTheDocument(); + expect(screen.getByText("Tasks")).toBeInTheDocument(); expect(screen.getByText("Implement feature A")).toBeInTheDocument(); expect(screen.getByText("Fix bug B")).toBeInTheDocument(); expect(screen.getByText("Deploy to production")).toBeInTheDocument(); }); it("displays correct status icons and badges", () => { - render(); + const { container } = render( + , + ); - // Check for status text (the icons are emojis) - expect(screen.getByText("todo")).toBeInTheDocument(); - expect(screen.getByText("in progress")).toBeInTheDocument(); - expect(screen.getByText("done")).toBeInTheDocument(); + // Status is represented by icons, not text. Verify task items are rendered with their titles + // which indicates the status icons are present (status affects icon rendering) + expect(screen.getByText("Implement feature A")).toBeInTheDocument(); + expect(screen.getByText("Fix bug B")).toBeInTheDocument(); + expect(screen.getByText("Deploy to production")).toBeInTheDocument(); + + // Verify task items are present (they contain the status icons) + const taskItems = container.querySelectorAll('[data-name="item"]'); + expect(taskItems).toHaveLength(3); }); it("displays task IDs and notes", () => { @@ -84,14 +92,9 @@ describe("TaskTrackingObservationContent", () => { expect(screen.getByText("ID: task-3")).toBeInTheDocument(); expect(screen.getByText("Notes: This is a test task")).toBeInTheDocument(); - expect(screen.getByText("Notes: Completed successfully")).toBeInTheDocument(); - }); - - it("renders result section when content exists", () => { - render(); - - expect(screen.getByText("Result")).toBeInTheDocument(); - expect(screen.getByText("Task tracking operation completed successfully")).toBeInTheDocument(); + expect( + screen.getByText("Notes: Completed successfully"), + ).toBeInTheDocument(); }); it("does not render task list when command is not 'plan'", () => { @@ -105,7 +108,7 @@ describe("TaskTrackingObservationContent", () => { render(); - expect(screen.queryByText("Task List")).not.toBeInTheDocument(); + expect(screen.queryByText("Tasks")).not.toBeInTheDocument(); }); it("does not render task list when task list is empty", () => { @@ -119,17 +122,6 @@ describe("TaskTrackingObservationContent", () => { render(); - expect(screen.queryByText("Task List")).not.toBeInTheDocument(); - }); - - it("does not render result section when content is empty", () => { - const eventWithoutContent = { - ...mockEvent, - content: "", - }; - - render(); - - expect(screen.queryByText("Result")).not.toBeInTheDocument(); + expect(screen.queryByText("Tasks")).not.toBeInTheDocument(); }); }); diff --git a/frontend/src/components/features/chat/event-message-components/task-tracking-event-message.tsx b/frontend/src/components/features/chat/event-message-components/task-tracking-event-message.tsx index 785305333c..cd6ff59a05 100644 --- a/frontend/src/components/features/chat/event-message-components/task-tracking-event-message.tsx +++ b/frontend/src/components/features/chat/event-message-components/task-tracking-event-message.tsx @@ -1,11 +1,7 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; import { OpenHandsObservation } from "#/types/core/observations"; import { isTaskTrackingObservation } from "#/types/core/guards"; -import { GenericEventMessage } from "../generic-event-message"; import { TaskTrackingObservationContent } from "../task-tracking-observation-content"; import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons"; -import { getObservationResult } from "../event-content-helpers/get-observation-result"; interface TaskTrackingEventMessageProps { event: OpenHandsObservation; @@ -16,34 +12,13 @@ export function TaskTrackingEventMessage({ event, shouldShowConfirmationButtons, }: TaskTrackingEventMessageProps) { - const { t } = useTranslation(); - if (!isTaskTrackingObservation(event)) { return null; } - const { command } = event.extras; - let title: React.ReactNode; - let initiallyExpanded = false; - - // Determine title and expansion state based on command - if (command === "plan") { - title = t("OBSERVATION_MESSAGE$TASK_TRACKING_PLAN"); - initiallyExpanded = true; - } else { - // command === "view" - title = t("OBSERVATION_MESSAGE$TASK_TRACKING_VIEW"); - initiallyExpanded = false; - } - return (
- } - success={getObservationResult(event)} - initiallyExpanded={initiallyExpanded} - /> + {shouldShowConfirmationButtons && }
); diff --git a/frontend/src/components/features/chat/task-tracking-observation-content.tsx b/frontend/src/components/features/chat/task-tracking-observation-content.tsx index e4dd95c2bf..7d9e7ff146 100644 --- a/frontend/src/components/features/chat/task-tracking-observation-content.tsx +++ b/frontend/src/components/features/chat/task-tracking-observation-content.tsx @@ -1,6 +1,5 @@ import { TaskTrackingObservation } from "#/types/core/observations"; import { TaskListSection } from "./task-tracking/task-list-section"; -import { ResultSection } from "./task-tracking/result-section"; interface TaskTrackingObservationContentProps { event: TaskTrackingObservation; @@ -16,11 +15,6 @@ export function TaskTrackingObservationContent({
{/* Task List section - only show for 'plan' command */} {shouldShowTaskList && } - - {/* Result message - only show if there's meaningful content */} - {event.content && event.content.trim() && ( - - )}
); } diff --git a/frontend/src/components/features/chat/task-tracking/result-section.tsx b/frontend/src/components/features/chat/task-tracking/result-section.tsx deleted file mode 100644 index 0cd06e3a4a..0000000000 --- a/frontend/src/components/features/chat/task-tracking/result-section.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Typography } from "#/ui/typography"; - -interface ResultSectionProps { - content: string; -} - -export function ResultSection({ content }: ResultSectionProps) { - const { t } = useTranslation(); - - return ( -
-
- {t("TASK_TRACKING_OBSERVATION$RESULT")} -
-
-
{content.trim()}
-
-
- ); -} diff --git a/frontend/src/components/features/chat/task-tracking/task-item.tsx b/frontend/src/components/features/chat/task-tracking/task-item.tsx index 923ed8ea3f..72e9e74aac 100644 --- a/frontend/src/components/features/chat/task-tracking/task-item.tsx +++ b/frontend/src/components/features/chat/task-tracking/task-item.tsx @@ -1,7 +1,11 @@ +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; +import CircleIcon from "#/icons/u-circle.svg?react"; +import CheckCircleIcon from "#/icons/u-check-circle.svg?react"; +import LoadingIcon from "#/icons/loading.svg?react"; +import { I18nKey } from "#/i18n/declaration"; +import { cn } from "#/utils/utils"; import { Typography } from "#/ui/typography"; -import { StatusIcon } from "./status-icon"; -import { StatusBadge } from "./status-badge"; interface TaskItemProps { task: { @@ -10,33 +14,47 @@ interface TaskItemProps { status: "todo" | "in_progress" | "done"; notes?: string; }; - index: number; } -export function TaskItem({ task, index }: TaskItemProps) { +export function TaskItem({ task }: TaskItemProps) { const { t } = useTranslation(); + const icon = useMemo(() => { + switch (task.status) { + case "todo": + return ; + case "in_progress": + return ; + case "done": + return ; + default: + return ; + } + }, [task.status]); + + const isDoneStatus = task.status === "done"; + return ( -
-
- -
-
- - {index + 1}. - - -
-

{task.title}

- - {t("TASK_TRACKING_OBSERVATION$TASK_ID")}: {task.id} - - {task.notes && ( - - {t("TASK_TRACKING_OBSERVATION$TASK_NOTES")}: {task.notes} - +
+
{icon}
+
+ + > + {task.title} + + + {t(I18nKey.TASK_TRACKING_OBSERVATION$TASK_ID)}: {task.id} + + + {t(I18nKey.TASK_TRACKING_OBSERVATION$TASK_NOTES)}: {task.notes} +
); diff --git a/frontend/src/components/features/chat/task-tracking/task-list-section.tsx b/frontend/src/components/features/chat/task-tracking/task-list-section.tsx index 9129202522..075517aacd 100644 --- a/frontend/src/components/features/chat/task-tracking/task-list-section.tsx +++ b/frontend/src/components/features/chat/task-tracking/task-list-section.tsx @@ -1,5 +1,7 @@ import { useTranslation } from "react-i18next"; import { TaskItem } from "./task-item"; +import LessonPlanIcon from "#/icons/lesson-plan.svg?react"; +import { I18nKey } from "#/i18n/declaration"; import { Typography } from "#/ui/typography"; interface TaskListSectionProps { @@ -15,19 +17,20 @@ export function TaskListSection({ taskList }: TaskListSectionProps) { const { t } = useTranslation(); return ( -
-
- - {t("TASK_TRACKING_OBSERVATION$TASK_LIST")} ({taskList.length}{" "} - {taskList.length === 1 ? "item" : "items"}) - +
+ {/* Header Tabs */} +
+ + + {t(I18nKey.COMMON$TASKS)} +
-
-
- {taskList.map((task, index) => ( - - ))} -
+ + {/* Task Items */} +
+ {taskList.map((task) => ( + + ))}
); diff --git a/frontend/src/components/v1/chat/event-content-helpers/get-event-content.tsx b/frontend/src/components/v1/chat/event-content-helpers/get-event-content.tsx index b2e7d69868..7eab7df1a7 100644 --- a/frontend/src/components/v1/chat/event-content-helpers/get-event-content.tsx +++ b/frontend/src/components/v1/chat/event-content-helpers/get-event-content.tsx @@ -1,10 +1,13 @@ import { Trans } from "react-i18next"; -import { OpenHandsEvent } from "#/types/v1/core"; +import React from "react"; +import { OpenHandsEvent, ObservationEvent } from "#/types/v1/core"; import { isActionEvent, isObservationEvent } from "#/types/v1/type-guards"; import { MonoComponent } from "../../../features/chat/mono-component"; import { PathComponent } from "../../../features/chat/path-component"; import { getActionContent } from "./get-action-content"; import { getObservationContent } from "./get-observation-content"; +import { TaskTrackingObservationContent } from "../task-tracking/task-tracking-observation-content"; +import { TaskTrackerObservation } from "#/types/v1/core/base/observation"; import i18n from "#/i18n"; const trimText = (text: string, maxLength: number): string => { @@ -158,14 +161,24 @@ const getObservationEventTitle = (event: OpenHandsEvent): React.ReactNode => { export const getEventContent = (event: OpenHandsEvent) => { let title: React.ReactNode = ""; - let details: string = ""; + let details: string | React.ReactNode = ""; if (isActionEvent(event)) { title = getActionEventTitle(event); details = getActionContent(event); } else if (isObservationEvent(event)) { title = getObservationEventTitle(event); - details = getObservationContent(event); + + // For TaskTrackerObservation, use React component instead of markdown + if (event.observation.kind === "TaskTrackerObservation") { + details = ( + } + /> + ); + } else { + details = getObservationContent(event); + } } return { diff --git a/frontend/src/components/v1/chat/event-message-components/finish-event-message.tsx b/frontend/src/components/v1/chat/event-message-components/finish-event-message.tsx index 6ad385e8f0..3a30b0c434 100644 --- a/frontend/src/components/v1/chat/event-message-components/finish-event-message.tsx +++ b/frontend/src/components/v1/chat/event-message-components/finish-event-message.tsx @@ -27,13 +27,16 @@ export function FinishEventMessage({ microagentPRUrl, actions, }: FinishEventMessageProps) { + const eventContent = getEventContent(event); + // For FinishAction, details is always a string (getActionContent returns string) + const message = + typeof eventContent.details === "string" + ? eventContent.details + : String(eventContent.details); + return ( <> - + {details}
; + } + return (
{ + switch (task.status) { + case "todo": + return ; + case "in_progress": + return ( + + ); + case "done": + return ; + default: + return ; + } + }, [task.status]); + + const isDoneStatus = task.status === "done"; + + return ( +
+
{icon}
+
+ + {task.title} + + + {t(I18nKey.TASK_TRACKING_OBSERVATION$TASK_NOTES)}: {task.notes} + +
+
+ ); +} diff --git a/frontend/src/components/v1/chat/task-tracking/task-list-section.tsx b/frontend/src/components/v1/chat/task-tracking/task-list-section.tsx new file mode 100644 index 0000000000..aa3821036f --- /dev/null +++ b/frontend/src/components/v1/chat/task-tracking/task-list-section.tsx @@ -0,0 +1,33 @@ +import { useTranslation } from "react-i18next"; +import { TaskItem } from "./task-item"; +import LessonPlanIcon from "#/icons/lesson-plan.svg?react"; +import { TaskItem as TaskItemType } from "#/types/v1/core/base/common"; +import { I18nKey } from "#/i18n/declaration"; +import { Typography } from "#/ui/typography"; + +interface TaskListSectionProps { + taskList: TaskItemType[]; +} + +export function TaskListSection({ taskList }: TaskListSectionProps) { + const { t } = useTranslation(); + + return ( +
+ {/* Header Tabs */} +
+ + + {t(I18nKey.COMMON$TASKS)} + +
+ + {/* Task Items */} +
+ {taskList.map((task, index) => ( + + ))} +
+
+ ); +} diff --git a/frontend/src/components/v1/chat/task-tracking/task-tracking-observation-content.tsx b/frontend/src/components/v1/chat/task-tracking/task-tracking-observation-content.tsx new file mode 100644 index 0000000000..167429cae8 --- /dev/null +++ b/frontend/src/components/v1/chat/task-tracking/task-tracking-observation-content.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { ObservationEvent } from "#/types/v1/core"; +import { TaskTrackerObservation } from "#/types/v1/core/base/observation"; +import { TaskListSection } from "./task-list-section"; + +interface TaskTrackingObservationContentProps { + event: ObservationEvent; +} + +export function TaskTrackingObservationContent({ + event, +}: TaskTrackingObservationContentProps): React.ReactNode { + const { observation } = event; + const { command, task_list: taskList } = observation; + const shouldShowTaskList = command === "plan" && taskList.length > 0; + + return ( +
+ {/* Task List section - only show for 'plan' command */} + {shouldShowTaskList && } +
+ ); +} diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts index f3fa1744e7..7fabded8df 100644 --- a/frontend/src/i18n/declaration.ts +++ b/frontend/src/i18n/declaration.ts @@ -937,6 +937,7 @@ export enum I18nKey { AGENT_STATUS$WAITING_FOR_USER_CONFIRMATION = "AGENT_STATUS$WAITING_FOR_USER_CONFIRMATION", COMMON$MORE_OPTIONS = "COMMON$MORE_OPTIONS", COMMON$CREATE_A_PLAN = "COMMON$CREATE_A_PLAN", + COMMON$TASKS = "COMMON$TASKS", COMMON$PLAN_MD = "COMMON$PLAN_MD", COMMON$READ_MORE = "COMMON$READ_MORE", COMMON$BUILD = "COMMON$BUILD", diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index 3765faee4f..992bc69ca7 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -14991,6 +14991,22 @@ "de": "Einen Plan erstellen", "uk": "Створити план" }, + "COMMON$TASKS": { + "en": "Tasks", + "ja": "タスク", + "zh-CN": "任务", + "zh-TW": "任務", + "ko-KR": "작업", + "no": "Oppgaver", + "it": "Attività", + "pt": "Tarefas", + "es": "Tareas", + "ar": "مهام", + "fr": "Tâches", + "tr": "Görevler", + "de": "Aufgaben", + "uk": "Завдання" + }, "COMMON$PLAN_MD": { "en": "Plan.md", "ja": "Plan.md", diff --git a/frontend/src/icons/loading.svg b/frontend/src/icons/loading.svg new file mode 100644 index 0000000000..2da678957f --- /dev/null +++ b/frontend/src/icons/loading.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/icons/u-check-circle.svg b/frontend/src/icons/u-check-circle.svg new file mode 100644 index 0000000000..e98e0c8f37 --- /dev/null +++ b/frontend/src/icons/u-check-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/icons/u-circle.svg b/frontend/src/icons/u-circle.svg new file mode 100644 index 0000000000..c562817d9b --- /dev/null +++ b/frontend/src/icons/u-circle.svg @@ -0,0 +1,3 @@ + + +