From 9c0621819fadd90e168a178d509190175d1217bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Sat, 15 Aug 2026 10:13:12 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 88 ++++++++++ web/static/i18n/en-US.json | 1 + web/static/i18n/zh-CN.json | 1 + web/static/js/chat-scroll-refresh.test.cjs | 21 ++- web/static/js/chat.js | 15 ++ web/static/js/hitl-approval-ui.test.cjs | 8 +- web/static/js/monitor.js | 188 ++++++++++++++++++++- web/templates/index.html | 6 +- 8 files changed, 317 insertions(+), 11 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 21da5159..a8018887 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -4448,11 +4448,16 @@ html[data-theme="dark"] .sidebar-content:hover::-webkit-scrollbar-thumb:hover { } .process-details-content { + position: relative; width: 100%; min-width: 0; max-width: 100%; } +.process-details-return-latest-host { + position: relative; +} + .process-details-content .progress-timeline { max-height: 0; overflow: hidden; @@ -4473,6 +4478,71 @@ html[data-theme="dark"] .sidebar-content:hover::-webkit-scrollbar-thumb:hover { scrollbar-gutter: stable; } +.process-details-return-latest { + position: absolute; + right: 18px; + bottom: 16px; + z-index: 7; + width: 34px; + height: 34px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid rgba(15, 23, 42, 0.14); + border-radius: 50%; + background: rgba(255, 255, 255, 0.96); + color: #5f6875; + box-shadow: 0 8px 22px rgba(15, 23, 42, 0.14), 0 1px 3px rgba(15, 23, 42, 0.08); + backdrop-filter: blur(10px); + cursor: pointer; + transition: color 0.16s ease, background 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease; +} + +.process-details-return-latest[hidden] { + display: none !important; +} + +.process-details-return-latest:hover { + border-color: rgba(37, 99, 235, 0.3); + background: #ffffff; + color: var(--accent-color, #0b6cff); + box-shadow: 0 10px 26px rgba(15, 23, 42, 0.18), 0 2px 5px rgba(15, 23, 42, 0.1); + transform: translateY(-1px); +} + +.process-details-return-latest:focus-visible { + outline: 2px solid rgba(37, 99, 235, 0.42); + outline-offset: 3px; +} + +.process-details-return-latest svg { + width: 18px; + height: 18px; +} + +.process-details-return-latest.has-pending-new::after, +.process-details-return-latest.is-streaming::after { + content: ""; + position: absolute; + top: 5px; + right: 5px; + width: 7px; + height: 7px; + border: 2px solid rgba(255, 255, 255, 0.96); + border-radius: 50%; + background: var(--accent-color, #0b6cff); +} + +.process-details-return-latest.is-streaming svg { + animation: process-details-return-latest-nudge 1.1s ease-in-out infinite; +} + +@keyframes process-details-return-latest-nudge { + 0%, 65%, 100% { transform: translateY(0); } + 35% { transform: translateY(2px); } +} + .process-details-auto-sentinel { display: block; width: 100%; @@ -36506,6 +36576,24 @@ html[data-theme="dark"] .chat-return-latest:hover { color: #ffffff; } +html[data-theme="dark"] .process-details-return-latest { + border-color: rgba(148, 163, 184, 0.28); + background: rgba(15, 23, 42, 0.96); + color: #cbd5e1; + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.38), 0 1px 3px rgba(0, 0, 0, 0.28); +} + +html[data-theme="dark"] .process-details-return-latest:hover { + border-color: rgba(96, 165, 250, 0.42); + background: #172033; + color: #93c5fd; +} + +html[data-theme="dark"] .process-details-return-latest.has-pending-new::after, +html[data-theme="dark"] .process-details-return-latest.is-streaming::after { + border-color: rgba(15, 23, 42, 0.96); +} + html[data-theme="dark"] .process-details-container { border-top-color: #263244; } diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 24834b4d..a7dcd404 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -562,6 +562,7 @@ "conversationPreviewViewed": "Viewed", "conversationPreviewConversation": "Conversation", "returnToLatest": "Jump to latest message", + "returnToLatestProcessDetail": "Jump to latest iteration", "taskProgressStep": "Step {{current}} of {{total}}", "taskProgressOpen": "View task progress", "taskProgressDetails": "Task progress details", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 6729c35a..2690b6b1 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -550,6 +550,7 @@ "conversationPreviewViewed": "已查看", "conversationPreviewConversation": "对话", "returnToLatest": "回到最新消息", + "returnToLatestProcessDetail": "回到最新迭代", "taskProgressStep": "第 {{current}} / {{total}} 步", "taskProgressOpen": "查看任务进度", "taskProgressDetails": "任务进度详情", diff --git a/web/static/js/chat-scroll-refresh.test.cjs b/web/static/js/chat-scroll-refresh.test.cjs index 7c3c905c..151063fa 100644 --- a/web/static/js/chat-scroll-refresh.test.cjs +++ b/web/static/js/chat-scroll-refresh.test.cjs @@ -226,10 +226,21 @@ test('刷新后迭代思考区独立跟随最新内容且允许用户上滑解 const startSource = functionSource(monitor, 'startProcessDetailsLatestFollow', 'loadProcessDetailsPaginated'); const loadSource = functionSource(monitor, 'loadProcessDetailsPaginated', 'shouldInitiallyOpenProcessDetailsAtLatest'); const attachSource = functionSource(monitor, 'attachRunningTaskEventStream', 'parseToolCallArgsFromData'); + const returnLatestSource = functionSource(monitor, 'ensureProcessDetailsReturnLatestControl', 'scrollProcessDetailsToLatest'); + assert.match(monitor, /const processDetailsReturnLatestControls = new WeakMap\(\)/); + assert.match(returnLatestSource, /className = 'process-details-return-latest'/); + assert.match(monitor, /function getProcessDetailsLatestFollowStateForTimeline\(timeline\)/); + assert.match(monitor, /const followingLatest = !!\(followState && !followState\.detached\)/); + assert.match(monitor, /const shouldShow = !followingLatest && expanded && scrollable && awayFromLatest/); + assert.match(returnLatestSource, /timeline\.scrollTo\(\{ top: targetTop, behavior: 'smooth' \}\)/); + assert.match(returnLatestSource, /timeline\.addEventListener\('scroll', onScroll/); + assert.match(returnLatestSource, /window\.ensureProcessDetailsReturnLatestControl = ensureProcessDetailsReturnLatestControl/); assert.match(startSource, /new MutationObserver\(scheduleFollowLatest\)/); assert.match(startSource, /characterData: true/); assert.match(startSource, /new ResizeObserver\(scheduleFollowLatest\)/); + assert.match(startSource, /ensureProcessDetailsReturnLatestControl\(timeline\)/); + assert.match(startSource, /markProcessDetailsReturnLatestPending\(timeline\)/); assert.match(startSource, /scrollProcessDetailsToLatest\(String\(assistantMessageId \|\| ''\), false\)/); assert.match(startSource, /event\.deltaY < -1/); assert.match(startSource, /state\.userScrollIntentUntil = Date\.now\(\) \+ 1200/); @@ -245,6 +256,8 @@ test('刷新后迭代思考区独立跟随最新内容且允许用户上滑解 assert.match(loadSource, /startProcessDetailsLatestFollow\(assistantMessageId/); assert.match(attachSource, /startProcessDetailsLatestFollow\(asEl\.id, \{ persistent: true \}\)/); assert.match(attachSource, /stopProcessDetailsLatestFollow\(asEl\.id\)/); + assert.match(chat, /window\.ensureProcessDetailsReturnLatestControl\(timeline\)/); + assert.doesNotMatch(returnLatestSource, /chat-return-latest/); }); test('刷新后的工具调用恢复与实时一致的成功失败徽标', () => { @@ -271,6 +284,8 @@ test('首次实时输出与刷新恢复都保留独立迭代滚动并跟随最 assert.match(css, /\.progress-container\.is-streaming \.progress-timeline\.expanded,[\s\S]{0,360}max-height: min\(64vh, 720px\);[\s\S]{0,180}overflow-y: auto;/); assert.match(css, /\.message\.progress-message \.progress-timeline\.expanded \{[\s\S]{0,260}max-height: min\(64vh, 720px\);[\s\S]{0,160}overflow-y: auto;/); + assert.match(css, /\.process-details-return-latest \{[\s\S]{0,260}position: absolute;[\s\S]{0,260}border-radius: 50%;/); + assert.match(css, /\.process-details-return-latest\.has-pending-new::after,/); assert.doesNotMatch(css, /流式执行中[\s\S]{0,320}overflow-y: visible;/); assert.match(addSource, /startLiveProgressLatestFollow\(id\)/); assert.match(liveSource, /stateKey: liveProgressLatestFollowKey\(id\)/); @@ -325,7 +340,7 @@ test('消息气泡内部流式增高时仅在跟随模式继续粘底', () => { test('页面在任务补流脚本之前加载智能滚动控制器', () => { const scrollIndex = html.indexOf('/static/js/chat-scroll.js?v=20260815-1'); - const monitorIndex = html.indexOf('/static/js/monitor.js?v=20260815-1'); + const monitorIndex = html.indexOf('/static/js/monitor.js?v=20260815-2'); assert.notEqual(scrollIndex, -1); assert.notEqual(monitorIndex, -1); @@ -383,7 +398,7 @@ test('刷新指定对话时立即恢复且加载完成前不闪出无项目状 assert.match(css, /\.chat-container\.is-conversation-restoring #chat-messages/); assert.match(css, /\.chat-container\.is-conversation-restoring #chat-input-container/); assert.match(html, /router\.js\?v=20260813-2/); - assert.match(html, /chat\.js\?v=20260813-4/); + assert.match(html, /chat\.js\?v=20260815-2/); }); test('刷新运行中回复会复用已持久化 planning 并继续追加未来增量', () => { @@ -447,5 +462,5 @@ test('暗色模式对话三点悬浮不会触发浅色父行背景', () => { const css = fs.readFileSync('web/static/css/style.css', 'utf8'); assert.match(css, /html\[data-theme="dark"\] \.project-conversation-row:hover \.project-conversation-item/); assert.match(css, /html\[data-theme="dark"\] \.project-folder-action:hover,[\s\S]*?background: rgba\(71, 85, 105, 0\.28\);[\s\S]*?box-shadow: none;/); - assert.match(html, /style\.css\?v=20260813-6/); + assert.match(html, /style\.css\?v=20260815-2/); }); diff --git a/web/static/js/chat.js b/web/static/js/chat.js index ed6645bc..a26c05f0 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -3890,6 +3890,9 @@ function renderProcessDetails(messageId, processDetails, options) { contentDiv.appendChild(timeline); detailsContainer.appendChild(contentDiv); } + if (typeof window.ensureProcessDetailsReturnLatestControl === 'function') { + window.ensureProcessDetailsReturnLatestControl(timeline); + } // processDetails === null 表示“尚未加载(懒加载)”;messages.reasoningContent 可先展示 const isLazyNotLoaded = isLazyRequest; @@ -3901,6 +3904,9 @@ function renderProcessDetails(messageId, processDetails, options) { timeline.innerHTML = '
' + lazyHint + '
'; bindProcessDetailsLazyHint(timeline, messageId); timeline.classList.remove('expanded'); + if (typeof window.updateProcessDetailsReturnLatestControl === 'function') { + window.updateProcessDetailsReturnLatestControl(timeline); + } prefetchProcessDetailsSummaryHint(messageId, messageElement); return; } @@ -3934,6 +3940,9 @@ function renderProcessDetails(messageId, processDetails, options) { if (!isProcessDetailsUserExpanded(messageId)) { timeline.classList.remove('expanded'); } + if (typeof window.updateProcessDetailsReturnLatestControl === 'function') { + window.updateProcessDetailsReturnLatestControl(timeline); + } } return; } @@ -4299,6 +4308,12 @@ function finishProcessDetailsRender(messageElement, processDetails, isLazyNotLoa window.restoreWorkflowHitlInlineForConversation(convId); } } + if (typeof window.ensureProcessDetailsReturnLatestControl === 'function') { + window.ensureProcessDetailsReturnLatestControl(timeline); + } + if (typeof window.updateProcessDetailsReturnLatestControl === 'function') { + window.updateProcessDetailsReturnLatestControl(timeline); + } } /** 懒加载折叠态:后台拉摘要,提示迭代规模而不加载全量详情 */ diff --git a/web/static/js/hitl-approval-ui.test.cjs b/web/static/js/hitl-approval-ui.test.cjs index 8b25bc9b..cb78246e 100644 --- a/web/static/js/hitl-approval-ui.test.cjs +++ b/web/static/js/hitl-approval-ui.test.cjs @@ -253,10 +253,10 @@ test('多对话并发时释放隐藏主流且旧请求不能覆盖新对话状 assert.match(chat, /let loadConversationAbortController = null/); assert.match(chat, /cancelPendingConversationLoad\(\);[\s\S]{0,220}const conversationLoadController = new AbortController\(\)/); assert.match(chat, /signal: conversationLoadController\.signal/); - assert.match(template, /monitor\.js\?v=20260813-9/); - assert.match(template, /chat-scroll\.js\?v=20260813-6/); - assert.match(template, /chat\.js\?v=20260813-4/); - assert.match(template, /style\.css\?v=20260813-6/); + assert.match(template, /monitor\.js\?v=20260815-2/); + assert.match(template, /chat-scroll\.js\?v=20260815-1/); + assert.match(template, /chat\.js\?v=20260815-2/); + assert.match(template, /style\.css\?v=20260815-2/); }); test('输入区 Agent 审查文字保留足够行高且不会裁切字形', () => { diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 129dc5f2..2cd2b18b 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -1611,6 +1611,9 @@ function toggleProgressDetails(progressId) { timeline.classList.add('expanded'); toggleBtns.forEach((btn) => { btn.textContent = collapseT; }); } + if (typeof updateProcessDetailsReturnLatestControl === 'function') { + updateProcessDetailsReturnLatestControl(timeline); + } syncProgressElapsedSummary(progressId); } @@ -1800,12 +1803,170 @@ function integrateProgressToMCPSection(progressId, assistantMessageId, mcpExecut const PROCESS_DETAILS_PAGE_SIZE = 50; const processDetailsAutoLoadObservers = new WeakMap(); +const processDetailsReturnLatestControls = new WeakMap(); const processDetailsLatestFollowStates = new Map(); const PROCESS_DETAILS_RESTORE_FOLLOW_MS = 6000; const PROCESS_DETAILS_FOLLOW_SCROLLBAR_GUTTER_PX = 18; // 只有用户真正滚到底部才恢复自动跟随;保留 2px 兼容亚像素滚动。 const PROCESS_DETAILS_FOLLOW_RESUME_THRESHOLD_PX = 2; +function processDetailsReturnLatestLabel() { + if (typeof window.t !== 'function') return '回到最新迭代'; + const value = window.t('chat.returnToLatestProcessDetail'); + return value && value !== 'chat.returnToLatestProcessDetail' ? value : '回到最新迭代'; +} + +function processDetailsDistanceFromLatest(timeline) { + if (!timeline) return 0; + return Math.max(0, timeline.scrollHeight - timeline.clientHeight - timeline.scrollTop); +} + +function isProcessDetailsTimelineStreaming(container) { + if (!container) return false; + if (container.classList && container.classList.contains('is-streaming')) return true; + return !!(container.closest && container.closest('.progress-container.is-streaming, .process-details-container.is-streaming')); +} + +function getProcessDetailsLatestFollowStateForTimeline(timeline) { + let matched = null; + processDetailsLatestFollowStates.forEach(function (state) { + if (!matched && state && state.timeline === timeline && !state.stopped) { + matched = state; + } + }); + return matched; +} + +function updateProcessDetailsReturnLatestControl(timeline) { + const state = processDetailsReturnLatestControls.get(timeline); + if (!state || !state.button) return false; + const scrollable = timeline.scrollHeight > timeline.clientHeight + 2; + const awayFromLatest = processDetailsDistanceFromLatest(timeline) > PROCESS_DETAILS_FOLLOW_RESUME_THRESHOLD_PX; + const expanded = timeline.classList && timeline.classList.contains('expanded'); + const followState = getProcessDetailsLatestFollowStateForTimeline(timeline); + // 运行中粘底会在 MutationObserver 与下一帧滚底之间短暂离底; + // 只要用户还没主动上滑 detached,就不要把“回到最新迭代”按钮闪出来。 + const followingLatest = !!(followState && !followState.detached); + const shouldShow = !followingLatest && expanded && scrollable && awayFromLatest; + const label = processDetailsReturnLatestLabel(); + state.button.hidden = !shouldShow; + state.button.title = label; + state.button.setAttribute('aria-label', label); + state.button.classList.toggle('has-pending-new', shouldShow && state.hasPendingNewBelow); + state.button.classList.toggle('is-streaming', shouldShow && isProcessDetailsTimelineStreaming(state.container)); + return shouldShow; +} + +function syncProcessDetailsLatestFollowAfterManualReturn(timeline) { + processDetailsLatestFollowStates.forEach(function (state) { + if (!state || state.timeline !== timeline) return; + state.detached = false; + state.hasPendingNewBelow = false; + state.lastScrollTop = timeline.scrollTop; + if (!state.stopped && typeof state.scheduleFollowLatest === 'function') { + state.scheduleFollowLatest(); + } + }); +} + +function markProcessDetailsReturnLatestPending(timeline) { + const state = processDetailsReturnLatestControls.get(timeline); + if (!state) return false; + if (processDetailsDistanceFromLatest(timeline) > PROCESS_DETAILS_FOLLOW_RESUME_THRESHOLD_PX) { + state.hasPendingNewBelow = true; + } + updateProcessDetailsReturnLatestControl(timeline); + return true; +} + +function ensureProcessDetailsReturnLatestControl(timeline) { + if (!timeline || timeline.nodeType !== 1) return false; + let state = processDetailsReturnLatestControls.get(timeline); + if (state) { + updateProcessDetailsReturnLatestControl(timeline); + return true; + } + + const container = timeline.closest + ? timeline.closest('.progress-container, .process-details-container') + : null; + const host = (container && container.querySelector && container.querySelector('.process-details-content')) || container || timeline.parentElement; + if (!host) return false; + host.classList.add('process-details-return-latest-host'); + + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'process-details-return-latest'; + button.hidden = true; + button.innerHTML = ''; + host.appendChild(button); + + state = { + container: container || host, + host: host, + button: button, + hasPendingNewBelow: false, + resizeObserver: null, + mutationObserver: null + }; + processDetailsReturnLatestControls.set(timeline, state); + + const clearPointer = function (event) { + if (event) event.stopPropagation(); + }; + const scrollToLatest = function (event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + const targetTop = Math.max(0, timeline.scrollHeight - timeline.clientHeight); + if (typeof timeline.scrollTo === 'function') { + timeline.scrollTo({ top: targetTop, behavior: 'smooth' }); + } else { + timeline.scrollTop = targetTop; + } + state.hasPendingNewBelow = false; + syncProcessDetailsLatestFollowAfterManualReturn(timeline); + updateProcessDetailsReturnLatestControl(timeline); + setTimeout(function () { + if (timeline.isConnected) { + updateProcessDetailsReturnLatestControl(timeline); + } + }, 360); + button.blur(); + }; + const onScroll = function () { + if (processDetailsDistanceFromLatest(timeline) <= PROCESS_DETAILS_FOLLOW_RESUME_THRESHOLD_PX) { + state.hasPendingNewBelow = false; + } + updateProcessDetailsReturnLatestControl(timeline); + }; + + button.addEventListener('pointerdown', clearPointer); + button.addEventListener('mousedown', clearPointer); + button.addEventListener('touchstart', clearPointer, { passive: true }); + button.addEventListener('click', scrollToLatest); + timeline.addEventListener('scroll', onScroll, { passive: true }); + if (typeof ResizeObserver === 'function') { + state.resizeObserver = new ResizeObserver(function () { + updateProcessDetailsReturnLatestControl(timeline); + }); + state.resizeObserver.observe(timeline); + } + if (typeof MutationObserver === 'function') { + state.mutationObserver = new MutationObserver(function () { + updateProcessDetailsReturnLatestControl(timeline); + }); + state.mutationObserver.observe(timeline, { childList: true, subtree: true, characterData: true }); + } + updateProcessDetailsReturnLatestControl(timeline); + return true; +} + +window.ensureProcessDetailsReturnLatestControl = ensureProcessDetailsReturnLatestControl; +window.updateProcessDetailsReturnLatestControl = updateProcessDetailsReturnLatestControl; +window.markProcessDetailsReturnLatestPending = markProcessDetailsReturnLatestPending; + function processDetailsContinuousLabel(kind) { if (kind === 'older') { return typeof window.t === 'function' ? window.t('chat.loadingEarlierDetails') : '正在加载更早记录…'; @@ -1943,6 +2104,11 @@ function scrollProcessDetailsToLatest(assistantMessageId, smooth = true) { } else { timeline.scrollTop = targetTop; } + const state = processDetailsReturnLatestControls.get(timeline); + if (state) { + state.hasPendingNewBelow = false; + updateProcessDetailsReturnLatestControl(timeline); + } return true; } @@ -1985,6 +2151,7 @@ function startProcessDetailsLatestFollow(assistantMessageId, options) { if (!container || !timeline) return false; stopProcessDetailsLatestFollow(key); + ensureProcessDetailsReturnLatestControl(timeline); const persistent = !!opts.persistent; const durationMs = Number.isFinite(Number(opts.durationMs)) ? Math.max(250, Number(opts.durationMs)) @@ -1993,6 +2160,8 @@ function startProcessDetailsLatestFollow(assistantMessageId, options) { stopped: false, detached: false, persistent: persistent, + timeline: timeline, + hasPendingNewBelow: false, lastScrollTop: timeline.scrollTop, userScrollIntentUntil: 0, touchLastY: null, @@ -2010,6 +2179,7 @@ function startProcessDetailsLatestFollow(assistantMessageId, options) { cancelAnimationFrame(state.rafId); state.rafId = 0; } + updateProcessDetailsReturnLatestControl(timeline); }; const onWheel = function (event) { if (!event) return; @@ -2071,9 +2241,14 @@ function startProcessDetailsLatestFollow(assistantMessageId, options) { distance <= PROCESS_DETAILS_FOLLOW_RESUME_THRESHOLD_PX ) { state.detached = false; + state.hasPendingNewBelow = false; scheduleFollowLatest(); } + if (distance <= PROCESS_DETAILS_FOLLOW_RESUME_THRESHOLD_PX) { + state.hasPendingNewBelow = false; + } state.lastScrollTop = currentTop; + updateProcessDetailsReturnLatestControl(timeline); }; timeline.addEventListener('wheel', onWheel, { passive: true }); timeline.addEventListener('pointerdown', onPointerDown, { passive: true }); @@ -2102,7 +2277,9 @@ function startProcessDetailsLatestFollow(assistantMessageId, options) { } else { scrollProcessDetailsToLatest(String(assistantMessageId || ''), false); } + state.hasPendingNewBelow = false; state.lastScrollTop = timeline.scrollTop; + updateProcessDetailsReturnLatestControl(timeline); // 内层迭代区与外层对话区分别粘底;用户上滑内层后,本状态会暂停两者的自动跟随。 if (window.CyberStrikeChatScroll && typeof window.CyberStrikeChatScroll.scrollIfPinned === 'function') { @@ -2110,9 +2287,15 @@ function startProcessDetailsLatestFollow(assistantMessageId, options) { } }; const scheduleFollowLatest = function () { - if (state.stopped || state.detached || state.rafId) return; + if (state.stopped || state.rafId) return; + if (state.detached) { + state.hasPendingNewBelow = true; + markProcessDetailsReturnLatestPending(timeline); + return; + } state.rafId = requestAnimationFrame(followLatest); }; + state.scheduleFollowLatest = scheduleFollowLatest; state.observer = new MutationObserver(scheduleFollowLatest); state.observer.observe(timeline, { childList: true, subtree: true, characterData: true }); @@ -2416,6 +2599,9 @@ function toggleProcessDetails(progressId, assistantMessageId) { } else if (timeline) { setExpanded(!timeline.classList.contains('expanded')); } + if (timeline && typeof updateProcessDetailsReturnLatestControl === 'function') { + updateProcessDetailsReturnLatestControl(timeline); + } if (typeof window.syncAssistantTurnSummary === 'function') { window.syncAssistantTurnSummary(document.getElementById(assistantMessageId)); } diff --git a/web/templates/index.html b/web/templates/index.html index 8b680c57..c898b36d 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -31,7 +31,7 @@ } })(); - + @@ -6841,8 +6841,8 @@ - - + +