From 3d097af835e5a6b5c7c7d25b908e3296322cf32d Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 25 May 2026 01:26:13 +0000 Subject: [PATCH] fix trace scroll preservation --- docs/reference/cloud-workbench.md | 6 ++++++ web/hwlab-cloud-web/app.mjs | 31 +++++++++++++++++++++++++++ web/hwlab-cloud-web/scripts/check.mjs | 9 ++++++++ 3 files changed, 46 insertions(+) diff --git a/docs/reference/cloud-workbench.md b/docs/reference/cloud-workbench.md index 9b16f0e2..898dacd2 100644 --- a/docs/reference/cloud-workbench.md +++ b/docs/reference/cloud-workbench.md @@ -108,6 +108,12 @@ session identifiers, the visible detail must include a short redacted [Code Agent readiness](code-agent-chat-readiness.md) 合同返回 completed 且非空的 assistant response 前,Workbench 不得暗示已有真实 assistant reply。 +Code Agent trace 在消息卡片内使用独立滚动容器展示。SSE、轮询或“回放 trace” +更新只能刷新内容,不得重置用户已展开/折叠状态,也不得把 +`.message-trace-events` 的内部滚动位置跳回顶部;渲染前后必须按 +`messageId/traceId` 保存并恢复 trace list 的 `scrollTop/scrollLeft`。完整 trace +可通过复制/下载获取,页面内默认保持“显示全部 + 内部滚动”的单一模式。 + ## Lightweight Checks Use the existing lightweight checks unless a task explicitly authorizes a diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 6006ce26..f756edde 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(), + traceScrollPositions: new Map(), canceledTraces: new Set(), currentRequest: null, sessionStatus: null, @@ -791,6 +792,7 @@ function initCommandBar() { for (const close of state.traceStreams.values()) close(); state.traceStreams.clear(); state.traceDetailsOpen.clear(); + state.traceScrollPositions.clear(); state.chatMessages = []; state.conversationId = null; state.sessionId = null; @@ -2004,6 +2006,7 @@ function workbenchApiSurfaceStatus(live, coreProbes = [live.healthLive, live.res } function renderConversation() { + captureTraceScrollPositions(); const introMessages = [ { role: "system", @@ -2014,6 +2017,30 @@ function renderConversation() { codeAgentStatusMessage(state.codeAgentAvailability) ]; replaceChildren(el.conversationList, ...[...introMessages, ...state.chatMessages].map(messageCard)); + restoreTraceScrollPositions(); +} + +function captureTraceScrollPositions(root = el.conversationList) { + for (const list of root.querySelectorAll(".message-trace-events[data-trace-ui-key]")) { + rememberTraceScrollPosition(list.dataset.traceUiKey, list); + } +} + +function rememberTraceScrollPosition(traceUiKey, list) { + if (!traceUiKey || !list) return; + state.traceScrollPositions.set(traceUiKey, { + top: list.scrollTop, + left: list.scrollLeft + }); +} + +function restoreTraceScrollPositions(root = el.conversationList) { + for (const list of root.querySelectorAll(".message-trace-events[data-trace-ui-key]")) { + const position = state.traceScrollPositions.get(list.dataset.traceUiKey); + if (!position) continue; + list.scrollTop = Math.min(position.top, Math.max(0, list.scrollHeight - list.clientHeight)); + list.scrollLeft = Math.min(position.left, Math.max(0, list.scrollWidth - list.clientWidth)); + } } function renderHardwareStatus(m3Status) { @@ -4066,6 +4093,10 @@ function messageTracePanel(message) { summary.textContent = runnerTraceHeadline(message, trace); const list = document.createElement("ol"); list.className = "message-trace-events"; + if (traceUiKey) { + list.dataset.traceUiKey = traceUiKey; + list.addEventListener("scroll", () => rememberTraceScrollPosition(traceUiKey, list), { passive: true }); + } const events = Array.isArray(trace?.events) ? trace.events : []; const rows = traceDisplayRows(trace, events); if (events.length === 0) { diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 7ad574ea..4feb15df 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -809,12 +809,21 @@ 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, /traceScrollPositions:\s*new Map\(\)/); assert.match(app, /state\.traceDetailsOpen\.clear\(\)/); +assert.match(app, /state\.traceScrollPositions\.clear\(\)/); assert.match(app, /function messageTraceUiKey/); assert.match(app, /function defaultTraceDetailsOpen/); +assert.match(app, /function captureTraceScrollPositions/); +assert.match(app, /function restoreTraceScrollPositions/); +assert.match(app, /function rememberTraceScrollPosition/); +assert.match(functionBody(app, "renderConversation"), /captureTraceScrollPositions\(\)/); +assert.match(functionBody(app, "renderConversation"), /restoreTraceScrollPositions\(\)/); 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\)/); +assert.match(functionBody(app, "messageTracePanel"), /list\.dataset\.traceUiKey\s*=\s*traceUiKey/); +assert.match(functionBody(app, "messageTracePanel"), /rememberTraceScrollPosition\(traceUiKey,\s*list\)/); assert.match(functionBody(app, "messageTracePanel"), /list\.dataset\.traceMode\s*=\s*"all"/); assert.match(functionBody(app, "messageTracePanel"), /renderTraceEventList\(list,\s*rows\)/); assert.doesNotMatch(app, /CODE_AGENT_TRACE_PREVIEW_LIMIT|tracePreviewRows|收起为最近|展开全部|data-trace-mode="tail"/);