fix session switch race on message send
This commit is contained in:
185
public/app.js
185
public/app.js
@@ -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 ---
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
28
server.js
28
server.js
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user