From f2bbdf198a9a67a99f4a945159c162307fce1aa5 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 25 May 2026 01:38:00 +0000 Subject: [PATCH] preserve conversation trace scroll --- docs/reference/cloud-workbench.md | 9 +++++---- web/hwlab-cloud-web/app.mjs | 18 ++++++++++++++++++ web/hwlab-cloud-web/scripts/check.mjs | 6 ++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/reference/cloud-workbench.md b/docs/reference/cloud-workbench.md index 898dacd2..7968f5f9 100644 --- a/docs/reference/cloud-workbench.md +++ b/docs/reference/cloud-workbench.md @@ -109,10 +109,11 @@ session identifiers, the visible detail must include a short redacted 且非空的 assistant response 前,Workbench 不得暗示已有真实 assistant reply。 Code Agent trace 在消息卡片内使用独立滚动容器展示。SSE、轮询或“回放 trace” -更新只能刷新内容,不得重置用户已展开/折叠状态,也不得把 -`.message-trace-events` 的内部滚动位置跳回顶部;渲染前后必须按 -`messageId/traceId` 保存并恢复 trace list 的 `scrollTop/scrollLeft`。完整 trace -可通过复制/下载获取,页面内默认保持“显示全部 + 内部滚动”的单一模式。 +更新只能刷新内容,不得重置用户已展开/折叠状态,也不得把 `#conversation-list` +或 `.message-trace-events` 的滚动位置跳回顶部;渲染前后必须保存并恢复外层 +conversation list 和按 `messageId/traceId` 记录的 trace list +`scrollTop/scrollLeft`。完整 trace 可通过复制/下载获取,页面内默认保持“显示全部 ++ 内部滚动”的单一模式。 ## Lightweight Checks diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index f756edde..f1c56617 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -180,6 +180,7 @@ const state = { chatMessages: [], traceStreams: new Map(), traceDetailsOpen: new Map(), + conversationScrollPosition: { top: 0, left: 0 }, traceScrollPositions: new Map(), canceledTraces: new Set(), currentRequest: null, @@ -792,6 +793,7 @@ function initCommandBar() { for (const close of state.traceStreams.values()) close(); state.traceStreams.clear(); state.traceDetailsOpen.clear(); + state.conversationScrollPosition = { top: 0, left: 0 }; state.traceScrollPositions.clear(); state.chatMessages = []; state.conversationId = null; @@ -2006,6 +2008,7 @@ function workbenchApiSurfaceStatus(live, coreProbes = [live.healthLive, live.res } function renderConversation() { + captureConversationScrollPosition(); captureTraceScrollPositions(); const introMessages = [ { @@ -2017,9 +2020,24 @@ function renderConversation() { codeAgentStatusMessage(state.codeAgentAvailability) ]; replaceChildren(el.conversationList, ...[...introMessages, ...state.chatMessages].map(messageCard)); + restoreConversationScrollPosition(); restoreTraceScrollPositions(); } +function captureConversationScrollPosition() { + state.conversationScrollPosition = { + top: el.conversationList.scrollTop, + left: el.conversationList.scrollLeft + }; +} + +function restoreConversationScrollPosition() { + const position = state.conversationScrollPosition; + if (!position) return; + el.conversationList.scrollTop = Math.min(position.top, Math.max(0, el.conversationList.scrollHeight - el.conversationList.clientHeight)); + el.conversationList.scrollLeft = Math.min(position.left, Math.max(0, el.conversationList.scrollWidth - el.conversationList.clientWidth)); +} + function captureTraceScrollPositions(root = el.conversationList) { for (const list of root.querySelectorAll(".message-trace-events[data-trace-ui-key]")) { rememberTraceScrollPosition(list.dataset.traceUiKey, list); diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 4feb15df..0dcbb634 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -809,14 +809,20 @@ assert.match(app, /function messageTraceToolbar/); assert.match(app, /function messageTraceCountText/); assert.match(app, /显示全部\s+\$\{displayTotal\}\s+\/\s+原始\s+\$\{rawTotal\}/); assert.match(app, /traceDetailsOpen:\s*new Map\(\)/); +assert.match(app, /conversationScrollPosition:\s*\{\s*top:\s*0,\s*left:\s*0\s*\}/); assert.match(app, /traceScrollPositions:\s*new Map\(\)/); assert.match(app, /state\.traceDetailsOpen\.clear\(\)/); +assert.match(app, /state\.conversationScrollPosition\s*=\s*\{\s*top:\s*0,\s*left:\s*0\s*\}/); assert.match(app, /state\.traceScrollPositions\.clear\(\)/); assert.match(app, /function messageTraceUiKey/); assert.match(app, /function defaultTraceDetailsOpen/); +assert.match(app, /function captureConversationScrollPosition/); +assert.match(app, /function restoreConversationScrollPosition/); assert.match(app, /function captureTraceScrollPositions/); assert.match(app, /function restoreTraceScrollPositions/); assert.match(app, /function rememberTraceScrollPosition/); +assert.match(functionBody(app, "renderConversation"), /captureConversationScrollPosition\(\)/); +assert.match(functionBody(app, "renderConversation"), /restoreConversationScrollPosition\(\)/); assert.match(functionBody(app, "renderConversation"), /captureTraceScrollPositions\(\)/); assert.match(functionBody(app, "renderConversation"), /restoreTraceScrollPositions\(\)/); assert.match(functionBody(app, "messageTracePanel"), /state\.traceDetailsOpen\.get\(traceUiKey\)/);