fix: restore running session streaming state
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
status,step
|
||||
DONE,定位会话切换与运行中恢复逻辑
|
||||
DONE,核对本次改动是否影响该链路
|
||||
DONE,查看相关运行日志和当前会话状态
|
||||
DONE,修复缺失 streaming 气泡的恢复逻辑
|
||||
IN_PROGRESS,验证并汇总影响范围
|
||||
|
134
public/app.js
134
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 = `
|
||||
<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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user