feat: support codex app goal command

This commit is contained in:
shiyue
2026-06-17 14:08:32 +08:00
parent 7e01f24e61
commit b4bcd170d2
8 changed files with 3129 additions and 23 deletions

View File

@@ -2,7 +2,7 @@
(function () {
'use strict';
const ASSET_VERSION = '20260616-child-agent-close-state';
const ASSET_VERSION = '20260617-codexapp-approval';
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
const RENDER_DEBOUNCE = 100;
const COMPOSER_SUGGESTION_DEBOUNCE = 120;
@@ -14,6 +14,7 @@
{ cmd: '/mode', desc: '查看/切换权限模式' },
{ cmd: '/cost', desc: '查看会话费用' },
{ cmd: '/compact', desc: '压缩上下文' },
{ cmd: '/goal', desc: '设置/查看 Codex App 持久目标' },
{ cmd: '/init', desc: '生成/更新 Agent 指南文件' },
{ cmd: '/help', desc: '显示帮助' },
];
@@ -158,6 +159,7 @@
let fileBrowserState = null;
let directoryPickerState = null;
let codexAppUserInputModal = null;
let codexAppApprovalModal = null;
let pendingNewSessionRequest = null;
let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1';
let pendingInitialSessionLoad = false;
@@ -1165,6 +1167,98 @@
}
}
function codexAppApprovalPayloadText(payload) {
if (payload === null || payload === undefined) return '';
if (typeof payload === 'string') return payload;
try {
return JSON.stringify(payload, null, 2);
} catch {
return String(payload);
}
}
function closeCodexAppApprovalModal(sendCancel = false) {
if (!codexAppApprovalModal) return;
const { overlay, escapeHandler, requestId, sessionId } = codexAppApprovalModal;
if (escapeHandler) document.removeEventListener('keydown', escapeHandler);
if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
codexAppApprovalModal = null;
if (sendCancel && requestId) {
send({
type: 'codex_app_approval_response',
action: 'cancel',
sessionId,
requestId,
});
}
}
function submitCodexAppApproval(action) {
if (!codexAppApprovalModal) return;
const { requestId, sessionId } = codexAppApprovalModal;
send({
type: 'codex_app_approval_response',
action,
sessionId,
requestId,
});
closeCodexAppApprovalModal(false);
}
function showCodexAppApprovalModal(msg) {
closeCodexAppApprovalModal(true);
const payloadText = codexAppApprovalPayloadText(msg.payload);
const overlay = document.createElement('div');
overlay.className = 'modal-overlay codex-approval-overlay';
overlay.innerHTML = `
<div class="modal-panel codex-approval-panel">
<div class="modal-header">
<span class="modal-title">${escapeHtml(msg.title || 'Codex App 请求审批')}</span>
<button class="modal-close-btn" type="button" data-codex-approval-cancel>✕</button>
</div>
<div class="modal-body codex-approval-body">
${msg.summary ? `<div class="codex-approval-summary">${escapeHtml(msg.summary)}</div>` : ''}
${msg.reason ? `<div class="codex-approval-reason">${escapeHtml(msg.reason)}</div>` : ''}
<div class="codex-approval-meta">
<span>${escapeHtml(msg.approvalType || 'request')}</span>
${msg.itemId ? `<span>${escapeHtml(msg.itemId)}</span>` : ''}
</div>
${payloadText ? `<pre class="codex-approval-payload">${escapeHtml(payloadText)}</pre>` : ''}
</div>
<div class="modal-footer codex-approval-footer">
<button class="modal-btn-secondary" type="button" data-codex-approval-cancel>取消</button>
<button class="modal-btn-secondary codex-approval-deny-btn" type="button" data-codex-approval-action="deny">拒绝</button>
<button class="modal-btn-primary" type="button" data-codex-approval-action="approve">本次批准</button>
${msg.allowSessionScope ? '<button class="modal-btn-primary" type="button" data-codex-approval-action="approve_session">本会话批准</button>' : ''}
</div>
</div>
`;
document.body.appendChild(overlay);
const escapeHandler = (e) => {
if (e.key === 'Escape') closeCodexAppApprovalModal(true);
};
document.addEventListener('keydown', escapeHandler);
codexAppApprovalModal = {
overlay,
requestId: msg.requestId || '',
sessionId: msg.sessionId || '',
escapeHandler,
};
overlay.querySelectorAll('[data-codex-approval-cancel]').forEach((button) => {
button.addEventListener('click', () => closeCodexAppApprovalModal(true));
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) closeCodexAppApprovalModal(true);
});
overlay.querySelectorAll('[data-codex-approval-action]').forEach((button) => {
button.addEventListener('click', () => submitCodexAppApproval(button.dataset.codexApprovalAction || 'cancel'));
});
overlay.querySelector('[data-codex-approval-action="approve"]')?.focus();
}
function cssEscape(value) {
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(String(value || ''));
return String(value || '').replace(/["\\]/g, '\\$&');
@@ -3135,6 +3229,13 @@
showCodexAppUserInputModal(msg);
break;
case 'codex_app_approval_request':
if (msg.sessionId && msg.sessionId !== currentSessionId) {
showToast('Codex App 需要审批', msg.sessionId);
}
showCodexAppApprovalModal(msg);
break;
case 'ccweb_mcp_child_agent_update':
applyCcwebMcpChildAgentUpdate(msg);
break;

View File

@@ -4153,6 +4153,61 @@ html[data-theme='coolvibe'] .settings-back:hover {
.codex-user-input-text:focus {
border-color: var(--accent);
}
.codex-approval-panel {
max-width: 560px;
}
.codex-approval-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.codex-approval-summary {
color: var(--text-primary);
font-size: 15px;
font-weight: 700;
line-height: 1.45;
word-break: break-word;
}
.codex-approval-reason {
color: var(--text-secondary);
font-size: 13px;
line-height: 1.5;
word-break: break-word;
}
.codex-approval-meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.codex-approval-meta span {
border: 1px solid var(--border-color);
border-radius: 999px;
background: var(--bg-primary);
color: var(--text-muted);
padding: 3px 8px;
font-size: 11px;
font-weight: 700;
}
.codex-approval-payload {
max-height: 260px;
overflow: auto;
margin: 0;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.codex-approval-footer {
flex-wrap: wrap;
}
.codex-approval-deny-btn {
color: var(--danger);
}
.modal-quick-picks {
display: flex;
flex-wrap: wrap;