fix session switch race on message send

This commit is contained in:
shiyue
2026-06-24 09:54:11 +08:00
parent 01c7fdd27a
commit 2f02270edc
4 changed files with 176 additions and 119 deletions

View File

@@ -67,7 +67,7 @@
const SESSION_CACHE_MAX_WEIGHT = 1_500_000; const SESSION_CACHE_MAX_WEIGHT = 1_500_000;
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_COLLAPSE_VISIBLE_LIMIT = 5; const OLD_SESSION_GROUP_INITIAL_VISIBLE = 3;
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;
@@ -194,6 +194,7 @@
let codexAppApprovalModal = null; let codexAppApprovalModal = null;
let pendingNewSessionRequest = null; let pendingNewSessionRequest = null;
let pendingSessionSwitchRequest = null; let pendingSessionSwitchRequest = null;
let sessionSwitchRequestSeq = 0;
let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1'; let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1';
let pendingInitialSessionLoad = false; let pendingInitialSessionLoad = false;
let initialSessionListHandled = false; let initialSessionListHandled = false;
@@ -1630,6 +1631,44 @@
} }
} }
function mergeSessionListSnapshot(snapshot) {
if (!snapshot?.sessionId) return;
const nextMeta = {
id: snapshot.sessionId,
sessionId: snapshot.sessionId,
cwd: snapshot.cwd || '',
projectName: snapshot.cwd ? getPathLeaf(snapshot.cwd) : snapshot.projectName || '',
title: snapshot.title || '新会话',
agent: normalizeAgent(snapshot.agent),
updated: snapshot.updated || new Date().toISOString(),
pinnedAt: snapshot.pinnedAt || null,
hasUnread: !!snapshot.hasUnread,
isRunning: !!snapshot.isRunning,
waitingOnChildren: !!snapshot.waitingOnChildren,
pendingReplyCount: Number(snapshot.pendingReplyCount || 0),
readyReplyCount: Number(snapshot.readyReplyCount || 0),
waitingReplyCount: Number(snapshot.waitingReplyCount || 0),
failedReplyCount: Number(snapshot.failedReplyCount || 0),
oversized: !!snapshot.oversized,
fileBytes: Number(snapshot.fileBytes || 0),
};
let found = false;
sessions = sessions.map((session) => {
if (session.id !== snapshot.sessionId) return session;
found = true;
return {
...session,
...nextMeta,
cwd: nextMeta.cwd || session.cwd || '',
projectName: nextMeta.cwd ? getPathLeaf(nextMeta.cwd) : session.projectName || nextMeta.projectName || '',
title: nextMeta.title || session.title,
};
});
if (!found) {
sessions = [nextMeta, ...sessions].sort(compareSessionUpdatedDesc);
}
}
function getSessionCacheDisposition(sessionId) { function getSessionCacheDisposition(sessionId) {
const entry = sessionCache.get(sessionId); const entry = sessionCache.get(sessionId);
const meta = getSessionMeta(sessionId); const meta = getSessionMeta(sessionId);
@@ -3009,30 +3048,36 @@
return `${normalizeAgent(currentAgent)}:ungrouped`; return `${normalizeAgent(currentAgent)}:ungrouped`;
} }
function splitCollapsedOldSessions(regularSessions, pinnedCount, getCollapseKey = () => '') { function expandOldSessionGroup(collapseKey) {
const totalCount = pinnedCount + regularSessions.length; if (!collapseKey) return;
if (totalCount <= OLD_SESSION_COLLAPSE_VISIBLE_LIMIT) { expandedOldSessionGroups.add(collapseKey);
return { visibleRegularSessions: regularSessions, hiddenOldSessions: [] }; }
}
function shouldAlwaysShowOldSession(session) {
return session.id === currentSessionId || session.isRunning || session.hasUnread || session.waitingOnChildren;
}
function splitCollapsedOldSessions(sessionItems, collapseKey) {
const isExpanded = collapseKey && expandedOldSessionGroups.has(collapseKey);
const nowMs = Date.now(); const nowMs = Date.now();
const visibleRegularLimit = Math.max(0, OLD_SESSION_COLLAPSE_VISIBLE_LIMIT - pinnedCount); const visibleSessions = [];
const visibleRegularSessions = [];
const hiddenOldSessions = []; const hiddenOldSessions = [];
regularSessions.forEach((session, index) => { sessionItems.forEach((session) => {
const collapseKey = getCollapseKey(session); const shouldHideOldSession = (
const isExpanded = collapseKey && expandedOldSessionGroups.has(collapseKey); !isExpanded
const shouldKeepVisible = session.id === currentSessionId || session.isRunning || session.hasUnread || session.waitingOnChildren; && !shouldAlwaysShowOldSession(session)
const canCollapse = !isExpanded && index >= visibleRegularLimit && isOlderThanOldSessionWindow(session, nowMs); && isOlderThanOldSessionWindow(session, nowMs)
if (canCollapse && !shouldKeepVisible) { && visibleSessions.length >= OLD_SESSION_GROUP_INITIAL_VISIBLE
);
if (shouldHideOldSession) {
hiddenOldSessions.push(session); hiddenOldSessions.push(session);
} else { } else {
visibleRegularSessions.push(session); visibleSessions.push(session);
} }
}); });
return { visibleRegularSessions, hiddenOldSessions }; return { visibleSessions, hiddenOldSessions };
} }
function createOldSessionLoadMoreButton(hiddenCount, collapseKey, projectName = '') { function createOldSessionLoadMoreButton(hiddenCount, collapseKey, projectName = '') {
@@ -3042,10 +3087,10 @@
button.setAttribute('aria-label', `加载更多${projectName ? ` ${projectName}` : ''} ${hiddenCount} 条 7 天前会话`); button.setAttribute('aria-label', `加载更多${projectName ? ` ${projectName}` : ''} ${hiddenCount} 条 7 天前会话`);
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} 7 天前会话</span> <span class="session-list-load-more-meta">还有 ${hiddenCount} 条</span>
`; `;
button.addEventListener('click', () => { button.addEventListener('click', () => {
if (collapseKey) expandedOldSessionGroups.add(collapseKey); expandOldSessionGroup(collapseKey);
renderSessionList(); renderSessionList();
}); });
return button; return button;
@@ -3393,7 +3438,8 @@
function setSessionLoading(sessionId, options = {}) { function setSessionLoading(sessionId, options = {}) {
const loading = !!sessionId; const loading = !!sessionId;
const blocking = options.blocking !== false; const blocking = options.blocking !== false;
activeSessionLoad = loading ? { sessionId, blocking, snapshot: null } : null; const requestId = loading ? (options.requestId || createSessionSwitchRequestId(sessionId)) : '';
activeSessionLoad = loading ? { sessionId, blocking, snapshot: null, requestId } : null;
const showOverlay = !!(loading && blocking); const showOverlay = !!(loading && blocking);
document.body.classList.toggle('session-loading-active', showOverlay); document.body.classList.toggle('session-loading-active', showOverlay);
sessionLoadingOverlay.hidden = !showOverlay; sessionLoadingOverlay.hidden = !showOverlay;
@@ -3413,6 +3459,11 @@
setSessionLoading(null, { blocking: false }); setSessionLoading(null, { blocking: false });
} }
function createSessionSwitchRequestId(sessionId) {
sessionSwitchRequestSeq += 1;
return `session-load-${Date.now()}-${sessionSwitchRequestSeq}-${String(sessionId || '').slice(0, 8)}`;
}
function isBlockingSessionLoad(sessionId) { function isBlockingSessionLoad(sessionId) {
return !!(activeSessionLoad && return !!(activeSessionLoad &&
activeSessionLoad.blocking && activeSessionLoad.blocking &&
@@ -3454,6 +3505,7 @@
sessionId, sessionId,
blocking: options.blocking !== false, blocking: options.blocking !== false,
label: options.label || '', label: options.label || '',
requestId: options.requestId || activeSessionLoad?.requestId || createSessionSwitchRequestId(sessionId),
}; };
if (ws && ws.readyState === 1 && wsAuthenticated) { if (ws && ws.readyState === 1 && wsAuthenticated) {
flushPendingSessionSwitch(); flushPendingSessionSwitch();
@@ -3471,9 +3523,10 @@
setSessionLoading(request.sessionId, { setSessionLoading(request.sessionId, {
blocking: request.blocking, blocking: request.blocking,
label: request.label || undefined, label: request.label || undefined,
requestId: request.requestId,
}); });
} }
ws.send(JSON.stringify({ type: 'load_session', sessionId: request.sessionId })); ws.send(JSON.stringify({ type: 'load_session', sessionId: request.sessionId, requestId: request.requestId }));
} }
function showCachedSession(sessionId) { function showCachedSession(sessionId) {
@@ -3971,6 +4024,7 @@
sessionId: activeSessionLoad.sessionId, sessionId: activeSessionLoad.sessionId,
blocking: activeSessionLoad.blocking, blocking: activeSessionLoad.blocking,
label: sessionLoadingLabel?.textContent || '', label: sessionLoadingLabel?.textContent || '',
requestId: activeSessionLoad.requestId || createSessionSwitchRequestId(activeSessionLoad.sessionId),
}; };
} }
clearSessionLoading(); clearSessionLoading();
@@ -4068,28 +4122,29 @@
break; break;
case 'session_info': case 'session_info':
if (pendingNewSessionRequest) pendingNewSessionRequest = null;
const snapshot = normalizeSessionSnapshot(msg); const snapshot = normalizeSessionSnapshot(msg);
sessions = sessions.map((session) => ( const activeLoad = activeSessionLoad;
session.id === snapshot.sessionId const pendingNewSession = pendingNewSessionRequest;
? { const messageRequestId = String(msg.requestId || '');
...session, const matchesActiveLoad = !!(activeLoad?.sessionId === msg.sessionId
cwd: snapshot.cwd || session.cwd || '', && (!activeLoad.requestId || activeLoad.requestId === messageRequestId));
projectName: snapshot.cwd ? getPathLeaf(snapshot.cwd) : session.projectName || '', const matchesPendingNewSession = !!(pendingNewSession
title: snapshot.title || session.title, && (!pendingNewSession.requestId || pendingNewSession.requestId === messageRequestId));
pinnedAt: snapshot.pinnedAt || session.pinnedAt || null, const canSwitchToSessionInfo = matchesActiveLoad
isRunning: snapshot.isRunning, || matchesPendingNewSession
waitingOnChildren: snapshot.waitingOnChildren, || msg.sessionId === currentSessionId
pendingReplyCount: snapshot.pendingReplyCount, || (!currentSessionId && !activeLoad && !pendingNewSession)
readyReplyCount: snapshot.readyReplyCount, || (!messageRequestId && !activeLoad && !pendingNewSession);
waitingReplyCount: snapshot.waitingReplyCount, mergeSessionListSnapshot(snapshot);
failedReplyCount: snapshot.failedReplyCount, if (matchesActiveLoad) {
}
: session
));
if (activeSessionLoad?.sessionId === msg.sessionId) {
activeSessionLoad.snapshot = snapshot; activeSessionLoad.snapshot = snapshot;
} }
if (!canSwitchToSessionInfo) {
if (!msg.historyPending) cacheSessionSnapshot(snapshot);
renderSessionList();
break;
}
if (matchesPendingNewSession) pendingNewSessionRequest = null;
applySessionSnapshot(snapshot, { applySessionSnapshot(snapshot, {
immediate: isBlockingSessionLoad(msg.sessionId), immediate: isBlockingSessionLoad(msg.sessionId),
suppressUnreadToast: false, suppressUnreadToast: false,
@@ -4099,7 +4154,7 @@
setCurrentSessionRunningState(!!msg.isRunning); setCurrentSessionRunningState(!!msg.isRunning);
} }
if (!msg.historyPending) { if (!msg.historyPending) {
if (activeSessionLoad?.sessionId === msg.sessionId) { if (matchesActiveLoad) {
finalizeLoadedSession(msg.sessionId); finalizeLoadedSession(msg.sessionId);
} else { } else {
cacheSessionSnapshot(snapshot); cacheSessionSnapshot(snapshot);
@@ -4380,6 +4435,7 @@
agent: request.agent, agent: request.agent,
mode: request.mode, mode: request.mode,
createCwd: true, createCwd: true,
requestId: request.requestId,
}); });
}, },
onCancel: () => { onCancel: () => {
@@ -6515,33 +6571,6 @@
const { pinnedSessions, regularSessions } = splitPinnedSessions(visibleSessions); const { pinnedSessions, regularSessions } = splitPinnedSessions(visibleSessions);
const { groups: projectGroups, ungroupedSessions } = groupSessionsByProject(regularSessions); const { groups: projectGroups, ungroupedSessions } = groupSessionsByProject(regularSessions);
const oldSessionCollapseKeysBySessionId = new Map();
projectGroups.forEach((group) => {
const oldSessionCollapseKey = getProjectOldSessionCollapseKey(group);
group.sessions.forEach((session) => {
oldSessionCollapseKeysBySessionId.set(session.id, oldSessionCollapseKey);
});
});
ungroupedSessions.forEach((session) => {
oldSessionCollapseKeysBySessionId.set(session.id, getUngroupedOldSessionCollapseKey());
});
const { visibleRegularSessions, hiddenOldSessions } = isSearchingSessions
? { visibleRegularSessions: regularSessions, hiddenOldSessions: [] }
: splitCollapsedOldSessions(
regularSessions,
pinnedSessions.length,
(session) => oldSessionCollapseKeysBySessionId.get(session.id) || ''
);
const hiddenOldSessionCountsByKey = new Map();
hiddenOldSessions.forEach((session) => {
const oldSessionCollapseKey = oldSessionCollapseKeysBySessionId.get(session.id);
if (!oldSessionCollapseKey) return;
hiddenOldSessionCountsByKey.set(
oldSessionCollapseKey,
(hiddenOldSessionCountsByKey.get(oldSessionCollapseKey) || 0) + 1
);
});
const visibleRegularSessionIds = new Set(visibleRegularSessions.map((session) => session.id));
if (pinnedSessions.length > 0) { if (pinnedSessions.length > 0) {
const pinnedGroupEl = document.createElement('section'); const pinnedGroupEl = document.createElement('section');
pinnedGroupEl.className = 'session-project-group session-pinned-group'; pinnedGroupEl.className = 'session-project-group session-pinned-group';
@@ -6563,8 +6592,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 visibleGroupSessions = group.sessions.filter((session) => visibleRegularSessionIds.has(session.id)); const { visibleSessions: visibleGroupSessions, hiddenOldSessions: hiddenGroupOldSessions } = isSearchingSessions
const hiddenGroupOldSessionCount = hiddenOldSessionCountsByKey.get(oldSessionCollapseKey) || 0; ? { visibleSessions: group.sessions, hiddenOldSessions: [] }
: splitCollapsedOldSessions(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);
@@ -6596,8 +6626,8 @@
for (const s of visibleGroupSessions) { for (const s of visibleGroupSessions) {
groupBody.appendChild(createSessionListItem(s)); groupBody.appendChild(createSessionListItem(s));
} }
if (hiddenGroupOldSessionCount > 0) { if (hiddenGroupOldSessions.length > 0) {
groupBody.appendChild(createOldSessionLoadMoreButton(hiddenGroupOldSessionCount, oldSessionCollapseKey, group.name)); groupBody.appendChild(createOldSessionLoadMoreButton(hiddenGroupOldSessions.length, oldSessionCollapseKey, group.name));
} }
groupEl.appendChild(groupBody); groupEl.appendChild(groupBody);
@@ -6614,15 +6644,16 @@
}); });
const ungroupedCollapseKey = getUngroupedOldSessionCollapseKey(); const ungroupedCollapseKey = getUngroupedOldSessionCollapseKey();
const visibleUngroupedSessions = ungroupedSessions.filter((session) => visibleRegularSessionIds.has(session.id)); const { visibleSessions: visibleUngroupedSessions, hiddenOldSessions: hiddenUngroupedOldSessions } = isSearchingSessions
const hiddenUngroupedOldSessionCount = hiddenOldSessionCountsByKey.get(ungroupedCollapseKey) || 0; ? { visibleSessions: ungroupedSessions, hiddenOldSessions: [] }
: splitCollapsedOldSessions(ungroupedSessions, ungroupedCollapseKey);
for (const s of visibleUngroupedSessions) { for (const s of visibleUngroupedSessions) {
sessionList.appendChild(createSessionListItem(s)); sessionList.appendChild(createSessionListItem(s));
} }
if (hiddenUngroupedOldSessionCount > 0) { if (hiddenUngroupedOldSessions.length > 0) {
sessionList.appendChild(createOldSessionLoadMoreButton(hiddenUngroupedOldSessionCount, ungroupedCollapseKey)); sessionList.appendChild(createOldSessionLoadMoreButton(hiddenUngroupedOldSessions.length, ungroupedCollapseKey));
} }
} }
@@ -8583,14 +8614,16 @@
const rawCwd = options.rawCwd !== undefined ? options.rawCwd : (cwd || ''); const rawCwd = options.rawCwd !== undefined ? options.rawCwd : (cwd || '');
const agent = normalizeAgent(options.agent || currentAgent); const agent = normalizeAgent(options.agent || currentAgent);
const mode = ['default', 'plan', 'yolo'].includes(options.mode) ? options.mode : currentMode; const mode = ['default', 'plan', 'yolo'].includes(options.mode) ? options.mode : currentMode;
const requestId = createSessionSwitchRequestId('new');
pendingNewSessionRequest = { pendingNewSessionRequest = {
cwd, cwd,
rawCwd, rawCwd,
agent, agent,
mode, mode,
requestId,
}; };
if (cwd) saveRecentCwd(cwd); if (cwd) saveRecentCwd(cwd);
send({ type: 'new_session', cwd, agent, mode }); send({ type: 'new_session', cwd, agent, mode, requestId });
} }
// --- New Session Modal --- // --- New Session Modal ---

View File

@@ -200,8 +200,8 @@ html[data-theme='coolvibe'] .session-search-clear:focus-visible {
html[data-theme='coolvibe'] .session-project-header { html[data-theme='coolvibe'] .session-project-header {
background: linear-gradient(180deg, rgba(247, 251, 252, 0.94), rgba(239, 248, 250, 0.88)); background: linear-gradient(180deg, rgba(247, 251, 252, 0.94), rgba(239, 248, 250, 0.88));
color: #5f7f87; border-color: rgba(156, 199, 211, 0.9);
border-bottom: 1px solid rgba(191, 220, 228, 0.56); color: #335e69;
} }
html[data-theme='coolvibe'] .session-project-count { html[data-theme='coolvibe'] .session-project-count {
@@ -337,8 +337,8 @@ html[data-theme='coolvibe'] .theme-card.active {
html[data-theme='editorial'] .session-project-header { html[data-theme='editorial'] .session-project-header {
background: linear-gradient(180deg, rgba(239, 232, 220, 0.94), rgba(246, 241, 232, 0.84)); background: linear-gradient(180deg, rgba(239, 232, 220, 0.94), rgba(246, 241, 232, 0.84));
color: #7f6f61; border-color: rgba(139, 94, 60, 0.22);
border-bottom: 1px solid rgba(139, 94, 60, 0.12); color: #4f4035;
} }
html[data-theme='editorial'] .session-project-count { html[data-theme='editorial'] .session-project-count {
@@ -1146,15 +1146,18 @@ body.session-loading-active {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
margin: 4px 2px 5px; margin: 6px 3px 6px;
padding: 6px 8px 5px; padding: 6px 8px;
background: rgba(242, 235, 226, 0.92); border: 1px solid rgba(134, 106, 80, 0.22);
border-radius: 8px;
background: rgba(255, 249, 242, 0.88);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
color: var(--text-muted); box-shadow: 0 1px 0 rgba(255, 255, 255, 0.72), 0 1px 6px rgba(45, 31, 20, 0.04);
font-size: 11px; color: var(--text-secondary);
font-size: 12px;
font-weight: 800; font-weight: 800;
letter-spacing: 0.08em; letter-spacing: 0;
text-transform: uppercase; text-transform: none;
} }
.session-project-name { .session-project-name {
min-width: 0; min-width: 0;
@@ -1162,14 +1165,15 @@ body.session-loading-active {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
flex: 1; flex: 1;
color: var(--text-primary);
} }
.session-project-toggle { .session-project-toggle {
min-width: 0; min-width: 0;
flex: 1; flex: 1;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 5px; gap: 6px;
height: 24px; height: 26px;
padding: 0; padding: 0;
border: 0; border: 0;
background: transparent; background: transparent;
@@ -1182,9 +1186,13 @@ body.session-loading-active {
} }
.session-project-toggle:hover, .session-project-toggle:hover,
.session-project-toggle:focus-visible { .session-project-toggle:focus-visible {
color: var(--text-secondary); color: var(--accent);
outline: none; outline: none;
} }
.session-project-toggle:hover .session-project-name,
.session-project-toggle:focus-visible .session-project-name {
color: var(--accent);
}
.session-project-toggle:focus-visible .session-project-name { .session-project-toggle:focus-visible .session-project-name {
text-decoration: underline; text-decoration: underline;
text-underline-offset: 3px; text-underline-offset: 3px;
@@ -1192,8 +1200,8 @@ body.session-loading-active {
.session-project-chevron { .session-project-chevron {
width: 12px; width: 12px;
flex-shrink: 0; flex-shrink: 0;
color: var(--text-muted); color: currentColor;
font-size: 10px; font-size: 11px;
line-height: 1; line-height: 1;
text-align: center; text-align: center;
transform: translateY(-0.5px); transform: translateY(-0.5px);
@@ -1211,9 +1219,10 @@ body.session-loading-active {
min-width: 22px; min-width: 22px;
height: 18px; height: 18px;
padding: 0 7px; padding: 0 7px;
border: 1px solid rgba(134, 106, 80, 0.14);
border-radius: 999px; border-radius: 999px;
background: var(--bg-tertiary); background: rgba(45, 31, 20, 0.06);
color: var(--text-secondary); color: var(--text-primary);
font-size: 10px; font-size: 10px;
letter-spacing: 0; letter-spacing: 0;
} }
@@ -1244,6 +1253,9 @@ body.session-loading-active {
.session-pinned-header { .session-pinned-header {
color: var(--accent); color: var(--accent);
} }
.session-pinned-header .session-project-name {
color: var(--accent);
}
.session-project-sessions[hidden] { .session-project-sessions[hidden] {
display: none; display: none;
} }
@@ -1256,6 +1268,9 @@ body.session-loading-active {
.session-project-group.has-active-session.collapsed .session-project-header { .session-project-group.has-active-session.collapsed .session-project-header {
color: var(--accent); color: var(--accent);
} }
.session-project-group.has-active-session.collapsed .session-project-name {
color: var(--accent);
}
.session-project-group.has-active-session.collapsed .session-project-count { .session-project-group.has-active-session.collapsed .session-project-count {
background: var(--accent-light); background: var(--accent-light);
color: var(--accent); color: var(--accent);
@@ -1460,23 +1475,24 @@ body.session-loading-active {
color: var(--danger); color: var(--danger);
} }
.session-list-load-more { .session-list-load-more {
width: calc(100% - 4px); width: fit-content;
margin: 6px 2px 10px; max-width: calc(100% - 18px);
padding: 9px 10px; margin: 2px 8px 6px;
border: 1px solid var(--border-color); padding: 3px 7px;
border-radius: 8px; border: 1px solid rgba(134, 106, 80, 0.18);
background: rgba(255, 249, 242, 0.72); border-radius: 6px;
background: transparent;
color: var(--text-secondary); color: var(--text-secondary);
cursor: pointer; cursor: pointer;
display: flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: flex-start;
gap: 10px; gap: 6px;
text-align: left; text-align: left;
transition: background 0.16s, border-color 0.16s, color 0.16s, transform 0.16s; transition: background 0.16s, border-color 0.16s, color 0.16s, transform 0.16s;
} }
.session-list-load-more:hover { .session-list-load-more:hover {
background: var(--bg-tertiary); background: rgba(255, 249, 242, 0.58);
border-color: rgba(192, 85, 58, 0.24); border-color: rgba(192, 85, 58, 0.24);
color: var(--accent); color: var(--accent);
transform: translateY(-1px); transform: translateY(-1px);
@@ -1487,13 +1503,13 @@ body.session-loading-active {
} }
.session-list-load-more-title { .session-list-load-more-title {
min-width: 0; min-width: 0;
font-size: 13px; font-size: 12px;
font-weight: 700; font-weight: 700;
} }
.session-list-load-more-meta { .session-list-load-more-meta {
flex-shrink: 0; flex-shrink: 0;
color: var(--text-muted); color: var(--text-muted);
font-size: 11px; font-size: 10px;
} }
/* Inline edit in sidebar */ /* Inline edit in sidebar */
.session-item-edit-input { .session-item-edit-input {

View File

@@ -633,8 +633,9 @@ async function main() {
mkdirp(path.join(pickerRoot, 'beta')); mkdirp(path.join(pickerRoot, 'beta'));
fs.writeFileSync(path.join(pickerRoot, 'note.txt'), 'not a directory'); fs.writeFileSync(path.join(pickerRoot, 'note.txt'), 'not a directory');
ws.send(JSON.stringify({ type: 'new_session', agent: 'codex', mode: 'plan' })); ws.send(JSON.stringify({ type: 'new_session', agent: 'codex', mode: 'plan', requestId: 'reg-new-default' }));
const defaultCodexSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codex' && msg.title === 'New Chat'); const defaultCodexSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codex' && msg.title === 'New Chat');
assert(defaultCodexSession.requestId === 'reg-new-default', 'new_session session_info should echo requestId');
assert(defaultCodexSession.cwd === homeDir, 'Codex new_session without cwd should default to HOME'); assert(defaultCodexSession.cwd === homeDir, 'Codex new_session without cwd should default to HOME');
const missingCwd = path.join(tempRoot, 'missing-space', 'nested-project'); const missingCwd = path.join(tempRoot, 'missing-space', 'nested-project');
@@ -1081,8 +1082,9 @@ async function main() {
assert(returnedPendingDetail.status === 200 && returnedPendingDetail.body?.ok, 'Returned pending reply detail should remain queryable from source history'); assert(returnedPendingDetail.status === 200 && returnedPendingDetail.body?.ok, 'Returned pending reply detail should remain queryable from source history');
assert(returnedPendingDetail.body.status === 'returned' && returnedPendingDetail.body.returned === true, 'Returned pending reply detail should report returned status'); assert(returnedPendingDetail.body.status === 'returned' && returnedPendingDetail.body.returned === true, 'Returned pending reply detail should report returned status');
ws.send(JSON.stringify({ type: 'load_session', sessionId: busySourceSession.sessionId })); ws.send(JSON.stringify({ type: 'load_session', sessionId: busySourceSession.sessionId, requestId: 'reg-load-busy-source' }));
const loadedBusySource = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.sessionId === busySourceSession.sessionId); const loadedBusySource = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.sessionId === busySourceSession.sessionId);
assert(loadedBusySource.requestId === 'reg-load-busy-source', 'load_session session_info should echo requestId');
assert(loadedBusySource.isRunning === false, 'Busy source should be idle after background run completed'); assert(loadedBusySource.isRunning === false, 'Busy source should be idle after background run completed');
assert(loadedBusySource.waitingOnChildren === false && loadedBusySource.pendingReplyCount === 0, 'Busy source should clear waiting state after queued reply is flushed'); assert(loadedBusySource.waitingOnChildren === false && loadedBusySource.pendingReplyCount === 0, 'Busy source should clear waiting state after queued reply is flushed');

View File

@@ -5038,7 +5038,7 @@ wss.on('connection', (ws, req) => {
handleNewSession(ws, msg); handleNewSession(ws, msg);
break; break;
case 'load_session': case 'load_session':
handleLoadSession(ws, msg.sessionId); handleLoadSession(ws, msg);
break; break;
case 'load_history_page': case 'load_history_page':
handleLoadHistoryPage(ws, msg); handleLoadHistoryPage(ws, msg);
@@ -5602,7 +5602,7 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent) {
clearRuntimeSessionId(session); clearRuntimeSessionId(session);
session.updated = new Date().toISOString(); session.updated = new Date().toISOString();
saveSession(session); saveSession(session);
wsSend(ws, { wsSend(ws, attachClientRequestId({
type: 'session_info', type: 'session_info',
sessionId: session.id, sessionId: session.id,
messages: [], messages: [],
@@ -5614,7 +5614,7 @@ function handleSlashCommand(ws, text, sessionId, fallbackAgent) {
cwd: session.cwd || null, cwd: session.cwd || null,
totalCost: session.totalCost || 0, totalCost: session.totalCost || 0,
totalUsage: session.totalUsage || null, totalUsage: session.totalUsage || null,
}); }, { sessionId }));
} }
wsSend(ws, { type: 'system_message', message: '会话已清除,上下文已重置。' }); wsSend(ws, { type: 'system_message', message: '会话已清除,上下文已重置。' });
break; break;
@@ -5885,6 +5885,11 @@ function buildSessionInfoPayload(session) {
}; };
} }
function attachClientRequestId(payload, source = {}) {
const requestId = String(source?.requestId || '').trim();
return requestId ? { ...payload, requestId } : payload;
}
function handleNewSession(ws, msg) { function handleNewSession(ws, msg) {
const result = createPersistentConversationSession(msg || {}, { const result = createPersistentConversationSession(msg || {}, {
defaultAgent: normalizeAgent(msg?.agent), defaultAgent: normalizeAgent(msg?.agent),
@@ -5902,7 +5907,7 @@ function handleNewSession(ws, msg) {
const { session } = result; const { session } = result;
detachWsFromActiveRuntimes(ws); detachWsFromActiveRuntimes(ws);
wsSessionMap.set(ws, session.id); wsSessionMap.set(ws, session.id);
wsSend(ws, buildSessionInfoPayload(session)); wsSend(ws, attachClientRequestId(buildSessionInfoPayload(session), msg));
sendSessionList(ws); sendSessionList(ws);
} }
@@ -5929,7 +5934,8 @@ function handleLoadHistoryPage(ws, msg = {}) {
}); });
} }
function handleLoadSession(ws, sessionId) { function handleLoadSession(ws, msg) {
const sessionId = sanitizeId(typeof msg === 'string' ? msg : msg?.sessionId);
reconcilePendingCrossConversationReplies(); reconcilePendingCrossConversationReplies();
const session = loadSession(sessionId); const session = loadSession(sessionId);
if (!session) { if (!session) {
@@ -5961,7 +5967,7 @@ function handleLoadSession(ws, sessionId) {
saveSession(refreshedSession); saveSession(refreshedSession);
} }
wsSend(ws, { wsSend(ws, attachClientRequestId({
type: 'session_info', type: 'session_info',
sessionId: refreshedSession.id, sessionId: refreshedSession.id,
messages: recentMessages, messages: recentMessages,
@@ -5987,7 +5993,7 @@ function handleLoadSession(ws, sessionId) {
waitingReplyCount: waitState.waitingReplyCount, waitingReplyCount: waitState.waitingReplyCount,
failedReplyCount: waitState.failedReplyCount, failedReplyCount: waitState.failedReplyCount,
pendingReplies: waitState.pendingReplies, pendingReplies: waitState.pendingReplies,
}); }, msg));
if (olderChunks.length > 0) { if (olderChunks.length > 0) {
olderChunks.forEach((chunk, index) => { olderChunks.forEach((chunk, index) => {
@@ -8331,7 +8337,7 @@ function handleImportNativeSession(ws, msg) {
}; };
saveSession(session); saveSession(session);
wsSessionMap.set(ws, id); wsSessionMap.set(ws, id);
wsSend(ws, { wsSend(ws, attachClientRequestId({
type: 'session_info', type: 'session_info',
sessionId: id, sessionId: id,
messages: session.messages, messages: session.messages,
@@ -8347,7 +8353,7 @@ function handleImportNativeSession(ws, msg) {
hasUnread: false, hasUnread: false,
historyPending: false, historyPending: false,
isRunning: false, isRunning: false,
}); }, msg));
sendSessionList(ws); sendSessionList(ws);
} }
@@ -8432,7 +8438,7 @@ function handleImportCodexSession(ws, msg) {
saveSession(session); saveSession(session);
wsSessionMap.set(ws, id); wsSessionMap.set(ws, id);
wsSend(ws, { wsSend(ws, attachClientRequestId({
type: 'session_info', type: 'session_info',
sessionId: id, sessionId: id,
messages: session.messages, messages: session.messages,
@@ -8448,7 +8454,7 @@ function handleImportCodexSession(ws, msg) {
hasUnread: false, hasUnread: false,
historyPending: false, historyPending: false,
isRunning: false, isRunning: false,
}); }, msg));
sendSessionList(ws); sendSessionList(ws);
} }