From d9de41dd675b927cfacb65446de90e78c0d37414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Wed, 26 Aug 2026 17:15:04 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend-web/src/pages/AgentChatPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend-web/src/pages/AgentChatPage.tsx b/frontend-web/src/pages/AgentChatPage.tsx index 5a1ec505..87f07992 100644 --- a/frontend-web/src/pages/AgentChatPage.tsx +++ b/frontend-web/src/pages/AgentChatPage.tsx @@ -851,7 +851,7 @@ export default function AgentChatPage({ agentName={agentDisplayName} disabled={thread.isLoading || isDemoMode} onAskQuestion={(text) => handleSubmit({ text, files: [] })} - defaultCollapsed + defaultCollapsed={taskCtxOn} showRecentChats={!taskCtxOn} compactAppearanceActions={compactAppearanceActions} /> From 90d29ed2320be3b6f420876449ee32d54b2f9a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Wed, 26 Aug 2026 17:37:23 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E6=99=BA=E8=83=BD=E4=BD=93?= =?UTF-8?q?=E9=97=AE=E7=AD=94=E9=A1=B5=E5=8F=B3=E4=BE=A7=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E5=8F=97=E6=8E=A7=E6=8A=98=E5=8F=A0=EF=BC=8C=E6=94=B6=E8=B5=B7?= =?UTF-8?q?=E6=95=B4=E5=88=97=E5=BD=92=E9=9B=B6=E5=B9=B6=E5=9C=A8=20header?= =?UTF-8?q?=20=E6=8F=90=E4=BE=9B=E5=B1=95=E5=BC=80=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../workspace/agents/agent-detail-panel.tsx | 56 +++++++++++++++++-- frontend-web/src/pages/AgentChatPage.tsx | 37 +++++++++++- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx b/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx index bf3e3751..ef2178b6 100644 --- a/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx +++ b/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx @@ -144,6 +144,8 @@ export function AgentDetailPanel({ onAskQuestion, disabled, defaultCollapsed, + collapsed: collapsedProp, + onCollapsedChange, showRecentChats, compactAppearanceActions, avatarSrc, @@ -163,6 +165,14 @@ export function AgentDetailPanel({ disabled?: boolean; /** 初始收起(任务工作区进入时默认收起智能体信息面板)。 */ defaultCollapsed?: boolean; + /** + * 受控折叠态:传入时由父级持有状态(智能体问答页把展开/收起按钮放在顶部 + * 「新对话」右侧)。此时收起 = 整列宽度动画归零(不保留细条),面板内置的 + * 收起按钮不再渲染。不传则沿用面板内置状态 + 细条收起交互(会商等工作区)。 + */ + collapsed?: boolean; + /** 受控折叠态变化回调(配合 `collapsed` 使用)。 */ + onCollapsedChange?: (collapsed: boolean) => void; /** 在面板底部展示「有关此智能体的最近对话」(仅智能体问答页开启,任务工作区不展示)。 */ showRecentChats?: boolean; /** 简洁模式下挂在右上角收起/展开按钮左侧的外观控件。 */ @@ -187,7 +197,15 @@ export function AgentDetailPanel({ editIcon?: ReactNode; }) { const router = useRouter(); - const [collapsed, setCollapsed] = useState(defaultCollapsed ?? false); + // 折叠态:受控(collapsedProp 传入,智能体问答页)优先;否则面板内置(会商等工作区)。 + const [internalCollapsed, setInternalCollapsed] = useState( + defaultCollapsed ?? false, + ); + const collapsed = collapsedProp ?? internalCollapsed; + const setCollapsed = (next: boolean) => { + if (collapsedProp === undefined) setInternalCollapsed(next); + onCollapsedChange?.(next); + }; const [mobileOpen, setMobileOpen] = useState(false); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(null); @@ -927,12 +945,14 @@ export function AgentDetailPanel({ const panelChrome = (collapseButton: ReactNode) => ( <>
- + {/* 标题不参与压缩(此前被简洁模式外观控件挤成单字);空间不足时只 + 裁剪 compactAppearanceActions,编辑等主操作与标题始终完整。 */} + {panelTitle ?? "智能体信息"} -
+
+
{headerActions}
- {headerActions} {compactAppearanceActions}
{collapseButton &&
{collapseButton}
} @@ -944,11 +964,37 @@ export function AgentDetailPanel({ return ( <> - {/* ── Desktop (lg+): inline aside, collapsible to a rail ───────────── */} + {/* ── Desktop (lg+) ────────────────────────────────────────────────── */} {embedded ? ( + ) : collapsedProp !== undefined ? ( + /* 受控模式(智能体问答页):收起时整列宽度动画归零(含左边框),不再保留细条; + 展开态的「收起」按钮在面板右上角,收起态的「展开」按钮在页面 + header「新对话」按钮右侧。 */ +
+ +
) : collapsed ? (
diff --git a/frontend-web/src/pages/AgentChatPage.tsx b/frontend-web/src/pages/AgentChatPage.tsx index 87f07992..28f2560d 100644 --- a/frontend-web/src/pages/AgentChatPage.tsx +++ b/frontend-web/src/pages/AgentChatPage.tsx @@ -1,6 +1,11 @@ "use client"; -import { BotIcon, Compass, PlusSquare } from "lucide-react"; +import { + BotIcon, + Compass, + PanelRightOpenIcon, + PlusSquare, +} from "lucide-react"; import { useParams, useRouter } from "next/navigation"; import { useLocation } from "react-router-dom"; import { useRouteBase } from "@/core/route-base"; @@ -191,6 +196,10 @@ export default function AgentChatPage({ // 任务深链上下文(taskId + 开场信息):闸门(rwfx)与纯绑定(3qfx/xdfx)都要读取。 const taskCtxOn = !!(taskGate || taskBind); + // 右侧智能体详情面板折叠态(受控):普通问答默认展开,任务工作区默认收起。 + // 收起时整列宽度动画关闭;展开态的「收起」按钮在面板右上角,收起态的 + // 「展开」按钮在顶部 header「新对话」按钮右侧。 + const [detailCollapsed, setDetailCollapsed] = useState(taskCtxOn); const isXdfxTaskBind = !!chatBasePath?.includes("/plan-task-action/"); const taskContext = useMemo(() => { if (!taskCtxOn) return null; @@ -621,7 +630,11 @@ export default function AgentChatPage({ size="sm" variant="ghost" className="text-muted-foreground hover:text-foreground mr-1" - onClick={() => startAgentGuide("chat")} + onClick={() => { + // 引导的 chat-detail 步骤会高亮右侧面板,重放前先展开。 + setDetailCollapsed(false); + startAgentGuide("chat"); + }} > 使用引导 @@ -687,6 +700,23 @@ export default function AgentChatPage({ {t.agents.newChat} + {/* 右侧智能体详情面板收起时的「展开」入口:仅收起态显示 + (展开态的「收起」按钮在面板右上角)。桌面端专用(lg+), + 窄屏面板走抽屉交互;嵌入态面板固定渲染,不显示该开关。 */} + {!isEmbedded && detailCollapsed && ( + + + + )} handleSubmit({ text, files: [] })} - defaultCollapsed={taskCtxOn} + collapsed={detailCollapsed} + onCollapsedChange={setDetailCollapsed} showRecentChats={!taskCtxOn} compactAppearanceActions={compactAppearanceActions} /> From 6e9e89a6cb7f9d6ab3fa3f2a7718da347fd239cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Wed, 26 Aug 2026 18:24:06 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E6=B5=81=E5=BC=8F=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=BC=80=E6=B2=99=E7=AE=B1=E5=88=86=E9=98=B6=E6=AE=B5=E6=94=B6?= =?UTF-8?q?=E8=B5=B7=E4=BE=A7=E6=A0=8F=EF=BC=8C=E9=81=BF=E5=85=8D=E5=90=8C?= =?UTF-8?q?=E5=B8=A7=E5=B8=83=E5=B1=80=E5=8D=A1=E9=A1=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 preOpen 两阶段标记,先错开收起左右侧栏再展开沙箱;智能体详情面板据此提前收起,减少与沙箱展开同帧叠加造成的卡顿。 Co-authored-by: Cursor --- .../workspace/agents/agent-detail-panel.tsx | 5 +- .../workspace/artifacts/context.tsx | 223 ++++++++++++------ 2 files changed, 154 insertions(+), 74 deletions(-) diff --git a/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx b/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx index ef2178b6..69cfc6a4 100644 --- a/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx +++ b/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx @@ -267,8 +267,11 @@ export function AgentDetailPanel({ const artifactsCtx = useArtifactsOptional(); const artifactCount = artifactsCtx?.artifacts.length ?? 0; const selectedArtifact = artifactsCtx?.selectedArtifact ?? null; + // preOpen:流式自动开沙箱的两阶段标记 —— 沙箱真正展开(open 翻转)前就 + // 先收起本面板,让收起布局先落地,避免与沙箱展开同帧叠加造成卡顿。 const artifactPanelVisible = - !!artifactsCtx?.open && (!!selectedArtifact || artifactCount > 0); + (!!artifactsCtx?.open || !!artifactsCtx?.preOpen) && + (!!selectedArtifact || artifactCount > 0); const prevArtifactPanelVisible = useRef(false); useEffect(() => { const opened = artifactPanelVisible && !prevArtifactPanelVisible.current; diff --git a/frontend-web/src/components/workspace/artifacts/context.tsx b/frontend-web/src/components/workspace/artifacts/context.tsx index 75163228..ab23b202 100644 --- a/frontend-web/src/components/workspace/artifacts/context.tsx +++ b/frontend-web/src/components/workspace/artifacts/context.tsx @@ -1,13 +1,14 @@ -import { - createContext, +import { + createContext, useCallback, useContext, + useEffect, useRef, useState, - type ReactNode, -} from "react"; - -import { useSidebarSafe } from "@/components/ui/sidebar"; + type ReactNode, +} from "react"; + +import { useSidebarSafe } from "@/components/ui/sidebar"; import { env } from "@/env"; export function artifactPreviewKey(artifact: string): string { @@ -26,11 +27,11 @@ export function artifactPreviewKey(artifact: string): string { } return artifact; } - -export interface ArtifactsContextType { - artifacts: string[]; - setArtifacts: (artifacts: string[]) => void; - + +export interface ArtifactsContextType { + artifacts: string[]; + setArtifacts: (artifacts: string[]) => void; + selectedArtifact: string | null; selectedArtifactKey: string | null; autoSelect: boolean; @@ -38,20 +39,33 @@ export interface ArtifactsContextType { /** Open/update a streamed write without overriding a manual close of that call. */ openStreamedArtifact: (artifact: string, previewKey: string) => boolean; deselect: () => void; - - open: boolean; - autoOpen: boolean; - setOpen: (open: boolean) => void; -} - -const ArtifactsContext = createContext( - undefined, -); - -interface ArtifactsProviderProps { - children: ReactNode; -} - + + open: boolean; + autoOpen: boolean; + setOpen: (open: boolean) => void; + /** + * 流式写文件自动开沙箱的两阶段标记:openStreamedArtifact 先收起两侧侧栏 + * (阶段一),等收起布局落定后再真正翻转 open(阶段二)。两阶段之间为 + * true —— 右侧智能体信息面板据此提前收起,避免与沙箱展开同帧叠加造成卡顿。 + */ + preOpen: boolean; +} + +const ArtifactsContext = createContext( + undefined, +); + +interface ArtifactsProviderProps { + 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) { const [artifacts, setArtifactsState] = useState([]); const setArtifacts = useCallback((next: string[]) => { @@ -69,14 +83,50 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) { const [open, setOpenState] = useState( env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true", ); + const [preOpen, setPreOpen] = useState(false); const [autoOpen, setAutoOpen] = useState(true); const suppressedAutoOpenKeyRef = useRef(null); // 供稳定的 setOpen 回调同步读取。写文件流会高频调用 select/setOpen,避免每次 // Context 状态变化都重建 setter 并触发依赖它的布局 effect。 const selectedArtifactKeyRef = useRef(selectedArtifactKey); const autoOpenRef = useRef(autoOpen); + const openStateRef = useRef(open); + const preOpenTimerRef = useRef(null); + const openTimerRef = useRef(null); 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( (artifact: string, autoSelect = false) => { const previewKey = artifactPreviewKey(artifact); @@ -84,12 +134,12 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) { setSelectedArtifact((previous) => (previous === artifact ? previous : artifact)); setSelectedArtifactKey((previous) => (previous === previewKey ? previous : previewKey)); if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true") { - sidebar?.setOpen(false); - } + sidebar?.setOpen(false); + } if (!autoSelect) { setAutoSelect((previous) => (previous ? false : previous)); } - }, + }, [sidebar], ); @@ -104,43 +154,70 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) { setSelectedArtifactKey((previous) => (previous === previewKey ? previous : previewKey)); setAutoSelect((previous) => (previous ? previous : true)); setAutoOpen((previous) => (previous ? previous : true)); - setOpenState((previous) => (previous ? previous : true)); + // 阶段一(t=0):立即收起左侧聊天记录侧栏。 if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true") { 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; }, - [sidebar], + [applyOpen, cancelPendingStreamedOpen, sidebar], ); - + 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)); + cancelPendingStreamedOpen(); + applyOpen(false); suppressedAutoOpenKeyRef.current = null; - }, []); + }, [applyOpen, cancelPendingStreamedOpen]); - 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, - setArtifacts, - - open, - autoOpen, - autoSelect, + const setOpen = useCallback( + (isOpen: boolean) => { + if (!isOpen) { + // 两阶段等待期间用户手动关闭:取消尚未触发的自动展开。 + cancelPendingStreamedOpen(); + if (autoOpenRef.current) { + autoOpenRef.current = false; + setAutoOpen((previous) => (previous ? false : previous)); + setAutoSelect((previous) => (previous ? false : previous)); + } + suppressedAutoOpenKeyRef.current = selectedArtifactKeyRef.current; + } + applyOpen(isOpen); + }, + [applyOpen, cancelPendingStreamedOpen], + ); + + const value: ArtifactsContextType = { + artifacts, + setArtifacts, + + open, + preOpen, + autoOpen, + autoSelect, setOpen, selectedArtifact, @@ -148,23 +225,23 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) { select, openStreamedArtifact, deselect, - }; - - return ( - - {children} - - ); -} - -export function useArtifacts() { - const context = useContext(ArtifactsContext); - if (context === undefined) { - throw new Error("useArtifacts must be used within an ArtifactsProvider"); - } - return context; -} - -export function useArtifactsOptional(): ArtifactsContextType | null { - return useContext(ArtifactsContext) ?? null; -} + }; + + return ( + + {children} + + ); +} + +export function useArtifacts() { + const context = useContext(ArtifactsContext); + if (context === undefined) { + throw new Error("useArtifacts must be used within an ArtifactsProvider"); + } + return context; +} + +export function useArtifactsOptional(): ArtifactsContextType | null { + return useContext(ArtifactsContext) ?? null; +}