"use client"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import type { StreamEvent } from "@/lib/unified-ws"; import { getTraceMeta } from "./TracePanels"; /** * A connected subagent's native run, rendered close to how its own CLI shows it: * each of DeepTutor's questions heads a round, then the agent's reply streams as * "●"-bulleted steps — its messages in a neutral bullet, tool calls in an amber * bullet — with command output in a muted block (collapsed when long). Text and * reasoning stream token-by-token: deltas sharing a merge id collapse to one row * that grows in place, so the answer types out and renders exactly once. * * Set in a Monaco/CJK monospace face (the CLI aesthetic) and the app's theme * tokens, so it matches the side viewer and follows light/dark theme. */ const MONO_FONT = 'Monaco, Menlo, Consolas, "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", monospace'; const RESULT_PREVIEW_LINES = 3; export default function SubagentRunTranscript({ events, className = "", }: { events: StreamEvent[]; className?: string; }) { const visible = useMemo(() => { // Events sharing a merge id collapse to ONE evolving row, rendered at the // first occurrence's position with the latest content. This drives two // things uniformly: a fill-in tool (web search shows "web search" on start, // fills in its query on finish) and token-level streaming (each text/think // delta carries the cumulative text under one id, so the row types out and // the final block finalizes it in place — the answer renders exactly once). const latestByMerge = new Map(); for (const e of events) { const id = getTraceMeta(e).subagent_merge_id; if (id) latestByMerge.set(id, e); } const seenMerge = new Set(); const out: StreamEvent[] = []; for (const e of events) { const mergeId = getTraceMeta(e).subagent_merge_id; if (mergeId) { if (seenMerge.has(mergeId)) continue; seenMerge.add(mergeId); out.push(latestByMerge.get(mergeId) ?? e); continue; } out.push(e); } return out; }, [events]); return (
{visible.length === 0 ? (
) : ( visible.map((event, idx) => ( )) )}
); } /** A "●"-bulleted step line (the agent's messages and tool calls). */ function BulletLine({ bulletClass, children, }: { bulletClass: string; children: React.ReactNode; }) { return (
{children}
); } function SubagentLine({ channel, text }: { channel: string; text: string }) { const { t } = useTranslation(); switch (channel) { case "question": case "user_question": // Heads each round: the question DeepTutor (or the user, from the sidebar) // put to the agent. return (
{channel === "user_question" ? t("You ask") : t("DeepTutor asks")}
{text}
); case "text": case "result": return ( {text} ); case "tool": return ( {text} ); case "reasoning": return (
{text}
); case "tool_result": return ; case "error": return (
{text}
); default: return (
{text}
); } } function ToolResultBlock({ text }: { text: string }) { const { i18n } = useTranslation(); const zh = i18n.language?.toLowerCase().startsWith("zh"); const [open, setOpen] = useState(false); const lines = text.split("\n"); const overflow = lines.length > RESULT_PREVIEW_LINES; const shown = open || !overflow ? lines : lines.slice(0, RESULT_PREVIEW_LINES); const hidden = lines.length - RESULT_PREVIEW_LINES; return (
        {shown.join("\n")}
      
{overflow && ( )}
); }