From b3c312b7958f96b9ba0c2ce56d0bf4b095897ba8 Mon Sep 17 00:00:00 2001 From: Huixin615 Date: Sun, 28 Jun 2026 23:30:40 +0800 Subject: [PATCH] fix(frontend): retain presented artifacts in header dropdown (#3854) * fix(frontend): retain presented artifacts in dropdown * refactor(agents): trim file editing prompt context * Revert "refactor(agents): trim file editing prompt context" This reverts commit cad105265024335222e374692dde37424d11024b. --- .../artifacts/artifact-file-detail.tsx | 40 ++++++++++++++++--- frontend/tests/e2e/artifact-preview.spec.ts | 30 +++++++++++--- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx index 0f30b1cf2..4fa21945d 100644 --- a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -84,13 +84,41 @@ export function ArtifactFileDetail({ } return filepathFromProps; }, [filepathFromProps, isWriteFile]); - const artifactOptions = useMemo(() => { - const list = artifacts ?? []; - if (list.includes(filepath)) { - return list; + // Keep these local because ChatBox replaces context artifacts with thread state. + const [openedPresentedFilepaths, setOpenedPresentedFilepaths] = useState< + string[] + >(() => { + if (isWriteFile || artifacts.includes(filepath)) { + return []; } - return [filepath, ...list]; - }, [artifacts, filepath]); + return [filepath]; + }); + useEffect(() => { + if (isWriteFile || artifacts.includes(filepath)) { + return; + } + setOpenedPresentedFilepaths((current) => { + if (current.includes(filepath)) { + return current; + } + return [...current, filepath]; + }); + }, [artifacts, filepath, isWriteFile]); + const artifactOptions = useMemo(() => { + if (isWriteFile) { + return artifacts; + } + const currentIsPresented = !artifacts.includes(filepath); + const presentedFilepaths = + currentIsPresented && !openedPresentedFilepaths.includes(filepath) + ? [...openedPresentedFilepaths, filepath] + : openedPresentedFilepaths; + const presentedSet = new Set(presentedFilepaths); + return [ + ...presentedFilepaths, + ...artifacts.filter((artifact) => !presentedSet.has(artifact)), + ]; + }, [artifacts, filepath, isWriteFile, openedPresentedFilepaths]); const isSkillFile = useMemo(() => { return filepath.endsWith(".skill"); }, [filepath]); diff --git a/frontend/tests/e2e/artifact-preview.spec.ts b/frontend/tests/e2e/artifact-preview.spec.ts index 8f32cb501..792964a48 100644 --- a/frontend/tests/e2e/artifact-preview.spec.ts +++ b/frontend/tests/e2e/artifact-preview.spec.ts @@ -262,16 +262,16 @@ test.describe("Artifact preview stability", () => { ).toBeVisible(); }); - test("shows the title for a presented artifact missing from thread artifacts", async ({ + test("keeps an opened presented artifact in the header dropdown", async ({ page, }) => { mockLangGraphAPI(page, { threads: [ { thread_id: PRESENTED_THREAD_ID, - title: "Presented artifact title fallback", + title: "Presented artifact dropdown history", messages: presentFilesMessages(), - artifacts: [], + artifacts: [MARKDOWN_ARTIFACT_PATH], }, ], }); @@ -284,6 +284,15 @@ test.describe("Artifact preview stability", () => { body: "# Presented Report\n\nGenerated content", }), ); + await page.route( + `**/api/threads/${PRESENTED_THREAD_ID}/artifacts/artifact-fixtures/report.md`, + (route) => + route.fulfill({ + status: 200, + contentType: "text/markdown", + body: "# Thread Report\n\nTracked artifact content", + }), + ); await page.goto(`/workspace/chats/${PRESENTED_THREAD_ID}`); @@ -295,11 +304,20 @@ test.describe("Artifact preview stability", () => { const artifactsPanel = page.locator("#artifacts"); - // 1. Header title should fall back to the current filepath even though - // thread.values.artifacts is empty. await expect(artifactsPanel.getByText("presented-report.md")).toBeVisible(); + await expect(artifactsPanel.getByText("Presented Report")).toBeVisible(); - // 2. Preview content should still render correctly. + const artifactSelect = artifactsPanel.getByRole("combobox"); + await artifactSelect.click(); + await page.getByRole("option", { name: "report.md", exact: true }).click(); + await expect(artifactsPanel.getByText("Thread Report")).toBeVisible(); + + await artifactSelect.click(); + const presentedOption = page.getByRole("option", { + name: "presented-report.md", + }); + await expect(presentedOption).toBeVisible(); + await presentedOption.click(); await expect(artifactsPanel.getByText("Presented Report")).toBeVisible(); }); });