fix cross-conversation replies and mobile session switching
This commit is contained in:
129
public/app.js
129
public/app.js
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user