diff --git a/src/components/chat/AgentActivityTimeline.test.tsx b/src/components/chat/AgentActivityTimeline.test.tsx new file mode 100644 index 0000000..35b9e93 --- /dev/null +++ b/src/components/chat/AgentActivityTimeline.test.tsx @@ -0,0 +1,69 @@ +import "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; + +import { AgentActivityTimeline } from "./AgentActivityTimeline"; + +describe("AgentActivityTimeline", () => { + it("shows only the first lines of a long tool command until expanded", () => { + const target = [ + "cd /app/data/conversation-workspaces/conversation-example", + "python3 - <<'EOF'", + "import json, glob", + "print('fourth line')", + "EOF", + ].join("\n"); + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "展开分析过程" })); + expect(screen.getByText(/import json, glob/u)).toBeInTheDocument(); + expect(screen.queryByText(/fourth line/u)).not.toBeInTheDocument(); + expect(screen.getByText("help data")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "展开完整内容" })).toHaveAttribute( + "aria-expanded", + "false", + ); + + fireEvent.click(screen.getByRole("button", { name: "展开完整内容" })); + expect(screen.getByText(/fourth line/u)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "收起内容" })).toHaveAttribute( + "aria-expanded", + "true", + ); + + fireEvent.click(screen.getByRole("button", { name: "收起内容" })); + expect(screen.queryByText(/fourth line/u)).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/chat/AgentActivityTimeline.tsx b/src/components/chat/AgentActivityTimeline.tsx index de91ba3..e685cfb 100644 --- a/src/components/chat/AgentActivityTimeline.tsx +++ b/src/components/chat/AgentActivityTimeline.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { Box, + Button, Collapse, IconButton, LinearProgress, @@ -89,6 +90,11 @@ const ActionStatusIcon = ({ status }: { status: AgentActivityAction["status"] }) const ActionRow = ({ action, now }: { action: AgentActivityAction; now: number }) => { const elapsed = getElapsedMs(action, now); + const [targetExpanded, setTargetExpanded] = useState(false); + const targetId = React.useId(); + const target = action.target ?? ""; + const targetPreview = target.split(/\r?\n/u).slice(0, 3).join("\n").slice(0, 240); + const hasLongTarget = targetPreview.length < target.length; return ( {action.target ? ( - - {action.target} - + + + {targetExpanded ? target : targetPreview} + + {hasLongTarget ? ( + + ) : null} + ) : null} {action.error ? (