fix(frontend): 岗位会商沙箱在 write_file 流式阶段实时打开并刷新

content 先于 path 到达时用临时文件名开沙箱,流式快照改走外部 store 避免整页重渲染卡顿。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-08-04 00:51:38 +08:00
1 parent e939dc4548
commit 69e455274a
5 files changed
+480 -117

No files matched your search

@@ -121,8 +121,9 @@ function findScrollableArtifactElement(root: HTMLElement | null) {
export function ArtifactFileDetail({
className,
filepath: filepathFromProps,
threadId,
filepath: filepathFromProps,
threadId,
mimeType = null,
onClose,
allowVirtualMarkdownEdit = false,
disableDesensitize = false,
@@ -131,6 +132,8 @@ export function ArtifactFileDetail({
className?: string;
filepath: string;
threadId: string;
/** Server/tool supplied type wins over path guessing when available. */
mimeType?: string | null;
/** 覆盖默认关闭行为(仅 setOpen(false));圆桌 Step3 文件预览用于退出沙箱模式。 */
onClose?: () => void;
/**
@@ -150,16 +153,38 @@ export function ArtifactFileDetail({
const { t } = useI18n();
const { artifacts, setOpen, select } = useArtifacts();
const { isMock, thread, publicShareCode } = useThread();
const isWriteFileVirtual = useMemo(() => {
return filepathFromProps.startsWith("write-file:");
}, [filepathFromProps]);
const filepath = useMemo(() => {
if (isWriteFileVirtual) {
const url = new URL(filepathFromProps);
return decodeURIComponent(url.pathname);
}
return filepathFromProps;
}, [filepathFromProps, isWriteFileVirtual]);
const isWriteFileVirtual = useMemo(() => {
return filepathFromProps.startsWith("write-file:");
}, [filepathFromProps]);
// A streamed tool call can be opened before the path has been normalized by
// the artifacts API. Keep the format supplied by the writer so Markdown is
// rendered as Markdown instead of falling back to a TEXT source preview.
const writeFileLanguageHint = useMemo(() => {
if (!isWriteFileVirtual) return null;
try {
const language = new URL(filepathFromProps).searchParams.get("language");
return language === "markdown" ? language : null;
} catch {
return null;
}
}, [filepathFromProps, isWriteFileVirtual]);
const mimeLanguageHint = useMemo(() => {
const normalized = mimeType?.toLowerCase().trim() ?? "";
if (normalized.includes("markdown")) return "markdown";
if (normalized.includes("html")) return "html";
if (normalized.includes("mermaid")) return "mermaid";
if (normalized.startsWith("text/")) return "text";
return null;
}, [mimeType]);
const filepath = useMemo(() => {
if (isWriteFileVirtual) {
const url = new URL(filepathFromProps);
return decodeURIComponent(url.pathname).trim();
}
// Tool arguments may contain harmless surrounding whitespace. Passing it
// into extension detection turns `report.md ` into a generic TEXT file.
return filepathFromProps.trim();
}, [filepathFromProps, isWriteFileVirtual]);
const isSkillFile = useMemo(() => {
return filepath.endsWith(".skill");
}, [filepath]);
@@ -186,17 +211,22 @@ export function ArtifactFileDetail({
const useWriteFileStreaming = useMemo(() => {
return isWriteFileVirtual && !isImageFile(filepath) && !editVirtualMarkdown;
}, [filepath, isWriteFileVirtual, editVirtualMarkdown]);
const { isCodeFile, language } = useMemo(() => {
if (useWriteFileStreaming) {
let nextLanguage = checkCodeFile(filepath).language;
nextLanguage ??= "text";
return { isCodeFile: true, language: nextLanguage };
const { isCodeFile, language } = useMemo(() => {
if (useWriteFileStreaming) {
let nextLanguage =
mimeLanguageHint ?? writeFileLanguageHint ?? checkCodeFile(filepath).language;
nextLanguage ??= "text";
return { isCodeFile: true, language: nextLanguage };
}
if (isSkillFile) {
return { isCodeFile: true, language: "markdown" };
}
return checkCodeFile(filepath);
}, [filepath, useWriteFileStreaming, isSkillFile]);
const detected = checkCodeFile(filepath);
if (mimeLanguageHint) {
return { isCodeFile: true as const, language: mimeLanguageHint };
}
return detected;
}, [filepath, useWriteFileStreaming, isSkillFile, mimeLanguageHint, writeFileLanguageHint]);
const isSupportPreview = useMemo(() => {
return (
language === "html" ||
@@ -209,7 +239,8 @@ export function ArtifactFileDetail({
// editVirtualMarkdown:用解码后的真实路径走 artifacts API(而非 write-file 虚拟
// URL),这样保存后 useUpdateArtifactContent 写入的查询缓存才能命中,编辑内容
// 不会被旧的内存 stub 值覆盖回退。
filepath: editVirtualMarkdown ? filepath : filepathFromProps,
filepath:
editVirtualMarkdown || !isWriteFileVirtual ? filepath : filepathFromProps,
enabled: isCodeFile && !useWriteFileStreaming,
});