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 @@
-
-
+
+