feat: improve codex app controls and recovery

This commit is contained in:
shiyue
2026-06-15 13:22:36 +08:00
parent 3a4006b7d3
commit ed3238fa49
8 changed files with 448 additions and 29 deletions

View File

@@ -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 = `
<span class="session-list-load-more-title">加载更多</span>
<span class="session-list-load-more-meta">${hiddenCount} 条 7 天前会话</span>
`;
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 @@
<span class="session-item-time">${timeAgo(session.updated)}</span>
<div class="session-item-actions">
<button class="session-item-btn pin${isPinned ? ' active' : ''}" title="${isPinned ? '取消置顶' : '置顶'}" aria-label="${isPinned ? '取消置顶' : '置顶'}">${isPinned ? '✓' : '⇧'}</button>
<button class="session-item-btn copy-id" title="复制 ID">ID</button>
<button class="session-item-btn edit" title="重命名"></button>
<button class="session-item-btn delete" title="删除">×</button>
<div class="session-item-more">
<button class="session-item-btn more" type="button" title="更多操作" aria-label="更多操作" aria-haspopup="menu"></button>
<div class="session-item-menu" role="menu" aria-label="会话操作">
<button class="session-item-menu-btn copy-id" type="button" role="menuitem">复制 ID</button>
<button class="session-item-menu-btn edit" type="button" role="menuitem">重命名</button>
<button class="session-item-menu-btn delete" type="button" role="menuitem">删除</button>
</div>
</div>
</div>
`;
item.addEventListener('click', (e) => {
const target = e.target;
if (target.classList.contains('copy-id')) {
const target = e.target instanceof Element
? e.target.closest('.session-item-btn, .session-item-menu-btn')
: null;
if (target?.classList.contains('more')) {
e.stopPropagation();
return;
}
if (target?.classList.contains('copy-id')) {
e.stopPropagation();
copyTextToClipboard(session.id, '会话 ID 已复制');
return;
}
if (target.classList.contains('pin')) {
if (target?.classList.contains('pin')) {
e.stopPropagation();
toggleSessionPinned(session);
return;
}
if (target.classList.contains('delete')) {
if (target?.classList.contains('delete')) {
e.stopPropagation();
const doDelete = () => {
if (getLastSessionForAgent(currentAgent) === session.id) {
@@ -2259,11 +2353,15 @@
}
return;
}
if (target.classList.contains('edit')) {
if (target?.classList.contains('edit')) {
e.stopPropagation();
startEditSessionTitle(item, session);
return;
}
if (e.target instanceof Element && e.target.closest('.session-item-more')) {
e.stopPropagation();
return;
}
openSession(session.id);
});
@@ -2318,6 +2416,7 @@
importSessionBtn.disabled = false;
}
}
updateReloadMcpButtonUI();
}
function setCurrentAgent(agent) {
@@ -2364,6 +2463,7 @@
chatTitle.textContent = '新会话';
updateSessionIdBadge();
updateCwdBadge();
updateReloadMcpButtonUI();
messagesDiv.innerHTML = buildWelcomeMarkup(currentAgent);
setStatsDisplay(null);
renderPendingAttachments();
@@ -3286,7 +3386,23 @@
if (role === 'system') {
const bubble = document.createElement('div');
bubble.className = 'msg-bubble';
bubble.textContent = content;
const text = document.createElement('span');
text.className = 'system-message-text';
text.textContent = content;
bubble.appendChild(text);
const closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'system-message-close';
closeBtn.title = '关闭提示';
closeBtn.setAttribute('aria-label', '关闭提示');
closeBtn.textContent = '×';
closeBtn.addEventListener('click', (event) => {
event.stopPropagation();
div.remove();
updateScrollbar();
});
bubble.appendChild(closeBtn);
div.appendChild(bubble);
return div;
}
@@ -4564,6 +4680,7 @@
}
const { pinnedSessions, regularSessions } = splitPinnedSessions(visibleSessions);
const { visibleRegularSessions, hiddenOldSessions } = splitCollapsedOldSessions(regularSessions, pinnedSessions.length);
if (pinnedSessions.length > 0) {
const pinnedGroupEl = document.createElement('section');
pinnedGroupEl.className = 'session-project-group session-pinned-group';
@@ -4582,7 +4699,7 @@
sessionList.appendChild(pinnedGroupEl);
}
const { groups: projectGroups, ungroupedSessions } = groupSessionsByProject(regularSessions);
const { groups: projectGroups, ungroupedSessions } = groupSessionsByProject(visibleRegularSessions);
for (const group of projectGroups) {
const groupEl = document.createElement('section');
groupEl.className = 'session-project-group';
@@ -4614,6 +4731,10 @@
for (const s of ungroupedSessions) {
sessionList.appendChild(createSessionListItem(s));
}
if (hiddenOldSessions.length > 0) {
sessionList.appendChild(createOldSessionLoadMoreButton(hiddenOldSessions.length));
}
}
function startEditSessionTitle(itemEl, session) {
@@ -5253,6 +5374,13 @@
});
}
if (reloadMcpBtn) {
reloadMcpBtn.addEventListener('click', (e) => {
e.stopPropagation();
reloadCurrentMcpServers();
});
}
// Split new-chat button
newChatBtn.addEventListener('click', () => showNewSessionModal());
newChatArrow.addEventListener('click', (e) => {

View File

@@ -14,7 +14,7 @@
document.documentElement.dataset.dividerTime = dividerTime;
})();
</script>
<link rel="stylesheet" href="style.css?v=20260614-divider-time-selectfix">
<link rel="stylesheet" href="style.css?v=20260615-reload-mcp">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
</head>
<body>
@@ -102,6 +102,7 @@
<button id="user-outline-btn" class="user-outline-btn" type="button" aria-expanded="false" aria-controls="user-outline-panel" title="定位用户消息">定位</button>
<div id="user-outline-panel" class="user-outline-panel" hidden></div>
</div>
<button id="reload-mcp-btn" class="reload-mcp-btn" type="button" title="重载 Codex App MCP 配置" hidden>重载 MCP</button>
<span id="cost-display" class="cost-display" hidden></span>
</div>
<div id="attachment-tray" class="attachment-tray" hidden></div>
@@ -149,6 +150,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=20260614-divider-time-selectfix"></script>
<script src="app.js?v=20260615-reload-mcp"></script>
</body>
</html>

View File

@@ -1194,32 +1194,39 @@ body.session-loading-active {
}
.session-item-actions {
display: none;
align-items: center;
gap: 2px;
margin-left: 4px;
position: relative;
flex-shrink: 0;
}
.session-item:hover .session-item-actions { display: flex; }
.session-item:hover .session-item-actions,
.session-item:focus-within .session-item-actions {
display: flex;
}
.session-item-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 2px 5px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
font-size: 13px;
border-radius: 4px;
line-height: 1;
}
.session-item-btn:hover { color: var(--text-primary); background: var(--bg-tertiary); }
.session-item-btn.copy-id {
min-width: 24px;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.02em;
}
.session-item-btn.pin {
min-width: 24px;
font-weight: 800;
}
.session-item-btn.more {
font-size: 16px;
letter-spacing: 0;
}
.session-item-btn.pin.active {
color: var(--accent);
background: rgba(192, 85, 58, 0.1);
@@ -1228,7 +1235,88 @@ body.session-loading-active {
color: var(--accent);
background: rgba(192, 85, 58, 0.12);
}
.session-item-btn.delete:hover { color: var(--danger); background: var(--accent-light); }
.session-item-more {
position: relative;
display: inline-flex;
}
.session-item-menu {
position: absolute;
right: 0;
top: calc(100% + 6px);
z-index: 20;
display: none;
min-width: 104px;
padding: 5px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-secondary);
box-shadow: 0 10px 24px rgba(61, 42, 26, 0.14);
}
.session-item-more:hover .session-item-menu,
.session-item-more:focus-within .session-item-menu {
display: grid;
gap: 2px;
}
.session-item-menu-btn {
width: 100%;
padding: 7px 9px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
font: inherit;
font-size: 12px;
line-height: 1.2;
text-align: left;
white-space: nowrap;
}
.session-item-menu-btn:hover,
.session-item-menu-btn:focus-visible {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.session-item-menu-btn.delete:hover,
.session-item-menu-btn.delete:focus-visible {
background: var(--accent-light);
color: var(--danger);
}
.session-list-load-more {
width: calc(100% - 4px);
margin: 6px 2px 10px;
padding: 9px 10px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: rgba(255, 249, 242, 0.72);
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
text-align: left;
transition: background 0.16s, border-color 0.16s, color 0.16s, transform 0.16s;
}
.session-list-load-more:hover {
background: var(--bg-tertiary);
border-color: rgba(192, 85, 58, 0.24);
color: var(--accent);
transform: translateY(-1px);
}
.session-list-load-more:focus-visible {
outline: 2px solid rgba(192, 85, 58, 0.22);
outline-offset: 2px;
}
.session-list-load-more-title {
min-width: 0;
font-size: 13px;
font-weight: 700;
}
.session-list-load-more-meta {
flex-shrink: 0;
color: var(--text-muted);
font-size: 11px;
}
/* Inline edit in sidebar */
.session-item-edit-input {
flex: 1;
@@ -1329,7 +1417,8 @@ body.session-loading-active {
.chat-agent-btn:disabled,
.chat-cwd:disabled,
.mode-select:disabled,
.user-outline-btn:disabled {
.user-outline-btn:disabled,
.reload-mcp-btn:disabled {
opacity: 0.5;
cursor: default;
}
@@ -1428,7 +1517,8 @@ body.session-loading-active {
display: none !important;
}
.cost-display:empty { display: none; }
.user-outline-btn {
.user-outline-btn,
.reload-mcp-btn {
appearance: none;
border: 1px solid rgba(91, 126, 161, 0.22);
border-radius: 999px;
@@ -1446,7 +1536,8 @@ body.session-loading-active {
display: inline-flex;
flex-shrink: 0;
}
.user-outline-btn:hover {
.user-outline-btn:hover,
.reload-mcp-btn:hover:not(:disabled) {
background: rgba(91, 126, 161, 0.16);
border-color: rgba(91, 126, 161, 0.34);
}
@@ -1987,9 +2078,37 @@ body.session-loading-active {
color: var(--text-secondary);
font-size: 13px;
padding: 10px 16px;
position: relative;
text-align: center;
white-space: pre-line;
}
.msg.system .system-message-text {
display: block;
padding-right: 26px;
}
.msg.system .system-message-close {
align-items: center;
background: transparent;
border: 0;
border-radius: 6px;
color: var(--text-muted);
cursor: pointer;
display: inline-flex;
font: inherit;
font-size: 16px;
height: 24px;
justify-content: center;
line-height: 1;
padding: 0;
position: absolute;
right: 7px;
top: 7px;
width: 24px;
}
.msg.system .system-message-close:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
/* Markdown content */
.msg-bubble p { margin: 0 0 8px 0; }
@@ -2930,7 +3049,8 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
padding: 4px 20px 4px 8px;
font-size: 11px;
}
.user-outline-btn {
.user-outline-btn,
.reload-mcp-btn {
padding: 4px 8px;
font-size: 10px;
}
@@ -2975,6 +3095,7 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
.chat-cwd,
.mode-select,
.user-outline-btn,
.reload-mcp-btn,
.chat-runtime-state {
width: 100%;
max-width: none;
@@ -4548,10 +4669,12 @@ html[data-theme='coolvibe'] .settings-back:hover {
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .session-list-empty,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .session-project-header,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .session-project-create,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .session-list-load-more,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .chat-agent-btn,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .mode-select,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .chat-cwd,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .user-outline-btn,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .reload-mcp-btn,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .user-outline-item,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .settings-back,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .settings-nav-card,