diff --git a/frontend/src/components/workspace/messages/message-list.tsx b/frontend/src/components/workspace/messages/message-list.tsx index ca8672a3a..75c691e74 100644 --- a/frontend/src/components/workspace/messages/message-list.tsx +++ b/frontend/src/components/workspace/messages/message-list.tsx @@ -16,7 +16,6 @@ import { import { extractContentFromMessage, extractPresentFilesFromMessage, - extractTextFromMessage, getAssistantTurnCopyData, getAssistantTurnUsageMessages, getMessageGroups, @@ -27,9 +26,7 @@ import { isAssistantMessageGroupStreaming, } from "@/core/messages/utils"; import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; -import type { Subtask } from "@/core/tasks"; -import { useUpdateSubtask } from "@/core/tasks/context"; -import { parseSubtaskResult } from "@/core/tasks/subtask-result"; +import { buildSubtaskMapFromMessages } from "@/core/tasks/derive"; import type { AgentThreadState } from "@/core/threads"; import { cn } from "@/lib/utils"; @@ -177,8 +174,8 @@ export function MessageList({ }) { const { t } = useI18n(); const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading); - const updateSubtask = useUpdateSubtask(); const messages = thread.messages; + const tasks = useMemo(() => buildSubtaskMapFromMessages(messages), [messages]); const groupedMessages = getMessageGroups(messages); const turnUsageMessagesByGroupIndex = getAssistantTurnUsageMessages(groupedMessages); @@ -354,42 +351,29 @@ export function MessageList({ ); } else if (group.type === "assistant:subagent") { - const tasks = new Set(); - for (const message of group.messages) { - if (message.type === "ai") { - for (const toolCall of message.tool_calls ?? []) { - if (toolCall.name === "task") { - const task: Subtask = { - id: toolCall.id!, - subagent_type: toolCall.args.subagent_type, - description: toolCall.args.description, - prompt: toolCall.args.prompt, - status: "in_progress", - }; - updateSubtask(task); - tasks.add(task); - } - } - } else if (message.type === "tool") { - const taskId = message.tool_call_id; - if (taskId) { - const parsed = parseSubtaskResult( - extractTextFromMessage(message), - ); - updateSubtask({ id: taskId, ...parsed }); - } - } - } - const results: React.ReactNode[] = []; const subagentDebugMessageIds: string[] = []; - if (tasks.size > 0) { + const groupTaskIds = Array.from( + new Set( + group.messages.flatMap((message) => + message.type === "ai" + ? (message.tool_calls ?? []) + .map((toolCall) => + toolCall.name === "task" ? toolCall.id : null, + ) + .filter((taskId): taskId is string => Boolean(taskId)) + : [], + ), + ), + ); + + if (groupTaskIds.length > 0) { results.push(
- {t.subtasks.executing(tasks.size)} + {t.subtasks.executing(groupTaskIds.length)}
, ); } @@ -417,10 +401,14 @@ export function MessageList({ ?.filter((toolCall) => toolCall.name === "task") .map((toolCall) => toolCall.id); for (const taskId of taskIds ?? []) { + const task = taskId ? tasks[taskId] : undefined; + if (!taskId || !task) { + continue; + } results.push( , ); diff --git a/frontend/src/components/workspace/messages/subtask-card.tsx b/frontend/src/components/workspace/messages/subtask-card.tsx index b2aa74b34..fed4b35d9 100644 --- a/frontend/src/components/workspace/messages/subtask-card.tsx +++ b/frontend/src/components/workspace/messages/subtask-card.tsx @@ -20,7 +20,8 @@ import { useI18n } from "@/core/i18n/hooks"; import { hasToolCalls } from "@/core/messages/utils"; import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import { streamdownPluginsWithWordAnimation } from "@/core/streamdown"; -import { useSubtask } from "@/core/tasks/context"; +import type { Subtask } from "@/core/tasks"; +import { useLatestSubtaskMessage } from "@/core/tasks/context"; import { explainLastToolCall } from "@/core/tools/utils"; import { cn } from "@/lib/utils"; @@ -31,26 +32,30 @@ import { MarkdownContent } from "./markdown-content"; export function SubtaskCard({ className, - taskId, + task, isLoading, }: { className?: string; - taskId: string; + task: Subtask; isLoading: boolean; }) { const { t } = useI18n(); const [collapsed, setCollapsed] = useState(true); const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); - const task = useSubtask(taskId)!; + const latestMessage = useLatestSubtaskMessage(task.id); + const mergedTask = useMemo( + () => (latestMessage ? { ...task, latestMessage } : task), + [latestMessage, task], + ); const icon = useMemo(() => { - if (task.status === "completed") { + if (mergedTask.status === "completed") { return ; - } else if (task.status === "failed") { + } else if (mergedTask.status === "failed") { return ; - } else if (task.status === "in_progress") { + } else if (mergedTask.status === "in_progress") { return ; } - }, [task.status]); + }, [mergedTask.status]); return ( - {task.status === "in_progress" && ( + {mergedTask.status === "in_progress" && ( <> - {task.description} + {mergedTask.description} ) : ( - task.description + mergedTask.description ) } icon={} @@ -96,19 +101,21 @@ export function SubtaskCard({
{icon} - {task.status === "in_progress" && - task.latestMessage && - hasToolCalls(task.latestMessage) - ? explainLastToolCall(task.latestMessage, t) - : t.subtasks[task.status]} + {mergedTask.status === "in_progress" && + mergedTask.latestMessage && + hasToolCalls(mergedTask.latestMessage) + ? explainLastToolCall(mergedTask.latestMessage, t) + : t.subtasks[mergedTask.status]}
)} @@ -123,29 +130,29 @@ export function SubtaskCard({ - {task.prompt && ( + {mergedTask.prompt && ( - {task.prompt} + {mergedTask.prompt} } > )} - {task.status === "in_progress" && - task.latestMessage && - hasToolCalls(task.latestMessage) && ( + {mergedTask.status === "in_progress" && + mergedTask.latestMessage && + hasToolCalls(mergedTask.latestMessage) && ( } > - {explainLastToolCall(task.latestMessage, t)} + {explainLastToolCall(mergedTask.latestMessage, t)} )} - {task.status === "completed" && ( + {mergedTask.status === "completed" && ( <> @@ -164,9 +171,9 @@ export function SubtaskCard({ > )} - {task.status === "failed" && ( + {mergedTask.status === "failed" && ( {task.error}} + label={
{mergedTask.error}
} icon={} >
)} diff --git a/frontend/src/core/tasks/context.tsx b/frontend/src/core/tasks/context.tsx index ea85772cb..efcc9d9fe 100644 --- a/frontend/src/core/tasks/context.tsx +++ b/frontend/src/core/tasks/context.tsx @@ -1,23 +1,26 @@ +import type { AIMessage } from "@langchain/langgraph-sdk"; import { createContext, useCallback, useContext, useState } from "react"; -import type { Subtask } from "./types"; - export interface SubtaskContextValue { - tasks: Record; - setTasks: (tasks: Record) => void; + latestMessages: Record; + setLatestMessages: React.Dispatch< + React.SetStateAction> + >; } export const SubtaskContext = createContext({ - tasks: {}, - setTasks: () => { + latestMessages: {}, + setLatestMessages: () => { /* noop */ }, }); export function SubtasksProvider({ children }: { children: React.ReactNode }) { - const [tasks, setTasks] = useState>({}); + const [latestMessages, setLatestMessages] = useState>( + {}, + ); return ( - + {children} ); @@ -33,21 +36,21 @@ export function useSubtaskContext() { return context; } -export function useSubtask(id: string) { - const { tasks } = useSubtaskContext(); - return tasks[id]; +export function useLatestSubtaskMessage(id: string) { + const { latestMessages } = useSubtaskContext(); + return latestMessages[id]; } -export function useUpdateSubtask() { - const { tasks, setTasks } = useSubtaskContext(); - const updateSubtask = useCallback( - (task: Partial & { id: string }) => { - tasks[task.id] = { ...tasks[task.id], ...task } as Subtask; - if (task.latestMessage) { - setTasks({ ...tasks }); - } +export function useUpdateLatestMessage() { + const { setLatestMessages } = useSubtaskContext(); + const updateLatestMessage = useCallback( + (taskId: string, message: AIMessage) => { + setLatestMessages((current) => ({ + ...current, + [taskId]: message, + })); }, - [tasks, setTasks], + [setLatestMessages], ); - return updateSubtask; + return updateLatestMessage; } diff --git a/frontend/src/core/tasks/derive.ts b/frontend/src/core/tasks/derive.ts new file mode 100644 index 000000000..47e5b1103 --- /dev/null +++ b/frontend/src/core/tasks/derive.ts @@ -0,0 +1,47 @@ +import type { Message } from "@langchain/langgraph-sdk"; + +import { extractTextFromMessage } from "@/core/messages/utils"; + +import { parseSubtaskResult } from "./subtask-result"; +import type { Subtask } from "./types"; + +export function buildSubtaskMapFromMessages( + messages: Message[], +): Record { + const tasks: Record = {}; + + for (const message of messages) { + if (message.type === "ai") { + for (const toolCall of message.tool_calls ?? []) { + if (toolCall.name !== "task" || !toolCall.id) { + continue; + } + + tasks[toolCall.id] = { + id: toolCall.id, + status: "in_progress", + subagent_type: String(toolCall.args?.subagent_type ?? ""), + description: String(toolCall.args?.description ?? ""), + prompt: String(toolCall.args?.prompt ?? ""), + }; + } + continue; + } + + if (message.type !== "tool" || !message.tool_call_id) { + continue; + } + + const task = tasks[message.tool_call_id]; + if (!task) { + continue; + } + + tasks[message.tool_call_id] = { + ...task, + ...parseSubtaskResult(extractTextFromMessage(message)), + }; + } + + return tasks; +} diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts index 4418a9e26..84ce9d05a 100644 --- a/frontend/src/core/threads/hooks.ts +++ b/frontend/src/core/threads/hooks.ts @@ -19,7 +19,7 @@ import { useI18n } from "../i18n/hooks"; import { isHiddenFromUIMessage } from "../messages/utils"; import type { FileInMessage } from "../messages/utils"; import type { LocalSettings } from "../settings"; -import { useUpdateSubtask } from "../tasks/context"; +import { useUpdateLatestMessage } from "../tasks/context"; import type { UploadedFileInfo } from "../uploads"; import { promptInputFilePartToFile, uploadFiles } from "../uploads"; @@ -393,7 +393,7 @@ export function useThreadStream({ }, []); const queryClient = useQueryClient(); - const updateSubtask = useUpdateSubtask(); + const updateLatestMessage = useUpdateLatestMessage(); const thread = useStream({ client: getAPIClient(isMock), @@ -503,7 +503,7 @@ export function useThreadStream({ task_id: string; message: AIMessage; }; - updateSubtask({ id: e.task_id, latestMessage: e.message }); + updateLatestMessage(e.task_id, e.message); return; }