diff --git a/features/agent/components/agent-message-details.tsx b/features/agent/components/agent-message-details.tsx
index 917ab9c..c4a79ab 100644
--- a/features/agent/components/agent-message-details.tsx
+++ b/features/agent/components/agent-message-details.tsx
@@ -157,7 +157,7 @@ export function AgentMessageProgress({
return null;
}
- return ;
+ return ;
}
function AgentNestedBlock({
@@ -200,35 +200,35 @@ function AnimatedDetailItem({ children }: { children: ReactNode }) {
function ProgressList({ progress, running }: { progress: AgentChatProgress[]; running: boolean }) {
const [expanded, setExpanded] = useState(false);
- const visibleProgress = running
- ? progress.slice(-STREAMING_PROGRESS_LIMIT)
- : expanded
- ? progress
+ const compact = running && !expanded;
+ const visibleProgress = expanded
+ ? progress
+ : running
+ ? progress.slice(-STREAMING_PROGRESS_LIMIT)
: [];
- const listMode = running ? "streaming" : expanded ? "expanded" : "collapsed";
+ const listMode = expanded ? "expanded" : running ? "streaming" : "collapsed";
return (
@@ -236,18 +236,21 @@ function ProgressList({ progress, running }: { progress: AgentChatProgress[]; ru
{visibleProgress.length ? (
-
+
{visibleProgress.map((item) => (
-
+
{item.title}
{item.detail ? (
-
+
{item.detail}
) : null}
diff --git a/tests/browser/agent-streaming-stability.e2e.ts b/tests/browser/agent-streaming-stability.e2e.ts
index 5986baf..e641563 100644
--- a/tests/browser/agent-streaming-stability.e2e.ts
+++ b/tests/browser/agent-streaming-stability.e2e.ts
@@ -7,6 +7,7 @@ const STREAM_CHUNK_DELAY_MS = 20;
let streamServer: Server;
let streamServerUrl: string;
+let appendProgress: (() => void) | null = null;
let failProgress: (() => void) | null = null;
let releaseStream: (() => void) | null = null;
@@ -121,18 +122,20 @@ test.beforeAll(async () => {
while (index < 3 && index < chunks.length) {
sendNextTextChunk();
}
- send({
- type: "data-progress",
- id: "assess-risk",
- data: {
+ appendProgress = () => {
+ send({
+ type: "data-progress",
id: "assess-risk",
- phase: "analysis",
- status: "running",
- title: "正在判断溢流风险",
- detail: "结合流量突变与液位趋势形成判断。",
- started_at: Date.now()
- }
- });
+ data: {
+ id: "assess-risk",
+ phase: "analysis",
+ status: "running",
+ title: "正在判断溢流风险",
+ detail: "结合流量突变与液位趋势形成判断。",
+ started_at: Date.now()
+ }
+ });
+ };
failProgress = () => {
send({
@@ -262,14 +265,65 @@ test("stream completion and the plan card remain positionally stable", async ({
await sendButton.click();
const liveProgress = page.getByRole("list", { name: "最近执行进度" });
+ await expect(liveProgress.locator("li")).toHaveCount(3);
+ await expect(liveProgress.getByText("正在分析运行上下文", { exact: true })).toBeVisible();
+
+ await page.evaluate(() => {
+ const state = window as typeof window & {
+ __agentMinVisibleProgressRows?: number;
+ __agentProgressWindowSampleDone?: boolean;
+ };
+ const startedAt = performance.now();
+ state.__agentMinVisibleProgressRows = Number.POSITIVE_INFINITY;
+ state.__agentProgressWindowSampleDone = false;
+
+ const sample = () => {
+ const rows = document.querySelectorAll('ol[aria-label="最近执行进度"] > li').length;
+ state.__agentMinVisibleProgressRows = Math.min(state.__agentMinVisibleProgressRows ?? rows, rows);
+ if (performance.now() - startedAt < 400) {
+ requestAnimationFrame(sample);
+ } else {
+ state.__agentProgressWindowSampleDone = true;
+ }
+ };
+
+ requestAnimationFrame(sample);
+ });
+ triggerProgressAppend();
await expect(liveProgress.getByText("正在判断溢流风险", { exact: true })).toBeVisible();
await expect(liveProgress.locator("li")).toHaveCount(3);
await expect(liveProgress.getByText("正在分析运行上下文", { exact: true })).toHaveCount(0);
+ await expect.poll(() =>
+ page.evaluate(() =>
+ (window as typeof window & { __agentProgressWindowSampleDone?: boolean }).__agentProgressWindowSampleDone ?? false
+ )
+ ).toBe(true);
+ expect(
+ await page.evaluate(() =>
+ (window as typeof window & { __agentMinVisibleProgressRows?: number }).__agentMinVisibleProgressRows
+ )
+ ).toBeGreaterThanOrEqual(3);
const liveProgressRowHeights = await liveProgress.locator("li").evaluateAll((rows) =>
rows.map((row) => row.getBoundingClientRect().height)
);
expect(liveProgressRowHeights.every((height) => Math.abs(height - 48) <= 0.01)).toBe(true);
+ await expect(liveProgress.locator("li").first().locator("span.min-w-0 > span").first()).toHaveCSS(
+ "white-space",
+ "nowrap"
+ );
+ const progressToggle = page.getByRole("button", { name: /执行进度/ }).filter({ visible: true }).first();
+ await expect(progressToggle).toBeEnabled();
+ await progressToggle.click();
+ const expandedLiveProgress = page.getByRole("list", { name: "全部执行进度" });
+ await expect(expandedLiveProgress.locator("li")).toHaveCount(4);
+ await expect(expandedLiveProgress.getByText("正在分析运行上下文", { exact: true })).toBeVisible();
+ await expect(expandedLiveProgress.getByText("正在核对降雨过程与汇水区响应关系", { exact: true })).toHaveCSS(
+ "white-space",
+ "normal"
+ );
+ await progressToggle.click();
+ await expect(page.getByRole("list", { name: "最近执行进度" }).locator("li")).toHaveCount(3);
await page.evaluate(() => {
const state = window as typeof window & { __agentFailureMotionObserved?: boolean };
@@ -300,7 +354,6 @@ test("stream completion and the plan card remain positionally stable", async ({
await expect(page.getByText("分析完成", { exact: true }).filter({ visible: true }).first()).toBeVisible({
timeout: 10_000
});
- const progressToggle = page.getByRole("button", { name: /执行进度/ }).filter({ visible: true }).first();
await expect(progressToggle).toContainText("共 4 条");
await expect(page.getByRole("list", { name: "最近执行进度" })).toHaveCount(0);
await expect(page.getByText("溢流风险判断完成", { exact: true }).filter({ visible: true })).toHaveCount(0);
@@ -358,8 +411,32 @@ test("stream completion and the plan card remain positionally stable", async ({
).toBeLessThanOrEqual(2);
await progressToggle.click();
- await expect(page.getByRole("list", { name: "全部执行进度" }).locator("li")).toHaveCount(4);
+ const expandedProgress = page.getByRole("list", { name: "全部执行进度" });
+ await expect(expandedProgress.locator("li")).toHaveCount(4);
await expect(page.getByText("正在分析运行上下文", { exact: true }).filter({ visible: true }).first()).toBeVisible();
+ const expandedLongTitle = expandedProgress.getByText("正在核对降雨过程与汇水区响应关系", { exact: true });
+ await expect(expandedLongTitle).toHaveCSS("white-space", "normal");
+ await expect(expandedProgress.locator("li").nth(1)).not.toHaveCSS("height", "48px");
+
+ await progressToggle.click();
+ await prompt.fill("验证展开状态在流结束后保持");
+ await sendButton.click();
+
+ const latestProgressToggle = page.getByRole("button", { name: /执行进度/ }).filter({ visible: true }).last();
+ await expect(latestProgressToggle).toContainText("最近 3 条");
+ triggerProgressAppend();
+ await expect(
+ page.getByRole("list", { name: "最近执行进度" }).getByText("正在判断溢流风险", { exact: true })
+ ).toBeVisible();
+ await latestProgressToggle.click();
+ await expect(latestProgressToggle).toContainText("全部 4 条");
+ resumeStream();
+
+ await expect(latestProgressToggle).toContainText("全部 4 条", { timeout: 10_000 });
+ await expect(page.getByRole("list", { name: "全部执行进度" })).toBeVisible();
+ await expect(
+ page.getByRole("list", { name: "全部执行进度" }).getByText("溢流风险判断完成", { exact: true })
+ ).toBeVisible();
});
function createLongMarkdownResponse() {
@@ -393,6 +470,16 @@ function resumeStream() {
resume();
}
+function triggerProgressAppend() {
+ if (!appendProgress) {
+ throw new Error("Progress append checkpoint was not reached");
+ }
+
+ const append = appendProgress;
+ appendProgress = null;
+ append();
+}
+
function triggerProgressFailure() {
if (!failProgress) {
throw new Error("Progress failure checkpoint was not reached");