fix(chat): collapse long activity commands
This commit is contained in:
@@ -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(
|
||||
<AgentActivityTimeline
|
||||
activities={[
|
||||
{
|
||||
id: "analysis",
|
||||
title: "处理分析数据",
|
||||
reason: "正在核查结果。",
|
||||
status: "completed",
|
||||
startedAt: 1000,
|
||||
endedAt: 2000,
|
||||
actions: [
|
||||
{
|
||||
id: "long-command",
|
||||
tool: "bash",
|
||||
title: "运行本地分析",
|
||||
status: "completed",
|
||||
target,
|
||||
startedAt: 1100,
|
||||
endedAt: 1900,
|
||||
},
|
||||
{
|
||||
id: "short-command",
|
||||
tool: "tjwater_cli",
|
||||
title: "查询后端数据",
|
||||
status: "completed",
|
||||
target: "help data",
|
||||
startedAt: 1900,
|
||||
endedAt: 1950,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -111,19 +117,45 @@ const ActionRow = ({ action, now }: { action: AgentActivityAction; now: number }
|
||||
</Typography>
|
||||
</Stack>
|
||||
{action.target ? (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
display: "block",
|
||||
mt: 0.2,
|
||||
fontFamily: "monospace",
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{action.target}
|
||||
</Typography>
|
||||
<Box sx={{ mt: 0.2 }}>
|
||||
<Typography
|
||||
id={targetId}
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
display: !targetExpanded && hasLongTarget ? "-webkit-box" : "block",
|
||||
fontFamily: "monospace",
|
||||
overflow: !targetExpanded && hasLongTarget ? "hidden" : "visible",
|
||||
overflowWrap: "anywhere",
|
||||
whiteSpace: "pre-wrap",
|
||||
...(!targetExpanded && hasLongTarget
|
||||
? { WebkitBoxOrient: "vertical", WebkitLineClamp: 3 }
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{targetExpanded ? target : targetPreview}
|
||||
</Typography>
|
||||
{hasLongTarget ? (
|
||||
<Button
|
||||
size="small"
|
||||
aria-controls={targetId}
|
||||
aria-expanded={targetExpanded}
|
||||
onClick={() => setTargetExpanded((current) => !current)}
|
||||
sx={{
|
||||
mt: 0.2,
|
||||
p: 0,
|
||||
minWidth: 0,
|
||||
color: activityAccent,
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 650,
|
||||
lineHeight: 1.5,
|
||||
textTransform: "none",
|
||||
}}
|
||||
>
|
||||
{targetExpanded ? "收起内容" : "展开完整内容"}
|
||||
</Button>
|
||||
) : null}
|
||||
</Box>
|
||||
) : null}
|
||||
{action.error ? (
|
||||
<Typography
|
||||
|
||||
Reference in New Issue
Block a user