diff --git a/lib/codex-app-runtime.js b/lib/codex-app-runtime.js index 279fb8b..3f8bf72 100644 --- a/lib/codex-app-runtime.js +++ b/lib/codex-app-runtime.js @@ -1,5 +1,10 @@ 'use strict'; +const CODEX_APP_ONCE_NOTICE_PATTERNS = [ + /^Under-development features enabled:/i, + /^Heads up: Long threads and multiple compactions/i, +]; + function createCodexAppRuntime(deps = {}) { const { wsSend, @@ -14,6 +19,23 @@ function createCodexAppRuntime(deps = {}) { return text.length > maxLen ? `${text.slice(0, maxLen)}...` : value; } + const shownOnceNoticeKeys = new Set(); + + function normalizeNoticeMessage(message) { + return String(message || '').trim().replace(/\s+/g, ' '); + } + + function shouldShowRuntimeNotice(method, message) { + const normalized = normalizeNoticeMessage(message); + const isOnceNotice = CODEX_APP_ONCE_NOTICE_PATTERNS.some((pattern) => pattern.test(normalized)); + if (!isOnceNotice) return true; + + const key = `${method}:${normalized}`; + if (shownOnceNoticeKeys.has(key)) return false; + shownOnceNoticeKeys.add(key); + return true; + } + function sendRuntime(entry, sessionId, payload) { wsSend(entry.ws, { ...payload, sessionId }); } @@ -483,7 +505,9 @@ function createCodexAppRuntime(deps = {}) { const message = params.message || params.title || ''; if (message) { if (method === 'error') entry.lastError = message; - sendRuntime(entry, sessionId, { type: 'system_message', message }); + if (method === 'error' || shouldShowRuntimeNotice(method, message)) { + sendRuntime(entry, sessionId, { type: 'system_message', message }); + } } return { done: false }; } diff --git a/lib/codex-app-server-client.js b/lib/codex-app-server-client.js index 16bb2d6..1ec67ec 100644 --- a/lib/codex-app-server-client.js +++ b/lib/codex-app-server-client.js @@ -133,6 +133,10 @@ function createCodexAppServerClient(options = {}) { sendRaw({ method, params }); } + function reloadMcpServers() { + return request('config/mcpServer/reload', {}, 30000); + } + function start() { if (initPromise) return initPromise; exited = false; @@ -212,6 +216,7 @@ function createCodexAppServerClient(options = {}) { stop, request, notification, + reloadMcpServers, isRunning, pid: () => proc?.pid || null, }; diff --git a/public/app.js b/public/app.js index a3b7d0e..33f036e 100644 --- a/public/app.js +++ b/public/app.js @@ -2,7 +2,7 @@ (function () { 'use strict'; - const ASSET_VERSION = '20260614-divider-time-selectfix'; + const ASSET_VERSION = '20260615-reload-mcp'; const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`; const RENDER_DEBOUNCE = 100; const COMPOSER_SUGGESTION_DEBOUNCE = 120; @@ -35,6 +35,9 @@ const SESSION_CACHE_MAX_WEIGHT = 1_500_000; const SIDEBAR_SWIPE_TRIGGER = 72; const SIDEBAR_SWIPE_MAX_VERTICAL_DRIFT = 42; + const OLD_SESSION_COLLAPSE_VISIBLE_LIMIT = 5; + const OLD_SESSION_COLLAPSE_DAYS = 7; + const OLD_SESSION_COLLAPSE_MS = OLD_SESSION_COLLAPSE_DAYS * 24 * 60 * 60 * 1000; const MODEL_OPTIONS = [ { value: 'opus', label: 'Opus', desc: '最强大,1M 上下文' }, @@ -159,8 +162,10 @@ let pendingInitialSessionLoad = false; let noteMode = false; let noteDraftSeq = 0; + let isReloadingMcp = false; const pendingNotesByTarget = new Map(); const userMessageIndex = new Map(); + const expandedOldSessionAgents = new Set(); document.documentElement.dataset.dividerTime = showAgentDividerTime ? 'show' : 'hide'; // --- DOM --- @@ -191,6 +196,7 @@ const chatCwd = $('#chat-cwd'); const userOutlineBtn = $('#user-outline-btn'); const userOutlinePanel = $('#user-outline-panel'); + const reloadMcpBtn = $('#reload-mcp-btn'); const costDisplay = $('#cost-display'); const attachmentTray = $('#attachment-tray'); const pendingNotesTray = $('#pending-notes-tray'); @@ -1087,6 +1093,32 @@ return data || {}; } + function updateReloadMcpButtonUI() { + if (!reloadMcpBtn) return; + const visible = !!currentSessionId && isCodexAppAgent(currentAgent); + reloadMcpBtn.hidden = !visible; + reloadMcpBtn.disabled = !visible || isReloadingMcp; + reloadMcpBtn.textContent = isReloadingMcp ? '重载中' : '重载 MCP'; + reloadMcpBtn.setAttribute('aria-busy', isReloadingMcp ? 'true' : 'false'); + } + + async function reloadCurrentMcpServers() { + if (!currentSessionId || !isCodexAppAgent(currentAgent) || isReloadingMcp) return; + isReloadingMcp = true; + updateReloadMcpButtonUI(); + try { + await fetchAuthJson(`/api/sessions/${encodeURIComponent(currentSessionId)}/reload-mcp`, { + method: 'POST', + }); + showToast('已请求重载 MCP'); + } catch (err) { + showToast(err?.message || '重载 MCP 失败'); + } finally { + isReloadingMcp = false; + updateReloadMcpButtonUI(); + } + } + function closeCodexAppUserInputModal(sendCancel = false) { if (!codexAppUserInputModal) return; const { overlay, escapeHandler, requestId, sessionId } = codexAppUserInputModal; @@ -1096,6 +1128,7 @@ if (sendCancel && requestId) { send({ type: 'codex_app_user_input_response', + action: 'cancel', sessionId, requestId, answers: {}, @@ -1219,6 +1252,7 @@ overlay.querySelector('[data-codex-ui-submit]')?.addEventListener('click', () => { send({ type: 'codex_app_user_input_response', + action: 'submit', sessionId: msg.sessionId, requestId: msg.requestId, answers: collectCodexAppUserInputAnswers(panel, questions), @@ -2186,6 +2220,55 @@ return { pinnedSessions, regularSessions }; } + function isOlderThanOldSessionWindow(session, nowMs = Date.now()) { + const updatedMs = new Date(session?.updated || 0).getTime(); + return Number.isFinite(updatedMs) && updatedMs > 0 && nowMs - updatedMs > OLD_SESSION_COLLAPSE_MS; + } + + function splitCollapsedOldSessions(regularSessions, pinnedCount) { + if (expandedOldSessionAgents.has(currentAgent)) { + return { visibleRegularSessions: regularSessions, hiddenOldSessions: [] }; + } + + const totalCount = pinnedCount + regularSessions.length; + if (totalCount <= OLD_SESSION_COLLAPSE_VISIBLE_LIMIT) { + return { visibleRegularSessions: regularSessions, hiddenOldSessions: [] }; + } + + const nowMs = Date.now(); + const visibleRegularLimit = Math.max(0, OLD_SESSION_COLLAPSE_VISIBLE_LIMIT - pinnedCount); + const visibleRegularSessions = []; + const hiddenOldSessions = []; + + regularSessions.forEach((session, index) => { + const shouldKeepVisible = session.id === currentSessionId || session.isRunning || session.hasUnread; + const canCollapse = index >= visibleRegularLimit && isOlderThanOldSessionWindow(session, nowMs); + if (canCollapse && !shouldKeepVisible) { + hiddenOldSessions.push(session); + } else { + visibleRegularSessions.push(session); + } + }); + + return { visibleRegularSessions, hiddenOldSessions }; + } + + function createOldSessionLoadMoreButton(hiddenCount) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'session-list-load-more'; + button.setAttribute('aria-label', `加载更多 ${hiddenCount} 条 7 天前会话`); + button.innerHTML = ` + 加载更多 + + `; + button.addEventListener('click', () => { + expandedOldSessionAgents.add(currentAgent); + renderSessionList(); + }); + return button; + } + function applySessionPinnedState(sessionId, pinnedAt) { sessions = sessions.map((session) => ( session.id === sessionId ? { ...session, pinnedAt: pinnedAt || null } : session @@ -2221,25 +2304,36 @@ ${timeAgo(session.updated)}