diff --git a/lib/ccweb-mcp-server.js b/lib/ccweb-mcp-server.js
index 654b183..f0ebad6 100644
--- a/lib/ccweb-mcp-server.js
+++ b/lib/ccweb-mcp-server.js
@@ -54,7 +54,7 @@ const TOOLS = [
mode: {
type: 'string',
enum: ['default', 'plan', 'yolo'],
- description: '可选。权限模式,默认继承来源对话;不会自动写死为 plan。',
+ description: '可选。权限模式,默认 yolo;只有显式传 default/plan/yolo 时才使用指定模式。',
},
initialMessage: {
type: 'string',
@@ -87,6 +87,36 @@ const TOOLS = [
additionalProperties: false,
},
},
+ {
+ name: 'ccweb_list_pending_replies',
+ description: '列出当前来源对话等待中的跨对话回复,包括已完成但尚未处理的子对话返回摘要。',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ status: {
+ type: 'string',
+ enum: ['all', 'waiting', 'ready', 'delivering', 'returned', 'failed'],
+ description: '可选。按回复状态过滤,默认 all。',
+ },
+ },
+ additionalProperties: false,
+ },
+ },
+ {
+ name: 'ccweb_get_pending_reply',
+ description: '读取指定 requestId 的跨对话回复状态和正文;用于主线程判断是否继续追问指定子对话。',
+ inputSchema: {
+ type: 'object',
+ properties: {
+ requestId: {
+ type: 'string',
+ description: '等待回复 requestId。',
+ },
+ },
+ required: ['requestId'],
+ additionalProperties: false,
+ },
+ },
{
name: 'ccweb_request_reply',
description: '向指定 ccweb 对话发送一条消息,并在目标对话本轮输出完成后把回复作为已处理的只读消息写回当前对话;写回不会再次触发当前对话运行。',
diff --git a/public/app.js b/public/app.js
index 7630aaa..65b36b0 100644
--- a/public/app.js
+++ b/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 @@
${escapeHtml(session.title || 'Untitled')}
${isPinned ? '顶' : ''}
${session.isRunning ? '运行中' : ''}
+ ${!session.isRunning && waitingOnChildren ? `${escapeHtml(waitingLabel)}` : ''}
${session.hasUnread ? '' : ''}
${timeAgo(session.updated)}
@@ -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', () => {
diff --git a/public/index.html b/public/index.html
index d531116..0ae6a72 100644
--- a/public/index.html
+++ b/public/index.html
@@ -154,6 +154,6 @@
-
+