From c6adf34f274443dfd9f7c02fe98af4b6f95b1aec Mon Sep 17 00:00:00 2001 From: lyon Date: Tue, 26 May 2026 00:37:29 +0800 Subject: [PATCH] fix: persist workbench code agent sessions --- docs/reference/cloud-workbench.md | 21 ++- docs/reference/code-agent-chat-readiness.md | 7 +- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 15 +- web/hwlab-cloud-web/app.mjs | 146 +++++++++++++++++- web/hwlab-cloud-web/auth.mjs | 22 ++- web/hwlab-cloud-web/scripts/check.mjs | 12 +- .../scripts/trace-scroll.test.mjs | 16 +- 7 files changed, 209 insertions(+), 30 deletions(-) diff --git a/docs/reference/cloud-workbench.md b/docs/reference/cloud-workbench.md index eda6bca6..21bdf964 100644 --- a/docs/reference/cloud-workbench.md +++ b/docs/reference/cloud-workbench.md @@ -123,11 +123,22 @@ delta、assistant chunk、command output chunk、reasoning delta 等噪声可以 但聚合不得吞掉中间的可读 message/tool call。命令和工具行必须至少保留状态、exit code、耗时、输出体量、关键 operation/evidence id 和可读摘要。 -“显示全部”只能用于已经覆盖完整 trace 的可读事件线,不能把 result polling 返回的 -head-tail 压缩窗口或 9 行摘要冒充全量。若当前只拿到压缩窗口,界面必须明确显示 -“压缩窗口/待回放完整 trace”,并通过 `/v1/agent/chat/trace/` 自动或手动回放 -完整 trace 后再标记为“显示全部”。若完整 trace 已过期或后端缺失,也必须显式展示 -缺失状态,而不是把已载入窗口说成全量。 +Workbench 面向用户的 trace 视图不得展示“压缩窗口”作为可读事件列表,也不得把 +result polling 返回的 head-tail 窗口或少量摘要冒充完整 trace。只要发现 +`eventsCompacted=true`,前端必须自动请求 `/v1/agent/chat/trace/` 回放完整 +trace;回放期间只能显示“完整 trace 回放中/当前已载入”状态,回放完成后才显示 +“显示全部可读事件”。如果完整 trace 已过期或后端缺失,必须显式展示缺失状态。 +assistant chunk、token delta、command output chunk 可以在同一 message/tool output 内 +合并成一条高可读行,但不能隐藏任意 assistant message、tool call、tool completed、 +stderr/error 或 turn/status 边界。 + +Code Agent 工作台状态必须跨浏览器刷新持久化。前端至少保存 +`conversationId/sessionId/threadId/sessionStatus`、最近消息、traceId 和必要的 runtime +摘要,刷新或重新打开页面后继续使用同一 repo-owned Codex session/thread 和固定 +workspace;除非 Pod 重建、Codex supervisor 重启、用户显式清空对话或登出,不得把下一轮 +请求降级成“首轮新会话”。登录态采用 server cookie 与本地 session 双轨:server session +可用时优先使用;server 内存 session 因 Pod/replica 切换丢失时,只要本地 session 未过期, +刷新页面不得要求用户重新登录。 ## Lightweight Checks diff --git a/docs/reference/code-agent-chat-readiness.md b/docs/reference/code-agent-chat-readiness.md index ec22d5a2..a99b15e3 100644 --- a/docs/reference/code-agent-chat-readiness.md +++ b/docs/reference/code-agent-chat-readiness.md @@ -73,7 +73,10 @@ blocker,不能降级到 M3 Skill CLI、受控硬件 shortcut、外网专用检 持久 session 是默认合同:同一个 `conversationId/sessionId` 必须映射到 repo-owned Codex thread 和固定 workspace,刷新前端、重新打开页面或短连接 result 轮询不得创建新的短期 runner。 除非 Pod 重建或 Codex supervisor 明确重启,workspace、thread/session 绑定和可见 trace 应持续 -存在。 +存在。Workbench 前端必须把这些会话标识和最近消息持久化到浏览器本地状态;用户显式清空 +对话或登出时才清除该本地状态。刷新后下一轮自然语言请求必须携带已保存的 +`conversationId/sessionId/threadId`,不能只因为 JS 内存重建就显示“首轮请求”或重新分配 +Codex workspace。 ## PC Gateway Windows Skill 调用 @@ -99,7 +102,7 @@ Workbench 与 Code Agent 的用户请求必须是短连接 submit + 短连接 re cloud-web 同源代理必须把短连接语义原样转发给 cloud-api,至少包括 `Prefer: respond-async`、`X-HWLAB-Short-Connection` 和 `X-Trace-Id`。如果这些 header 在 cloud-web 层被过滤,cloud-api 会把同一个请求当成长同步请求处理,用户入口会表现为 16666 卡住或代理超时,而 16667 直连 cloud-api 正常。此类问题应先比对同一 trace 在 16666 与 16667 的 submit 行为,再修代理 header 透传,而不是调大前端等待时间。 -`/v1/agent/chat/result/` 是终态摘要接口,不是完整 trace 下载接口。它可以携带压缩后的 `runnerTrace` 窗口用于当前 UI 刷新,但必须保留 `eventCount`、`lastEvent`、`providerTrace`、`threadId/sessionId` 和终态 reply/blocker;完整 trace 只能从 `/v1/agent/chat/trace/`、复制 JSON 或下载 trace 入口取得。默认 result trace 窗口上限由 `HWLAB_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT` 控制;不要把数百个大 chunk 原样塞进 result 响应,避免 cloud-web 代理层或浏览器 fetch 把“正常执行中的大响应”表现成 503、非 JSON 或空响应。 +`/v1/agent/chat/result/` 是终态摘要接口,不是完整 trace 下载接口。它可以携带压缩后的 `runnerTrace` 窗口用于传输保活,但 Workbench 用户界面不得把该窗口显示为“压缩窗口”或“显示全部”。只要结果或轮询快照声明 `eventsCompacted=true`,前端必须自动请求 `/v1/agent/chat/trace/` 并用完整 trace 替换可视事件线;回放完成前只能显示“完整 trace 回放中/当前已载入”状态。result 响应仍必须保留 `eventCount`、`lastEvent`、`providerTrace`、`threadId/sessionId` 和终态 reply/blocker;完整 trace 只能从 `/v1/agent/chat/trace/`、复制 JSON 或下载 trace 入口取得。默认 result trace 窗口上限由 `HWLAB_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT` 控制;不要把数百个大 chunk 原样塞进 result 响应,避免 cloud-web 代理层或浏览器 fetch 把“正常执行中的大响应”表现成 503、非 JSON 或空响应。 result 轮询的 408/425/429/5xx、浏览器 timeout、非 JSON 或空响应应按“可恢复传输抖动”处理:前端先拉取一次 trace 刷新活性,再带退避继续轮询,只有后端返回结构化 terminal blocker、真实终态失败,或 trace 按无新事件 idle timeout 超时,才向用户显示失败。只要 `/trace` 仍显示新事件或 `waitingFor` 仍在推进,就不能把一次 result poll 失败标成“Code Agent API 错误”并停止。 diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index f0028847..7d02e233 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -628,9 +628,9 @@ function runStaticSmoke() { evidence: ["DEFAULT_API_TIMEOUT_MS=4500 for light probes", `DEFAULT_CODE_AGENT_TIMEOUT_MS=${codeAgentLongTimeoutMs}`, "sendAgentMessage passes timeoutMs"] }); - addCheck(checks, blockers, "code-agent-trace-replay-disclosure", hasCodeAgentTraceReplayDisclosure(files), "Trace replay panels disclose full trace access and preserve open/scroll state while live trace updates.", { + addCheck(checks, blockers, "code-agent-trace-replay-disclosure", hasCodeAgentTraceReplayDisclosure(files), "Trace replay panels auto-replay full trace, avoid compressed-window UI, and preserve open/scroll state while live trace updates.", { blocker: "observability_blocker", - evidence: ["显示全部可读事件 / 压缩窗口", "复制 JSON", "下载 trace", "traceDetailsOpen", "traceScrollPositions", "internal scroll for full trace"] + evidence: ["显示全部可读事件 / 完整 trace 回放中", "复制 JSON", "下载 trace", "traceDetailsOpen", "traceScrollPositions", "internal scroll for full trace"] }); addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider/stdio blockers without exposing credential internals and only long-lived Codex stdio replies can become full Code Agent completion.", { @@ -1978,6 +1978,8 @@ function hasDefaultLoginEntry({ html, app, auth, styles }, artifactPublisher = " /DEFAULT_AUTH_PASSWORD\s*=\s*["']hwlab2026["']/u.test(auth) && /HWLAB_CLOUD_WEB_CONFIG\?\.auth/u.test(auth) && /AUTH_STORAGE_KEY\s*=\s*["']hwlab\.cloudWorkbench\.auth\.v1["']/u.test(auth) && + /const localSession = readLocalSession\(config\)/u.test(auth) && + /writeLocalSession\(config\)/u.test(functionBody(auth, "attemptLogin")) && /账号或密码不正确,请重新输入。/u.test(auth) && /ensureWorkbenchAuth/u.test(app) && /await\s+ensureWorkbenchAuth/u.test(app) && @@ -2672,7 +2674,14 @@ function hasCodeAgentTraceReplayDisclosure({ app, styles }) { return ( /function\s+messageTraceCountText\s*\(/u.test(app) && /显示全部可读事件\s+\$\{readableTotal\}\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/u.test(app) && - /压缩窗口\s+\$\{readableTotal\}\s+条可读事件\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/u.test(app) && + /完整 trace 回放中\s+\/\s+当前可读事件\s+\$\{readableTotal\}\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/u.test(app) && + !/压缩窗口\s+\$\{readableTotal\}/u.test(app) && + /CODE_AGENT_SESSION_STORAGE_KEY\s*=\s*"hwlab\.workbench\.codeAgentSession\.v1"/u.test(app) && + /function\s+restoreCodeAgentSessionState\s*\(/u.test(app) && + /function\s+persistCodeAgentSessionState\s*\(/u.test(app) && + /window\.localStorage\?\.setItem\(CODE_AGENT_SESSION_STORAGE_KEY/u.test(app) && + /window\.localStorage\?\.removeItem\(CODE_AGENT_SESSION_STORAGE_KEY/u.test(app) && + /refreshRestoredCodeAgentTraces/u.test(app) && /function\s+maybeReplayFullTraceForMessage\s*\(/u.test(app) && /function\s+replayFullTrace\s*\(/u.test(app) && /function\s+renderTraceEventList\s*\(/u.test(app) && diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index aafe9c60..ad8017cd 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -25,6 +25,12 @@ const DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS = 30000; const DEFAULT_GATEWAY_SHELL_TIMEOUT_MS = 120000; const CODE_AGENT_TIMEOUT_STORAGE_KEY = "hwlab.workbench.codeAgentTimeoutMs.v1"; const GATEWAY_SHELL_TIMEOUT_STORAGE_KEY = "hwlab.workbench.gatewayShellTimeoutMs.v1"; +const CODE_AGENT_SESSION_STORAGE_KEY = "hwlab.workbench.codeAgentSession.v1"; +const CODE_AGENT_SESSION_STORAGE_VERSION = 1; +const CODE_AGENT_SESSION_STORAGE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; +const CODE_AGENT_SESSION_STORAGE_MESSAGE_LIMIT = 30; +const CODE_AGENT_SESSION_STORAGE_TRACE_EVENT_LIMIT = 80; +const CODE_AGENT_SESSION_PERSIST_DEBOUNCE_MS = 250; const TRACE_STREAM_FALLBACK_MS = 2500; const TRACE_POLL_INTERVAL_MS = 1000; const FULL_TRACE_REPLAY_TIMEOUT_MS = 15000; @@ -219,6 +225,10 @@ const state = { } }; +let codeAgentSessionPersistTimer = null; + +restoreCodeAgentSessionState(); + initRoutes(); initLayoutSizing(); initLeftSidebarToggle(); @@ -232,6 +242,7 @@ initConversationScrollMemory(); initCommandBar(); initLiveBuildOverlay(); initWorkbenchLogout(el.logoutButton); +el.logoutButton.addEventListener("click", clearCodeAgentSessionState); initM3Control(); initGateControls(); installWorkbenchTestHooks(); @@ -240,6 +251,7 @@ renderProbePending(); renderCodeAgentSummary(); loadHelpSurface(); loadLiveSurface().then(renderLiveSurface); +window.setTimeout(refreshRestoredCodeAgentTraces, 0); function byId(id) { const element = document.getElementById(id); @@ -336,6 +348,131 @@ function syncCodeAgentTimeoutControl() { el.gatewayShellTimeout.title = `Code Agent 调用 PC gateway wrapper 时默认使用 --timeout-ms ${GATEWAY_SHELL_TIMEOUT_MS}`; } +function restoreCodeAgentSessionState() { + let payload = null; + try { + const raw = window.localStorage?.getItem(CODE_AGENT_SESSION_STORAGE_KEY); + payload = raw ? JSON.parse(raw) : null; + } catch { + payload = null; + } + if (!payload || payload.version !== CODE_AGENT_SESSION_STORAGE_VERSION) return; + const updatedAtMs = Number(payload.updatedAtMs); + if (!Number.isFinite(updatedAtMs) || Date.now() - updatedAtMs > CODE_AGENT_SESSION_STORAGE_MAX_AGE_MS) { + clearCodeAgentSessionState(); + return; + } + state.conversationId = nonEmptyString(payload.conversationId); + state.sessionId = nonEmptyString(payload.sessionId); + state.threadId = nonEmptyString(payload.threadId); + state.sessionStatus = nonEmptyString(payload.sessionStatus); + state.chatMessages = Array.isArray(payload.chatMessages) + ? payload.chatMessages.map(restoreStoredChatMessage).filter(Boolean) + : []; +} + +function restoreStoredChatMessage(message) { + if (!message || typeof message !== "object") return null; + const restored = { ...message }; + restored.id = nonEmptyString(restored.id) ?? nextProtocolId("msg"); + restored.role = restored.role === "user" ? "user" : restored.role === "system" ? "system" : "agent"; + restored.title = nonEmptyString(restored.title) ?? (restored.role === "user" ? "用户" : "Agent"); + restored.text = typeof restored.text === "string" ? restored.text : ""; + restored.status = nonEmptyString(restored.status) ?? "source"; + if (restored.status === "running") restored.status = "source"; + restored.conversationId = nonEmptyString(restored.conversationId) ?? state.conversationId; + restored.sessionId = nonEmptyString(restored.sessionId) ?? state.sessionId; + restored.threadId = nonEmptyString(restored.threadId) ?? state.threadId; + restored.traceId = nonEmptyString(restored.traceId); + if (restored.runnerTrace && typeof restored.runnerTrace === "object") { + restored.runnerTrace = restoreStoredRunnerTrace(restored.runnerTrace); + } + return restored; +} + +function restoreStoredRunnerTrace(trace) { + return { + ...trace, + events: Array.isArray(trace.events) ? trace.events : [], + eventCount: Number.isInteger(trace.eventCount) ? trace.eventCount : Array.isArray(trace.events) ? trace.events.length : 0, + eventsCompacted: trace.eventsCompacted === true, + fullTraceLoaded: trace.fullTraceLoaded === true + }; +} + +function scheduleCodeAgentSessionPersist() { + if (codeAgentSessionPersistTimer !== null) window.clearTimeout(codeAgentSessionPersistTimer); + codeAgentSessionPersistTimer = window.setTimeout(() => { + codeAgentSessionPersistTimer = null; + persistCodeAgentSessionState(); + }, CODE_AGENT_SESSION_PERSIST_DEBOUNCE_MS); +} + +function persistCodeAgentSessionState() { + if (!state.conversationId && !state.sessionId && !state.threadId && state.chatMessages.length === 0) { + clearCodeAgentSessionState(); + return; + } + const payload = { + version: CODE_AGENT_SESSION_STORAGE_VERSION, + updatedAtMs: Date.now(), + conversationId: state.conversationId, + sessionId: state.sessionId, + threadId: state.threadId, + sessionStatus: state.sessionStatus, + chatMessages: state.chatMessages.slice(-CODE_AGENT_SESSION_STORAGE_MESSAGE_LIMIT).map(storedChatMessage) + }; + try { + window.localStorage?.setItem(CODE_AGENT_SESSION_STORAGE_KEY, JSON.stringify(payload)); + } catch { + // Keep the in-memory conversation when localStorage is unavailable or full. + } +} + +function clearCodeAgentSessionState() { + if (codeAgentSessionPersistTimer !== null) { + window.clearTimeout(codeAgentSessionPersistTimer); + codeAgentSessionPersistTimer = null; + } + try { + window.localStorage?.removeItem(CODE_AGENT_SESSION_STORAGE_KEY); + } catch { + // Ignore storage availability errors. + } +} + +function storedChatMessage(message) { + const stored = { ...message }; + if (stored.runnerTrace && typeof stored.runnerTrace === "object") { + stored.runnerTrace = storedRunnerTrace(stored.runnerTrace); + } + delete stored.m3Evidence; + delete stored.availability; + return stored; +} + +function storedRunnerTrace(trace) { + const events = Array.isArray(trace.events) ? trace.events : []; + const storedEvents = trace.fullTraceLoaded === true && events.length <= CODE_AGENT_SESSION_STORAGE_TRACE_EVENT_LIMIT + ? events + : []; + const fullTraceLoaded = storedEvents.length > 0 && trace.fullTraceLoaded === true; + return { + ...trace, + events: storedEvents, + eventsCompacted: !fullTraceLoaded, + fullTraceLoaded + }; +} + +function refreshRestoredCodeAgentTraces() { + for (const message of state.chatMessages) { + if (!message.traceId) continue; + if (message.runnerTrace?.fullTraceLoaded === true) continue; + replayFullTrace(message.id, { quiet: true }); + } +} + function initLiveBuildOverlay() { el.liveBuildSummary.open = false; el.liveBuildToggle.addEventListener("click", (event) => { @@ -826,6 +963,7 @@ function initCommandBar() { state.currentRequest = null; state.canceledTraces.clear(); state.chatPending = false; + clearCodeAgentSessionState(); el.commandInput.value = ""; renderAgentChatStatus("idle"); renderCodeAgentSummary(); @@ -1383,6 +1521,9 @@ function updateMessageTrace(messageId, snapshot, options = {}) { if (state.currentRequest?.traceId === (snapshot.traceId ?? current.traceId) && threadId) { state.currentRequest.threadId = threadId; } + if (runnerTrace.eventsCompacted === true && runnerTrace.fullTraceLoaded !== true) { + maybeReplayFullTraceForMessage(state.chatMessages[index]); + } if (options.quiet !== true) { renderCodeAgentSummary(); renderConversation(); @@ -2103,6 +2244,7 @@ function renderConversation() { replaceChildren(el.conversationList, ...[...introMessages, ...state.chatMessages].map(messageCard)); restoreConversationScrollPosition(); restoreTraceScrollPositions(); + scheduleCodeAgentSessionPersist(); window.requestAnimationFrame(() => { if (renderVersion !== state.conversationRenderVersion) return; restoreConversationScrollPosition({ deferred: true }); @@ -4332,7 +4474,7 @@ function messageTraceToolbar(message, trace, events, rows) { function messageTraceCountText(trace, rawTotal, loadedTotal, readableTotal) { if (trace?.eventsCompacted === true && trace?.fullTraceLoaded !== true) { - return `压缩窗口 ${readableTotal} 条可读事件 / 已载入原始 ${loadedTotal} / 后端原始 ${rawTotal}`; + return `完整 trace 回放中 / 当前可读事件 ${readableTotal} / 已载入原始 ${loadedTotal} / 后端原始 ${rawTotal}`; } return `显示全部可读事件 ${readableTotal} / 已载入原始 ${loadedTotal} / 后端原始 ${rawTotal}`; } @@ -4588,7 +4730,7 @@ function traceNoiseSummaryBody(events, assistantChunks) { const message = cleanTraceText(text); const waitingFor = events.at(-1)?.waitingFor ? `waiting=${events.at(-1).waitingFor}` : null; return [ - `compressed=${assistantChunks.length} assistant chunks`, + `chunks=${assistantChunks.length} assistant message chunks`, waitingFor, message ? `message=${message}` : null ].filter(Boolean).join("\n"); diff --git a/web/hwlab-cloud-web/auth.mjs b/web/hwlab-cloud-web/auth.mjs index b99f7f4d..ce64315f 100644 --- a/web/hwlab-cloud-web/auth.mjs +++ b/web/hwlab-cloud-web/auth.mjs @@ -71,6 +71,10 @@ async function readActiveSession(config) { if (config.mode !== "local") { const serverSession = await fetchServerSession(); if (serverSession.available) { + if (!serverSession.authenticated && config.mode !== "server") { + const localSession = readLocalSession(config); + if (localSession.authenticated) return localSession; + } return { authenticated: serverSession.authenticated, mode: "server", @@ -210,14 +214,16 @@ async function attemptLogin({ username, password, config }) { if (config.mode !== "local") { const serverResult = await attemptServerLogin(username, password); if (serverResult.available) { - return serverResult.authenticated - ? { - authenticated: true, - mode: "server", - user: serverResult.user, - expiresAt: serverResult.expiresAt - } - : { authenticated: false, mode: "server" }; + if (serverResult.authenticated) { + writeLocalSession(config); + return { + authenticated: true, + mode: "server", + user: serverResult.user, + expiresAt: serverResult.expiresAt + }; + } + return { authenticated: false, mode: "server" }; } if (config.mode === "server") { return { authenticated: false, mode: "server" }; diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 39f298d4..d3010293 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -801,7 +801,14 @@ assert.match(app, /function messageTracePanel/); assert.match(app, /function messageTraceToolbar/); assert.match(app, /function messageTraceCountText/); assert.match(app, /显示全部可读事件\s+\$\{readableTotal\}\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/); -assert.match(app, /压缩窗口\s+\$\{readableTotal\}\s+条可读事件\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/); +assert.match(app, /完整 trace 回放中\s+\/\s+当前可读事件\s+\$\{readableTotal\}\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/); +assert.doesNotMatch(app, /压缩窗口\s+\$\{readableTotal\}/); +assert.match(app, /CODE_AGENT_SESSION_STORAGE_KEY\s*=\s*"hwlab\.workbench\.codeAgentSession\.v1"/); +assert.match(app, /function restoreCodeAgentSessionState/); +assert.match(app, /function persistCodeAgentSessionState/); +assert.match(app, /window\.localStorage\?\.setItem\(CODE_AGENT_SESSION_STORAGE_KEY/); +assert.match(app, /window\.localStorage\?\.removeItem\(CODE_AGENT_SESSION_STORAGE_KEY/); +assert.match(app, /refreshRestoredCodeAgentTraces/); assert.match(app, /FULL_TRACE_REPLAY_TIMEOUT_MS\s*=\s*15000/); assert.match(app, /fullTraceReplayInFlight:\s*new Set\(\)/); assert.match(app, /function maybeReplayFullTraceForMessage/); @@ -867,7 +874,8 @@ assert.match(app, /tool gateway\.shell/); assert.match(app, /function traceNoiseSummaryRow/); assert.match(styles, /max-height:\s*min\(520px,\s*54dvh\)/); assert.match(app, /assistant message x\$\{visibleEvents\.length\}/); -assert.match(app, /compressed=\$\{assistantChunks\.length\} assistant chunks/); +assert.match(app, /chunks=\$\{assistantChunks\.length\} assistant message chunks/); +assert.doesNotMatch(app, /compressed=\$\{assistantChunks\.length\} assistant chunks/); assert.match(app, /message=\$\{text\}/); assert.match(app, /waiting first assistant token/); assert.match(app, /function compactTraceTextTail/); diff --git a/web/hwlab-cloud-web/scripts/trace-scroll.test.mjs b/web/hwlab-cloud-web/scripts/trace-scroll.test.mjs index a93cf2ed..13cae9aa 100644 --- a/web/hwlab-cloud-web/scripts/trace-scroll.test.mjs +++ b/web/hwlab-cloud-web/scripts/trace-scroll.test.mjs @@ -135,13 +135,13 @@ test("trace display full means complete readable timeline, not compacted result }); }); await page.waitForSelector(".message-trace-count", { timeout: 12000 }); - const compacted = await tracePanelText(page); - assert.match(compacted.count, /压缩窗口/); - assert.doesNotMatch(compacted.count, /显示全部/); - assert.equal(compacted.rows.filter((row) => row.includes("tool gateway.shell")).length, 2, compacted.rows.join("\n---\n")); - assert.ok(compacted.rows.some((row) => row.includes("op=op_one")), compacted.rows.join("\n")); - assert.ok(compacted.rows.some((row) => row.includes("op=op_two")), compacted.rows.join("\n")); - assert.match(compacted.bodies.join("\n"), /first readable sentence middle detail final conclusion/); + const replaying = await tracePanelText(page); + assert.match(replaying.count, /完整 trace 回放中/); + assert.doesNotMatch(replaying.count, /压缩窗口|显示全部/); + assert.equal(replaying.rows.filter((row) => row.includes("tool gateway.shell")).length, 2, replaying.rows.join("\n---\n")); + assert.ok(replaying.rows.some((row) => row.includes("op=op_one")), replaying.rows.join("\n")); + assert.ok(replaying.rows.some((row) => row.includes("op=op_two")), replaying.rows.join("\n")); + assert.match(replaying.bodies.join("\n"), /first readable sentence middle detail final conclusion/); await page.evaluate(() => { const message = window.__hwlabWorkbenchTestHooks.tracePanelText(); @@ -157,7 +157,7 @@ test("trace display full means complete readable timeline, not compacted result }); const full = await tracePanelText(page); assert.match(full.count, /显示全部可读事件/); - assert.doesNotMatch(full.count, /压缩窗口/); + assert.doesNotMatch(full.count, /完整 trace 回放中|压缩窗口/); await page.close(); } finally {