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

@@ -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 = `
<span class="session-list-load-more-title">加载更多</span>
<span class="session-list-load-more-meta">还有 ${hiddenCount} 条</span>
@@ -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));
}
}