fix(frontend): detect artifact types from file basenames (#6091)

This commit is contained in:
Daoyuan Li 2026-09-30 05:11:26 -07:00 committed by GitHub
parent 8e282c4c24
commit 5c67d61d65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 57 additions and 5 deletions

View File

@ -1822,7 +1822,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.
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.
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

@ -13,6 +13,8 @@ is documented in `docs/skill-usage-ui.md`.
Artifact URLs encode raw filesystem paths, preserving literal percent sequences.
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.
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

@ -172,7 +172,7 @@ export function getFileName(filepath: string) {
}
export function getFileExtension(filepath: string) {
return filepath.split(".").pop()!.toLocaleLowerCase();
return getFileName(filepath).split(".").pop()!.toLocaleLowerCase();
}
export function checkCodeFile(

View File

@ -48,10 +48,13 @@ rs.mock("@/core/auth/AuthProvider", () => ({
useAuth: () => ({ user: null }),
}));
// The real editor pulls CodeMirror into the worker bundle; these tests only
// exercise the browser-preview iframe branch, which never mounts it.
// Inspect the source passed to the editor without bundling CodeMirror.
rs.mock("@/components/workspace/code-editor", () => ({
CodeEditor: () => null,
CodeEditor: ({ value, language }: { value: string; language: string }) => (
<pre data-testid="artifact-source" data-language={language}>
{value}
</pre>
),
}));
rs.mock("@/core/config", () => ({
@ -88,6 +91,24 @@ function renderDetail(filepath: string) {
beforeEach(() => {
mocks.artifactContent.content = undefined;
mocks.artifactContent.sha256 = undefined;
});
describe("ArtifactFileDetail extensionless source files", () => {
it.each([
["Dockerfile", "dockerfile", "FROM python:3.13\n"],
["Makefile", "makefile", "all:\n\techo hello\n"],
])("opens a stored %s as editable source", (filename, language, content) => {
mocks.artifactContent.content = content;
mocks.artifactContent.sha256 = "a".repeat(64);
const view = renderDetail(`/mnt/user-data/outputs/${filename}`);
const source = view.getByTestId("artifact-source");
expect(source.textContent).toBe(content);
expect(source.getAttribute("data-language")).toBe(language);
expect(view.getByRole("button", { name: "Edit" })).toBeTruthy();
});
});
afterEach(() => {

View File

@ -0,0 +1,29 @@
import { describe, expect, it } from "@rstest/core";
import { checkCodeFile, getFileExtension } from "@/core/utils/files";
describe("file type detection from the basename", () => {
it.each([
["/mnt/user-data/outputs/Dockerfile", "dockerfile"],
["/mnt/user-data/outputs/Makefile", "makefile"],
["/mnt/user-data/outputs/project.v1/Dockerfile", "dockerfile"],
["/mnt/user-data/outputs/project.v1/Makefile", "makefile"],
["Dockerfile", "dockerfile"],
["Makefile", "makefile"],
["/mnt/user-data/outputs/project.v1/main.PY", "python"],
["/mnt/user-data/outputs/project.v1/.gitignore", "git-commit"],
["/mnt/user-data/outputs/project.v1/.env", "dotenv"],
["/mnt/user-data/outputs/project.v1/report%20final.py", "python"],
])("recognizes %s as %s", (filepath, language) => {
expect(checkCodeFile(filepath)).toEqual({ isCodeFile: true, language });
});
it("does not infer a type from a dotted parent directory", () => {
const filepath = "/mnt/user-data/outputs/main.py/unknown";
expect(getFileExtension(filepath)).toBe("unknown");
expect(checkCodeFile(filepath)).toEqual({
isCodeFile: false,
language: null,
});
});
});