diff --git a/lib/codex-app-worker-client.js b/lib/codex-app-worker-client.js index ba5cb4c..5c14f72 100644 --- a/lib/codex-app-worker-client.js +++ b/lib/codex-app-worker-client.js @@ -42,7 +42,7 @@ function createCodexAppWorkerClient(options = {}) { appServerRunning = false; worker = fork(workerPath, [], { cwd: options.cwd || process.cwd(), - env: process.env, + env: options.env || process.env, stdio: ['ignore', 'ignore', 'ignore', 'ipc'], }); diff --git a/public/app.js b/public/app.js index 3370456..4385622 100644 --- a/public/app.js +++ b/public/app.js @@ -2,7 +2,7 @@ (function () { 'use strict'; - const ASSET_VERSION = '20260616-composer-mcp-list'; + const ASSET_VERSION = '20260616-child-agent-close-state'; const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`; const RENDER_DEBOUNCE = 100; const COMPOSER_SUGGESTION_DEBOUNCE = 120; @@ -130,6 +130,7 @@ let generatingSessionId = null; let activeToolCalls = new Map(); let activeTodoCallTargets = new Map(); + let closedCollabAgentIds = new Set(); let toolDomSeq = 0; let toolGroupCount = 0; // 当前 .msg-tools 直接子节点数(含已有父目录) let hasGrouped = false; // 本次输出是否已触发过折叠 @@ -785,6 +786,13 @@ return value ? value.slice(0, 8) : ''; } + function shortChildAgentId(threadId) { + const value = String(threadId || ''); + if (!value) return ''; + if (value.length <= 13) return value; + return `${value.slice(0, 8)}…${value.slice(-4)}`; + } + function shortMessagePreview(text, maxLength = 60) { const value = String(text || '').replace(/\s+/g, ' ').trim(); if (!value) return '空消息'; @@ -2500,6 +2508,7 @@ uploadingAttachments = []; activeToolCalls.clear(); activeTodoCallTargets.clear(); + closedCollabAgentIds = new Set(); updateGenerationControls(); chatTitle.textContent = '新会话'; updateSessionIdBadge(); @@ -2989,6 +2998,7 @@ }); } if (msg.sessionId === currentSessionId && msg.message) { + collectClosedCollabAgentIds([msg.message]).forEach((id) => closedCollabAgentIds.add(id)); const welcome = messagesDiv.querySelector('.welcome-msg'); if (welcome) welcome.remove(); messagesDiv.appendChild(buildMsgElement(msg.message)); @@ -3167,7 +3177,12 @@ pendingText = msg.text || ''; flushRender(); if (msg.toolCalls && msg.toolCalls.length > 0) { - for (const tc of msg.toolCalls) { + const mergedCollabTool = mergeCollabAgentTools(msg.toolCalls); + const resumeToolCalls = [ + ...(mergedCollabTool ? [mergedCollabTool] : []), + ...msg.toolCalls.filter((tc) => toolKind(tc) !== 'collab_agent_tool_call'), + ]; + for (const tc of resumeToolCalls) { activeToolCalls.set(tc.id, { name: tc.name, input: tc.input, @@ -3867,14 +3882,32 @@ return text.length > 140 ? `${text.slice(0, 140)}…` : text; } + function normalizeCollabAgentAction(value) { + const raw = String(value || '').trim(); + if (!raw) return ''; + const name = raw.split(/[./]/).filter(Boolean).pop() || raw; + const normalized = name.toLowerCase().replace(/[\s_-]/g, ''); + if (normalized === 'spawn' || normalized === 'spawnagent') return 'spawn_agent'; + if (normalized === 'wait' || normalized === 'waitagent') return 'wait_agent'; + if (normalized === 'close' || normalized === 'closeagent') return 'close_agent'; + if (normalized === 'sendinput' || normalized === 'sendmessage') return 'send_input'; + if (normalized === 'resume' || normalized === 'resumeagent') return 'resume_agent'; + return normalized; + } + + function getCollabAgentAction(tool, data = null) { + const value = data?.tool || tool?.name || tool?.meta?.title || ''; + return normalizeCollabAgentAction(value); + } + function normalizeCollabAgentData(tool) { const inputData = effectiveObject(tool?.input); const resultData = effectiveObject(tool?.result); const merged = { ...inputData, ...resultData, - agentsStates: resultData.agentsStates || inputData.agentsStates || {}, - receiverThreadIds: resultData.receiverThreadIds || inputData.receiverThreadIds || [], + agentsStates: resultData.agentsStates || resultData.agents_states || inputData.agentsStates || inputData.agents_states || {}, + receiverThreadIds: resultData.receiverThreadIds || resultData.receiver_thread_ids || resultData.targets || inputData.receiverThreadIds || inputData.receiver_thread_ids || inputData.targets || [], prompt: inputData.prompt || resultData.prompt || '', tool: inputData.tool || resultData.tool || tool?.name || '', status: resultData.status || inputData.status || tool?.meta?.status || null, @@ -3896,12 +3929,178 @@ const state = value && typeof value === 'object' ? value : { status: value }; 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.candidateResult || state.finalMessage || state.summary || state.lastMessage || state.step || state.description || '').trim(); + let status = String(state.status || state.state || 'pending').trim() || 'pending'; + if (state.closedAt && collabStateTone(status) !== 'closed') status = 'closed'; + const detail = String(state.candidateResult || state.finalMessage || state.summary || state.message || state.lastMessage || state.step || state.description || '').trim(); return { id, label, role, status, detail }; }); } + function getCollabAgentIdsFromTool(tool) { + const data = normalizeCollabAgentData(tool); + const ids = new Set(); + for (const entry of collabAgentStateEntries(data)) { + if (entry.id) ids.add(entry.id); + } + if (Array.isArray(data.receiverThreadIds)) { + data.receiverThreadIds.forEach((id) => { + if (id) ids.add(String(id)); + }); + } + const directIds = [ + data.threadId, + data.thread_id, + data.agentId, + data.agent_id, + data.childThreadId, + data.child_thread_id, + data.childAgentId, + data.child_agent_id, + data.targetThreadId, + data.target_thread_id, + data.target, + ]; + directIds.forEach((id) => { + if (id) ids.add(String(id)); + }); + const directArrays = [ + data.threadIds, + data.thread_ids, + data.childThreadIds, + data.child_thread_ids, + data.agentIds, + data.agent_ids, + data.targets, + ]; + directArrays.forEach((value) => { + if (!Array.isArray(value)) return; + value.forEach((id) => { + if (id) ids.add(String(id)); + }); + }); + return Array.from(ids); + } + + function getClosedCollabAgentIdsFromTool(tool) { + if (toolKind(tool) !== 'collab_agent_tool_call') return []; + const data = normalizeCollabAgentData(tool); + const ids = new Set(); + const action = getCollabAgentAction(tool, data); + const allIds = getCollabAgentIdsFromTool(tool); + if (action === 'close_agent' || collabStateTone(data.status) === 'closed') { + allIds.forEach((id) => ids.add(id)); + } + collabAgentStateEntries(data).forEach((entry) => { + if (entry.id && collabStateTone(entry.status) === 'closed') ids.add(entry.id); + }); + return Array.from(ids); + } + + function collectClosedCollabAgentIds(messages) { + const ids = new Set(); + (Array.isArray(messages) ? messages : []).forEach((message) => { + (Array.isArray(message?.toolCalls) ? message.toolCalls : []).forEach((tool) => { + getClosedCollabAgentIdsFromTool(tool).forEach((id) => ids.add(id)); + }); + }); + return ids; + } + + function rememberClosedCollabAgentIdsFromTool(tool) { + getClosedCollabAgentIdsFromTool(tool).forEach((id) => closedCollabAgentIds.add(id)); + } + + function isGenericCollabAgentLabel(label, id) { + const value = String(label || '').trim(); + if (!value) return true; + if (/^子代理\s*\d+$/i.test(value)) return true; + return !!id && value === String(id); + } + + function mergeCollabAgentTools(tools, options = {}) { + const list = Array.isArray(tools) ? tools.filter((tool) => toolKind(tool) === 'collab_agent_tool_call') : []; + if (list.length === 0) return null; + const states = {}; + const receiverThreadIds = []; + const knownClosedIds = options.closedAgentIds instanceof Set ? options.closedAgentIds : closedCollabAgentIds; + const localClosedIds = new Set(knownClosedIds || []); + let toolName = '子代'; + let prompt = ''; + let status = ''; + let done = false; + + list.forEach((tool, toolIndex) => { + const data = normalizeCollabAgentData(tool); + const action = getCollabAgentAction(tool, data); + const isCloseAction = action === 'close_agent'; + const displayAction = String(data.tool || tool.name || '').trim(); + if (displayAction && !['wait_agent', 'close_agent'].includes(action)) toolName = displayAction; + if (!prompt && data.prompt) prompt = data.prompt; + if (isCloseAction) { + getCollabAgentIdsFromTool(tool).forEach((id) => localClosedIds.add(id)); + } + const dataStatus = isCloseAction ? 'closed' : data.status; + if (dataStatus) status = dataStatus; + done = done || !!tool.done; + + collabAgentStateEntries(data).forEach((entry) => { + if (!entry.id) return; + states[entry.id] = { + ...(states[entry.id] || {}), + ...entry, + status: isCloseAction || localClosedIds.has(entry.id) ? 'closed' : entry.status, + }; + if (collabStateTone(states[entry.id].status) === 'closed') localClosedIds.add(entry.id); + }); + + getCollabAgentIdsFromTool(tool).forEach((id) => { + if (!receiverThreadIds.includes(id)) receiverThreadIds.push(id); + states[id] = { + ...(states[id] || {}), + label: states[id]?.label || `子代理 ${receiverThreadIds.length}`, + status: isCloseAction || localClosedIds.has(id) + ? 'closed' + : (data.status || states[id]?.status || (tool.done ? 'completed' : 'running')), + }; + }); + + if (receiverThreadIds.length === 0 && list.length === 1) { + const fallbackId = tool.id || `tool-${toolIndex + 1}`; + receiverThreadIds.push(fallbackId); + states[fallbackId] = { + label: '子代理', + status: isCloseAction ? 'closed' : (data.status || (tool.done ? 'completed' : 'running')), + }; + } + }); + + receiverThreadIds.forEach((id, index) => { + states[id] = { + ...(states[id] || {}), + label: states[id]?.label || `子代理 ${index + 1}`, + status: localClosedIds.has(id) ? 'closed' : (states[id]?.status || 'pending'), + }; + }); + + const allClosed = receiverThreadIds.length > 0 + && receiverThreadIds.every((id) => collabStateTone(states[id]?.status) === 'closed'); + const mergedStatus = allClosed ? 'closed' : (status || (done ? 'completed' : 'running')); + + return { + id: list[0].id || 'collab-agent-merged', + name: list[0].name || 'ccweb_mcp_child_agent', + kind: 'collab_agent_tool_call', + done, + input: { + tool: toolName, + prompt, + status: mergedStatus, + receiverThreadIds, + agentsStates: states, + }, + }; + } + function collabStateTone(statusText) { const normalized = String(statusText || '').toLowerCase(); if (!normalized) return 'pending'; @@ -3934,6 +4133,12 @@ const stack = document.createElement('div'); stack.className = 'collab-agent-stack'; + const stateEntries = collabAgentStateEntries(data); + const threadCount = Array.isArray(data.receiverThreadIds) ? data.receiverThreadIds.length : 0; + const agentCount = stateEntries.length; + const totalCount = agentCount || threadCount || 0; + const promptText = summarizePrompt(data.prompt); + const header = document.createElement('div'); header.className = 'collab-agent-header'; @@ -3942,55 +4147,71 @@ const kicker = document.createElement('div'); kicker.className = 'collab-agent-kicker'; - kicker.textContent = 'ccweb MCP 子代理'; + kicker.textContent = '子代'; titleWrap.appendChild(kicker); const title = document.createElement('div'); title.className = 'collab-agent-title'; - title.textContent = data.tool || '协作任务'; + title.textContent = `${totalCount || 0} 个`; titleWrap.appendChild(title); const meta = document.createElement('div'); meta.className = 'collab-agent-meta'; - const threadCount = Array.isArray(data.receiverThreadIds) ? data.receiverThreadIds.length : 0; - const agentCount = collabAgentStateEntries(data).length; - meta.textContent = `${agentCount || threadCount || 0} 个子代理`; - titleWrap.appendChild(meta); + meta.textContent = ''; + if (promptText) meta.title = promptText; + if (promptText) titleWrap.appendChild(meta); header.appendChild(titleWrap); + const headerActions = document.createElement('div'); + headerActions.className = 'collab-agent-actions'; + const statusChip = document.createElement('span'); const overallTone = collabStateTone(data.status || (tool.done ? 'completed' : 'running')); statusChip.className = `collab-agent-overall-status ${overallTone}`; statusChip.textContent = collabStateLabel(data.status || (tool.done ? 'completed' : 'running')); - header.appendChild(statusChip); + headerActions.appendChild(statusChip); + + header.appendChild(headerActions); stack.appendChild(header); - const promptText = summarizePrompt(data.prompt); - if (promptText) { - const promptBlock = document.createElement('div'); - promptBlock.className = 'collab-agent-prompt'; - promptBlock.textContent = promptText; - stack.appendChild(promptBlock); - } - - const stateEntries = collabAgentStateEntries(data); if (stateEntries.length > 0) { const list = document.createElement('div'); list.className = 'collab-agent-list'; stateEntries.forEach((entry, index) => { + const tone = collabStateTone(entry.status); const item = document.createElement('div'); item.className = 'collab-agent-item'; + item.title = [ + entry.label || `子代理 ${index + 1}`, + entry.role ? `角色: ${entry.role}` : '', + entry.detail ? `结果: ${entry.detail}` : '', + entry.id ? `ID: ${entry.id}` : '', + ].filter(Boolean).join('\n'); + if (entry.id) { + item.setAttribute('role', 'button'); + item.tabIndex = 0; + item.addEventListener('click', () => { + copyTextToClipboard(entry.id, '子代理线程 ID 已复制'); + }); + item.addEventListener('keydown', (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + copyTextToClipboard(entry.id, '子代理线程 ID 已复制'); + } + }); + } const row = document.createElement('div'); row.className = 'collab-agent-item-row'; const label = document.createElement('div'); label.className = 'collab-agent-item-label'; - label.textContent = entry.label || `子代理 ${index + 1}`; + label.textContent = !isGenericCollabAgentLabel(entry.label, entry.id) + ? entry.label + : `ID ${shortChildAgentId(entry.id || '')}`; row.appendChild(label); const chip = document.createElement('span'); - const tone = collabStateTone(entry.status); chip.className = `collab-agent-item-status ${tone}`; chip.textContent = collabStateLabel(entry.status); row.appendChild(chip); @@ -4015,28 +4236,11 @@ } item.appendChild(row); - if (entry.role) { - const role = document.createElement('div'); - role.className = 'collab-agent-item-role'; - role.textContent = entry.role; - item.appendChild(role); - } - - if (entry.detail) { - const detail = document.createElement('div'); - detail.className = 'collab-agent-item-detail'; - detail.textContent = entry.detail; - item.appendChild(detail); - } - - if (entry.id) { + if (entry.id || entry.role) { const footer = document.createElement('div'); footer.className = 'collab-agent-item-footer'; - footer.textContent = `ID ${shortSessionId(entry.id)}`; - footer.title = entry.id; - footer.addEventListener('click', () => { - copyTextToClipboard(entry.id, '子代理线程 ID 已复制'); - }); + footer.textContent = entry.role || ''; + if (!footer.textContent) footer.hidden = true; item.appendChild(footer); } @@ -4045,14 +4249,14 @@ stack.appendChild(list); } - if (Array.isArray(data.receiverThreadIds) && data.receiverThreadIds.length > 0) { + if (stateEntries.length === 0 && Array.isArray(data.receiverThreadIds) && data.receiverThreadIds.length > 0) { const threads = document.createElement('div'); threads.className = 'collab-agent-threads'; data.receiverThreadIds.forEach((threadId) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'collab-agent-thread-chip'; - btn.textContent = `ID ${shortSessionId(threadId)}`; + btn.textContent = `ID ${shortChildAgentId(threadId)}`; btn.title = `复制子代理线程 ID\n${threadId}`; btn.addEventListener('click', () => { copyTextToClipboard(threadId, '子代理线程 ID 已复制'); @@ -4108,7 +4312,12 @@ const bubble = el.querySelector('.msg-bubble'); const FOLD_AT = 3; let grouped = false; - for (const tc of m.toolCalls) { + const mergedCollabTool = mergeCollabAgentTools(m.toolCalls); + const renderToolCalls = [ + ...(mergedCollabTool ? [mergedCollabTool] : []), + ...m.toolCalls.filter((tc) => toolKind(tc) !== 'collab_agent_tool_call'), + ]; + for (const tc of renderToolCalls) { if (isEmptyReasoningTool(tc)) continue; const details = createToolCallElement(tc.id || `saved-${Math.random().toString(36).slice(2)}`, tc, true); @@ -4153,6 +4362,7 @@ function renderMessages(messages, options = {}) { renderEpoch++; const epoch = renderEpoch; + closedCollabAgentIds = collectClosedCollabAgentIds(messages); messagesDiv.innerHTML = ''; clearUserMessageIndex(); if (messages.length === 0) { @@ -4216,6 +4426,7 @@ function prependHistoryMessages(messages, options = {}) { if (!Array.isArray(messages) || messages.length === 0) return; + collectClosedCollabAgentIds(messages).forEach((id) => closedCollabAgentIds.add(id)); const preserveScroll = options.preserveScroll !== false; const skipScrollbar = options.skipScrollbar === true; const welcome = messagesDiv.querySelector('.welcome-msg'); @@ -4477,6 +4688,7 @@ wrapper.dataset.toolUseId = toolUseId ? String(toolUseId) : ''; wrapper.dataset.toolName = tool.name || ''; wrapper.dataset.toolKind = kind; + wrapper.dataset.childIds = getCollabAgentIdsFromTool(tool).join(','); wrapper.appendChild(buildToolContentElement({ ...tool, done })); return wrapper; } @@ -4508,6 +4720,47 @@ return details; } + function upsertCollabAgentToolCall(toolsDiv, toolUseId, tool, done) { + if (!toolsDiv || !tool) return null; + const existing = toolsDiv.querySelector(':scope > .ccweb-mcp-child-agent-tool-call[data-collab-merged="true"]') + || toolsDiv.querySelector(':scope > .tool-group .ccweb-mcp-child-agent-tool-call[data-collab-merged="true"]'); + const nextTool = { ...tool, id: toolUseId, done }; + rememberClosedCollabAgentIdsFromTool(nextTool); + const map = existing?.__collabTools instanceof Map ? existing.__collabTools : new Map(); + map.set(toolUseId || nextTool.id || `collab-${map.size + 1}`, nextTool); + const merged = mergeCollabAgentTools(Array.from(map.values())); + if (!merged) return existing; + + if (existing) { + existing.__collabTools = map; + existing.dataset.toolUseId = merged.id || existing.dataset.toolUseId || ''; + existing.dataset.childIds = getCollabAgentIdsFromTool(merged).join(','); + existing.replaceChildren(buildToolContentElement(merged)); + removeDuplicateCollabAgentNodes(toolsDiv); + return existing; + } + + const el = createToolCallElement(merged.id, merged, !!merged.done); + el.dataset.collabMerged = 'true'; + el.dataset.childIds = getCollabAgentIdsFromTool(merged).join(','); + el.__collabTools = map; + toolsDiv.appendChild(el); + removeDuplicateCollabAgentNodes(toolsDiv); + return el; + } + + function removeDuplicateCollabAgentNodes(scope) { + if (!scope) return; + const seen = new Set(); + const nodes = Array.from(scope.querySelectorAll('.ccweb-mcp-child-agent-tool-call')); + nodes.forEach((node) => { + const ids = String(node.dataset.childIds || '').split(',').filter(Boolean); + const duplicate = ids.length > 0 && ids.some((id) => seen.has(id)); + ids.forEach((id) => seen.add(id)); + if (duplicate) node.remove(); + }); + } + function appendToolCall(toolUseId, name, input, done, kind = null, meta = null, result = undefined) { const streamEl = document.getElementById('streaming-msg'); if (!streamEl) return; @@ -4519,6 +4772,12 @@ const tool = { id: toolUseId, name, input, kind, meta, done }; if (result !== undefined) tool.result = result; if (isEmptyReasoningTool(tool)) return; + if (toolKind(tool) === 'collab_agent_tool_call') { + const el = upsertCollabAgentToolCall(toolsDiv, toolUseId, tool, done); + if (el) rememberToolCallTarget(toolUseId, tool, el); + scrollToBottom(); + return; + } // 如果是 todo_list,检查是否已存在相同 id 的 todo_list if (kind === 'todo_list' && input?.id) { @@ -4592,6 +4851,13 @@ const tool = activeToolCalls.get(toolUseId) || null; const toolUseIdText = toolUseId ? String(toolUseId) : ''; const scope = getLatestAssistantToolScope(); + if (toolKind(tool) === 'collab_agent_tool_call') { + const toolsDiv = scope?.querySelector?.('.msg-tools') || scope?.querySelector?.('.msg-bubble') || scope; + const nextTool = { ...tool, result, done }; + const el = upsertCollabAgentToolCall(toolsDiv, toolUseId, nextTool, done); + if (el) rememberToolCallTarget(toolUseId, nextTool, el); + return; + } let el = tool?.domElement && tool.domElement.isConnected ? tool.domElement : null; if (!el) { @@ -4649,6 +4915,13 @@ const tool = msg?.tool; const toolUseId = msg?.toolUseId || tool?.id; if (!toolUseId || !tool) return; + const isCurrentSessionUpdate = msg.sessionId === currentSessionId; + if (isCurrentSessionUpdate) { + if (msg?.child?.threadId && collabStateTone(msg.child.status) === 'closed') { + closedCollabAgentIds.add(String(msg.child.threadId)); + } + rememberClosedCollabAgentIdsFromTool(tool); + } updateCachedSession(msg.sessionId, (snapshot) => { const messages = Array.isArray(snapshot.messages) ? snapshot.messages : []; @@ -4667,7 +4940,7 @@ } }); - if (msg.sessionId !== currentSessionId) return; + if (!isCurrentSessionUpdate) return; activeToolCalls.set(toolUseId, { id: toolUseId, name: tool.name, diff --git a/public/index.html b/public/index.html index 6e268a7..8c72ae9 100644 --- a/public/index.html +++ b/public/index.html @@ -14,7 +14,7 @@ document.documentElement.dataset.dividerTime = dividerTime; })(); - + @@ -150,6 +150,6 @@ - + diff --git a/public/style.css b/public/style.css index 180c2bf..393e625 100644 --- a/public/style.css +++ b/public/style.css @@ -2334,11 +2334,18 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span { .code-block-wrapper.preview-mode pre { display: none; } /* Tool calls */ +.msg-tools { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 4px 6px; +} .tool-call { margin: 8px 0; border: 1px solid var(--border-color); border-radius: 10px; overflow: hidden; + width: 100%; } .tool-call.codex-command { border-color: rgba(91, 126, 161, 0.24); @@ -2351,12 +2358,16 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span { border-color: rgba(93, 138, 84, 0.24); } .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)); + border-color: transparent; + background: transparent; } .tool-call.ccweb-mcp-child-agent-tool-call.collab-agent-inline { + display: inline-flex; + width: auto; + max-width: 100%; + margin: 0; overflow: visible; + border-radius: 0; } .tool-call summary { padding: 8px 12px; @@ -2468,8 +2479,8 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span { word-break: normal; max-height: none; overflow: visible; - background: - linear-gradient(180deg, rgba(252, 253, 255, 0.96), rgba(242, 247, 252, 0.98)); + padding: 0; + background: transparent; color: var(--text-primary); } .tool-call-content.todo-list-content .todo-list-container { @@ -2519,6 +2530,7 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span { border: 1px solid var(--border-color); border-radius: 10px; overflow: hidden; + width: 100%; } .tool-group-summary { padding: 8px 12px; @@ -2539,12 +2551,19 @@ html[data-divider-time='hide'] .msg-bubble .agent-message-divider span { .tool-group[open] > .tool-group-summary::before { transform: rotate(90deg); } .tool-group-summary:hover { background: var(--bg-tertiary); } .tool-group-inner { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 4px 6px; padding: 4px 8px; background: var(--bg-primary); } .tool-group-inner .tool-call { margin: 4px 0; } +.tool-group-inner .tool-call.ccweb-mcp-child-agent-tool-call.collab-agent-inline { + margin: 0; +} /* AskUserQuestion preview */ .ask-user-question { @@ -4237,48 +4256,65 @@ html[data-theme='coolvibe'] .settings-back:hover { } .collab-agent-stack { - display: flex; + display: inline-flex; flex-direction: column; - gap: 10px; + gap: 4px; + min-width: 0; } .collab-agent-header { display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 10px; + align-items: center; + justify-content: flex-start; + gap: 4px; + min-width: 0; } .collab-agent-title-wrap { min-width: 0; display: flex; - flex-direction: column; - gap: 2px; + flex-direction: row; + align-items: baseline; + gap: 4px; + overflow: hidden; } .collab-agent-kicker { - font-size: 10px; + flex-shrink: 0; + font-size: 11px; font-weight: 800; - letter-spacing: 0.08em; + letter-spacing: 0.02em; text-transform: uppercase; color: var(--info); } .collab-agent-title { - font-size: 14px; + min-width: 0; + max-width: 56px; + font-size: 12px; font-weight: 700; - line-height: 1.35; + line-height: 1.2; color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .collab-agent-meta { - font-size: 11px; + flex-shrink: 0; + font-size: 12px; color: var(--text-muted); } +.collab-agent-actions { + display: inline-flex; + align-items: center; + gap: 4px; + flex-shrink: 0; +} .collab-agent-overall-status, .collab-agent-item-status { display: inline-flex; align-items: center; justify-content: center; - min-height: 24px; - padding: 0 9px; + min-height: 20px; + padding: 0 7px; border-radius: 999px; - font-size: 10px; + font-size: 12px; font-weight: 800; line-height: 1; white-space: nowrap; @@ -4319,31 +4355,41 @@ html[data-theme='coolvibe'] .settings-back:hover { } .collab-agent-list { display: flex; - flex-direction: column; - gap: 8px; + flex-wrap: wrap; + gap: 4px; } .collab-agent-item { - display: flex; - flex-direction: column; - gap: 6px; - padding: 10px 12px; - border: 1px solid rgba(91, 126, 161, 0.14); - border-radius: 10px; - background: rgba(255, 255, 255, 0.74); + appearance: none; + max-width: 100%; + min-height: 40px; + display: inline-flex; + flex-direction: row; + align-items: center; + gap: 4px; + padding: 5px 10px; + border: 1px solid rgba(91, 126, 161, 0.16); + border-radius: 7px; + background: rgba(255, 255, 255, 0.58); + color: inherit; + font: inherit; + cursor: pointer; + text-align: left; } .collab-agent-item-row { display: flex; align-items: center; - justify-content: space-between; - gap: 8px; + gap: 4px; } .collab-agent-item-label { min-width: 0; - flex: 1; + max-width: 150px; font-size: 13px; font-weight: 700; color: var(--text-primary); - line-height: 1.35; + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .collab-agent-item-role { font-size: 11px; @@ -4356,10 +4402,10 @@ html[data-theme='coolvibe'] .settings-back:hover { line-height: 1.55; } .collab-agent-item-footer { - align-self: flex-start; font-size: 11px; color: var(--text-muted); - cursor: pointer; + line-height: 1.1; + display: none; } .collab-agent-item-footer:hover { color: var(--info); @@ -4371,9 +4417,10 @@ html[data-theme='coolvibe'] .settings-back:hover { border-radius: 999px; background: rgba(255, 255, 255, 0.86); color: var(--text-secondary); - padding: 4px 9px; + min-height: 22px; + padding: 0 8px; font: inherit; - font-size: 11px; + font-size: 12px; font-weight: 800; cursor: pointer; } @@ -4448,11 +4495,18 @@ html[data-theme='coolvibe'] .settings-back:hover { opacity: 0.6; } @media (max-width: 640px) { - .collab-agent-header, - .collab-agent-item-row { - flex-direction: column; + .collab-agent-header { align-items: flex-start; } + .collab-agent-title-wrap { + flex-wrap: wrap; + } + .collab-agent-actions { + margin-left: auto; + } + .collab-agent-item-label { + max-width: 96px; + } } .import-item-btn:hover { background: var(--accent-hover); } @@ -4942,8 +4996,8 @@ 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.ccweb-mcp-child-agent-tool-call { - background: var(--dark-panel-bg); - border-color: var(--note-border); + background: transparent; + border-color: transparent; box-shadow: none; } @@ -4970,6 +5024,10 @@ html[data-theme='coolvibe'] .settings-back:hover { color: var(--text-primary); } +:is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .tool-call-content.collab-agent-content { + background: transparent; +} + :is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .tool-call-state, :is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .session-item-status, :is(html[data-theme='carbon'], html[data-theme='nocturne'], html[data-theme='cinder']) .collab-agent-overall-status, diff --git a/server.js b/server.js index 05d7ec8..e02e121 100644 --- a/server.js +++ b/server.js @@ -2,6 +2,7 @@ const http = require('http'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); +const os = require('os'); const { spawn, spawnSync } = require('child_process'); const { WebSocketServer } = require('ws'); const { createAgentRuntime } = require('./lib/agent-runtime'); @@ -69,6 +70,50 @@ const CROSS_CONVERSATION_MAX_CONTENT_CHARS = readPositiveIntEnv('CC_WEB_CROSS_CO const MCP_CREATE_CONVERSATION_MAX_HOP_COUNT = readPositiveIntEnv('CC_WEB_MCP_CREATE_CONVERSATION_MAX_HOP_COUNT', 3, { min: 1, max: 20 }); const CODEX_APP_WORKER_DISABLED = /^(0|false|no|off)$/i.test(String(process.env.CC_WEB_CODEX_APP_WORKER || '')); const CODEX_APP_WORKER_ENABLED = !CODEX_APP_WORKER_DISABLED; +const CODEX_APP_PROCESS_ENV_STRIP_KEYS = [ + 'CC_WEB_MCP_URL', + 'CC_WEB_MCP_TOKEN', + 'CC_WEB_SOURCE_SESSION_ID', + 'CC_WEB_CROSS_HOP_COUNT', + 'CODEX_THREAD_ID', + 'CODEX_CI', +]; +const PROCESS_CLEAN_PATH_FALLBACK = [ + path.join(os.homedir(), '.local/bin'), + '/usr/local/sbin', + '/usr/local/bin', + '/usr/sbin', + '/usr/bin', + '/sbin', + '/bin', +].join(path.delimiter); + +function isCodexInjectedPathEntry(value) { + const normalized = String(value || '').replace(/\\/g, '/'); + if (!normalized) return true; + if (normalized.includes('/.codex/tmp/arg0')) return true; + return normalized.includes('/node_modules/@openai/codex/') + && /\/(?:codex-path|path)$/.test(normalized); +} + +function cleanProcessPathValue(value) { + const override = String(process.env.CC_WEB_PROCESS_CLEAN_PATH || '').trim(); + const source = override || value || PROCESS_CLEAN_PATH_FALLBACK; + const fallbackEntries = PROCESS_CLEAN_PATH_FALLBACK.split(path.delimiter).filter(Boolean); + const output = []; + const seen = new Set(); + const addEntry = (entry) => { + const text = String(entry || '').trim(); + if (!text || seen.has(text) || isCodexInjectedPathEntry(text)) return; + seen.add(text); + output.push(text); + }; + + String(source).split(path.delimiter).forEach(addEntry); + fallbackEntries.forEach(addEntry); + return output.length > 0 ? output.join(path.delimiter) : PROCESS_CLEAN_PATH_FALLBACK; +} + const PERSIST_TRUNCATED_TEXT = '[cc-web: 内容过大,已截断以保护服务稳定性]'; const IMAGE_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']); const TEXT_PREVIEW_EXTENSIONS = new Set([ @@ -6260,6 +6305,12 @@ function buildCodexAppClientSpec() { if (runtimeConfig?.error) return { error: runtimeConfig.error }; const env = { ...process.env }; + const strippedEnvKeys = []; + for (const key of CODEX_APP_PROCESS_ENV_STRIP_KEYS) { + if (Object.prototype.hasOwnProperty.call(env, key)) strippedEnvKeys.push(key); + delete env[key]; + } + env.PATH = cleanProcessPathValue(env.PATH); delete env.CC_WEB_PASSWORD; delete env.CLAUDECODE; delete env.CLAUDE_CODE; @@ -6286,6 +6337,7 @@ function buildCodexAppClientSpec() { env, cwd: process.env.HOME || process.env.USERPROFILE || process.cwd(), signature, + strippedEnvKeys, }; } @@ -6328,6 +6380,7 @@ function getCodexAppClient() { plog('INFO', 'codex_app_client_created', { worker: CODEX_APP_WORKER_ENABLED, command: path.basename(spec.command || ''), + strippedEnvKeys: spec.strippedEnvKeys || [], }); }