From 7d4b5eb3cae5da2f3baadeb5f97d63adc741a8d4 Mon Sep 17 00:00:00 2001 From: Henry Li Date: Sun, 8 Feb 2026 22:43:51 +0800 Subject: [PATCH] feat: add realtime subagent status report --- .../workspace/messages/subtask-card.tsx | 25 +++++++++++++++- frontend/src/core/i18n/locales/en-US.ts | 1 + frontend/src/core/i18n/locales/types.ts | 1 + frontend/src/core/i18n/locales/zh-CN.ts | 1 + frontend/src/core/tasks/context.tsx | 23 ++++++++++----- frontend/src/core/tasks/types.ts | 3 ++ frontend/src/core/threads/hooks.ts | 18 +++++++++++- frontend/src/core/tools/utils.ts | 29 +++++++++++++++++++ 8 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 frontend/src/core/tools/utils.ts diff --git a/frontend/src/components/workspace/messages/subtask-card.tsx b/frontend/src/components/workspace/messages/subtask-card.tsx index 87057c2..3ffc60d 100644 --- a/frontend/src/components/workspace/messages/subtask-card.tsx +++ b/frontend/src/components/workspace/messages/subtask-card.tsx @@ -17,13 +17,17 @@ import { Shimmer } from "@/components/ai-elements/shimmer"; import { Button } from "@/components/ui/button"; import { ShineBorder } from "@/components/ui/shine-border"; import { useI18n } from "@/core/i18n/hooks"; +import { hasToolCalls } from "@/core/messages/utils"; import { streamdownPlugins, streamdownPluginsWithWordAnimation, } from "@/core/streamdown"; import { useSubtask } from "@/core/tasks/context"; +import { explainLastToolCall } from "@/core/tools/utils"; import { cn } from "@/lib/utils"; +import { FlipDisplay } from "../flip-display"; + export function SubtaskCard({ className, taskId, @@ -84,7 +88,16 @@ export function SubtaskCard({ )} > {icon} - {t.subtasks[task.status]} + + {task.status === "in_progress" && + task.latestMessage && + hasToolCalls(task.latestMessage) + ? explainLastToolCall(task.latestMessage, t) + : t.subtasks[task.status]} + )} )} + {task.status === "in_progress" && + task.latestMessage && + hasToolCalls(task.latestMessage) && ( + } + > + {explainLastToolCall(task.latestMessage, t)} + + )} {task.status === "completed" && ( <> `Use "${toolName}" tool`, + searchFor: (query: string) => `Search for "${query}"`, searchForRelatedInfo: "Search for related information", searchForRelatedImages: "Search for related images", searchForRelatedImagesFor: (query: string) => diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts index c5d261f..c9e3706 100644 --- a/frontend/src/core/i18n/locales/types.ts +++ b/frontend/src/core/i18n/locales/types.ts @@ -144,6 +144,7 @@ export interface Translations { useTool: (toolName: string) => string; searchForRelatedInfo: string; searchForRelatedImages: string; + searchFor: (query: string) => string; searchForRelatedImagesFor: (query: string) => string; searchOnWebFor: (query: string) => string; viewWebPage: string; diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts index 6de95e1..6d6db64 100644 --- a/frontend/src/core/i18n/locales/zh-CN.ts +++ b/frontend/src/core/i18n/locales/zh-CN.ts @@ -191,6 +191,7 @@ export const zhCN: Translations = { presentFiles: "展示文件", needYourHelp: "需要你的协助", useTool: (toolName: string) => `使用 “${toolName}” 工具`, + searchFor: (query: string) => `搜索 “${query}”`, searchForRelatedInfo: "搜索相关信息", searchForRelatedImages: "搜索相关图片", searchForRelatedImagesFor: (query: string) => `搜索相关图片 “${query}”`, diff --git a/frontend/src/core/tasks/context.tsx b/frontend/src/core/tasks/context.tsx index 887013e..ea85772 100644 --- a/frontend/src/core/tasks/context.tsx +++ b/frontend/src/core/tasks/context.tsx @@ -3,17 +3,21 @@ import { createContext, useCallback, useContext, useState } from "react"; import type { Subtask } from "./types"; export interface SubtaskContextValue { - tasks: Map; + tasks: Record; + setTasks: (tasks: Record) => void; } export const SubtaskContext = createContext({ - tasks: new Map(), + tasks: {}, + setTasks: () => { + /* noop */ + }, }); export function SubtasksProvider({ children }: { children: React.ReactNode }) { - const [tasks] = useState>(new Map()); + const [tasks, setTasks] = useState>({}); return ( - + {children} ); @@ -31,16 +35,19 @@ export function useSubtaskContext() { export function useSubtask(id: string) { const { tasks } = useSubtaskContext(); - return tasks.get(id); + return tasks[id]; } export function useUpdateSubtask() { - const { tasks } = useSubtaskContext(); + const { tasks, setTasks } = useSubtaskContext(); const updateSubtask = useCallback( (task: Partial & { id: string }) => { - tasks.set(task.id, { ...tasks.get(task.id), ...task } as Subtask); + tasks[task.id] = { ...tasks[task.id], ...task } as Subtask; + if (task.latestMessage) { + setTasks({ ...tasks }); + } }, - [tasks], + [tasks, setTasks], ); return updateSubtask; } diff --git a/frontend/src/core/tasks/types.ts b/frontend/src/core/tasks/types.ts index cf256ab..98f9490 100644 --- a/frontend/src/core/tasks/types.ts +++ b/frontend/src/core/tasks/types.ts @@ -1,8 +1,11 @@ +import type { AIMessage } from "@langchain/langgraph-sdk"; + export interface Subtask { id: string; status: "in_progress" | "completed" | "failed"; subagent_type: string; description: string; + latestMessage?: AIMessage; prompt: string; result?: string; error?: string; diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts index fe9ac7e..078b9a6 100644 --- a/frontend/src/core/threads/hooks.ts +++ b/frontend/src/core/threads/hooks.ts @@ -1,4 +1,5 @@ import type { HumanMessage } from "@langchain/core/messages"; +import type { AIMessage } from "@langchain/langgraph-sdk"; import type { ThreadsClient } from "@langchain/langgraph-sdk/client"; import { useStream, type UseStream } from "@langchain/langgraph-sdk/react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -7,6 +8,7 @@ import { useCallback } from "react"; import type { PromptInputMessage } from "@/components/ai-elements/prompt-input"; import { getAPIClient } from "../api"; +import { useUpdateSubtask } from "../tasks/context"; import { uploadFiles } from "../uploads"; import type { @@ -25,14 +27,28 @@ export function useThreadStream({ onFinish?: (state: AgentThreadState) => void; }) { const queryClient = useQueryClient(); + const updateSubtask = useUpdateSubtask(); const thread = useStream({ client: getAPIClient(), assistantId: "lead_agent", threadId: isNewThread ? undefined : threadId, reconnectOnMount: true, fetchStateHistory: true, - onCustomEvent(event) { + onCustomEvent(event: unknown) { console.info(event); + if ( + typeof event === "object" && + event !== null && + "type" in event && + event.type === "task_running" + ) { + const e = event as { + type: "task_running"; + task_id: string; + message: AIMessage; + }; + updateSubtask({ id: e.task_id, latestMessage: e.message }); + } }, onFinish(state) { onFinish?.(state.values); diff --git a/frontend/src/core/tools/utils.ts b/frontend/src/core/tools/utils.ts new file mode 100644 index 0000000..10f8c6f --- /dev/null +++ b/frontend/src/core/tools/utils.ts @@ -0,0 +1,29 @@ +import type { ToolCall } from "@langchain/core/messages"; +import type { AIMessage } from "@langchain/langgraph-sdk"; + +import type { Translations } from "../i18n"; +import { hasToolCalls } from "../messages/utils"; + +export function explainLastToolCall(message: AIMessage, t: Translations) { + if (hasToolCalls(message)) { + const lastToolCall = message.tool_calls![message.tool_calls!.length - 1]!; + return explainToolCall(lastToolCall, t); + } + return t.common.thinking; +} + +export function explainToolCall(toolCall: ToolCall, t: Translations) { + if (toolCall.name === "web_search" || toolCall.name === "image_search") { + return t.toolCalls.searchFor(toolCall.args.query); + } else if (toolCall.name === "web_fetch") { + return t.toolCalls.viewWebPage; + } else if (toolCall.name === "present_files") { + return t.toolCalls.presentFiles; + } else if (toolCall.name === "write_todos") { + return t.toolCalls.writeTodos; + } else if (toolCall.args.description) { + return toolCall.args.description; + } else { + return t.toolCalls.useTool(toolCall.name); + } +}