From 59743568bab6e3ef50e2daf6b74e969c710cc5bf Mon Sep 17 00:00:00 2001 From: Daoyuan Li <94409450+DaoyuanLi2816@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:42:06 -0700 Subject: [PATCH] fix(frontend): ignore inherited artifact language keys (#6152) --- README.md | 2 +- frontend/src/AGENTS.md | 2 ++ frontend/src/core/utils/files.tsx | 2 +- frontend/tests/e2e/artifact-preview.spec.ts | 34 +++++++++++++++++++ .../artifact-file-detail.dom.test.tsx | 20 +++++++++++ frontend/tests/unit/core/utils/files.test.ts | 11 ++++++ 6 files changed, 69 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a54cce2a54..b42d767449 100644 --- a/README.md +++ b/README.md @@ -1836,7 +1836,7 @@ Image bytes loaded for a vision-model call are transient: DeerFlow removes the h After each run, DeerFlow records a workspace change summary for the run-owned `workspace` and `outputs` directories. The Web UI shows a compact "files changed" badge on the assistant turn; opening it reveals created, modified, and deleted files with text diffs when safe to display. Uploads are excluded because they are user inputs, not agent-generated changes, and stdio MCP temporary/debug files under the DeerFlow-owned `.mcp/` namespace are excluded because they are process-internal state (like `.git/` and `node_modules/`, any directory named `.mcp` is excluded at any depth). Large, binary, or sensitive-looking files are shown as metadata only. -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. +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. CSV and TSV artifacts open as tables in the artifact panel and in a separate window. The preview preserves text values (including leading zeros), supports an optional header row, and pages through up to 200 rows and 50 columns from the initial sample. Long or multiline cells can be opened and copied in full. Switch to source to inspect or edit the file; downloads and separate windows use the saved version. diff --git a/frontend/src/AGENTS.md b/frontend/src/AGENTS.md index 6e7257a6cf..1ea07862f0 100644 --- a/frontend/src/AGENTS.md +++ b/frontend/src/AGENTS.md @@ -15,6 +15,8 @@ Only Markdown destinations decode once; relative images match decoded names against raw artifact paths before encoding. File-type detection uses the basename so extensionless `Dockerfile` and `Makefile` artifacts remain recognizable under nested or dotted directories. +Language-map membership checks only own properties; inherited names such as +`constructor` and `__proto__` must keep the unknown-file download fallback. 1. Optional composer helpers such as `core/input-polish` can rewrite the local draft before submission, and `core/voice-input` can transcribe browser microphone input into that same local draft; confirmed user input then flows to thread hooks (`core/threads/hooks.ts`) → LangGraph SDK streaming 2. Stream events update thread state (messages, artifacts, todos, goal). The main thread stream uses the LangGraph SDK's `throttle: true` mode so updates received in the same macrotask coalesce before React is notified; do not replace it with a numeric delay without validating the SDK's trailing-debounce behavior on a continuous stream. diff --git a/frontend/src/core/utils/files.tsx b/frontend/src/core/utils/files.tsx index 97e0574109..cacd9d52c1 100644 --- a/frontend/src/core/utils/files.tsx +++ b/frontend/src/core/utils/files.tsx @@ -182,7 +182,7 @@ export function checkCodeFile( | { isCodeFile: true; language: string } | { isCodeFile: false; language: null } { const extension = getFileExtension(filepath); - const isCodeFile = extension in extensionMap; + const isCodeFile = Object.hasOwn(extensionMap, extension); if (isCodeFile) { return { isCodeFile: true, diff --git a/frontend/tests/e2e/artifact-preview.spec.ts b/frontend/tests/e2e/artifact-preview.spec.ts index 76db665b1d..35c2f88044 100644 --- a/frontend/tests/e2e/artifact-preview.spec.ts +++ b/frontend/tests/e2e/artifact-preview.spec.ts @@ -89,6 +89,40 @@ function presentFilesMessages(path = PRESENTED_ARTIFACT_PATH) { } test.describe("Artifact preview stability", () => { + for (const [index, filename] of ["constructor", "data.__proto__"].entries()) { + test(`keeps ${filename} artifacts on the download fallback`, async ({ + page, + }) => { + const threadId = `00000000-0000-0000-0000-00000000313${index}`; + const filepath = `/mnt/user-data/outputs/${filename}`; + mockLangGraphAPI(page, { + threads: [ + { + thread_id: threadId, + title: "Unknown artifact type", + messages: presentFilesMessages(filepath), + artifacts: [filepath], + }, + ], + }); + + await page.goto(`/workspace/chats/${threadId}`); + await page.getByText(filename, { exact: true }).first().click(); + + const artifactsPanel = page.locator("#artifacts"); + const download = artifactsPanel.getByRole("link", { name: "Download" }); + await expect(download).toBeVisible(); + await expect(download).toHaveAttribute( + "href", + `/api/threads/${threadId}/artifacts${filepath}?download=true`, + ); + await expect(artifactsPanel.locator(".cm-editor")).toHaveCount(0); + await expect( + artifactsPanel.getByRole("button", { name: "Edit", exact: true }), + ).toHaveCount(0); + }); + } + test("renders preview iframe for an in-progress write artifact", async ({ page, }) => { diff --git a/frontend/tests/unit/components/workspace/artifacts/artifact-file-detail.dom.test.tsx b/frontend/tests/unit/components/workspace/artifacts/artifact-file-detail.dom.test.tsx index 1fd1d55824..23e3766efe 100644 --- a/frontend/tests/unit/components/workspace/artifacts/artifact-file-detail.dom.test.tsx +++ b/frontend/tests/unit/components/workspace/artifacts/artifact-file-detail.dom.test.tsx @@ -111,6 +111,26 @@ describe("ArtifactFileDetail extensionless source files", () => { }); }); +describe("ArtifactFileDetail unknown file types", () => { + it.each(["constructor", "data.constructor", "__proto__", "data.__proto__"])( + "keeps %s on the download fallback instead of passing an invalid language to the editor", + (filename) => { + mocks.artifactContent.content = "file contents"; + mocks.artifactContent.sha256 = "a".repeat(64); + + const view = renderDetail(`/mnt/user-data/outputs/${filename}`); + + expect(view.queryByTestId("artifact-source")).toBeNull(); + expect(view.queryByRole("button", { name: "Edit" })).toBeNull(); + expect( + view.getByRole("link", { name: "Download" }).getAttribute("href"), + ).toBe( + `/backend/api/threads/${THREAD_ID}/artifacts/mnt/user-data/outputs/${filename}?download=true`, + ); + }, + ); +}); + afterEach(() => { cleanup(); rs.clearAllMocks(); diff --git a/frontend/tests/unit/core/utils/files.test.ts b/frontend/tests/unit/core/utils/files.test.ts index 97a9d2a0d8..a34a9f271c 100644 --- a/frontend/tests/unit/core/utils/files.test.ts +++ b/frontend/tests/unit/core/utils/files.test.ts @@ -20,6 +20,17 @@ test("keeps the document icon for an unknown extension", () => { expect(getFileIcon("data.unknown").type).toBe(FileTextIcon); }); +test.each(["constructor", "data.constructor", "__proto__", "data.__proto__"])( + "does not classify inherited map properties as languages for %s", + (filename) => { + expect(checkCodeFile(`/mnt/user-data/outputs/${filename}`)).toEqual({ + isCodeFile: false, + language: null, + }); + expect(getFileIcon(filename).type).toBe(FileTextIcon); + }, +); + // `extensionMap` maps a file extension to a language name. Both halves of that // direction matter: `checkCodeFile` tests extension membership, and the language // is what the editor and previewers receive.