fix: 流式自动开沙箱分阶段收起侧栏,避免同帧布局卡顿
新增 preOpen 两阶段标记,先错开收起左右侧栏再展开沙箱;智能体详情面板据此提前收起,减少与沙箱展开同帧叠加造成的卡顿。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
90d29ed232
commit
6e9e89a6cb
2 files changed
+96
-16
No files matched your search
@@ -267,8 +267,11 @@ export function AgentDetailPanel({
|
|||||||
const artifactsCtx = useArtifactsOptional();
|
const artifactsCtx = useArtifactsOptional();
|
||||||
const artifactCount = artifactsCtx?.artifacts.length ?? 0;
|
const artifactCount = artifactsCtx?.artifacts.length ?? 0;
|
||||||
const selectedArtifact = artifactsCtx?.selectedArtifact ?? null;
|
const selectedArtifact = artifactsCtx?.selectedArtifact ?? null;
|
||||||
|
// preOpen:流式自动开沙箱的两阶段标记 —— 沙箱真正展开(open 翻转)前就
|
||||||
|
// 先收起本面板,让收起布局先落地,避免与沙箱展开同帧叠加造成卡顿。
|
||||||
const artifactPanelVisible =
|
const artifactPanelVisible =
|
||||||
!!artifactsCtx?.open && (!!selectedArtifact || artifactCount > 0);
|
(!!artifactsCtx?.open || !!artifactsCtx?.preOpen) &&
|
||||||
|
(!!selectedArtifact || artifactCount > 0);
|
||||||
const prevArtifactPanelVisible = useRef(false);
|
const prevArtifactPanelVisible = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const opened = artifactPanelVisible && !prevArtifactPanelVisible.current;
|
const opened = artifactPanelVisible && !prevArtifactPanelVisible.current;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
createContext,
|
createContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
@@ -42,6 +43,12 @@ export interface ArtifactsContextType {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
autoOpen: boolean;
|
autoOpen: boolean;
|
||||||
setOpen: (open: boolean) => void;
|
setOpen: (open: boolean) => void;
|
||||||
|
/**
|
||||||
|
* 流式写文件自动开沙箱的两阶段标记:openStreamedArtifact 先收起两侧侧栏
|
||||||
|
* (阶段一),等收起布局落定后再真正翻转 open(阶段二)。两阶段之间为
|
||||||
|
* true —— 右侧智能体信息面板据此提前收起,避免与沙箱展开同帧叠加造成卡顿。
|
||||||
|
*/
|
||||||
|
preOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArtifactsContext = createContext<ArtifactsContextType | undefined>(
|
const ArtifactsContext = createContext<ArtifactsContextType | undefined>(
|
||||||
@@ -52,6 +59,13 @@ interface ArtifactsProviderProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 流式写文件自动开沙箱的分步延迟:t=0 收起左侧聊天记录侧栏,延迟
|
||||||
|
// STREAMED_PREOPEN_DELAY_MS 后再收右侧智能体信息面板(两次收起错开、
|
||||||
|
// 不做「左边收完再收右边」的判断,只给固定延迟,避免收起瞬间同帧叠加
|
||||||
|
// 卡顿),到 STREAMED_OPEN_DELAY_MS(右侧 300ms 收起动画结束)才展开沙箱。
|
||||||
|
const STREAMED_PREOPEN_DELAY_MS = 200;
|
||||||
|
const STREAMED_OPEN_DELAY_MS = 500;
|
||||||
|
|
||||||
export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
||||||
const [artifacts, setArtifactsState] = useState<string[]>([]);
|
const [artifacts, setArtifactsState] = useState<string[]>([]);
|
||||||
const setArtifacts = useCallback((next: string[]) => {
|
const setArtifacts = useCallback((next: string[]) => {
|
||||||
@@ -69,14 +83,50 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
|||||||
const [open, setOpenState] = useState(
|
const [open, setOpenState] = useState(
|
||||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true",
|
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true",
|
||||||
);
|
);
|
||||||
|
const [preOpen, setPreOpen] = useState(false);
|
||||||
const [autoOpen, setAutoOpen] = useState(true);
|
const [autoOpen, setAutoOpen] = useState(true);
|
||||||
const suppressedAutoOpenKeyRef = useRef<string | null>(null);
|
const suppressedAutoOpenKeyRef = useRef<string | null>(null);
|
||||||
// 供稳定的 setOpen 回调同步读取。写文件流会高频调用 select/setOpen,避免每次
|
// 供稳定的 setOpen 回调同步读取。写文件流会高频调用 select/setOpen,避免每次
|
||||||
// Context 状态变化都重建 setter 并触发依赖它的布局 effect。
|
// Context 状态变化都重建 setter 并触发依赖它的布局 effect。
|
||||||
const selectedArtifactKeyRef = useRef<string | null>(selectedArtifactKey);
|
const selectedArtifactKeyRef = useRef<string | null>(selectedArtifactKey);
|
||||||
const autoOpenRef = useRef(autoOpen);
|
const autoOpenRef = useRef(autoOpen);
|
||||||
|
const openStateRef = useRef(open);
|
||||||
|
const preOpenTimerRef = useRef<number | null>(null);
|
||||||
|
const openTimerRef = useRef<number | null>(null);
|
||||||
const sidebar = useSidebarSafe();
|
const sidebar = useSidebarSafe();
|
||||||
|
|
||||||
|
const applyOpen = useCallback((next: boolean) => {
|
||||||
|
openStateRef.current = next;
|
||||||
|
setOpenState((previous) => (previous === next ? previous : next));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 取消尚未触发的分步自动开沙箱(用户手动关闭 / 切换会话时)。
|
||||||
|
const cancelPendingStreamedOpen = useCallback(() => {
|
||||||
|
if (preOpenTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(preOpenTimerRef.current);
|
||||||
|
preOpenTimerRef.current = null;
|
||||||
|
}
|
||||||
|
if (openTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(openTimerRef.current);
|
||||||
|
openTimerRef.current = null;
|
||||||
|
}
|
||||||
|
setPreOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (preOpenTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(preOpenTimerRef.current);
|
||||||
|
preOpenTimerRef.current = null;
|
||||||
|
}
|
||||||
|
if (openTimerRef.current !== null) {
|
||||||
|
window.clearTimeout(openTimerRef.current);
|
||||||
|
openTimerRef.current = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const select = useCallback(
|
const select = useCallback(
|
||||||
(artifact: string, autoSelect = false) => {
|
(artifact: string, autoSelect = false) => {
|
||||||
const previewKey = artifactPreviewKey(artifact);
|
const previewKey = artifactPreviewKey(artifact);
|
||||||
@@ -104,13 +154,33 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
|||||||
setSelectedArtifactKey((previous) => (previous === previewKey ? previous : previewKey));
|
setSelectedArtifactKey((previous) => (previous === previewKey ? previous : previewKey));
|
||||||
setAutoSelect((previous) => (previous ? previous : true));
|
setAutoSelect((previous) => (previous ? previous : true));
|
||||||
setAutoOpen((previous) => (previous ? previous : true));
|
setAutoOpen((previous) => (previous ? previous : true));
|
||||||
setOpenState((previous) => (previous ? previous : true));
|
// 阶段一(t=0):立即收起左侧聊天记录侧栏。
|
||||||
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true") {
|
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true") {
|
||||||
sidebar?.setOpen(false);
|
sidebar?.setOpen(false);
|
||||||
}
|
}
|
||||||
|
if (openStateRef.current) {
|
||||||
|
// 沙箱已展开(如连续写多个文件):只需切换选中文件,无需分步。
|
||||||
|
cancelPendingStreamedOpen();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (openTimerRef.current === null) {
|
||||||
|
// 阶段二(t=+200ms):延迟固定时长再置 preOpen,右侧智能体信息
|
||||||
|
// 面板此时才开始收起 —— 与左侧错开,避免两次收起同帧叠加卡顿。
|
||||||
|
preOpenTimerRef.current = window.setTimeout(() => {
|
||||||
|
preOpenTimerRef.current = null;
|
||||||
|
setPreOpen(true);
|
||||||
|
}, STREAMED_PREOPEN_DELAY_MS);
|
||||||
|
// 阶段三(t=+500ms,右侧 300ms 收起动画结束):翻转 open 展开
|
||||||
|
// 沙箱,把「聊天列变窄 + 沙箱内容挂载」与侧栏收起彻底分帧。
|
||||||
|
openTimerRef.current = window.setTimeout(() => {
|
||||||
|
openTimerRef.current = null;
|
||||||
|
applyOpen(true);
|
||||||
|
setPreOpen(false);
|
||||||
|
}, STREAMED_OPEN_DELAY_MS);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
[sidebar],
|
[applyOpen, cancelPendingStreamedOpen, sidebar],
|
||||||
);
|
);
|
||||||
|
|
||||||
const deselect = useCallback(() => {
|
const deselect = useCallback(() => {
|
||||||
@@ -118,27 +188,34 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
|
|||||||
setSelectedArtifact((previous) => (previous === null ? previous : null));
|
setSelectedArtifact((previous) => (previous === null ? previous : null));
|
||||||
setSelectedArtifactKey((previous) => (previous === null ? previous : null));
|
setSelectedArtifactKey((previous) => (previous === null ? previous : null));
|
||||||
setAutoSelect((previous) => (previous ? previous : true));
|
setAutoSelect((previous) => (previous ? previous : true));
|
||||||
setOpenState((previous) => (previous ? false : previous));
|
cancelPendingStreamedOpen();
|
||||||
|
applyOpen(false);
|
||||||
suppressedAutoOpenKeyRef.current = null;
|
suppressedAutoOpenKeyRef.current = null;
|
||||||
}, []);
|
}, [applyOpen, cancelPendingStreamedOpen]);
|
||||||
|
|
||||||
const setOpen = useCallback((isOpen: boolean) => {
|
const setOpen = useCallback(
|
||||||
if (!isOpen && autoOpenRef.current) {
|
(isOpen: boolean) => {
|
||||||
autoOpenRef.current = false;
|
if (!isOpen) {
|
||||||
setAutoOpen((previous) => (previous ? false : previous));
|
// 两阶段等待期间用户手动关闭:取消尚未触发的自动展开。
|
||||||
setAutoSelect((previous) => (previous ? false : previous));
|
cancelPendingStreamedOpen();
|
||||||
}
|
if (autoOpenRef.current) {
|
||||||
if (!isOpen) {
|
autoOpenRef.current = false;
|
||||||
suppressedAutoOpenKeyRef.current = selectedArtifactKeyRef.current;
|
setAutoOpen((previous) => (previous ? false : previous));
|
||||||
}
|
setAutoSelect((previous) => (previous ? false : previous));
|
||||||
setOpenState((previous) => (previous === isOpen ? previous : isOpen));
|
}
|
||||||
}, []);
|
suppressedAutoOpenKeyRef.current = selectedArtifactKeyRef.current;
|
||||||
|
}
|
||||||
|
applyOpen(isOpen);
|
||||||
|
},
|
||||||
|
[applyOpen, cancelPendingStreamedOpen],
|
||||||
|
);
|
||||||
|
|
||||||
const value: ArtifactsContextType = {
|
const value: ArtifactsContextType = {
|
||||||
artifacts,
|
artifacts,
|
||||||
setArtifacts,
|
setArtifacts,
|
||||||
|
|
||||||
open,
|
open,
|
||||||
|
preOpen,
|
||||||
autoOpen,
|
autoOpen,
|
||||||
autoSelect,
|
autoSelect,
|
||||||
setOpen,
|
setOpen,
|
||||||
|
|||||||
Reference in new issue
Block a user