From d4fe22f956293ba2824fe818c1e23d3742988d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=AF=BC=EC=84=9D?= Date: Wed, 15 Apr 2026 10:57:16 +0900 Subject: [PATCH 1/2] feat: add collapsible activity view for completed turns --- frontend/e2e/completed-turn-task-view.spec.ts | 91 +++++++++++++++++++ .../chat/components/CompletedTurnTaskView.tsx | 80 ++++++++++++++++ .../features/chat/components/MessageList.tsx | 13 +++ 3 files changed, 184 insertions(+) create mode 100644 frontend/e2e/completed-turn-task-view.spec.ts create mode 100644 frontend/src/features/chat/components/CompletedTurnTaskView.tsx diff --git a/frontend/e2e/completed-turn-task-view.spec.ts b/frontend/e2e/completed-turn-task-view.spec.ts new file mode 100644 index 0000000..a2733dd --- /dev/null +++ b/frontend/e2e/completed-turn-task-view.spec.ts @@ -0,0 +1,91 @@ +/** + * E2E: CompletedTurnTaskView — verify completed turn activity renders correctly + * + * Tests that: + * 1. The page loads without errors + * 2. If activity items exist, they don't show generic node names + * 3. CompletedTurnTaskView component renders a collapsible "Turn Activity" section + */ + +import { test, expect } from "@playwright/test"; + +test.describe("CompletedTurnTaskView", () => { + test("page loads and CompletedTurnTaskView does not show generic node names", async ({ + page, + }) => { + test.setTimeout(30_000); + + await page.goto("/"); + await page.waitForLoadState("networkidle"); + await page.waitForTimeout(3000); + + // Check for activity items in the DOM + const activityItems = await page.evaluate(() => { + const items = document.querySelectorAll("[data-activity-item]"); + return Array.from(items).map((el) => ({ + kind: el.getAttribute("data-kind"), + status: el.getAttribute("data-status"), + depth: el.getAttribute("data-depth"), + text: (el.textContent || "").substring(0, 200), + })); + }); + + if (activityItems.length === 0) { + // No activity items on main page — this is expected when no thread is loaded + console.log("No activity items found — skipping assertions (no thread loaded)"); + return; + } + + // Verify no generic node names in LLM output items + const llmOutputs = activityItems.filter((i) => i.kind === "llm_output"); + for (const item of llmOutputs) { + const textStart = item.text.trim().substring(0, 20); + expect(textStart).not.toMatch(/^Model\s/); + expect(textStart).not.toMatch(/^Tools\s/); + } + + // Verify no generic names in subgraph depth=1 items + const depth1Items = activityItems.filter((i) => i.depth === "1"); + const hasGenericNames = depth1Items.some( + (i) => + i.text.startsWith("Model") || + i.text.startsWith("Tools") || + i.text.startsWith("Agent"), + ); + expect(hasGenericNames).toBe(false); + }); + + test("Turn Activity section is collapsible when present", async ({ page }) => { + test.setTimeout(30_000); + + await page.goto("/"); + await page.waitForLoadState("networkidle"); + await page.waitForTimeout(3000); + + // Look for "Turn Activity" text which indicates CompletedTurnTaskView is rendered + const turnActivityButton = page.locator("text=Turn Activity").first(); + + if (await turnActivityButton.isVisible().catch(() => false)) { + // Click to expand + await turnActivityButton.click(); + await page.waitForTimeout(500); + + // Take screenshot for visual verification + await page.screenshot({ + path: "test-results/completed-turn-expanded.png", + fullPage: true, + }); + + // Click again to collapse + await turnActivityButton.click(); + await page.waitForTimeout(500); + + await page.screenshot({ + path: "test-results/completed-turn-collapsed.png", + fullPage: true, + }); + } else { + console.log("No Turn Activity section found — skipping (no completed turns)"); + } + }); +}); diff --git a/frontend/src/features/chat/components/CompletedTurnTaskView.tsx b/frontend/src/features/chat/components/CompletedTurnTaskView.tsx new file mode 100644 index 0000000..6f7850e --- /dev/null +++ b/frontend/src/features/chat/components/CompletedTurnTaskView.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useState, memo } from "react"; +import type { Message } from "@langchain/langgraph-sdk"; +import { motion, AnimatePresence } from "framer-motion"; +import { ChevronRight, ChevronDown, Activity } from "lucide-react"; +import { useTaskProgress } from "@/features/chat/hooks/useTaskProgress"; +import { StreamingTaskView } from "./StreamingTaskView"; + +interface CompletedTurnTaskViewProps { + turnMessages: Message[]; + finalNodeNames: string[]; +} + +export const CompletedTurnTaskView = memo(function CompletedTurnTaskView({ + turnMessages, + finalNodeNames, +}: CompletedTurnTaskViewProps) { + const [isCollapsed, setIsCollapsed] = useState(true); + + const { progress, activityItems } = useTaskProgress({ + messages: turnMessages, + isStreaming: false, + finalNodeNames, + }); + + const hasContent = progress.length > 0 || activityItems.length > 0; + if (!hasContent) return null; + + const todoCount = progress.filter((p) => p.source === "todo").length; + const completedTodoCount = progress.filter( + (p) => p.source === "todo" && p.status === "completed", + ).length; + + const ChevronIcon = isCollapsed ? ChevronRight : ChevronDown; + + return ( +
+
setIsCollapsed(!isCollapsed)} + > + + + Turn Activity + {todoCount > 0 && ( + + ({completedTodoCount}/{todoCount}) + + )} + {activityItems.length > 0 && ( + + {todoCount > 0 ? "· " : ""} + {activityItems.length} activities + + )} +
+ + + {!isCollapsed && ( + +
+ +
+
+ )} +
+
+ ); +}); diff --git a/frontend/src/features/chat/components/MessageList.tsx b/frontend/src/features/chat/components/MessageList.tsx index 52a41ab..8d36ef9 100644 --- a/frontend/src/features/chat/components/MessageList.tsx +++ b/frontend/src/features/chat/components/MessageList.tsx @@ -12,6 +12,7 @@ import { DO_NOT_RENDER_ID_PREFIX } from "@/lib/utils/ensure-tool-responses"; import { AssistantMessage, AssistantMessageLoading } from "./messages/ai"; import { HumanMessage } from "./messages/human"; import { StreamingTaskView } from "./StreamingTaskView"; +import { CompletedTurnTaskView } from "./CompletedTurnTaskView"; import { shouldRenderMessage, buildSubagentContext } from "./utils"; import type { HierarchicalTask } from "@/types/task-hierarchy"; import type { TaskProgressItem, ActivityItem } from "@/types/task-progress"; @@ -319,6 +320,18 @@ export function MessageList({ return; if (isIntermediateNodeMessage(message)) return; if (!completedTurnFinalAiIds.has(message.id || "")) return; + + // Show collapsible activity view before the final AI message of completed turns + const turnStart = humanIndices[owningHumanIdx]; + const turnEnd = humanIndices[owningHumanIdx + 1]; + const turnMessages = filteredMessages.slice(turnStart, turnEnd); + elements.push( + , + ); } } else { // Current turn (or pre-turn messages): existing logic From b9f6f22953e87ee46b24f543b08fb2466b659822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=AF=BC=EC=84=9D?= Date: Wed, 15 Apr 2026 11:54:39 +0900 Subject: [PATCH 2/2] style: fix prettier formatting --- frontend/e2e/completed-turn-task-view.spec.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/e2e/completed-turn-task-view.spec.ts b/frontend/e2e/completed-turn-task-view.spec.ts index a2733dd..2862581 100644 --- a/frontend/e2e/completed-turn-task-view.spec.ts +++ b/frontend/e2e/completed-turn-task-view.spec.ts @@ -32,7 +32,9 @@ test.describe("CompletedTurnTaskView", () => { if (activityItems.length === 0) { // No activity items on main page — this is expected when no thread is loaded - console.log("No activity items found — skipping assertions (no thread loaded)"); + console.log( + "No activity items found — skipping assertions (no thread loaded)", + ); return; } @@ -55,7 +57,9 @@ test.describe("CompletedTurnTaskView", () => { expect(hasGenericNames).toBe(false); }); - test("Turn Activity section is collapsible when present", async ({ page }) => { + test("Turn Activity section is collapsible when present", async ({ + page, + }) => { test.setTimeout(30_000); await page.goto("/"); @@ -85,7 +89,9 @@ test.describe("CompletedTurnTaskView", () => { fullPage: true, }); } else { - console.log("No Turn Activity section found — skipping (no completed turns)"); + console.log( + "No Turn Activity section found — skipping (no completed turns)", + ); } }); });