fix: 节流会商流式写文件预览,避免 Maximum update depth
write_file 的 content 在每个 SSE chunk 中持续增长,原先会反复触发 React 状态更新并打断整页;现改为按身份入参去重、节流发布 stub,并收口第 3 步为 Markdown 报告工作台。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
dcfaa199bf
commit
30799f17be
14 files changed
+644
-703
No files matched your search
@@ -155,6 +155,7 @@ export function ArtifactFileDetail({
|
||||
onDownloadClick,
|
||||
toolbarActions,
|
||||
headerLeadingAction,
|
||||
headerLeadingActionBare = false,
|
||||
researchSessionId,
|
||||
}: {
|
||||
className?: string;
|
||||
@@ -192,6 +193,11 @@ export function ArtifactFileDetail({
|
||||
toolbarActions?: ReactNode;
|
||||
/** Replaces the left file-type badge for an embedded workspace action. */
|
||||
headerLeadingAction?: ReactNode;
|
||||
/**
|
||||
* 嵌入式工作区的标题动作可直接放在标题栏中,避免再被文件类型徽标的胶囊容器包一层。
|
||||
* 默认保留原有徽标容器,避免影响普通聊天和产物预览。
|
||||
*/
|
||||
headerLeadingActionBare?: boolean;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { artifacts, setOpen, select } = useArtifacts();
|
||||
@@ -1205,7 +1211,13 @@ export function ArtifactFileDetail({
|
||||
<Artifact className={cn(className)}>
|
||||
<ArtifactHeader className="flex flex-wrap items-center gap-3 px-4 py-3">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div className="bg-muted/65 inline-flex min-w-0 items-center rounded-full border border-black/6 p-1 shadow-sm dark:border-white/8">
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex min-w-0 items-center gap-2",
|
||||
!headerLeadingActionBare &&
|
||||
"bg-muted/65 rounded-full border border-black/6 p-1 shadow-sm dark:border-white/8",
|
||||
)}
|
||||
>
|
||||
{headerLeadingAction ?? (
|
||||
<span className="bg-background text-muted-foreground inline-flex h-7 shrink-0 items-center rounded-full border border-black/6 px-2.5 text-[10px] font-semibold tracking-[0.16em] dark:border-white/8">
|
||||
{fileTypeLabel}
|
||||
|
||||
@@ -52,11 +52,17 @@ interface ArtifactsProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||
const [artifacts, setArtifactsState] = useState<string[]>([]);
|
||||
const setArtifacts = useCallback((next: string[]) => {
|
||||
setArtifactsState(Array.isArray(next) ? next : []);
|
||||
}, []);
|
||||
export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||
const [artifacts, setArtifactsState] = useState<string[]>([]);
|
||||
const setArtifacts = useCallback((next: string[]) => {
|
||||
const normalized = Array.isArray(next) ? next : [];
|
||||
setArtifactsState((previous) =>
|
||||
previous.length === normalized.length &&
|
||||
previous.every((artifact, index) => artifact === normalized[index])
|
||||
? previous
|
||||
: normalized,
|
||||
);
|
||||
}, []);
|
||||
const [selectedArtifact, setSelectedArtifact] = useState<string | null>(null);
|
||||
const [selectedArtifactKey, setSelectedArtifactKey] = useState<string | null>(null);
|
||||
const [autoSelect, setAutoSelect] = useState(true);
|
||||
@@ -65,18 +71,24 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||
);
|
||||
const [autoOpen, setAutoOpen] = useState(true);
|
||||
const suppressedAutoOpenKeyRef = useRef<string | null>(null);
|
||||
// 供稳定的 setOpen 回调同步读取。写文件流会高频调用 select/setOpen,避免每次
|
||||
// Context 状态变化都重建 setter 并触发依赖它的布局 effect。
|
||||
const selectedArtifactKeyRef = useRef<string | null>(selectedArtifactKey);
|
||||
const autoOpenRef = useRef(autoOpen);
|
||||
const sidebar = useSidebarSafe();
|
||||
|
||||
const select = useCallback(
|
||||
const select = useCallback(
|
||||
(artifact: string, autoSelect = false) => {
|
||||
setSelectedArtifact(artifact);
|
||||
setSelectedArtifactKey(artifactPreviewKey(artifact));
|
||||
const previewKey = artifactPreviewKey(artifact);
|
||||
selectedArtifactKeyRef.current = previewKey;
|
||||
setSelectedArtifact((previous) => (previous === artifact ? previous : artifact));
|
||||
setSelectedArtifactKey((previous) => (previous === previewKey ? previous : previewKey));
|
||||
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true") {
|
||||
sidebar?.setOpen(false);
|
||||
}
|
||||
if (!autoSelect) {
|
||||
setAutoSelect(false);
|
||||
}
|
||||
if (!autoSelect) {
|
||||
setAutoSelect((previous) => (previous ? false : previous));
|
||||
}
|
||||
},
|
||||
[sidebar],
|
||||
);
|
||||
@@ -86,11 +98,13 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||
if (suppressedAutoOpenKeyRef.current === previewKey) {
|
||||
return false;
|
||||
}
|
||||
setSelectedArtifact(artifact);
|
||||
setSelectedArtifactKey(previewKey);
|
||||
setAutoSelect(true);
|
||||
setAutoOpen(true);
|
||||
setOpenState(true);
|
||||
selectedArtifactKeyRef.current = previewKey;
|
||||
autoOpenRef.current = true;
|
||||
setSelectedArtifact((previous) => (previous === artifact ? previous : artifact));
|
||||
setSelectedArtifactKey((previous) => (previous === previewKey ? previous : previewKey));
|
||||
setAutoSelect((previous) => (previous ? previous : true));
|
||||
setAutoOpen((previous) => (previous ? previous : true));
|
||||
setOpenState((previous) => (previous ? previous : true));
|
||||
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true") {
|
||||
sidebar?.setOpen(false);
|
||||
}
|
||||
@@ -99,13 +113,26 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||
[sidebar],
|
||||
);
|
||||
|
||||
const deselect = useCallback(() => {
|
||||
setSelectedArtifact(null);
|
||||
setSelectedArtifactKey(null);
|
||||
setAutoSelect(true);
|
||||
setOpenState(false);
|
||||
const deselect = useCallback(() => {
|
||||
selectedArtifactKeyRef.current = null;
|
||||
setSelectedArtifact((previous) => (previous === null ? previous : null));
|
||||
setSelectedArtifactKey((previous) => (previous === null ? previous : null));
|
||||
setAutoSelect((previous) => (previous ? previous : true));
|
||||
setOpenState((previous) => (previous ? false : previous));
|
||||
suppressedAutoOpenKeyRef.current = null;
|
||||
}, []);
|
||||
|
||||
const setOpen = useCallback((isOpen: boolean) => {
|
||||
if (!isOpen && autoOpenRef.current) {
|
||||
autoOpenRef.current = false;
|
||||
setAutoOpen((previous) => (previous ? false : previous));
|
||||
setAutoSelect((previous) => (previous ? false : previous));
|
||||
}
|
||||
if (!isOpen) {
|
||||
suppressedAutoOpenKeyRef.current = selectedArtifactKeyRef.current;
|
||||
}
|
||||
setOpenState((previous) => (previous === isOpen ? previous : isOpen));
|
||||
}, []);
|
||||
|
||||
const value: ArtifactsContextType = {
|
||||
artifacts,
|
||||
@@ -114,16 +141,7 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||
open,
|
||||
autoOpen,
|
||||
autoSelect,
|
||||
setOpen: (isOpen: boolean) => {
|
||||
if (!isOpen && autoOpen) {
|
||||
setAutoOpen(false);
|
||||
setAutoSelect(false);
|
||||
}
|
||||
if (!isOpen) {
|
||||
suppressedAutoOpenKeyRef.current = selectedArtifactKey;
|
||||
}
|
||||
setOpenState(isOpen);
|
||||
},
|
||||
setOpen,
|
||||
|
||||
selectedArtifact,
|
||||
selectedArtifactKey,
|
||||
|
||||
Reference in new issue
Block a user