fix: restore running session streaming state

This commit is contained in:
shiyue
2026-06-24 10:40:54 +08:00
parent 67914ba10f
commit 54edeec802
2 changed files with 108 additions and 32 deletions

View File

@@ -0,0 +1,6 @@
status,step
DONE,定位会话切换与运行中恢复逻辑
DONE,核对本次改动是否影响该链路
DONE,查看相关运行日志和当前会话状态
DONE,修复缺失 streaming 气泡的恢复逻辑
IN_PROGRESS,验证并汇总影响范围
1 status step
2 DONE 定位会话切换与运行中恢复逻辑
3 DONE 核对本次改动是否影响该链路
4 DONE 查看相关运行日志和当前会话状态
5 DONE 修复缺失 streaming 气泡的恢复逻辑
6 IN_PROGRESS 验证并汇总影响范围

View File

@@ -68,8 +68,10 @@
const SIDEBAR_SWIPE_TRIGGER = 72; const SIDEBAR_SWIPE_TRIGGER = 72;
const SIDEBAR_SWIPE_MAX_VERTICAL_DRIFT = 42; const SIDEBAR_SWIPE_MAX_VERTICAL_DRIFT = 42;
const OLD_SESSION_GROUP_INITIAL_VISIBLE = 3; const OLD_SESSION_GROUP_INITIAL_VISIBLE = 3;
const SESSION_GROUP_COMPACT_VISIBLE_LIMIT = 8;
const OLD_SESSION_COLLAPSE_DAYS = 7; const OLD_SESSION_COLLAPSE_DAYS = 7;
const OLD_SESSION_COLLAPSE_MS = OLD_SESSION_COLLAPSE_DAYS * 24 * 60 * 60 * 1000; const OLD_SESSION_COLLAPSE_MS = OLD_SESSION_COLLAPSE_DAYS * 24 * 60 * 60 * 1000;
const SESSION_LOAD_OVERLAY_TIMEOUT_MS = 12_000;
const MODEL_OPTIONS = [ const MODEL_OPTIONS = [
{ value: 'opus', label: 'Opus', desc: '最强大1M 上下文' }, { value: 'opus', label: 'Opus', desc: '最强大1M 上下文' },
@@ -176,6 +178,7 @@
let codexConfigCache = null; let codexConfigCache = null;
let loadedHistorySessionId = null; let loadedHistorySessionId = null;
let activeSessionLoad = null; let activeSessionLoad = null;
let sessionLoadOverlayTimer = null;
let sidebarSwipe = null; let sidebarSwipe = null;
let activeComposerToken = null; let activeComposerToken = null;
let composerSuggestionTimer = null; let composerSuggestionTimer = null;
@@ -3057,34 +3060,37 @@
return session.id === currentSessionId || session.isRunning || session.hasUnread || session.waitingOnChildren; 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 isExpanded = collapseKey && expandedOldSessionGroups.has(collapseKey);
const nowMs = Date.now(); const nowMs = Date.now();
const shouldCompactByCount = sessionItems.length > SESSION_GROUP_COMPACT_VISIBLE_LIMIT;
const visibleSessions = []; const visibleSessions = [];
const hiddenOldSessions = []; const hiddenSessions = [];
sessionItems.forEach((session) => { 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 !isExpanded
&& !shouldAlwaysShowOldSession(session) && !shouldAlwaysShowOldSession(session)
&& isOlderThanOldSessionWindow(session, nowMs) && (shouldHideByAge || shouldHideByCount)
&& visibleSessions.length >= OLD_SESSION_GROUP_INITIAL_VISIBLE
); );
if (shouldHideOldSession) { if (shouldHideSession) {
hiddenOldSessions.push(session); hiddenSessions.push(session);
} else { } else {
visibleSessions.push(session); visibleSessions.push(session);
} }
}); });
return { visibleSessions, hiddenOldSessions }; return { visibleSessions, hiddenSessions };
} }
function createOldSessionLoadMoreButton(hiddenCount, collapseKey, projectName = '') { function createOldSessionLoadMoreButton(hiddenCount, collapseKey, projectName = '') {
const button = document.createElement('button'); const button = document.createElement('button');
button.type = 'button'; button.type = 'button';
button.className = 'session-list-load-more'; button.className = 'session-list-load-more';
button.setAttribute('aria-label', `加载更多${projectName ? ` ${projectName}` : ''} ${hiddenCount} 7 天前会话`); button.setAttribute('aria-label', `加载更多${projectName ? ` ${projectName}` : ''}会话,还有 ${hiddenCount}`);
button.innerHTML = ` button.innerHTML = `
<span class="session-list-load-more-title">加载更多</span> <span class="session-list-load-more-title">加载更多</span>
<span class="session-list-load-more-meta">还有 ${hiddenCount} 条</span> <span class="session-list-load-more-meta">还有 ${hiddenCount} 条</span>
@@ -3435,15 +3441,45 @@
return `正在载入 ${title} 的完整消息记录…`; 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 = {}) { function setSessionLoading(sessionId, options = {}) {
clearSessionLoadOverlayTimer();
const loading = !!sessionId; const loading = !!sessionId;
const blocking = options.blocking !== false; const blocking = options.blocking !== false;
const requestId = loading ? (options.requestId || createSessionSwitchRequestId(sessionId)) : ''; 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); const showOverlay = !!(loading && blocking);
document.body.classList.toggle('session-loading-active', showOverlay); if (showOverlay) {
sessionLoadingOverlay.hidden = !showOverlay; document.body.classList.add('session-loading-active');
sessionLoadingOverlay.setAttribute('aria-hidden', showOverlay ? 'false' : 'true'); 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)) : '正在整理消息与上下文…'; sessionLoadingLabel.textContent = loading ? (options.label || getSessionLoadLabel(sessionId)) : '正在整理消息与上下文…';
msgInput.disabled = showOverlay; msgInput.disabled = showOverlay;
modeSelect.disabled = showOverlay; modeSelect.disabled = showOverlay;
@@ -3491,7 +3527,7 @@
if (!sessionId) return; if (!sessionId) return;
const blocking = options.blocking !== false; const blocking = options.blocking !== false;
const force = options.force === true; 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; if (!force && sessionId === currentSessionId && !activeSessionLoad) return;
renderEpoch++; renderEpoch++;
loadedHistorySessionId = null; loadedHistorySessionId = null;
@@ -4012,7 +4048,14 @@
if (ws !== socket) return; if (ws !== socket) return;
let msg; let msg;
try { msg = JSON.parse(e.data); } catch { return; } try { msg = JSON.parse(e.data); } catch { return; }
try {
handleServerMessage(msg); handleServerMessage(msg);
} catch (err) {
console.error('[cc-web] failed to handle server message', err, msg);
if (activeSessionLoad) {
releaseSessionLoadingOverlay({ keepActiveLoad: true, allowRetry: true });
}
}
}; };
socket.onclose = () => { socket.onclose = () => {
@@ -4126,10 +4169,23 @@
const activeLoad = activeSessionLoad; const activeLoad = activeSessionLoad;
const pendingNewSession = pendingNewSessionRequest; const pendingNewSession = pendingNewSessionRequest;
const messageRequestId = String(msg.requestId || ''); 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 const matchesActiveLoad = !!(activeLoad?.sessionId === msg.sessionId
&& (!activeLoad.requestId || activeLoad.requestId === messageRequestId)); && (!activeLoad.requestId || activeLoad.requestId === messageRequestId));
const matchesPendingNewSession = !!(pendingNewSession const matchesPendingNewSession = !!(pendingNewSession
&& (!pendingNewSession.requestId || pendingNewSession.requestId === messageRequestId)); && ((!pendingNewSession.requestId || pendingNewSession.requestId === messageRequestId)
|| matchesPendingNewSessionFallback));
const canSwitchToSessionInfo = matchesActiveLoad const canSwitchToSessionInfo = matchesActiveLoad
|| matchesPendingNewSession || matchesPendingNewSession
|| msg.sessionId === currentSessionId || msg.sessionId === currentSessionId
@@ -4144,7 +4200,10 @@
renderSessionList(); renderSessionList();
break; break;
} }
if (matchesPendingNewSession) pendingNewSessionRequest = null; if (matchesPendingNewSession) {
pendingNewSessionRequest = null;
if (!matchesActiveLoad) clearSessionLoading();
}
applySessionSnapshot(snapshot, { applySessionSnapshot(snapshot, {
immediate: isBlockingSessionLoad(msg.sessionId), immediate: isBlockingSessionLoad(msg.sessionId),
suppressUnreadToast: false, suppressUnreadToast: false,
@@ -4210,8 +4269,9 @@
collectClosedCollabAgentIds([msg.message]).forEach((id) => closedCollabAgentIds.add(id)); collectClosedCollabAgentIds([msg.message]).forEach((id) => closedCollabAgentIds.add(id));
const welcome = messagesDiv.querySelector('.welcome-msg'); const welcome = messagesDiv.querySelector('.welcome-msg');
if (welcome) welcome.remove(); if (welcome) welcome.remove();
const shouldFollow = !(currentSessionRunning || isGenerating) || isNearBottom();
messagesDiv.appendChild(buildMsgElement(msg.message)); messagesDiv.appendChild(buildMsgElement(msg.message));
scrollToBottom(); followOutputIfNeeded(shouldFollow);
setCurrentSessionRunningState(!!getSessionMeta(currentSessionId)?.isRunning); setCurrentSessionRunningState(!!getSessionMeta(currentSessionId)?.isRunning);
} }
renderSessionList(); renderSessionList();
@@ -4644,6 +4704,7 @@
if (!streamEl) return; if (!streamEl) return;
const bubble = streamEl.querySelector('.msg-bubble'); const bubble = streamEl.querySelector('.msg-bubble');
if (!bubble) return; if (!bubble) return;
const shouldFollow = isNearBottom();
if (window.pendingContentBlocks && window.pendingContentBlocks.length > 0) { if (window.pendingContentBlocks && window.pendingContentBlocks.length > 0) {
bubble.innerHTML = ''; bubble.innerHTML = '';
@@ -4654,7 +4715,7 @@
setRenderedMarkdown(textDiv, pendingText); setRenderedMarkdown(textDiv, pendingText);
} }
syncAssistantLastSectionButton(streamEl); syncAssistantLastSectionButton(streamEl);
scrollToBottom(); followOutputIfNeeded(shouldFollow);
} }
function renderMarkdown(text) { function renderMarkdown(text) {
@@ -6169,6 +6230,7 @@
if (!streamEl) return; if (!streamEl) return;
const bubble = streamEl.querySelector('.msg-bubble'); const bubble = streamEl.querySelector('.msg-bubble');
if (!bubble) return; if (!bubble) return;
const shouldFollow = isNearBottom();
let toolsDiv = bubble.querySelector('.msg-tools'); let toolsDiv = bubble.querySelector('.msg-tools');
if (!toolsDiv) { toolsDiv = bubble; } if (!toolsDiv) { toolsDiv = bubble; }
@@ -6178,7 +6240,7 @@
if (toolKind(tool) === 'collab_agent_tool_call') { if (toolKind(tool) === 'collab_agent_tool_call') {
const el = upsertCollabAgentToolCall(toolsDiv, toolUseId, tool, done); const el = upsertCollabAgentToolCall(toolsDiv, toolUseId, tool, done);
if (el) rememberToolCallTarget(toolUseId, tool, el); if (el) rememberToolCallTarget(toolUseId, tool, el);
scrollToBottom(); followOutputIfNeeded(shouldFollow);
return; return;
} }
@@ -6189,7 +6251,7 @@
const details = createToolCallElement(toolUseId, tool, done); const details = createToolCallElement(toolUseId, tool, done);
existingTodo.replaceWith(details); existingTodo.replaceWith(details);
rememberToolCallTarget(toolUseId, tool, details); rememberToolCallTarget(toolUseId, tool, details);
scrollToBottom(); followOutputIfNeeded(shouldFollow);
return; return;
} }
} }
@@ -6221,7 +6283,7 @@
} }
toolsDiv.appendChild(details); toolsDiv.appendChild(details);
rememberToolCallTarget(toolUseId, tool, details); rememberToolCallTarget(toolUseId, tool, details);
scrollToBottom(); followOutputIfNeeded(shouldFollow);
} }
function _refreshGroupSummary(group) { function _refreshGroupSummary(group) {
@@ -6474,6 +6536,14 @@
return true; return true;
} }
function followOutputIfNeeded(shouldFollow) {
if (shouldFollow) {
scrollToBottom();
} else {
updateScrollbar();
}
}
// --- Custom Scrollbar --- // --- Custom Scrollbar ---
const scrollbarEl = document.getElementById('custom-scrollbar'); const scrollbarEl = document.getElementById('custom-scrollbar');
const thumbEl = document.getElementById('custom-scrollbar-thumb'); const thumbEl = document.getElementById('custom-scrollbar-thumb');
@@ -6592,9 +6662,9 @@
projectGroups.forEach((group, groupIndex) => { projectGroups.forEach((group, groupIndex) => {
const groupKey = getProjectCollapseKey(group); const groupKey = getProjectCollapseKey(group);
const oldSessionCollapseKey = getProjectOldSessionCollapseKey(group); const oldSessionCollapseKey = getProjectOldSessionCollapseKey(group);
const { visibleSessions: visibleGroupSessions, hiddenOldSessions: hiddenGroupOldSessions } = isSearchingSessions const { visibleSessions: visibleGroupSessions, hiddenSessions: hiddenGroupSessions } = isSearchingSessions
? { visibleSessions: group.sessions, hiddenOldSessions: [] } ? { visibleSessions: group.sessions, hiddenSessions: [] }
: splitCollapsedOldSessions(group.sessions, oldSessionCollapseKey); : splitCollapsedSessions(group.sessions, oldSessionCollapseKey);
const isCollapsed = !isSearchingSessions && collapsedProjectKeys.has(groupKey); const isCollapsed = !isSearchingSessions && collapsedProjectKeys.has(groupKey);
const hasActiveSession = group.sessions.some((session) => session.id === currentSessionId); const hasActiveSession = group.sessions.some((session) => session.id === currentSessionId);
const hasUnreadSession = group.sessions.some((session) => session.hasUnread); const hasUnreadSession = group.sessions.some((session) => session.hasUnread);
@@ -6626,8 +6696,8 @@
for (const s of visibleGroupSessions) { for (const s of visibleGroupSessions) {
groupBody.appendChild(createSessionListItem(s)); groupBody.appendChild(createSessionListItem(s));
} }
if (hiddenGroupOldSessions.length > 0) { if (hiddenGroupSessions.length > 0) {
groupBody.appendChild(createOldSessionLoadMoreButton(hiddenGroupOldSessions.length, oldSessionCollapseKey, group.name)); groupBody.appendChild(createOldSessionLoadMoreButton(hiddenGroupSessions.length, oldSessionCollapseKey, group.name));
} }
groupEl.appendChild(groupBody); groupEl.appendChild(groupBody);
@@ -6644,16 +6714,16 @@
}); });
const ungroupedCollapseKey = getUngroupedOldSessionCollapseKey(); const ungroupedCollapseKey = getUngroupedOldSessionCollapseKey();
const { visibleSessions: visibleUngroupedSessions, hiddenOldSessions: hiddenUngroupedOldSessions } = isSearchingSessions const { visibleSessions: visibleUngroupedSessions, hiddenSessions: hiddenUngroupedSessions } = isSearchingSessions
? { visibleSessions: ungroupedSessions, hiddenOldSessions: [] } ? { visibleSessions: ungroupedSessions, hiddenSessions: [] }
: splitCollapsedOldSessions(ungroupedSessions, ungroupedCollapseKey); : splitCollapsedSessions(ungroupedSessions, ungroupedCollapseKey);
for (const s of visibleUngroupedSessions) { for (const s of visibleUngroupedSessions) {
sessionList.appendChild(createSessionListItem(s)); sessionList.appendChild(createSessionListItem(s));
} }
if (hiddenUngroupedOldSessions.length > 0) { if (hiddenUngroupedSessions.length > 0) {
sessionList.appendChild(createOldSessionLoadMoreButton(hiddenUngroupedOldSessions.length, ungroupedCollapseKey)); sessionList.appendChild(createOldSessionLoadMoreButton(hiddenUngroupedSessions.length, ungroupedCollapseKey));
} }
} }