Update ccweb codex app integration

This commit is contained in:
shiyue
2026-06-16 14:36:06 +08:00
parent 2e119fd7e3
commit 51838a2ce1
7 changed files with 1254 additions and 164 deletions

View File

@@ -2,7 +2,7 @@
(function () {
'use strict';
const ASSET_VERSION = '20260615-codexapp-steer-status-session-menu';
const ASSET_VERSION = '20260616-composer-mcp-list';
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
const RENDER_DEBOUNCE = 100;
const COMPOSER_SUGGESTION_DEBOUNCE = 120;
@@ -274,6 +274,21 @@
pendingNotesByTarget.delete(draftKey);
}
function updateGenerationControls() {
const noteActive = !!noteMode;
const allowRuntimeInsert = isGenerating && isCodexAppAgent(currentAgent) && !noteActive;
const sendLabel = noteActive ? '记录笔记' : (allowRuntimeInsert ? '插入' : '发送');
if (sendBtn) {
sendBtn.classList.toggle('note-send', noteActive);
sendBtn.title = sendLabel;
sendBtn.setAttribute('aria-label', sendLabel);
sendBtn.hidden = isGenerating ? !(noteActive || allowRuntimeInsert) : false;
}
if (abortBtn) {
abortBtn.hidden = !isGenerating;
}
}
function updateNoteModeUI() {
const active = !!noteMode;
if (noteModeBtn) {
@@ -283,16 +298,11 @@
noteModeBtn.setAttribute('aria-label', active ? '关闭笔记模式' : '笔记模式');
}
if (inputWrapper) inputWrapper.classList.toggle('note-mode-active', active);
if (sendBtn) {
const allowRuntimeInsert = isGenerating && isCodexAppAgent(currentAgent) && !active;
sendBtn.classList.toggle('note-send', active);
sendBtn.title = active ? '记录笔记' : (allowRuntimeInsert ? '插入' : '发送');
sendBtn.hidden = isGenerating ? (!active && !allowRuntimeInsert) : false;
}
if (msgInput) {
msgInput.placeholder = active ? '记录笔记,稍后从气泡发送…' : defaultMsgInputPlaceholder;
}
if (active) hideCmdMenu();
updateGenerationControls();
}
function createNoteActionButton(action, label, title = label) {
@@ -800,11 +810,19 @@
}
function buildUserOutlineItems() {
return Array.from(userMessageIndex.values()).map((entry) => ({
id: entry.id,
targetMessageId: entry.element?.id || '',
label: shortMessagePreview(entry.content, 64),
})).filter((entry) => entry.targetMessageId);
const seen = new Set();
return Array.from(messagesDiv.querySelectorAll('.msg.user[data-message-id]')).map((element) => {
const id = String(element.dataset.messageId || '').trim();
if (!id || seen.has(id)) return null;
seen.add(id);
const indexed = userMessageIndex.get(id);
const content = indexed?.content || element.querySelector('.msg-text')?.textContent || '';
return {
id,
targetMessageId: element.id || '',
label: shortMessagePreview(content, 64),
};
}).filter((entry) => entry && entry.targetMessageId);
}
function updateUserOutlinePanel() {
@@ -844,8 +862,11 @@
function scrollToMessage(anchorId) {
if (!anchorId) return;
const target = document.getElementById(anchorId);
if (!target) return;
target.scrollIntoView({ block: 'start', behavior: 'smooth' });
if (!target || !messagesDiv.contains(target)) return;
const containerRect = messagesDiv.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
const targetTop = messagesDiv.scrollTop + targetRect.top - containerRect.top - 12;
messagesDiv.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
}
function updateSessionIdBadge() {
@@ -2445,6 +2466,7 @@
currentMode = localStorage.getItem(getAgentModeStorageKey(currentAgent)) || 'yolo';
modeSelect.value = currentMode;
updateAgentScopedUI();
updateGenerationControls();
}
function closeAgentMenu() {
@@ -2478,8 +2500,7 @@
uploadingAttachments = [];
activeToolCalls.clear();
activeTodoCallTargets.clear();
sendBtn.hidden = false;
abortBtn.hidden = true;
updateGenerationControls();
chatTitle.textContent = '新会话';
updateSessionIdBadge();
updateCwdBadge();
@@ -2501,8 +2522,7 @@
if (isGenerating && !preserveStreaming) {
isGenerating = false;
generatingSessionId = null;
sendBtn.hidden = false;
abortBtn.hidden = true;
updateGenerationControls();
pendingText = '';
window.pendingContentBlocks = [];
activeToolCalls.clear();
@@ -3105,6 +3125,10 @@
showCodexAppUserInputModal(msg);
break;
case 'ccweb_mcp_child_agent_update':
applyCcwebMcpChildAgentUpdate(msg);
break;
case 'mode_changed':
if (msg.mode && MODE_LABELS[msg.mode]) {
currentMode = msg.mode;
@@ -3132,8 +3156,7 @@
if (!isGenerating || !document.getElementById('streaming-msg')) {
startGenerating(msg.sessionId || currentSessionId);
} else {
sendBtn.hidden = true;
abortBtn.hidden = false;
updateGenerationControls();
toolGroupCount = 0;
hasGrouped = false;
activeToolCalls.clear();
@@ -3287,8 +3310,6 @@
activeTodoCallTargets.clear();
toolGroupCount = 0;
hasGrouped = false;
sendBtn.hidden = true;
abortBtn.hidden = false;
updateNoteModeUI();
// 不禁用输入框,允许用户继续输入(但无法发送)
@@ -3317,8 +3338,6 @@
if (sessionId && currentSessionId && sessionId !== currentSessionId) return;
isGenerating = false;
generatingSessionId = null;
sendBtn.hidden = false;
abortBtn.hidden = true;
updateNoteModeUI();
setCurrentSessionRunningState(false);
msgInput.focus();
@@ -3454,7 +3473,7 @@
function createMsgElement(role, content, attachments = [], meta = {}) {
const div = document.createElement('div');
const isCrossConversation = role === 'user' && !!meta.crossConversation;
const isCrossConversation = !!meta.crossConversation;
const isCrossConversationReply = isCrossConversation && !!(meta.crossConversation.reply || meta.crossConversation.replyToRequestId);
const resolvedMessageId = meta?.messageId || meta?.id || createLocalId('user');
div.className = `msg ${role}${role === 'assistant' ? ' agent-' + currentAgent : ''}${isCrossConversation ? ' cross-conversation' : ''}${isCrossConversationReply ? ' cross-conversation-reply' : ''}`;
@@ -3512,36 +3531,37 @@
const bubble = document.createElement('div');
bubble.className = 'msg-bubble';
if (role === 'user') {
if (isCrossConversation) {
const source = meta.crossConversation || {};
const sourceTitle = source.sourceTitle || '未命名对话';
const sourceId = source.sourceSessionId || '';
const sourceMeta = document.createElement('div');
sourceMeta.className = 'cross-conversation-meta';
if (isCrossConversation) {
const source = meta.crossConversation || {};
const sourceTitle = source.sourceTitle || '未命名对话';
const sourceId = source.sourceSessionId || '';
const sourceMeta = document.createElement('div');
sourceMeta.className = 'cross-conversation-meta';
const label = document.createElement('span');
label.className = 'cross-conversation-label';
label.textContent = isCrossConversationReply
? `来自「${sourceTitle}」的回复`
: `来自「${sourceTitle}」的对话`;
sourceMeta.appendChild(label);
const label = document.createElement('span');
label.className = 'cross-conversation-label';
label.textContent = isCrossConversationReply
? `来自「${sourceTitle}」的回复`
: `来自「${sourceTitle}」的对话`;
sourceMeta.appendChild(label);
if (sourceId) {
const copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.className = 'cross-conversation-id-btn';
copyBtn.textContent = `ID ${shortSessionId(sourceId)}`;
copyBtn.title = `复制来源会话 ID\n${sourceId}`;
copyBtn.addEventListener('click', (event) => {
event.stopPropagation();
copyTextToClipboard(sourceId, '来源会话 ID 已复制');
});
sourceMeta.appendChild(copyBtn);
}
bubble.appendChild(sourceMeta);
if (sourceId) {
const copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.className = 'cross-conversation-id-btn';
copyBtn.textContent = `ID ${shortSessionId(sourceId)}`;
copyBtn.title = `复制来源会话 ID\n${sourceId}`;
copyBtn.addEventListener('click', (event) => {
event.stopPropagation();
copyTextToClipboard(sourceId, '来源会话 ID 已复制');
});
sourceMeta.appendChild(copyBtn);
}
bubble.appendChild(sourceMeta);
}
if (role === 'user') {
if (content) {
const textNode = document.createElement('div');
textNode.className = 'msg-text';
@@ -3877,7 +3897,7 @@
const label = String(state.label || state.title || state.nickname || state.name || `子代理 ${index + 1}`);
const role = String(state.role || state.agent || state.agentType || '').trim();
const status = String(state.status || state.state || 'pending').trim() || 'pending';
const detail = String(state.summary || state.lastMessage || state.step || state.description || '').trim();
const detail = String(state.candidateResult || state.finalMessage || state.summary || state.lastMessage || state.step || state.description || '').trim();
return { id, label, role, status, detail };
});
}
@@ -3885,7 +3905,8 @@
function collabStateTone(statusText) {
const normalized = String(statusText || '').toLowerCase();
if (!normalized) return 'pending';
if (/(done|completed|success|finished|idle)/.test(normalized)) return 'done';
if (/(closed|close)/.test(normalized)) return 'closed';
if (/(returned|done|completed|success|finished|idle)/.test(normalized)) return 'done';
if (/(fail|error|cancel|aborted|rejected)/.test(normalized)) return 'error';
if (/(running|working|active|inprogress|in_progress|executing)/.test(normalized)) return 'running';
return 'pending';
@@ -3895,7 +3916,9 @@
const normalized = String(statusText || '').trim();
if (!normalized) return '等待中';
const lower = normalized.toLowerCase();
if (/(done|completed|success|finished)/.test(lower)) return '已完成';
if (/(closed|close)/.test(lower)) return '已关闭';
if (/(returned)/.test(lower)) return '已返回';
if (/(done|completed|success|finished)/.test(lower)) return '已返回';
if (/(fail|error|rejected)/.test(lower)) return '失败';
if (/(cancel|aborted)/.test(lower)) return '已取消';
if (/(running|working|active|inprogress|in_progress|executing)/.test(lower)) return '进行中';
@@ -3919,7 +3942,7 @@
const kicker = document.createElement('div');
kicker.className = 'collab-agent-kicker';
kicker.textContent = 'Codex App 子代理';
kicker.textContent = 'ccweb MCP 子代理';
titleWrap.appendChild(kicker);
const title = document.createElement('div');
@@ -3971,6 +3994,25 @@
chip.className = `collab-agent-item-status ${tone}`;
chip.textContent = collabStateLabel(entry.status);
row.appendChild(chip);
if (entry.id && tone !== 'closed') {
const closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'collab-agent-close-btn';
closeBtn.textContent = '关闭';
closeBtn.title = `关闭子代理\n${entry.id}`;
closeBtn.addEventListener('click', (event) => {
event.stopPropagation();
closeBtn.disabled = true;
closeBtn.textContent = '关闭中';
send({
type: 'ccweb_mcp_child_agent_close',
sessionId: currentSessionId,
threadId: entry.id,
});
});
row.appendChild(closeBtn);
}
item.appendChild(row);
if (entry.role) {
@@ -4032,7 +4074,9 @@
}
function isGroupableToolCall(node) {
return !!(node?.classList?.contains('tool-call') && node.dataset.toolKind !== 'todo_list');
return !!(node?.classList?.contains('tool-call')
&& node.dataset.toolKind !== 'todo_list'
&& node.dataset.toolKind !== 'collab_agent_tool_call');
}
function rememberToolCallTarget(toolUseId, tool, element) {
@@ -4425,6 +4469,18 @@
}
function createToolCallElement(toolUseId, tool, done) {
const kind = toolKind(tool);
if (kind === 'collab_agent_tool_call') {
const wrapper = document.createElement('div');
wrapper.className = 'tool-call ccweb-mcp-child-agent-tool-call collab-agent-inline';
wrapper.id = `tool-node-${++toolDomSeq}`;
wrapper.dataset.toolUseId = toolUseId ? String(toolUseId) : '';
wrapper.dataset.toolName = tool.name || '';
wrapper.dataset.toolKind = kind;
wrapper.appendChild(buildToolContentElement({ ...tool, done }));
return wrapper;
}
const details = document.createElement('details');
details.className = 'tool-call';
details.id = `tool-node-${++toolDomSeq}`;
@@ -4439,7 +4495,6 @@
// - For non-Codex sessions, auto-open in-flight command execution so users can watch output.
// - For Codex sessions, keep everything collapsed by default (less noise), including in-flight commands.
const agent = normalizeAgent(currentAgent);
const kind = toolKind(tool);
if (tool.name === 'AskUserQuestion') {
details.open = true;
} else if (!isCodexLikeAgent(agent) && !done && kind === 'command_execution') {
@@ -4552,6 +4607,10 @@
el = findLatestToolCallElement(scope, (candidate) => candidate.dataset.toolUseId === toolUseIdText);
}
if (!el) {
el = findLatestToolCallElement(messagesDiv, (candidate) => candidate.dataset.toolUseId === toolUseIdText);
}
if (!el && tool?.kind === 'todo_list' && tool?.input?.id) {
el = findTodoToolCallByTodoId(scope, tool.input.id);
}
@@ -4586,6 +4645,41 @@
}
}
function applyCcwebMcpChildAgentUpdate(msg) {
const tool = msg?.tool;
const toolUseId = msg?.toolUseId || tool?.id;
if (!toolUseId || !tool) return;
updateCachedSession(msg.sessionId, (snapshot) => {
const messages = Array.isArray(snapshot.messages) ? snapshot.messages : [];
for (let i = messages.length - 1; i >= 0; i -= 1) {
const calls = Array.isArray(messages[i]?.toolCalls) ? messages[i].toolCalls : [];
const target = calls.find((item) => item.id === toolUseId);
if (!target) continue;
target.name = tool.name || target.name;
target.kind = tool.kind || target.kind;
target.input = tool.input !== undefined ? tool.input : target.input;
target.result = tool.result !== undefined ? tool.result : target.result;
target.meta = tool.meta || target.meta || null;
target.done = tool.done !== undefined ? !!tool.done : target.done;
snapshot.updated = new Date().toISOString();
break;
}
});
if (msg.sessionId !== currentSessionId) return;
activeToolCalls.set(toolUseId, {
id: toolUseId,
name: tool.name,
input: tool.input,
result: tool.result,
kind: tool.kind || null,
meta: tool.meta || null,
done: !!tool.done,
});
updateToolCall(toolUseId, tool.result, !!tool.done);
}
function getDeleteConfirmMessage(agent) {
const normalized = normalizeAgent(agent);
if (normalized === 'codex') {
@@ -5098,7 +5192,9 @@
? 'Prompt'
: item.kind === 'file'
? (item.itemType === 'directory' ? 'Dir' : 'File')
: 'Cmd';
: item.kind === 'mcp'
? 'MCP'
: 'Cmd';
return `<div class="cmd-item${i === 0 ? ' active' : ''}" data-index="${i}">
<span class="cmd-item-kind">${kindLabel}</span>
<span class="cmd-item-main">
@@ -5127,7 +5223,6 @@
if (token.trigger === '/') {
showCmdMenu(token, getLocalSlashSuggestions(token.query));
return;
}
clearTimeout(composerSuggestionTimer);

View File

@@ -14,7 +14,7 @@
document.documentElement.dataset.dividerTime = dividerTime;
})();
</script>
<link rel="stylesheet" href="style.css?v=20260615-codexapp-steer-status-session-menu">
<link rel="stylesheet" href="style.css?v=20260616-runtime-insert-controls">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
</head>
<body>
@@ -150,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=20260615-codexapp-steer-status-session-menu"></script>
<script src="app.js?v=20260616-runtime-insert-controls"></script>
</body>
</html>

View File

@@ -1986,14 +1986,14 @@ body.session-loading-active {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.28);
}
.msg.user.cross-conversation .msg-avatar {
.msg.cross-conversation .msg-avatar {
background: var(--info);
color: #fff;
}
.msg.user.cross-conversation-reply .msg-avatar {
.msg.cross-conversation-reply .msg-avatar {
background: var(--success);
}
.msg.user.cross-conversation .msg-bubble {
.msg.cross-conversation .msg-bubble {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.76), transparent),
rgba(91, 126, 161, 0.1);
@@ -2001,20 +2001,20 @@ body.session-loading-active {
color: var(--text-primary);
box-shadow: 0 10px 22px rgba(45, 31, 20, 0.05);
}
.msg.user.cross-conversation-reply .msg-bubble {
.msg.cross-conversation-reply .msg-bubble {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.78), transparent),
rgba(93, 138, 84, 0.12);
border-color: rgba(93, 138, 84, 0.28);
}
.msg.user.cross-conversation-reply .cross-conversation-meta,
.msg.user.cross-conversation-reply .cross-conversation-id-btn {
.msg.cross-conversation-reply .cross-conversation-meta,
.msg.cross-conversation-reply .cross-conversation-id-btn {
color: var(--success);
}
.msg.user.cross-conversation-reply .cross-conversation-id-btn {
.msg.cross-conversation-reply .cross-conversation-id-btn {
border-color: rgba(93, 138, 84, 0.28);
}
.msg.user.cross-conversation-reply .cross-conversation-id-btn:hover {
.msg.cross-conversation-reply .cross-conversation-id-btn:hover {
background: rgba(93, 138, 84, 0.14);
}
.cross-conversation-meta {
@@ -2350,11 +2350,14 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
.tool-call.codex-file-change {
border-color: rgba(93, 138, 84, 0.24);
}
.tool-call.codex-collab-agent-tool-call {
.tool-call.ccweb-mcp-child-agent-tool-call {
border-color: rgba(91, 126, 161, 0.28);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.66), rgba(91, 126, 161, 0.04));
}
.tool-call.ccweb-mcp-child-agent-tool-call.collab-agent-inline {
overflow: visible;
}
.tool-call summary {
padding: 8px 12px;
cursor: pointer;
@@ -2463,6 +2466,8 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span {
font-family: inherit;
white-space: normal;
word-break: normal;
max-height: none;
overflow: visible;
background:
linear-gradient(180deg, rgba(252, 253, 255, 0.96), rgba(242, 247, 252, 0.98));
color: var(--text-primary);
@@ -4293,6 +4298,11 @@ html[data-theme='coolvibe'] .settings-back:hover {
background: rgba(192, 85, 58, 0.14);
color: var(--danger);
}
.collab-agent-overall-status.closed,
.collab-agent-item-status.closed {
background: rgba(122, 110, 100, 0.12);
color: var(--text-muted);
}
.collab-agent-overall-status.pending,
.collab-agent-item-status.pending {
background: rgba(91, 126, 161, 0.12);
@@ -4354,6 +4364,28 @@ html[data-theme='coolvibe'] .settings-back:hover {
.collab-agent-item-footer:hover {
color: var(--info);
}
.collab-agent-close-btn {
appearance: none;
flex-shrink: 0;
border: 1px solid rgba(122, 110, 100, 0.18);
border-radius: 999px;
background: rgba(255, 255, 255, 0.86);
color: var(--text-secondary);
padding: 4px 9px;
font: inherit;
font-size: 11px;
font-weight: 800;
cursor: pointer;
}
.collab-agent-close-btn:hover {
color: var(--danger);
border-color: rgba(192, 85, 58, 0.22);
background: rgba(192, 85, 58, 0.08);
}
.collab-agent-close-btn:disabled {
cursor: default;
opacity: 0.6;
}
.collab-agent-threads {
display: flex;
flex-wrap: wrap;
@@ -4777,10 +4809,11 @@ html[data-theme='coolvibe'] .settings-back:hover {
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .file-browser-path,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .file-browser-pane,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .file-browser-preview-content,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .collab-agent-prompt,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .collab-agent-item,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .collab-agent-thread-chip,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .todo-list-container {
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .collab-agent-prompt,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .collab-agent-item,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .collab-agent-thread-chip,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .collab-agent-close-btn,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .todo-list-container {
background: var(--dark-panel-soft);
border-color: var(--border-color);
color: var(--text-primary);
@@ -4857,8 +4890,8 @@ html[data-theme='coolvibe'] .settings-back:hover {
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .msg.assistant .msg-bubble,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .msg.user.cross-conversation .msg-bubble,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .msg.user.cross-conversation-reply .msg-bubble {
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .msg.cross-conversation .msg-bubble,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .msg.cross-conversation-reply .msg-bubble {
background: var(--bg-bubble-assistant);
border-color: var(--border-color);
color: var(--text-primary);
@@ -4908,7 +4941,7 @@ html[data-theme='coolvibe'] .settings-back:hover {
}
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .tool-call.codex-command,
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .tool-call.codex-collab-agent-tool-call {
:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .tool-call.ccweb-mcp-child-agent-tool-call {
background: var(--dark-panel-bg);
border-color: var(--note-border);
box-shadow: none;