From db7f91911872d1d23be1668ab365986b63c3c4c0 Mon Sep 17 00:00:00 2001 From: jianyx1 <166109249+jianyx1@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:55:58 +0800 Subject: [PATCH] fix(frontend): keep artifact lists scrollable (#7284) Use the shared ScrollArea so long artifact lists remain reachable in desktop panels and mobile drawers. Add browser coverage for short and long lists before clicking the final file. Adapt the ScrollArea approach from LeonSGP43 in bytedance/deer-flow#2076 and verify it against current main for issue #2074. --- README.md | 2 +- .../components/workspace/chats/chat-box.tsx | 5 +- .../tests/e2e/artifact-list-scroll.spec.ts | 84 +++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 frontend/tests/e2e/artifact-list-scroll.spec.ts diff --git a/README.md b/README.md index e502db7c8d..e8ed793f87 100644 --- a/README.md +++ b/README.md @@ -2930,7 +2930,7 @@ recording the others. A concurrent scratch-file removal therefore does not discard the whole snapshot or suppress detection of other produced outputs. This remains a best-effort scan, not an atomic filesystem snapshot. -Files presented through `present_files` remain part of the thread's artifact state, and the Web UI restores the artifact panel and selected document after a page refresh. When a completed response successfully presents between 2 and 50 files, its final file card also offers one ZIP download. Archive membership comes from the terminal delivery receipt rather than browser-supplied paths, and the ZIP contains the current file versions, which may have changed since the response. The currently selected formal artifact is refreshed once when the run finishes so edits become visible without a manual reload. Existing UTF-8 text artifacts under `/mnt/user-data/outputs` can also be edited and explicitly saved from the panel on Unix and Windows while the thread is idle; saves use content revisions to prevent overwriting agent changes. Source previews also recognize extensionless `Dockerfile` and `Makefile` artifacts by their file names. Unknown file types, including names such as `constructor` and `__proto__`, retain the download fallback. +Files presented through `present_files` remain part of the thread's artifact state, and the Web UI restores the artifact panel and selected document after a page refresh. Long artifact lists scroll within the panel on desktop and in the mobile drawer, keeping every file reachable. When a completed response successfully presents between 2 and 50 files, its final file card also offers one ZIP download. Archive membership comes from the terminal delivery receipt rather than browser-supplied paths, and the ZIP contains the current file versions, which may have changed since the response. The currently selected formal artifact is refreshed once when the run finishes so edits become visible without a manual reload. Existing UTF-8 text artifacts under `/mnt/user-data/outputs` can also be edited and explicitly saved from the panel on Unix and Windows while the thread is idle; saves use content revisions to prevent overwriting agent changes. Source previews also recognize extensionless `Dockerfile` and `Makefile` artifacts by their file names. Unknown file types, including names such as `constructor` and `__proto__`, retain the download fallback. Unsaved artifact edits survive switching chats, workspace pages, or visiting the home page and returning in the same browser tab. Drafts stay in memory for the diff --git a/frontend/src/components/workspace/chats/chat-box.tsx b/frontend/src/components/workspace/chats/chat-box.tsx index a57b99e311..cdfd6749e6 100644 --- a/frontend/src/components/workspace/chats/chat-box.tsx +++ b/frontend/src/components/workspace/chats/chat-box.tsx @@ -15,6 +15,7 @@ import { ResizablePanel, ResizablePanelGroup, } from "@/components/ui/resizable"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { Sheet, SheetContent, @@ -406,13 +407,13 @@ const ChatBoxContent: React.FC = ({

Artifacts

-
+ -
+ )} diff --git a/frontend/tests/e2e/artifact-list-scroll.spec.ts b/frontend/tests/e2e/artifact-list-scroll.spec.ts new file mode 100644 index 0000000000..ae5be8244b --- /dev/null +++ b/frontend/tests/e2e/artifact-list-scroll.spec.ts @@ -0,0 +1,84 @@ +import { expect, test } from "@playwright/test"; + +import { mockLangGraphAPI } from "./utils/mock-api"; + +const THREAD_ID = "00000000-0000-0000-0000-000000002074"; + +for (const viewport of [ + { name: "desktop", width: 1280, height: 720 }, + { name: "narrow", width: 390, height: 844 }, +]) { + test.describe(`Artifact list (${viewport.name})`, () => { + test.use({ viewport }); + + for (const count of [1, 30]) { + test(`can reach and open the last of ${count} files`, async ({ + page, + }, testInfo) => { + const artifacts = Array.from( + { length: count }, + (_, index) => `/mnt/user-data/outputs/report-${index + 1}.txt`, + ); + const lastPath = artifacts[artifacts.length - 1]!; + const content = `Contents of report ${count}`; + mockLangGraphAPI(page, { + threads: [ + { + thread_id: THREAD_ID, + title: "Artifact list scrolling", + artifacts, + messages: [ + { type: "human", id: "human", content: "Create the reports" }, + { type: "ai", id: "ai", content: "The reports are ready." }, + ], + }, + ], + }); + await page.route( + `**/api/threads/${THREAD_ID}/artifacts${lastPath}`, + (route) => + route.fulfill({ contentType: "text/plain", body: content }), + ); + await page.goto(`/workspace/chats/${THREAD_ID}`); + await page.getByTestId("artifact-trigger").click(); + + const panel = + viewport.name === "desktop" + ? page.locator("#artifacts") + : page.getByRole("dialog", { name: "Artifacts", exact: true }); + const firstFile = panel.getByText("report-1.txt", { exact: true }); + const lastFile = panel.getByText(`report-${count}.txt`, { + exact: true, + }); + await expect(firstFile).toBeInViewport({ ratio: 1 }); + if (count > 1) { + await expect(lastFile).not.toBeInViewport(); + } + await firstFile.hover(); + await page.mouse.wheel(0, 10_000); + + try { + // Check reachability before click(), which would auto-scroll and + // hide the missing user-scrollable region on the old layout. + await expect(lastFile).toBeInViewport({ ratio: 1 }); + await expect( + panel + .getByRole("heading", { name: "Artifacts", exact: true }) + .last(), + ).toBeInViewport({ ratio: 1 }); + } finally { + const screenshot = testInfo.outputPath( + "artifact-list-after-scroll.png", + ); + await page.screenshot({ path: screenshot }); + await testInfo.attach("artifact-list-after-scroll", { + path: screenshot, + contentType: "image/png", + }); + } + await lastFile.click(); + await expect(panel.locator(".cm-content")).toHaveText(content); + }); + } + }); +}