// @ts-nocheck import { debugLog } from '../common'; import { usePolicyStore } from '../../../stores/policy'; import { useModelStore } from '../../../stores/model'; import { usePersonalizationStore } from '../../../stores/personalization'; import { useTutorialStore } from '../../../stores/tutorial'; import { renderMarkdown as renderMarkdownHelper } from '../../../composables/useMarkdownRenderer'; import { scrollToBottom as scrollToBottomHelper, conditionalScrollToBottom as conditionalScrollToBottomHelper, scrollThinkingToBottom as scrollThinkingToBottomHelper } from '../../../composables/useScrollControl'; import { startResize as startPanelResize, handleResize as handlePanelResize, stopResize as stopPanelResize } from '../../../composables/usePanelResize'; import { SUB_AGENT_DONE_PREFIX_RE, BG_RUN_COMMAND_DONE_PREFIX_RE, userMDebug, UI_BOUNCE_TRACE_MAX, uiBounceTraceLastTsByKey, isUiBounceTraceEnabled, uiBounceTrace, isConnectionDiagEnabled, pushConnectionDiagRecord, connectionDiag, parseSubAgentDoneLabel, parseBackgroundRunCommandDoneLabel, parseSystemNoticeLabel, } from './shared'; export const scrollMethods = { clearLocalTaskUiState(reason = 'safe-navigation') { // 只清理当前浏览器视图里的运行态/工具态,不取消后端任务。 // 用于切换对话/工作区后,避免旧任务残留让新视图的发送按钮显示为“停止”。 try { if (typeof this.stopWaitingTaskProbe === 'function') { this.stopWaitingTaskProbe(); } } catch (_) {} this.streamingMessage = false; this.taskInProgress = false; this.stopRequested = false; this.waitingForSubAgent = false; this.waitingForBackgroundCommand = false; this.dropToolEvents = false; this.runtimeQueuedMessages = []; this.runtimeGuidanceFallbackQueue = []; this.runtimeQueueAutoSendInProgress = false; this.runtimeQueueSyncLockKey = ''; this.runtimeQueueSyncLockUntil = 0; this.goalModeArmed = false; this.goalRunning = false; this.goalProgress = null; this.goalDialogOpen = false; if (typeof this.clearRuntimeQueueSuppressionState === 'function') { this.clearRuntimeQueueSuppressionState(); } else { this.runtimeQueueSuppressedMessageIds = new Set(); this.runtimeGuidanceSuppressedTextCounts = {}; } if (typeof this.clearPendingTools === 'function') { this.clearPendingTools(reason); } else { this.preparingTools?.clear?.(); this.activeTools?.clear?.(); this.toolActionIndex?.clear?.(); this.toolStacks?.clear?.(); } if (typeof this.chatClearThinkingLocks === 'function') { this.chatClearThinkingLocks(); } this.$forceUpdate?.(); }, ensureScrollListener() { if (this._scrollListenerReady) { return; } const area = this.getMessagesAreaElement(); if (!area) { return; } this.initScrollListener(); this._scrollListenerReady = true; }, handleStickStateChange(payload) { const escaped = !!payload?.escapedFromLock; const isAtBottom = !!payload?.isAtBottom; const isNearBottom = !!payload?.isNearBottom; const nearBottom = isAtBottom || isNearBottom; const userEscaped = !!this._escapedByUserScroll; uiBounceTrace( 'stick-state-change', { isAtBottom, isNearBottom, escapedFromLock: escaped, userEscaped, autoScrollEnabled: this.autoScrollEnabled, userScrolling: this.userScrolling }, 'stick-state-change', 180 ); this.stickIsAtBottom = isAtBottom; this.stickIsNearBottom = isNearBottom; // 用户主动脱离锁定:只要没回到底部/近底部,就保持“手动滚动中”状态 if (userEscaped) { if (nearBottom) { this._escapedByUserScroll = false; this.chatSetScrollState({ userScrolling: false }); } else { this.chatSetScrollState({ userScrolling: true }); } return; } // 非用户触发的 escaped(例如高度突增导致的短暂锚点漂移)不应解除锁定 this.chatSetScrollState({ userScrolling: false }); if (!escaped) { return; } const active = typeof this.isOutputActive === 'function' ? this.isOutputActive() : true; if (!this.autoScrollEnabled || !active) { return; } const chatArea = this.getChatAreaController(); if (chatArea && typeof chatArea.conditionalStickToBottom === 'function') { chatArea.conditionalStickToBottom({ force: false }); return; } conditionalScrollToBottomHelper(this); }, handleUserScrollIntent(payload) { const ts = Number(payload?.ts || Date.now()); const chatArea = this.getChatAreaController(); if (chatArea && typeof chatArea.stopStickScroll === 'function') { chatArea.stopStickScroll(); } // 用户手动滚动后,短时间内禁止任何自动追底,规避 escapedFromLock 状态抖动竞态 this._manualScrollSuppressUntil = Math.max(this._manualScrollSuppressUntil || 0, ts + 1600); this._escapedByUserScroll = true; this.chatSetScrollState({ userScrolling: true }); uiBounceTrace( 'ui.user-scroll-intent', { ts, delta: Number(payload?.delta || 0), top: Number(payload?.top || 0), suppressUntil: this._manualScrollSuppressUntil }, 'ui.user-scroll-intent', 80 ); }, scrollHistoryToBottomInstant() { this._escapedByUserScroll = false; const chatArea = this.getChatAreaController(); if (chatArea && typeof chatArea.stopStickScroll === 'function') { chatArea.stopStickScroll(); } const area = this.getMessagesAreaElement(); if (!area) { return; } const jump = () => { area.scrollTop = area.scrollHeight; }; jump(); requestAnimationFrame(() => { jump(); requestAnimationFrame(jump); }); this.chatSetScrollState({ userScrolling: false }); }, async settleHistoryRenderAndScroll() { this._escapedByUserScroll = false; const chatArea = this.getChatAreaController(); if (chatArea && typeof chatArea.stopStickScroll === 'function') { chatArea.stopStickScroll(); } const area = this.getMessagesAreaElement(); if (!area) { return; } const nextFrame = () => new Promise((resolve) => requestAnimationFrame(resolve)); const sleep = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms)); const jump = () => { area.scrollTop = area.scrollHeight; }; let lastHeight = -1; let stableFrames = 0; const startedAt = Date.now(); while (Date.now() - startedAt < 900 && stableFrames < 4) { await nextFrame(); jump(); const height = area.scrollHeight; if (Math.abs(height - lastHeight) <= 1) { stableFrames += 1; } else { stableFrames = 0; lastHeight = height; } } // 给 show_html / 表格滚动壳等异步同步 DOM 一次短暂稳定窗口;仍在 historyLoading 隐藏期内完成。 await sleep(80); jump(); await nextFrame(); jump(); this.chatSetScrollState({ userScrolling: false }); }, scrollToBottom() { uiBounceTrace( 'ui.scrollToBottom:called', { autoScrollEnabled: this.autoScrollEnabled, userScrolling: this.userScrolling }, 'ui.scrollToBottom:called', 80 ); this._escapedByUserScroll = false; const chatArea = this.getChatAreaController(); if (chatArea && typeof chatArea.scrollToBottom === 'function') { chatArea.scrollToBottom({ behavior: 'auto', force: this.autoScrollEnabled }); this.chatSetScrollState({ userScrolling: false }); return; } scrollToBottomHelper(this); }, conditionalScrollToBottom() { const active = typeof this.isOutputActive === 'function' ? this.isOutputActive() : true; const chatArea = this.getChatAreaController(); const stickState = chatArea && typeof chatArea.getStickState === 'function' ? chatArea.getStickState() : null; uiBounceTrace( 'ui.conditionalScrollToBottom:called', { active, autoScrollEnabled: this.autoScrollEnabled, userScrolling: this.userScrolling, stickIsNearBottom: this.stickIsNearBottom, escapedFromLock: !!stickState?.escapedFromLock }, 'ui.conditionalScrollToBottom:called', 120 ); if (!active) { return; } const now = Date.now(); if (now <= (this._manualScrollSuppressUntil || 0)) { uiBounceTrace( 'ui.conditionalScrollToBottom:skip-manual-cooldown', { now, suppressUntil: this._manualScrollSuppressUntil }, 'ui.conditionalScrollToBottom:skip-manual-cooldown', 120 ); return; } // 仅在“确认是用户主动脱离锁定”时才阻断自动追底; // 对于内容高度突变导致的 escapedFromLock,不应长期卡住锁定。 if (stickState?.escapedFromLock && this._escapedByUserScroll) { uiBounceTrace( 'ui.conditionalScrollToBottom:skip-escaped-lock', { escapedFromLock: !!stickState?.escapedFromLock, escapedByUser: !!this._escapedByUserScroll, isNearBottom: !!stickState?.isNearBottom, isAtBottom: !!stickState?.isAtBottom }, 'ui.conditionalScrollToBottom:skip-escaped-lock', 120 ); return; } // 只要用户处于手动滚动状态,就停止自动追底,避免“被弹回” // (stickIsNearBottom 在 show_html 动态重排时可能短暂失真,不可作为阻断前置条件) if (this.userScrolling) { uiBounceTrace( 'ui.conditionalScrollToBottom:skip-user-scrolling', { userScrolling: this.userScrolling, stickIsNearBottom: this.stickIsNearBottom }, 'ui.conditionalScrollToBottom:skip-user-scrolling', 120 ); return; } if (chatArea && typeof chatArea.conditionalStickToBottom === 'function') { chatArea.conditionalStickToBottom({ force: false }); return; } conditionalScrollToBottomHelper(this); }, toggleScrollLock() { uiBounceTrace( 'ui.scrollToBottomButton:clicked', { autoScrollEnabled: this.autoScrollEnabled, userScrolling: this.userScrolling, stickNearBottom: this.stickIsNearBottom }, 'ui.scrollToBottomButton:clicked', 0 ); const chatArea = this.getChatAreaController(); if (chatArea && typeof chatArea.scrollToBottom === 'function') { chatArea.scrollToBottom({ behavior: 'smooth', force: true }); } else { scrollToBottomHelper(this, { ignoreUserScrolling: true, resetUserScrolling: true, behavior: 'smooth', force: true }); } this._escapedByUserScroll = false; this.chatSetScrollState({ autoScrollEnabled: true, userScrolling: false }); return true; }, scrollThinkingToBottom(blockId) { scrollThinkingToBottomHelper(this, blockId); }, initScrollListener() { const chatArea = this.getChatAreaController(); if ( chatArea && typeof chatArea.isUsingStickToBottom === 'function' && chatArea.isUsingStickToBottom() ) { this._scrollListenerReady = true; return; } const messagesArea = this.getMessagesAreaElement(); if (!messagesArea) { console.warn('消息区域未找到'); return; } this._scrollListenerReady = true; let scrollDebugCount = 0; const SCROLL_DEBUG_MAX = 1200; let scrollDebugLastTs = 0; const isScrollDebugEnabled = () => { if (typeof window === 'undefined') return false; const until = Number((window as any).__SHOW_TAG_DRAWING_UNTIL__ || 0); return Date.now() <= until; }; const scrollDebug = (event, payload = {}, throttleMs = 80) => { if (!isScrollDebugEnabled()) { return; } if (scrollDebugCount >= SCROLL_DEBUG_MAX) { return; } const now = Date.now(); if (throttleMs > 0 && now - scrollDebugLastTs < throttleMs) { return; } scrollDebugLastTs = now; scrollDebugCount += 1; if (scrollDebugCount === SCROLL_DEBUG_MAX) { console.warn('[SCROLL_DEBUG]', 'listener:log-limit-reached', { max: SCROLL_DEBUG_MAX }); return; } console.log('[SCROLL_DEBUG]', event, payload); }; scrollDebug( 'listener:setup', { autoScrollEnabled: this.autoScrollEnabled, userScrolling: this.userScrolling }, 0 ); let isProgrammaticScroll = false; const bottomThreshold = 12; this._setScrollingFlag = (flag) => { isProgrammaticScroll = !!flag; scrollDebug('listener:programmatic-flag', { flag: !!flag }, 0); }; messagesArea.addEventListener('scroll', () => { if (isProgrammaticScroll) { scrollDebug( 'listener:scroll:ignore-programmatic', { top: messagesArea.scrollTop, height: messagesArea.scrollHeight, client: messagesArea.clientHeight }, 120 ); return; } const scrollTop = messagesArea.scrollTop; const scrollHeight = messagesArea.scrollHeight; const clientHeight = messagesArea.clientHeight; const isAtBottom = scrollHeight - scrollTop - clientHeight < bottomThreshold; const nearBottomThreshold = 48; const isNearBottom = scrollHeight - scrollTop - clientHeight < nearBottomThreshold; const active = typeof this.isOutputActive === 'function' ? this.isOutputActive() : true; if (this.autoScrollEnabled && active) { // 锁定模式下不让 userScrolling 状态来回抖动 this.chatSetScrollState({ userScrolling: false }); scrollDebug('listener:scroll:locked-keep-bottom-state', { top: scrollTop, height: scrollHeight, client: clientHeight, remain: scrollHeight - scrollTop - clientHeight, isAtBottom, isNearBottom }); return; } // 仅维护状态,不在 scroll 事件中强制写 scrollTop(避免上下抖动) this.chatSetScrollState({ userScrolling: !(isAtBottom || isNearBottom) }); scrollDebug('listener:scroll:update-state', { top: scrollTop, height: scrollHeight, client: clientHeight, remain: scrollHeight - scrollTop - clientHeight, isAtBottom, isNearBottom, autoScrollEnabled: this.autoScrollEnabled, userScrollingBefore: this.userScrolling, userScrollingNext: !(isAtBottom || isNearBottom) }); }); }, handleThinkingScroll(blockId, event) { if (!blockId || !event || !event.target) { return; } const el = event.target; const threshold = 12; const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold; this.chatSetThinkingLock(blockId, atBottom); }, toggleBlock(blockId) { if (!blockId) { return; } if (this.expandedBlocks && this.expandedBlocks.has(blockId)) { this.chatCollapseBlock(blockId); } else { this.chatExpandBlock(blockId); } } };