diff --git a/docs/reference/cloud-workbench.md b/docs/reference/cloud-workbench.md index 7968f5f9..8b2e2de7 100644 --- a/docs/reference/cloud-workbench.md +++ b/docs/reference/cloud-workbench.md @@ -112,8 +112,10 @@ Code Agent trace 在消息卡片内使用独立滚动容器展示。SSE、轮询 更新只能刷新内容,不得重置用户已展开/折叠状态,也不得把 `#conversation-list` 或 `.message-trace-events` 的滚动位置跳回顶部;渲染前后必须保存并恢复外层 conversation list 和按 `messageId/traceId` 记录的 trace list -`scrollTop/scrollLeft`。完整 trace 可通过复制/下载获取,页面内默认保持“显示全部 -+ 内部滚动”的单一模式。 +`scrollTop/scrollLeft`。`#conversation-list` 需要绑定 scroll listener 持续记录用户 +位置,并在 DOM 替换后立即恢复一次、下一帧再恢复一次,避免布局重算或 scroll +anchoring 把列表拉回顶部。完整 trace 可通过复制/下载获取,页面内默认保持“显示全部 + +内部滚动”的单一模式。 ## Lightweight Checks diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index f1c56617..db863cef 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -219,6 +219,7 @@ initSideTabs(); initHardwareTabs(); initQuickActions(); initCodeAgentTimeoutControl(); +initConversationScrollMemory(); initCommandBar(); initLiveBuildOverlay(); initWorkbenchLogout(el.logoutButton); @@ -2022,6 +2023,16 @@ function renderConversation() { replaceChildren(el.conversationList, ...[...introMessages, ...state.chatMessages].map(messageCard)); restoreConversationScrollPosition(); restoreTraceScrollPositions(); + window.requestAnimationFrame(() => { + restoreConversationScrollPosition(); + restoreTraceScrollPositions(); + }); +} + +function initConversationScrollMemory() { + el.conversationList.addEventListener("scroll", () => { + captureConversationScrollPosition(); + }, { passive: true }); } function captureConversationScrollPosition() { diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 0dcbb634..13cc1d5a 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -816,6 +816,8 @@ assert.match(app, /state\.conversationScrollPosition\s*=\s*\{\s*top:\s*0,\s*left assert.match(app, /state\.traceScrollPositions\.clear\(\)/); assert.match(app, /function messageTraceUiKey/); assert.match(app, /function defaultTraceDetailsOpen/); +assert.match(app, /initConversationScrollMemory\(\)/); +assert.match(app, /function initConversationScrollMemory/); assert.match(app, /function captureConversationScrollPosition/); assert.match(app, /function restoreConversationScrollPosition/); assert.match(app, /function captureTraceScrollPositions/); @@ -825,6 +827,7 @@ assert.match(functionBody(app, "renderConversation"), /captureConversationScroll assert.match(functionBody(app, "renderConversation"), /restoreConversationScrollPosition\(\)/); assert.match(functionBody(app, "renderConversation"), /captureTraceScrollPositions\(\)/); assert.match(functionBody(app, "renderConversation"), /restoreTraceScrollPositions\(\)/); +assert.match(functionBody(app, "renderConversation"), /requestAnimationFrame/); assert.match(functionBody(app, "messageTracePanel"), /state\.traceDetailsOpen\.get\(traceUiKey\)/); assert.match(functionBody(app, "messageTracePanel"), /defaultTraceDetailsOpen\(message\)/); assert.match(functionBody(app, "messageTracePanel"), /state\.traceDetailsOpen\.set\(traceUiKey,\s*details\.open\)/);