diff --git a/static/src/App.vue b/static/src/App.vue index 7d3cfcc..46de6d3 100644 --- a/static/src/App.vue +++ b/static/src/App.vue @@ -235,6 +235,7 @@ :format-search-topic="formatSearchTopic" :format-search-time="formatSearchTime" :format-search-domains="formatSearchDomains" + :history-loading="historyLoading" @stick-state-change="handleStickStateChange" @user-scroll-intent="handleUserScrollIntent" /> diff --git a/static/src/app/methods/history.ts b/static/src/app/methods/history.ts index c9b0dc2..e94f68b 100644 --- a/static/src/app/methods/history.ts +++ b/static/src/app/methods/history.ts @@ -199,7 +199,7 @@ export const historyMethods = { // 滚动到底部 this.$nextTick(() => { - this.scrollToBottom(); + this.scrollHistoryToBottomInstant(); }); debugLog('历史对话内容显示完成'); @@ -532,7 +532,7 @@ export const historyMethods = { // 确保滚动到底部 this.$nextTick(() => { - this.scrollToBottom(); + this.scrollHistoryToBottomInstant(); setTimeout(() => { const blockCount = this.$el && this.$el.querySelectorAll diff --git a/static/src/app/methods/ui/scroll.ts b/static/src/app/methods/ui/scroll.ts index 27eca15..b33c4fa 100644 --- a/static/src/app/methods/ui/scroll.ts +++ b/static/src/app/methods/ui/scroll.ts @@ -148,6 +148,26 @@ export const scrollMethods = { 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 }); + }, scrollToBottom() { uiBounceTrace( 'ui.scrollToBottom:called', diff --git a/static/src/app/methods/ui/socket.ts b/static/src/app/methods/ui/socket.ts index 854f463..9932412 100644 --- a/static/src/app/methods/ui/socket.ts +++ b/static/src/app/methods/ui/socket.ts @@ -234,7 +234,6 @@ export const socketMethods = { throw new Error(`状态接口请求失败: ${statusResponse.status}`); } const statusData = await statusResponse.json(); - this.isConnected = true; this.socket = null; this.projectPath = statusData.project_path || ''; this.agentVersion = statusData.version || this.agentVersion; @@ -296,6 +295,7 @@ export const socketMethods = { this.thinkingMode = false; } } + this.isConnected = true; const focusPromise = this.focusFetchFiles(); let treePromise: Promise | null = null; diff --git a/static/src/components/chat/ChatArea.vue b/static/src/components/chat/ChatArea.vue index b1f8d11..acc332a 100644 --- a/static/src/components/chat/ChatArea.vue +++ b/static/src/components/chat/ChatArea.vue @@ -24,7 +24,8 @@ class="message-text user-bubble-text" :class="{ 'is-collapsed': isUserBubbleCollapsed(msg, index), - 'is-expandable': isUserBubbleExpandable(msg, index) + 'is-expandable': isUserBubbleExpandable(msg, index), + 'is-history-loading': props.historyLoading || userBubbleTransitionSuppressed }" >
) => string; formatSearchTime: (filters: Record) => string; formatSearchDomains: (filters: Record) => string; + historyLoading?: boolean; }>(); const emit = defineEmits<{ ( @@ -715,8 +717,10 @@ const USER_BUBBLE_FOLD_LINES = 10; const userBubbleRefs = new Map(); const userBubbleFoldStates = ref>({}); const copiedBubbleKeys = ref>(new Set()); +const userBubbleTransitionSuppressed = ref(false); let userBubbleResizeObserver: ResizeObserver | null = null; let copiedBubbleTimeouts = new Map(); +let userBubbleTransitionSeq = 0; const getUserBubbleKey = (msg: any, index: number) => msg?.id || `user-bubble-${index}`; @@ -867,6 +871,17 @@ const observeUserBubbles = () => { }); }; +const releaseUserBubbleTransitionAfterLayout = () => { + const seq = ++userBubbleTransitionSeq; + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (seq === userBubbleTransitionSeq) { + userBubbleTransitionSuppressed.value = false; + } + }); + }); +}; + const isSystemAutoUserMessage = (message: any) => { if (!message || message.role !== 'user') { return false; @@ -1807,12 +1822,14 @@ function compactBriefLabel(msg: any): string { watch( () => filteredMessages.value.map((m) => m?.id ?? m?.content?.slice(0, 80)), () => { + userBubbleTransitionSuppressed.value = true; // 先同步估算折叠状态,再异步精确测量,避免长气泡先展开后收起 preMeasureUserBubbles(); nextTick(() => { requestAnimationFrame(() => { measureUserBubbles(); observeUserBubbles(); + releaseUserBubbleTransitionAfterLayout(); }); }); }, diff --git a/static/src/stores/personalization.ts b/static/src/stores/personalization.ts index 56e5ddb..f596627 100644 --- a/static/src/stores/personalization.ts +++ b/static/src/stores/personalization.ts @@ -98,6 +98,7 @@ const DEFAULT_DEEP_COMPRESS_TRIGGER_TOKENS = 150000; const RUN_MODE_OPTIONS: RunMode[] = ['fast', 'thinking', 'deep']; const EXPERIMENT_STORAGE_KEY = 'agents_personalization_experiments'; const THEME_STORAGE_KEY = 'agents_ui_theme'; +const STACKED_HIDE_BORDERS_STORAGE_KEY = 'agents_stacked_hide_borders'; const loadCachedTheme = (): PersonalForm['theme'] => { if (typeof window === 'undefined' || !window.localStorage) { @@ -107,6 +108,24 @@ const loadCachedTheme = (): PersonalForm['theme'] => { return saved === 'light' || saved === 'dark' || saved === 'classic' ? saved : 'classic'; }; +const loadCachedStackedHideBorders = (): boolean => { + if (typeof window === 'undefined' || !window.localStorage) { + return false; + } + return window.localStorage.getItem(STACKED_HIDE_BORDERS_STORAGE_KEY) === 'true'; +}; + +const persistStackedHideBorders = (value: boolean) => { + if (typeof window === 'undefined' || !window.localStorage) { + return; + } + try { + window.localStorage.setItem(STACKED_HIDE_BORDERS_STORAGE_KEY, value ? 'true' : 'false'); + } catch (error) { + console.warn('写入堆叠块边线设置失败:', error); + } +}; + const defaultForm = (): PersonalForm => ({ enabled: false, communication_style: 'default', @@ -125,7 +144,7 @@ const defaultForm = (): PersonalForm => ({ compact_message_display: 'full', show_git_status_bar: true, auto_open_terminal_panel: true, - stacked_hide_borders: false, + stacked_hide_borders: loadCachedStackedHideBorders(), enhanced_tool_display_categories: [], enabled_skills: [], self_identify: '', @@ -398,6 +417,7 @@ export const usePersonalizationStore = defineStore('personalization', { this.applyTheme(this.form.theme); } persistDefaultHideWorkspace(this.form.default_hide_workspace); + persistStackedHideBorders(this.form.stacked_hide_borders); if (this.form.default_hide_workspace) { useUiStore().setWorkspaceCollapsed(true); } @@ -593,6 +613,9 @@ export const usePersonalizationStore = defineStore('personalization', { ...this.form, [payload.key]: payload.value }; + if (payload.key === 'stacked_hide_borders') { + persistStackedHideBorders(!!payload.value); + } this.clearFeedback(); this.scheduleAutoSave(); }, diff --git a/static/src/styles/components/chat/_chat-area.scss b/static/src/styles/components/chat/_chat-area.scss index 7678bfc..24020e1 100644 --- a/static/src/styles/components/chat/_chat-area.scss +++ b/static/src/styles/components/chat/_chat-area.scss @@ -585,6 +585,10 @@ word-break: break-all; } +.user-message .message-text.user-bubble-text.is-history-loading .bubble-text { + transition: none !important; +} + .user-message .message-text.user-bubble-text.is-expandable .bubble-text:not(.is-expanded) { max-height: var(--bubble-fold-height, 240px); }