fix: prevent agent progress layout jitter
The previous popLayout stabilization kept margin-based spacing, so exiting rows temporarily added 6px to bounded Agent lists. Use flex gaps and a geometry regression check so exited rows no longer change container height.
This commit is contained in:
@@ -237,7 +237,7 @@ function ProgressList({ progress, running }: { progress: AgentChatProgress[]; ru
|
|||||||
<motion.ol
|
<motion.ol
|
||||||
key={listMode}
|
key={listMode}
|
||||||
aria-label={expanded ? "全部执行进度" : "最近执行进度"}
|
aria-label={expanded ? "全部执行进度" : "最近执行进度"}
|
||||||
className="mt-2 space-y-1.5 overflow-hidden"
|
className="mt-2 flex flex-col gap-1.5 overflow-hidden"
|
||||||
initial={{ height: 0, opacity: 0 }}
|
initial={{ height: 0, opacity: 0 }}
|
||||||
animate={{ height: "auto", opacity: 1 }}
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
exit={{ height: 0, opacity: 0 }}
|
exit={{ height: 0, opacity: 0 }}
|
||||||
@@ -379,7 +379,7 @@ function PermissionList({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<AgentNestedBlock icon={LockKeyhole} iconClassName="text-orange-600" title="权限请求">
|
<AgentNestedBlock icon={LockKeyhole} iconClassName="text-orange-600" title="权限请求">
|
||||||
<motion.div className="space-y-1.5" variants={agentPanelListVariants} animate="animate">
|
<motion.div className="flex flex-col gap-1.5" variants={agentPanelListVariants} animate="animate">
|
||||||
<AnimatePresence initial={false} mode="popLayout">
|
<AnimatePresence initial={false} mode="popLayout">
|
||||||
{permissions.slice(-4).map((permission) => (
|
{permissions.slice(-4).map((permission) => (
|
||||||
<AnimatedDetailItem key={permission.requestId}>
|
<AnimatedDetailItem key={permission.requestId}>
|
||||||
@@ -496,7 +496,7 @@ function QuestionList({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<AgentNestedBlock icon={HelpCircle} iconClassName="text-blue-600" title="补充信息" floating>
|
<AgentNestedBlock icon={HelpCircle} iconClassName="text-blue-600" title="补充信息" floating>
|
||||||
<motion.div className="space-y-1.5" variants={agentPanelListVariants} animate="animate">
|
<motion.div className="flex flex-col gap-1.5" variants={agentPanelListVariants} animate="animate">
|
||||||
<AnimatePresence initial={false} mode="popLayout">
|
<AnimatePresence initial={false} mode="popLayout">
|
||||||
{questions.slice(-3).map((request) => (
|
{questions.slice(-3).map((request) => (
|
||||||
<AnimatedDetailItem key={request.requestId}>
|
<AnimatedDetailItem key={request.requestId}>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { expect, test } from "playwright/test";
|
|||||||
|
|
||||||
const STREAM_CHUNK_SIZE = 18;
|
const STREAM_CHUNK_SIZE = 18;
|
||||||
const STREAM_CHUNK_DELAY_MS = 20;
|
const STREAM_CHUNK_DELAY_MS = 20;
|
||||||
|
const PROGRESS_GEOMETRY_TOLERANCE_PX = 0.1;
|
||||||
|
|
||||||
let streamServer: Server;
|
let streamServer: Server;
|
||||||
let streamServerUrl: string;
|
let streamServerUrl: string;
|
||||||
@@ -267,19 +268,54 @@ test("stream completion and the plan card remain positionally stable", async ({
|
|||||||
const liveProgress = page.getByRole("list", { name: "最近执行进度" });
|
const liveProgress = page.getByRole("list", { name: "最近执行进度" });
|
||||||
await expect(liveProgress.locator("li")).toHaveCount(3);
|
await expect(liveProgress.locator("li")).toHaveCount(3);
|
||||||
await expect(liveProgress.getByText("正在分析运行上下文", { exact: true })).toBeVisible();
|
await expect(liveProgress.getByText("正在分析运行上下文", { exact: true })).toBeVisible();
|
||||||
|
await expect(liveProgress).toHaveCSS("height", "156px");
|
||||||
|
await expect.poll(() => liveProgress.evaluate((list) => list.style.height)).toBe("auto");
|
||||||
|
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const state = window as typeof window & {
|
const state = window as typeof window & {
|
||||||
__agentMinVisibleProgressRows?: number;
|
__agentMinVisibleProgressRows?: number;
|
||||||
|
__agentProgressGeometry?: {
|
||||||
|
maxContainerHeight: number;
|
||||||
|
maxListHeight: number;
|
||||||
|
minContainerHeight: number;
|
||||||
|
minListHeight: number;
|
||||||
|
};
|
||||||
__agentProgressWindowSampleDone?: boolean;
|
__agentProgressWindowSampleDone?: boolean;
|
||||||
};
|
};
|
||||||
const startedAt = performance.now();
|
const startedAt = performance.now();
|
||||||
state.__agentMinVisibleProgressRows = Number.POSITIVE_INFINITY;
|
state.__agentMinVisibleProgressRows = Number.POSITIVE_INFINITY;
|
||||||
|
state.__agentProgressGeometry = {
|
||||||
|
maxContainerHeight: Number.NEGATIVE_INFINITY,
|
||||||
|
maxListHeight: Number.NEGATIVE_INFINITY,
|
||||||
|
minContainerHeight: Number.POSITIVE_INFINITY,
|
||||||
|
minListHeight: Number.POSITIVE_INFINITY
|
||||||
|
};
|
||||||
state.__agentProgressWindowSampleDone = false;
|
state.__agentProgressWindowSampleDone = false;
|
||||||
|
|
||||||
const sample = () => {
|
const sample = () => {
|
||||||
const rows = document.querySelectorAll('ol[aria-label="最近执行进度"] > li').length;
|
const list = document.querySelector<HTMLOListElement>('ol[aria-label="最近执行进度"]');
|
||||||
|
const rows = list?.children.length ?? 0;
|
||||||
state.__agentMinVisibleProgressRows = Math.min(state.__agentMinVisibleProgressRows ?? rows, rows);
|
state.__agentMinVisibleProgressRows = Math.min(state.__agentMinVisibleProgressRows ?? rows, rows);
|
||||||
|
if (list && state.__agentProgressGeometry) {
|
||||||
|
const listHeight = list.getBoundingClientRect().height;
|
||||||
|
const containerHeight = list.parentElement?.getBoundingClientRect().height ?? 0;
|
||||||
|
state.__agentProgressGeometry.minListHeight = Math.min(
|
||||||
|
state.__agentProgressGeometry.minListHeight,
|
||||||
|
listHeight
|
||||||
|
);
|
||||||
|
state.__agentProgressGeometry.maxListHeight = Math.max(
|
||||||
|
state.__agentProgressGeometry.maxListHeight,
|
||||||
|
listHeight
|
||||||
|
);
|
||||||
|
state.__agentProgressGeometry.minContainerHeight = Math.min(
|
||||||
|
state.__agentProgressGeometry.minContainerHeight,
|
||||||
|
containerHeight
|
||||||
|
);
|
||||||
|
state.__agentProgressGeometry.maxContainerHeight = Math.max(
|
||||||
|
state.__agentProgressGeometry.maxContainerHeight,
|
||||||
|
containerHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
if (performance.now() - startedAt < 400) {
|
if (performance.now() - startedAt < 400) {
|
||||||
requestAnimationFrame(sample);
|
requestAnimationFrame(sample);
|
||||||
} else {
|
} else {
|
||||||
@@ -303,6 +339,23 @@ test("stream completion and the plan card remain positionally stable", async ({
|
|||||||
(window as typeof window & { __agentMinVisibleProgressRows?: number }).__agentMinVisibleProgressRows
|
(window as typeof window & { __agentMinVisibleProgressRows?: number }).__agentMinVisibleProgressRows
|
||||||
)
|
)
|
||||||
).toBeGreaterThanOrEqual(3);
|
).toBeGreaterThanOrEqual(3);
|
||||||
|
const progressGeometry = await page.evaluate(() =>
|
||||||
|
(window as typeof window & {
|
||||||
|
__agentProgressGeometry?: {
|
||||||
|
maxContainerHeight: number;
|
||||||
|
maxListHeight: number;
|
||||||
|
minContainerHeight: number;
|
||||||
|
minListHeight: number;
|
||||||
|
};
|
||||||
|
}).__agentProgressGeometry
|
||||||
|
);
|
||||||
|
expect(progressGeometry).toBeDefined();
|
||||||
|
expect(progressGeometry!.maxListHeight - progressGeometry!.minListHeight).toBeLessThanOrEqual(
|
||||||
|
PROGRESS_GEOMETRY_TOLERANCE_PX
|
||||||
|
);
|
||||||
|
expect(progressGeometry!.maxContainerHeight - progressGeometry!.minContainerHeight).toBeLessThanOrEqual(
|
||||||
|
PROGRESS_GEOMETRY_TOLERANCE_PX
|
||||||
|
);
|
||||||
|
|
||||||
const liveProgressRowHeights = await liveProgress.locator("li").evaluateAll((rows) =>
|
const liveProgressRowHeights = await liveProgress.locator("li").evaluateAll((rows) =>
|
||||||
rows.map((row) => row.getBoundingClientRect().height)
|
rows.map((row) => row.getBoundingClientRect().height)
|
||||||
|
|||||||
Reference in New Issue
Block a user