From 8684ff954921824e645b8eb90987ce483f951c2e Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Thu, 18 Jun 2026 15:02:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E5=AE=9E=E7=8E=B0=E5=A4=9A?= =?UTF-8?q?=E8=A7=86=E5=9B=BE=E6=BB=9A=E5=8A=A8=E4=BD=8D=E7=BD=AE=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E4=B8=8E=E6=81=A2=E5=A4=8D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 App 组件中新增 viewKey,用于标识当前视图 - 通过属性链传递 viewKey 至 MessageList 以区分不同滚动上下文 - MessageList 中新增滚动位置缓存机制,使用 Map 保存各视图的滚动状态 - 在视图切换时自动恢复对应滚动位置,提升用户体验 - 保持消息新增时的自动滚动行为,兼顾视图切换与消息更新场景 --- web/src/App.tsx | 8 +++++ web/src/components/Chat/ChatContainer.tsx | 5 ++- web/src/components/Chat/MessageList.tsx | 42 +++++++++++++++++++++-- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 67a7e73..91ef78e 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -474,6 +474,13 @@ function App() { return result }, [messages]) + // 视图标识:用于 MessageList 保存/恢复每个视图的滚动位置 + const viewKey = useMemo(() => { + if (schedulerView) return `scheduler:${schedulerView.jobId}` + if (subAgentView) return `subagent:${subAgentView.taskId}` + return 'main' + }, [schedulerView, subAgentView]) + return (
{/* Header */} @@ -686,6 +693,7 @@ function App() { onNavigateToSubAgent={handleNavigateToSubAgent} onStop={handleStopExecution} showThinking={showThinking} + viewKey={viewKey} todoPanel={
- + {todoPanel}
void showThinking?: boolean + /** 视图标识,用于保存/恢复滚动位置。不同视图间切换时保持各自的滚动位置。 */ + viewKey?: string } -export function MessageList({ messages, onNavigateToSubAgent, showThinking = true }: MessageListProps) { +export function MessageList({ messages, onNavigateToSubAgent, showThinking = true, viewKey }: MessageListProps) { const bottomRef = useRef(null) const containerRef = useRef(null) const isAtBottomRef = useRef(true) const prevShowBottomRef = useRef(false) const prevShowTopRef = useRef(false) + const prevViewKeyRef = useRef(viewKey) + const viewKeyRef = useRef(viewKey) + viewKeyRef.current = viewKey + + // Per-view scroll position memory + const scrollPositionsRef = useRef>(new Map()) const [showScrollToBottom, setShowScrollToBottom] = useState(false) const [showScrollToTop, setShowScrollToTop] = useState(false) @@ -42,6 +50,12 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight const nearBottom = distanceFromBottom < 120 + // Save scroll position for current view + const key = viewKeyRef.current + if (key) { + scrollPositionsRef.current.set(key, el.scrollTop) + } + isAtBottomRef.current = nearBottom const shouldShowTop = el.scrollTop > el.clientHeight * 0.8 @@ -61,14 +75,36 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru } }, [hasNewMessage]) - // ---- auto-scroll: useLayoutEffect runs before browser processes scroll events ---- + // ---- auto-scroll: handle view switches and message updates ---- useLayoutEffect(() => { + const prevKey = prevViewKeyRef.current + const viewChanged = prevKey !== viewKey + prevViewKeyRef.current = viewKey + if (messages.length === 0) { isAtBottomRef.current = true return } + if (viewChanged) { + // View switched (e.g. breadcrumb navigation): restore saved scroll position + const key = viewKey ?? '' + const savedPos = scrollPositionsRef.current.get(key) + if (savedPos !== undefined && containerRef.current) { + containerRef.current.scrollTop = savedPos + const el = containerRef.current + const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight + isAtBottomRef.current = distanceFromBottom < 120 + return + } + // First time viewing this view: scroll to bottom + isAtBottomRef.current = true + bottomRef.current?.scrollIntoView({ behavior: 'instant' }) + return + } + + // Same view, messages changed: normal auto-scroll logic const lastMessage = messages[messages.length - 1] if (lastMessage.role === 'user' || isAtBottomRef.current) { @@ -76,7 +112,7 @@ export function MessageList({ messages, onNavigateToSubAgent, showThinking = tru } else { setHasNewMessage(true) } - }, [messages]) + }, [messages, viewKey]) // ---- mount: always scroll to bottom if messages already loaded ----