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 ? (