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");