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));
}
}