fix(frontend): ignore inherited artifact language keys (#6152)

This commit is contained in:
Daoyuan Li 2026-10-01 04:42:06 -07:00 committed by GitHub
parent d2cfe4a986
commit 59743568ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 69 additions and 2 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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,

View File

@ -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,
}) => {

View File

@ -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();

View File

@ -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.