From 54edeec8028664adb1fc1fd2effe8a96dbddc0f1 Mon Sep 17 00:00:00 2001 From: shiyue Date: Wed, 24 Jun 2026 10:40:54 +0800 Subject: [PATCH] fix: restore running session streaming state --- ...on streaming bubble restore TO DO list.csv | 6 + public/app.js | 134 +++++++++++++----- 2 files changed, 108 insertions(+), 32 deletions(-) create mode 100644 Fix running session streaming bubble restore TO DO list.csv diff --git a/Fix running session streaming bubble restore TO DO list.csv b/Fix running session streaming bubble restore TO DO list.csv new file mode 100644 index 0000000..3d1208b --- /dev/null +++ b/Fix running session streaming bubble restore TO DO list.csv @@ -0,0 +1,6 @@ +status,step +DONE,定位会话切换与运行中恢复逻辑 +DONE,核对本次改动是否影响该链路 +DONE,查看相关运行日志和当前会话状态 +DONE,修复缺失 streaming 气泡的恢复逻辑 +IN_PROGRESS,验证并汇总影响范围 diff --git a/public/app.js b/public/app.js index cfaef13..a15c7c1 100644 --- a/public/app.js +++ b/public/app.js @@ -68,8 +68,10 @@ const SIDEBAR_SWIPE_TRIGGER = 72; const SIDEBAR_SWIPE_MAX_VERTICAL_DRIFT = 42; const OLD_SESSION_GROUP_INITIAL_VISIBLE = 3; + const SESSION_GROUP_COMPACT_VISIBLE_LIMIT = 8; const OLD_SESSION_COLLAPSE_DAYS = 7; const OLD_SESSION_COLLAPSE_MS = OLD_SESSION_COLLAPSE_DAYS * 24 * 60 * 60 * 1000; + const SESSION_LOAD_OVERLAY_TIMEOUT_MS = 12_000; const MODEL_OPTIONS = [ { value: 'opus', label: 'Opus', desc: '最强大,1M 上下文' }, @@ -176,6 +178,7 @@ let codexConfigCache = null; let loadedHistorySessionId = null; let activeSessionLoad = null; + let sessionLoadOverlayTimer = null; let sidebarSwipe = null; let activeComposerToken = null; let composerSuggestionTimer = null; @@ -3057,34 +3060,37 @@ return session.id === currentSessionId || session.isRunning || session.hasUnread || session.waitingOnChildren; } - function splitCollapsedOldSessions(sessionItems, collapseKey) { + function splitCollapsedSessions(sessionItems, collapseKey) { const isExpanded = collapseKey && expandedOldSessionGroups.has(collapseKey); const nowMs = Date.now(); + const shouldCompactByCount = sessionItems.length > SESSION_GROUP_COMPACT_VISIBLE_LIMIT; const visibleSessions = []; - const hiddenOldSessions = []; + const hiddenSessions = []; sessionItems.forEach((session) => { - const shouldHideOldSession = ( + const isOldSession = isOlderThanOldSessionWindow(session, nowMs); + const shouldHideByAge = isOldSession && visibleSessions.length >= OLD_SESSION_GROUP_INITIAL_VISIBLE; + const shouldHideByCount = shouldCompactByCount && visibleSessions.length >= SESSION_GROUP_COMPACT_VISIBLE_LIMIT; + const shouldHideSession = ( !isExpanded && !shouldAlwaysShowOldSession(session) - && isOlderThanOldSessionWindow(session, nowMs) - && visibleSessions.length >= OLD_SESSION_GROUP_INITIAL_VISIBLE + && (shouldHideByAge || shouldHideByCount) ); - if (shouldHideOldSession) { - hiddenOldSessions.push(session); + if (shouldHideSession) { + hiddenSessions.push(session); } else { visibleSessions.push(session); } }); - return { visibleSessions, hiddenOldSessions }; + return { visibleSessions, hiddenSessions }; } function createOldSessionLoadMoreButton(hiddenCount, collapseKey, projectName = '') { const button = document.createElement('button'); button.type = 'button'; button.className = 'session-list-load-more'; - button.setAttribute('aria-label', `加载更多${projectName ? ` ${projectName}` : ''} ${hiddenCount} 条 7 天前会话`); + button.setAttribute('aria-label', `加载更多${projectName ? ` ${projectName}` : ''}会话,还有 ${hiddenCount} 条`); button.innerHTML = ` 加载更多 还有 ${hiddenCount} 条 @@ -3435,15 +3441,45 @@ return `正在载入 ${title} 的完整消息记录…`; } + function clearSessionLoadOverlayTimer() { + if (!sessionLoadOverlayTimer) return; + clearTimeout(sessionLoadOverlayTimer); + sessionLoadOverlayTimer = null; + } + + function releaseSessionLoadingOverlay({ keepActiveLoad = true, allowRetry = false } = {}) { + clearSessionLoadOverlayTimer(); + document.body.classList.remove('session-loading-active'); + sessionLoadingOverlay.hidden = true; + sessionLoadingOverlay.setAttribute('aria-hidden', 'true'); + msgInput.disabled = false; + modeSelect.disabled = false; + sendBtn.disabled = false; + abortBtn.disabled = false; + if (keepActiveLoad && activeSessionLoad) { + if (allowRetry) activeSessionLoad.overlayReleased = true; + } + } + function setSessionLoading(sessionId, options = {}) { + clearSessionLoadOverlayTimer(); const loading = !!sessionId; const blocking = options.blocking !== false; const requestId = loading ? (options.requestId || createSessionSwitchRequestId(sessionId)) : ''; - activeSessionLoad = loading ? { sessionId, blocking, snapshot: null, requestId } : null; + activeSessionLoad = loading ? { sessionId, blocking, snapshot: null, requestId, overlayReleased: false } : null; const showOverlay = !!(loading && blocking); - document.body.classList.toggle('session-loading-active', showOverlay); - sessionLoadingOverlay.hidden = !showOverlay; - sessionLoadingOverlay.setAttribute('aria-hidden', showOverlay ? 'false' : 'true'); + if (showOverlay) { + document.body.classList.add('session-loading-active'); + sessionLoadingOverlay.hidden = false; + sessionLoadingOverlay.setAttribute('aria-hidden', 'false'); + sessionLoadOverlayTimer = setTimeout(() => { + sessionLoadOverlayTimer = null; + if (!activeSessionLoad || activeSessionLoad.sessionId !== sessionId || activeSessionLoad.requestId !== requestId) return; + releaseSessionLoadingOverlay({ keepActiveLoad: true, allowRetry: true }); + }, SESSION_LOAD_OVERLAY_TIMEOUT_MS); + } else { + releaseSessionLoadingOverlay({ keepActiveLoad: loading }); + } sessionLoadingLabel.textContent = loading ? (options.label || getSessionLoadLabel(sessionId)) : '正在整理消息与上下文…'; msgInput.disabled = showOverlay; modeSelect.disabled = showOverlay; @@ -3491,7 +3527,7 @@ if (!sessionId) return; const blocking = options.blocking !== false; const force = options.force === true; - if (!force && activeSessionLoad?.sessionId === sessionId) return; + if (!force && activeSessionLoad?.sessionId === sessionId && !activeSessionLoad.overlayReleased) return; if (!force && sessionId === currentSessionId && !activeSessionLoad) return; renderEpoch++; loadedHistorySessionId = null; @@ -4012,7 +4048,14 @@ if (ws !== socket) return; let msg; try { msg = JSON.parse(e.data); } catch { return; } - handleServerMessage(msg); + try { + handleServerMessage(msg); + } catch (err) { + console.error('[cc-web] failed to handle server message', err, msg); + if (activeSessionLoad) { + releaseSessionLoadingOverlay({ keepActiveLoad: true, allowRetry: true }); + } + } }; socket.onclose = () => { @@ -4126,10 +4169,23 @@ const activeLoad = activeSessionLoad; const pendingNewSession = pendingNewSessionRequest; const messageRequestId = String(msg.requestId || ''); + const looksLikeCreatedSession = Array.isArray(msg.messages) + && msg.messages.length === 0 + && !msg.historyPending + && !msg.isRunning; + const matchesPendingNewSessionFallback = !!(pendingNewSession + && !messageRequestId + && looksLikeCreatedSession + && snapshot.sessionId + && snapshot.sessionId !== currentSessionId + && snapshot.agent === pendingNewSession.agent + && (!pendingNewSession.cwd || snapshot.cwd === pendingNewSession.cwd) + && (!pendingNewSession.mode || snapshot.mode === pendingNewSession.mode)); const matchesActiveLoad = !!(activeLoad?.sessionId === msg.sessionId && (!activeLoad.requestId || activeLoad.requestId === messageRequestId)); const matchesPendingNewSession = !!(pendingNewSession - && (!pendingNewSession.requestId || pendingNewSession.requestId === messageRequestId)); + && ((!pendingNewSession.requestId || pendingNewSession.requestId === messageRequestId) + || matchesPendingNewSessionFallback)); const canSwitchToSessionInfo = matchesActiveLoad || matchesPendingNewSession || msg.sessionId === currentSessionId @@ -4144,7 +4200,10 @@ renderSessionList(); break; } - if (matchesPendingNewSession) pendingNewSessionRequest = null; + if (matchesPendingNewSession) { + pendingNewSessionRequest = null; + if (!matchesActiveLoad) clearSessionLoading(); + } applySessionSnapshot(snapshot, { immediate: isBlockingSessionLoad(msg.sessionId), suppressUnreadToast: false, @@ -4210,8 +4269,9 @@ collectClosedCollabAgentIds([msg.message]).forEach((id) => closedCollabAgentIds.add(id)); const welcome = messagesDiv.querySelector('.welcome-msg'); if (welcome) welcome.remove(); + const shouldFollow = !(currentSessionRunning || isGenerating) || isNearBottom(); messagesDiv.appendChild(buildMsgElement(msg.message)); - scrollToBottom(); + followOutputIfNeeded(shouldFollow); setCurrentSessionRunningState(!!getSessionMeta(currentSessionId)?.isRunning); } renderSessionList(); @@ -4644,6 +4704,7 @@ if (!streamEl) return; const bubble = streamEl.querySelector('.msg-bubble'); if (!bubble) return; + const shouldFollow = isNearBottom(); if (window.pendingContentBlocks && window.pendingContentBlocks.length > 0) { bubble.innerHTML = ''; @@ -4654,7 +4715,7 @@ setRenderedMarkdown(textDiv, pendingText); } syncAssistantLastSectionButton(streamEl); - scrollToBottom(); + followOutputIfNeeded(shouldFollow); } function renderMarkdown(text) { @@ -6169,6 +6230,7 @@ if (!streamEl) return; const bubble = streamEl.querySelector('.msg-bubble'); if (!bubble) return; + const shouldFollow = isNearBottom(); let toolsDiv = bubble.querySelector('.msg-tools'); if (!toolsDiv) { toolsDiv = bubble; } @@ -6178,7 +6240,7 @@ if (toolKind(tool) === 'collab_agent_tool_call') { const el = upsertCollabAgentToolCall(toolsDiv, toolUseId, tool, done); if (el) rememberToolCallTarget(toolUseId, tool, el); - scrollToBottom(); + followOutputIfNeeded(shouldFollow); return; } @@ -6189,7 +6251,7 @@ const details = createToolCallElement(toolUseId, tool, done); existingTodo.replaceWith(details); rememberToolCallTarget(toolUseId, tool, details); - scrollToBottom(); + followOutputIfNeeded(shouldFollow); return; } } @@ -6221,7 +6283,7 @@ } toolsDiv.appendChild(details); rememberToolCallTarget(toolUseId, tool, details); - scrollToBottom(); + followOutputIfNeeded(shouldFollow); } function _refreshGroupSummary(group) { @@ -6474,6 +6536,14 @@ return true; } + function followOutputIfNeeded(shouldFollow) { + if (shouldFollow) { + scrollToBottom(); + } else { + updateScrollbar(); + } + } + // --- Custom Scrollbar --- const scrollbarEl = document.getElementById('custom-scrollbar'); const thumbEl = document.getElementById('custom-scrollbar-thumb'); @@ -6592,9 +6662,9 @@ projectGroups.forEach((group, groupIndex) => { const groupKey = getProjectCollapseKey(group); const oldSessionCollapseKey = getProjectOldSessionCollapseKey(group); - const { visibleSessions: visibleGroupSessions, hiddenOldSessions: hiddenGroupOldSessions } = isSearchingSessions - ? { visibleSessions: group.sessions, hiddenOldSessions: [] } - : splitCollapsedOldSessions(group.sessions, oldSessionCollapseKey); + const { visibleSessions: visibleGroupSessions, hiddenSessions: hiddenGroupSessions } = isSearchingSessions + ? { visibleSessions: group.sessions, hiddenSessions: [] } + : splitCollapsedSessions(group.sessions, oldSessionCollapseKey); const isCollapsed = !isSearchingSessions && collapsedProjectKeys.has(groupKey); const hasActiveSession = group.sessions.some((session) => session.id === currentSessionId); const hasUnreadSession = group.sessions.some((session) => session.hasUnread); @@ -6626,8 +6696,8 @@ for (const s of visibleGroupSessions) { groupBody.appendChild(createSessionListItem(s)); } - if (hiddenGroupOldSessions.length > 0) { - groupBody.appendChild(createOldSessionLoadMoreButton(hiddenGroupOldSessions.length, oldSessionCollapseKey, group.name)); + if (hiddenGroupSessions.length > 0) { + groupBody.appendChild(createOldSessionLoadMoreButton(hiddenGroupSessions.length, oldSessionCollapseKey, group.name)); } groupEl.appendChild(groupBody); @@ -6644,16 +6714,16 @@ }); const ungroupedCollapseKey = getUngroupedOldSessionCollapseKey(); - const { visibleSessions: visibleUngroupedSessions, hiddenOldSessions: hiddenUngroupedOldSessions } = isSearchingSessions - ? { visibleSessions: ungroupedSessions, hiddenOldSessions: [] } - : splitCollapsedOldSessions(ungroupedSessions, ungroupedCollapseKey); + const { visibleSessions: visibleUngroupedSessions, hiddenSessions: hiddenUngroupedSessions } = isSearchingSessions + ? { visibleSessions: ungroupedSessions, hiddenSessions: [] } + : splitCollapsedSessions(ungroupedSessions, ungroupedCollapseKey); for (const s of visibleUngroupedSessions) { sessionList.appendChild(createSessionListItem(s)); } - if (hiddenUngroupedOldSessions.length > 0) { - sessionList.appendChild(createOldSessionLoadMoreButton(hiddenUngroupedOldSessions.length, ungroupedCollapseKey)); + if (hiddenUngroupedSessions.length > 0) { + sessionList.appendChild(createOldSessionLoadMoreButton(hiddenUngroupedSessions.length, ungroupedCollapseKey)); } }