fix cross-conversation replies and mobile session switching

This commit is contained in:
shiyue
2026-06-18 13:07:51 +08:00
parent a2126f4138
commit c1dc793841
7 changed files with 678 additions and 74 deletions

View File

@@ -2,7 +2,7 @@
(function () {
'use strict';
const ASSET_VERSION = '20260617-codexapp-approval';
const ASSET_VERSION = '20260618-mobile-session-switch';
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
const RENDER_DEBOUNCE = 100;
const COMPOSER_SUGGESTION_DEBOUNCE = 120;
@@ -119,6 +119,7 @@
// --- State ---
let ws = null;
let wsAuthenticated = false;
let authToken = localStorage.getItem('cc-web-token');
let currentSessionId = null;
let sessions = [];
@@ -162,6 +163,7 @@
let codexAppUserInputModal = null;
let codexAppApprovalModal = null;
let pendingNewSessionRequest = null;
let pendingSessionSwitchRequest = null;
let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1';
let pendingInitialSessionLoad = false;
let noteMode = false;
@@ -959,8 +961,10 @@
}
function normalizeSessionSnapshot(payload, options = {}) {
const sessionId = payload.sessionId || payload.id || '';
return {
sessionId: payload.sessionId,
sessionId,
id: sessionId,
messages: cloneMessages(payload.messages || []),
title: payload.title || '新会话',
mode: payload.mode || 'yolo',
@@ -969,10 +973,19 @@
pinnedAt: payload.pinnedAt || null,
hasUnread: !!payload.hasUnread,
cwd: payload.cwd || null,
projectName: payload.projectName || '',
oversized: !!payload.oversized,
fileBytes: Number(payload.fileBytes || 0),
totalCost: typeof payload.totalCost === 'number' ? payload.totalCost : 0,
totalUsage: payload.totalUsage ? deepClone(payload.totalUsage) : null,
updated: payload.updated || null,
isRunning: !!payload.isRunning,
waitingOnChildren: !!payload.waitingOnChildren,
pendingReplyCount: Number(payload.pendingReplyCount || 0),
readyReplyCount: Number(payload.readyReplyCount || 0),
waitingReplyCount: Number(payload.waitingReplyCount || 0),
failedReplyCount: Number(payload.failedReplyCount || 0),
pendingReplies: Array.isArray(payload.pendingReplies) ? deepClone(payload.pendingReplies) : [],
historyPending: !!payload.historyPending,
complete: options.complete !== undefined ? !!options.complete : !payload.historyPending,
};
@@ -1053,7 +1066,7 @@
const entry = sessionCache.get(sessionId);
const meta = getSessionMeta(sessionId);
if (!entry?.snapshot?.complete || !meta) return 'miss';
if (entry.version === (meta.updated || null) && !meta.hasUnread && !meta.isRunning) {
if (entry.version === (meta.updated || null) && !meta.hasUnread && !meta.isRunning && !meta.waitingOnChildren) {
return 'strong';
}
return 'weak';
@@ -1071,6 +1084,11 @@
snapshot.updated = meta.updated || snapshot.updated;
snapshot.pinnedAt = meta.pinnedAt || null;
snapshot.isRunning = !!meta.isRunning;
snapshot.waitingOnChildren = !!meta.waitingOnChildren;
snapshot.pendingReplyCount = Number(meta.pendingReplyCount || 0);
snapshot.readyReplyCount = Number(meta.readyReplyCount || 0);
snapshot.waitingReplyCount = Number(meta.waitingReplyCount || 0);
snapshot.failedReplyCount = Number(meta.failedReplyCount || 0);
}
return snapshot;
}
@@ -2431,7 +2449,7 @@
const hiddenOldSessions = [];
regularSessions.forEach((session, index) => {
const shouldKeepVisible = session.id === currentSessionId || session.isRunning || session.hasUnread;
const shouldKeepVisible = session.id === currentSessionId || session.isRunning || session.hasUnread || session.waitingOnChildren;
const canCollapse = index >= visibleRegularLimit && isOlderThanOldSessionWindow(session, nowMs);
if (canCollapse && !shouldKeepVisible) {
hiddenOldSessions.push(session);
@@ -2492,7 +2510,10 @@
function createSessionListItem(session) {
const item = document.createElement('div');
const isPinned = !!session.pinnedAt;
item.className = `session-item${session.id === currentSessionId ? ' active' : ''}${isPinned ? ' pinned' : ''}`;
const waitingOnChildren = !!session.waitingOnChildren;
const readyReplyCount = Number(session.readyReplyCount || 0);
const waitingLabel = readyReplyCount > 0 ? `子对话已返回 ${readyReplyCount}` : `等待子对话 ${Number(session.pendingReplyCount || 0) || ''}`.trim();
item.className = `session-item${session.id === currentSessionId ? ' active' : ''}${isPinned ? ' pinned' : ''}${waitingOnChildren ? ' waiting-children' : ''}`;
item.dataset.id = session.id;
const sessionCwd = getSessionEffectiveCwd(session);
if (sessionCwd) item.title = sessionCwd;
@@ -2501,6 +2522,7 @@
<span class="session-item-title">${escapeHtml(session.title || 'Untitled')}</span>
${isPinned ? '<span class="session-item-pin-badge" title="已置顶">顶</span>' : ''}
${session.isRunning ? '<span class="session-item-status">运行中</span>' : ''}
${!session.isRunning && waitingOnChildren ? `<span class="session-item-status waiting">${escapeHtml(waitingLabel)}</span>` : ''}
</div>
${session.hasUnread ? '<span class="session-unread-dot"></span>' : ''}
<span class="session-item-time">${timeAgo(session.updated)}</span>
@@ -2572,6 +2594,7 @@
return;
}
closeSessionActionMenus();
if (isMobileInputMode()) closeSidebar();
openSession(session.id);
});
@@ -2595,12 +2618,36 @@
chatCwd.hidden = !currentCwd;
}
function currentSessionWaitState() {
const meta = currentSessionId ? getSessionMeta(currentSessionId) : null;
const cached = currentSessionId ? sessionCache.get(currentSessionId)?.snapshot : null;
return {
waitingOnChildren: !!(meta?.waitingOnChildren || cached?.waitingOnChildren),
pendingReplyCount: Number(meta?.pendingReplyCount ?? cached?.pendingReplyCount ?? 0),
readyReplyCount: Number(meta?.readyReplyCount ?? cached?.readyReplyCount ?? 0),
};
}
function setCurrentSessionRunningState(isRunning) {
const running = !!isRunning;
currentSessionRunning = running;
if (chatRuntimeState) {
chatRuntimeState.hidden = !running;
chatRuntimeState.textContent = running ? '运行中' : '';
const waitState = currentSessionWaitState();
if (running) {
chatRuntimeState.hidden = false;
chatRuntimeState.classList.remove('waiting');
chatRuntimeState.textContent = '运行中';
} else if (waitState.waitingOnChildren) {
chatRuntimeState.hidden = false;
chatRuntimeState.classList.add('waiting');
chatRuntimeState.textContent = waitState.readyReplyCount > 0
? `子对话已返回 ${waitState.readyReplyCount}`
: `等待子对话 ${waitState.pendingReplyCount || ''}`.trim();
} else {
chatRuntimeState.hidden = true;
chatRuntimeState.classList.remove('waiting');
chatRuntimeState.textContent = '';
}
}
updateCwdBadge();
}
@@ -2817,7 +2864,35 @@
renderEpoch++;
loadedHistorySessionId = null;
setSessionLoading(sessionId, { blocking, label: options.label });
send({ type: 'load_session', sessionId });
requestSessionLoad(sessionId, { blocking, label: options.label });
}
function requestSessionLoad(sessionId, options = {}) {
if (!sessionId) return;
pendingSessionSwitchRequest = {
sessionId,
blocking: options.blocking !== false,
label: options.label || '',
};
if (ws && ws.readyState === 1 && wsAuthenticated) {
flushPendingSessionSwitch();
return;
}
if (!ws || ws.readyState > 1) connect();
}
function flushPendingSessionSwitch() {
if (!pendingSessionSwitchRequest) return;
if (!ws || ws.readyState !== 1 || !wsAuthenticated) return;
const request = pendingSessionSwitchRequest;
pendingSessionSwitchRequest = null;
if (!activeSessionLoad) {
setSessionLoading(request.sessionId, {
blocking: request.blocking,
label: request.label || undefined,
});
}
ws.send(JSON.stringify({ type: 'load_session', sessionId: request.sessionId }));
}
function showCachedSession(sessionId) {
@@ -3000,6 +3075,7 @@
const socket = new WebSocket(WS_URL);
ws = socket;
wsAuthenticated = false;
socket.onopen = () => {
if (ws !== socket) return;
@@ -3018,6 +3094,14 @@
socket.onclose = () => {
if (ws !== socket) return;
ws = null;
wsAuthenticated = false;
if (activeSessionLoad?.sessionId && !isPageUnloading) {
pendingSessionSwitchRequest = {
sessionId: activeSessionLoad.sessionId,
blocking: activeSessionLoad.blocking,
label: sessionLoadingLabel?.textContent || '',
};
}
clearSessionLoading();
scheduleReconnect();
};
@@ -3058,10 +3142,12 @@
case 'auth_result':
if (msg.success) {
authToken = msg.token;
wsAuthenticated = true;
localStorage.setItem('cc-web-token', msg.token);
document.dispatchEvent(new CustomEvent('cc-web-auth-restored'));
loginOverlay.hidden = true;
app.hidden = false;
flushPendingSessionSwitch();
send({ type: 'get_codex_config' });
// Check if must change password
if (msg.mustChangePassword) {
@@ -3070,7 +3156,10 @@
pendingInitialSessionLoad = true;
}
} else {
pendingSessionSwitchRequest = null;
clearSessionLoading();
authToken = null;
wsAuthenticated = false;
localStorage.removeItem('cc-web-token');
document.dispatchEvent(new CustomEvent('cc-web-auth-failed'));
loginOverlay.hidden = false;
@@ -3088,7 +3177,7 @@
break;
case 'session_list':
sessions = msg.sessions || [];
sessions = Array.isArray(msg.sessions) ? msg.sessions.map(normalizeSessionSnapshot) : [];
reconcileSessionCacheWithSessions();
renderSessionList();
if (currentSessionId) {
@@ -3113,6 +3202,12 @@
projectName: snapshot.cwd ? getPathLeaf(snapshot.cwd) : session.projectName || '',
title: snapshot.title || session.title,
pinnedAt: snapshot.pinnedAt || session.pinnedAt || null,
isRunning: snapshot.isRunning,
waitingOnChildren: snapshot.waitingOnChildren,
pendingReplyCount: snapshot.pendingReplyCount,
readyReplyCount: snapshot.readyReplyCount,
waitingReplyCount: snapshot.waitingReplyCount,
failedReplyCount: snapshot.failedReplyCount,
}
: session
));
@@ -3124,6 +3219,9 @@
suppressUnreadToast: false,
preserveStreaming: msg.sessionId === currentSessionId && msg.isRunning,
});
if (msg.sessionId === currentSessionId) {
setCurrentSessionRunningState(!!msg.isRunning);
}
if (!msg.historyPending) {
if (activeSessionLoad?.sessionId === msg.sessionId) {
finalizeLoadedSession(msg.sessionId);
@@ -3156,6 +3254,11 @@
snapshot.messages = Array.isArray(snapshot.messages) ? snapshot.messages : [];
snapshot.messages.push(deepClone(msg.message));
snapshot.updated = msg.message.timestamp || new Date().toISOString();
if (msg.message.crossConversation?.replyToRequestId) {
snapshot.readyReplyCount = Math.max(0, Number(snapshot.readyReplyCount || 0) - 1);
snapshot.pendingReplyCount = Math.max(0, Number(snapshot.pendingReplyCount || 0) - 1);
snapshot.waitingOnChildren = Number(snapshot.pendingReplyCount || 0) > 0;
}
});
}
if (msg.sessionId === currentSessionId && msg.message) {
@@ -3164,7 +3267,9 @@
if (welcome) welcome.remove();
messagesDiv.appendChild(buildMsgElement(msg.message));
scrollToBottom();
setCurrentSessionRunningState(!!getSessionMeta(currentSessionId)?.isRunning);
}
renderSessionList();
break;
case 'session_renamed':
@@ -5479,9 +5584,10 @@
const hasActiveSession = group.sessions.some((session) => session.id === currentSessionId);
const hasUnreadSession = group.sessions.some((session) => session.hasUnread);
const hasRunningSession = group.sessions.some((session) => session.isRunning);
const hasWaitingSession = group.sessions.some((session) => session.waitingOnChildren);
const groupBodyId = `session-project-body-${groupIndex}`;
const groupEl = document.createElement('section');
groupEl.className = `session-project-group${isCollapsed ? ' collapsed' : ''}${hasActiveSession ? ' has-active-session' : ''}${hasUnreadSession ? ' has-unread-session' : ''}${hasRunningSession ? ' has-running-session' : ''}`;
groupEl.className = `session-project-group${isCollapsed ? ' collapsed' : ''}${hasActiveSession ? ' has-active-session' : ''}${hasUnreadSession ? ' has-unread-session' : ''}${hasRunningSession ? ' has-running-session' : ''}${hasWaitingSession ? ' has-waiting-session' : ''}`;
const header = document.createElement('div');
header.className = 'session-project-header';
@@ -6278,9 +6384,6 @@
if (currentSessionId) {
send({ type: 'set_mode', sessionId: currentSessionId, mode: currentMode });
}
if (currentMode === 'default') {
appendSystemMessage('⚠ 由于项目设计与 CLI 原生逻辑不同,默认模式的授权申请功能暂未实现,建议搭配 Plan 或 YOLO 模式使用。');
}
});
msgInput.addEventListener('input', () => {

View File

@@ -154,6 +154,6 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="app.js?v=20260617-skill-openai-yaml"></script>
<script src="app.js?v=20260618-mobile-session-switch"></script>
</body>
</html>

View File

@@ -1260,6 +1260,17 @@ body.session-loading-active {
.session-project-group.has-running-session.collapsed .session-project-count::after {
animation: pulse 1.1s infinite;
}
.session-project-group.has-waiting-session:not(.has-running-session).collapsed .session-project-count::after {
content: '';
width: 6px;
height: 6px;
margin-left: 5px;
border-radius: 50%;
background: rgba(120, 126, 140, 0.72);
}
.session-project-group.has-waiting-session:not(.has-running-session).collapsed .session-project-count::after {
animation: none;
}
.session-item {
display: flex;
align-items: center;
@@ -1331,6 +1342,14 @@ body.session-loading-active {
background: currentColor;
animation: pulse 1.1s infinite;
}
.session-item-status.waiting {
background: rgba(120, 126, 140, 0.12);
border-color: rgba(120, 126, 140, 0.22);
color: #667085;
}
.session-item-status.waiting::before {
animation: none;
}
.session-item-time {
font-size: 11px;
color: var(--text-muted);
@@ -1660,6 +1679,14 @@ body.session-loading-active {
background: currentColor;
animation: pulse 1.1s infinite;
}
.chat-runtime-state.waiting {
background: rgba(120, 126, 140, 0.12);
border-color: rgba(120, 126, 140, 0.22);
color: #667085;
}
.chat-runtime-state.waiting::before {
animation: none;
}
.cost-display {
display: none !important;
}