Refine codex app controls and message navigation
This commit is contained in:
66
AGENTS.md
66
AGENTS.md
@@ -20,3 +20,69 @@ If you're using Codex, project-scoped helpers may also live in:
|
|||||||
Keep this managed block so 'trellis update' can refresh the instructions.
|
Keep this managed block so 'trellis update' can refresh the instructions.
|
||||||
|
|
||||||
<!-- TRELLIS:END -->
|
<!-- TRELLIS:END -->
|
||||||
|
|
||||||
|
## Codex App / hapi 对齐经验
|
||||||
|
|
||||||
|
以下约定来自本项目对 `/home/hdzx/2026/hapi` 中 Codex app-server 接入方式的对比结果。后续如果继续维护 `codexapp`,默认按这些约定实现,避免再走偏到“看起来能跑、但拿不到原生协作能力”的分叉路径。
|
||||||
|
|
||||||
|
### 1. 初始化链路优先对齐上游协议
|
||||||
|
|
||||||
|
- `initialize` 成功后,先发送 `initialized` notification。
|
||||||
|
- 然后再做 best-effort 的能力探测:
|
||||||
|
- `experimentalFeature/enablement/set`,当前至少尝试 `{ enablement: { goals: true } }`
|
||||||
|
- `collaborationMode/list`
|
||||||
|
- 上述探测失败时:
|
||||||
|
- 只记录日志
|
||||||
|
- 不直接中断 `codexapp` 启动
|
||||||
|
|
||||||
|
### 2. collaborationMode 的参数形状必须贴近 hapi
|
||||||
|
|
||||||
|
- 一旦本轮使用 `collaborationMode`,`turn/start` 顶层参数应保持精简。
|
||||||
|
- `model`、`reasoning_effort`、`developer_instructions` 应放入:
|
||||||
|
- `collaborationMode.settings.model`
|
||||||
|
- `collaborationMode.settings.reasoning_effort`
|
||||||
|
- `collaborationMode.settings.developer_instructions`
|
||||||
|
- 使用 `collaborationMode` 时,不要再重复传顶层 `model` / `effort`,否则容易让 app-server 落到非原生协作路径。
|
||||||
|
|
||||||
|
### 3. 自定义跨会话能力走 MCP,不优先走 dynamicTools
|
||||||
|
|
||||||
|
- `ccweb_list_conversations`、`ccweb_send_message` 这类自定义能力,优先通过 `thread/start.config.mcp_servers.*` 挂载。
|
||||||
|
- 不要把它们当成 `dynamicTools` 主路径注入给 `codexapp`。
|
||||||
|
- 原因:
|
||||||
|
- `dynamicTools` 容易污染 app-server 原生工具集
|
||||||
|
- 会增加拿不到 `spawn_agent` / `wait_agent` / 原生协作工具的风险
|
||||||
|
|
||||||
|
### 4. MCP 配置要做成“线程级”而不是“进程级”
|
||||||
|
|
||||||
|
- `codexapp` 是长驻 app-server,不是一次一进程的 CLI 调用。
|
||||||
|
- 因此 `CC_WEB_SOURCE_SESSION_ID`、`CC_WEB_CROSS_HOP_COUNT` 这类来源上下文,不应只放在 app-server 进程全局环境里。
|
||||||
|
- 正确做法是随 `thread/start.config.mcp_servers.ccweb.env` 一起下发,让每个线程拿到自己的来源会话上下文。
|
||||||
|
|
||||||
|
### 5. guided input 依赖 plan 协作模式
|
||||||
|
|
||||||
|
- `request_user_input` 类能力默认按“协作 / plan 模式可用”来设计。
|
||||||
|
- Default / YOLO 模式下,不要假设引导输入一定可用。
|
||||||
|
- 如果要做降级:
|
||||||
|
- 优先退回普通文本交互
|
||||||
|
- 不要让整轮对话因为 guided input 不可用而直接失效
|
||||||
|
|
||||||
|
### 6. 能力降级要明确而保守
|
||||||
|
|
||||||
|
- 如果运行时拒绝 `collaborationMode`(例如 `unknown field collaborationMode` 或 `unsupported collaboration mode`):
|
||||||
|
- 应记录一次能力降级
|
||||||
|
- 后续本轮可退回普通 turn 发送
|
||||||
|
- 但如果只是 `goals` 或 `collaborationMode/list` 探测失败:
|
||||||
|
- 不应直接判定整个 app-server 不可用
|
||||||
|
|
||||||
|
### 7. 回归检查要覆盖协议形状,不只看 UI
|
||||||
|
|
||||||
|
后续涉及 `codexapp` 的改动,至少要检查这些点:
|
||||||
|
|
||||||
|
- `initialize` 后是否真的调用了:
|
||||||
|
- `experimentalFeature/enablement/set`
|
||||||
|
- `collaborationMode/list`
|
||||||
|
- `collaborationMode` 存在时:
|
||||||
|
- 顶层是否没有重复 `model`
|
||||||
|
- 顶层是否没有重复 `effort`
|
||||||
|
- `ccweb` 能力是否走 `mcpToolCall`,而不是再次退回 `dynamicToolCall`
|
||||||
|
- mock / regression 中是否覆盖了上述断言
|
||||||
|
|||||||
@@ -235,9 +235,10 @@ function createCodexAppRuntime(deps = {}) {
|
|||||||
if (!nextText) return '';
|
if (!nextText) return '';
|
||||||
if (!entry.agentMessageItems) entry.agentMessageItems = new Map();
|
if (!entry.agentMessageItems) entry.agentMessageItems = new Map();
|
||||||
const currentItemText = entry.agentMessageItems.get(itemId) || '';
|
const currentItemText = entry.agentMessageItems.get(itemId) || '';
|
||||||
|
const separator = agentMessageSeparator(entry, itemId, nextText);
|
||||||
entry.agentMessageItems.set(itemId, currentItemText + nextText);
|
entry.agentMessageItems.set(itemId, currentItemText + nextText);
|
||||||
entry.fullText = (entry.fullText || '') + nextText;
|
entry.fullText = (entry.fullText || '') + separator + nextText;
|
||||||
return nextText;
|
return separator + nextText;
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendAgentCompletedText(entry, item) {
|
function appendAgentCompletedText(entry, item) {
|
||||||
@@ -252,9 +253,18 @@ function createCodexAppRuntime(deps = {}) {
|
|||||||
return remainder;
|
return remainder;
|
||||||
}
|
}
|
||||||
if (currentItemText === text) return '';
|
if (currentItemText === text) return '';
|
||||||
|
const separator = agentMessageSeparator(entry, item.id, text);
|
||||||
entry.agentMessageItems.set(item.id, text);
|
entry.agentMessageItems.set(item.id, text);
|
||||||
entry.fullText = (entry.fullText || '') + text;
|
entry.fullText = (entry.fullText || '') + separator + text;
|
||||||
return text;
|
return separator + text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function agentMessageSeparator(entry, itemId, nextText) {
|
||||||
|
if (entry.agentMessageItems?.get(itemId)) return '';
|
||||||
|
const currentText = entry.fullText || '';
|
||||||
|
if (!/\S/.test(currentText)) return '';
|
||||||
|
const hasVisualBoundary = /\n\s*(?:---|\*\*\*|___)\s*$/.test(currentText) || /^\s*(?:---|\*\*\*|___)\s*\n/.test(String(nextText || ''));
|
||||||
|
return hasVisualBoundary ? '' : '\n\n---\n\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateToolResult(entry, sessionId, itemId, result, done = false, patch = {}) {
|
function updateToolResult(entry, sessionId, itemId, result, done = false, patch = {}) {
|
||||||
|
|||||||
351
public/app.js
351
public/app.js
@@ -122,6 +122,7 @@
|
|||||||
let noteMode = false;
|
let noteMode = false;
|
||||||
let noteDraftSeq = 0;
|
let noteDraftSeq = 0;
|
||||||
const pendingNotesByTarget = new Map();
|
const pendingNotesByTarget = new Map();
|
||||||
|
const userMessageIndex = new Map();
|
||||||
|
|
||||||
// --- DOM ---
|
// --- DOM ---
|
||||||
const $ = (sel) => document.querySelector(sel);
|
const $ = (sel) => document.querySelector(sel);
|
||||||
@@ -149,6 +150,8 @@
|
|||||||
const chatAgentMenu = $('#chat-agent-menu');
|
const chatAgentMenu = $('#chat-agent-menu');
|
||||||
const chatRuntimeState = $('#chat-runtime-state');
|
const chatRuntimeState = $('#chat-runtime-state');
|
||||||
const chatCwd = $('#chat-cwd');
|
const chatCwd = $('#chat-cwd');
|
||||||
|
const userOutlineBtn = $('#user-outline-btn');
|
||||||
|
const userOutlinePanel = $('#user-outline-panel');
|
||||||
const costDisplay = $('#cost-display');
|
const costDisplay = $('#cost-display');
|
||||||
const attachmentTray = $('#attachment-tray');
|
const attachmentTray = $('#attachment-tray');
|
||||||
const pendingNotesTray = $('#pending-notes-tray');
|
const pendingNotesTray = $('#pending-notes-tray');
|
||||||
@@ -682,6 +685,79 @@
|
|||||||
return value ? value.slice(0, 8) : '';
|
return value ? value.slice(0, 8) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shortMessagePreview(text, maxLength = 60) {
|
||||||
|
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
||||||
|
if (!value) return '空消息';
|
||||||
|
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLocalId(prefix = 'local') {
|
||||||
|
if (window.crypto?.randomUUID) return `${prefix}-${window.crypto.randomUUID()}`;
|
||||||
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearUserMessageIndex() {
|
||||||
|
userMessageIndex.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerUserMessage(messageId, element, content) {
|
||||||
|
if (!messageId || !element) return;
|
||||||
|
userMessageIndex.set(messageId, {
|
||||||
|
id: messageId,
|
||||||
|
element,
|
||||||
|
content: String(content || ''),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUserOutlinePanel() {
|
||||||
|
if (!userOutlinePanel || !userOutlineBtn) return;
|
||||||
|
const items = buildUserOutlineItems();
|
||||||
|
if (items.length === 0) {
|
||||||
|
userOutlinePanel.innerHTML = '<div class="user-outline-empty">暂无用户消息</div>';
|
||||||
|
userOutlineBtn.disabled = true;
|
||||||
|
} else {
|
||||||
|
userOutlinePanel.innerHTML = items.map((item, index) => `
|
||||||
|
<button type="button" class="user-outline-item" data-target="${escapeHtml(item.targetMessageId)}">
|
||||||
|
<span class="user-outline-index">${index + 1}</span>
|
||||||
|
<span class="user-outline-text">${escapeHtml(item.label)}</span>
|
||||||
|
</button>
|
||||||
|
`).join('');
|
||||||
|
userOutlineBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeUserOutlinePanel() {
|
||||||
|
if (!userOutlinePanel || !userOutlineBtn) return;
|
||||||
|
userOutlinePanel.hidden = true;
|
||||||
|
userOutlineBtn.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleUserOutlinePanel() {
|
||||||
|
if (!userOutlinePanel || !userOutlineBtn) return;
|
||||||
|
if (userOutlinePanel.hidden) {
|
||||||
|
updateUserOutlinePanel();
|
||||||
|
userOutlinePanel.hidden = false;
|
||||||
|
userOutlineBtn.setAttribute('aria-expanded', 'true');
|
||||||
|
} else {
|
||||||
|
closeUserOutlinePanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToMessage(anchorId) {
|
||||||
|
if (!anchorId) return;
|
||||||
|
const target = document.getElementById(anchorId);
|
||||||
|
if (!target) return;
|
||||||
|
target.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
function updateSessionIdBadge() {
|
function updateSessionIdBadge() {
|
||||||
if (!chatSessionIdBtn) return;
|
if (!chatSessionIdBtn) return;
|
||||||
if (!currentSessionId) {
|
if (!currentSessionId) {
|
||||||
@@ -2183,6 +2259,7 @@
|
|||||||
|
|
||||||
function resetChatView(agent) {
|
function resetChatView(agent) {
|
||||||
setCurrentAgent(agent);
|
setCurrentAgent(agent);
|
||||||
|
closeUserOutlinePanel();
|
||||||
closeFileBrowser();
|
closeFileBrowser();
|
||||||
currentSessionId = null;
|
currentSessionId = null;
|
||||||
loadedHistorySessionId = null;
|
loadedHistorySessionId = null;
|
||||||
@@ -2236,6 +2313,7 @@
|
|||||||
migratePendingNotesToSession(snapshot.sessionId, snapshotAgent);
|
migratePendingNotesToSession(snapshot.sessionId, snapshotAgent);
|
||||||
setCurrentSessionRunningState(snapshot.isRunning);
|
setCurrentSessionRunningState(snapshot.isRunning);
|
||||||
setStatsDisplay(snapshot);
|
setStatsDisplay(snapshot);
|
||||||
|
closeUserOutlinePanel();
|
||||||
currentCwd = snapshot.cwd || null;
|
currentCwd = snapshot.cwd || null;
|
||||||
updateCwdBadge();
|
updateCwdBadge();
|
||||||
if (snapshot.mode && MODE_LABELS[snapshot.mode]) {
|
if (snapshot.mode && MODE_LABELS[snapshot.mode]) {
|
||||||
@@ -2261,6 +2339,7 @@
|
|||||||
const targetAgent = normalizeAgent(agent);
|
const targetAgent = normalizeAgent(agent);
|
||||||
const { preserveCurrent = true, loadLast = true } = options;
|
const { preserveCurrent = true, loadLast = true } = options;
|
||||||
setCurrentAgent(targetAgent);
|
setCurrentAgent(targetAgent);
|
||||||
|
closeUserOutlinePanel();
|
||||||
renderSessionList();
|
renderSessionList();
|
||||||
|
|
||||||
const currentMeta = currentSessionId ? getSessionMeta(currentSessionId) : null;
|
const currentMeta = currentSessionId ? getSessionMeta(currentSessionId) : null;
|
||||||
@@ -2353,6 +2432,7 @@
|
|||||||
if (currentSessionId && currentSessionId !== sessionId) {
|
if (currentSessionId && currentSessionId !== sessionId) {
|
||||||
send({ type: 'detach_view' });
|
send({ type: 'detach_view' });
|
||||||
}
|
}
|
||||||
|
closeUserOutlinePanel();
|
||||||
clearSessionLoading();
|
clearSessionLoading();
|
||||||
touchSessionCache(sessionId);
|
touchSessionCache(sessionId);
|
||||||
applySessionSnapshot(snapshot, { immediate: true, suppressUnreadToast: true });
|
applySessionSnapshot(snapshot, { immediate: true, suppressUnreadToast: true });
|
||||||
@@ -2361,6 +2441,7 @@
|
|||||||
|
|
||||||
function openSession(sessionId, options = {}) {
|
function openSession(sessionId, options = {}) {
|
||||||
if (!sessionId) return;
|
if (!sessionId) return;
|
||||||
|
closeUserOutlinePanel();
|
||||||
if (options.forceSync) {
|
if (options.forceSync) {
|
||||||
beginSessionSwitch(sessionId, { blocking: options.blocking !== false, force: true, label: options.label });
|
beginSessionSwitch(sessionId, { blocking: options.blocking !== false, force: true, label: options.label });
|
||||||
return;
|
return;
|
||||||
@@ -3111,7 +3192,12 @@
|
|||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
const isCrossConversation = role === 'user' && !!meta.crossConversation;
|
const isCrossConversation = role === 'user' && !!meta.crossConversation;
|
||||||
const isCrossConversationReply = isCrossConversation && !!(meta.crossConversation.reply || meta.crossConversation.replyToRequestId);
|
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' : ''}`;
|
div.className = `msg ${role}${role === 'assistant' ? ' agent-' + currentAgent : ''}${isCrossConversation ? ' cross-conversation' : ''}${isCrossConversationReply ? ' cross-conversation-reply' : ''}`;
|
||||||
|
if (role === 'user') {
|
||||||
|
div.id = `hapi-message-${resolvedMessageId}`;
|
||||||
|
div.dataset.messageId = resolvedMessageId;
|
||||||
|
}
|
||||||
|
|
||||||
if (role === 'system') {
|
if (role === 'system') {
|
||||||
const bubble = document.createElement('div');
|
const bubble = document.createElement('div');
|
||||||
@@ -3172,6 +3258,23 @@
|
|||||||
textNode.style.whiteSpace = 'pre-wrap';
|
textNode.style.whiteSpace = 'pre-wrap';
|
||||||
textNode.textContent = content;
|
textNode.textContent = content;
|
||||||
bubble.appendChild(textNode);
|
bubble.appendChild(textNode);
|
||||||
|
|
||||||
|
const copyBtn = document.createElement('button');
|
||||||
|
copyBtn.type = 'button';
|
||||||
|
copyBtn.className = 'msg-copy-btn';
|
||||||
|
copyBtn.title = '复制用户消息';
|
||||||
|
copyBtn.setAttribute('aria-label', '复制用户消息');
|
||||||
|
copyBtn.innerHTML = `
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="9" y="9" width="10" height="10" rx="2"></rect>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"></path>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
copyBtn.addEventListener('click', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
copyTextToClipboard(content, '用户消息已复制');
|
||||||
|
});
|
||||||
|
bubble.appendChild(copyBtn);
|
||||||
}
|
}
|
||||||
if (attachments.length > 0) {
|
if (attachments.length > 0) {
|
||||||
bubble.insertAdjacentHTML('beforeend', renderAttachmentPreviews(attachments));
|
bubble.insertAdjacentHTML('beforeend', renderAttachmentPreviews(attachments));
|
||||||
@@ -3186,6 +3289,9 @@
|
|||||||
hydrateAttachmentPreviews(bubble, attachments);
|
hydrateAttachmentPreviews(bubble, attachments);
|
||||||
div.appendChild(avatar);
|
div.appendChild(avatar);
|
||||||
div.appendChild(bubble);
|
div.appendChild(bubble);
|
||||||
|
if (role === 'user') {
|
||||||
|
registerUserMessage(resolvedMessageId, div, content);
|
||||||
|
}
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3429,6 +3535,209 @@
|
|||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseMaybeJsonObject(value) {
|
||||||
|
if (value && typeof value === 'object') return value;
|
||||||
|
if (typeof value !== 'string') return null;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed);
|
||||||
|
return parsed && typeof parsed === 'object' ? parsed : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizePrompt(prompt) {
|
||||||
|
const text = typeof prompt === 'string' ? prompt.trim().replace(/\s+/g, ' ') : '';
|
||||||
|
if (!text) return '';
|
||||||
|
return text.length > 140 ? `${text.slice(0, 140)}…` : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || [],
|
||||||
|
prompt: inputData.prompt || resultData.prompt || '',
|
||||||
|
tool: inputData.tool || resultData.tool || tool?.name || '',
|
||||||
|
status: resultData.status || inputData.status || tool?.meta?.status || null,
|
||||||
|
};
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function effectiveObject(value) {
|
||||||
|
const parsed = parseMaybeJsonObject(value);
|
||||||
|
if (parsed && !Array.isArray(parsed)) return parsed;
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function collabAgentStateEntries(data) {
|
||||||
|
const states = data?.agentsStates;
|
||||||
|
if (!states || typeof states !== 'object') return [];
|
||||||
|
return Object.entries(states).map(([id, value], index) => {
|
||||||
|
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.summary || state.lastMessage || state.step || state.description || '').trim();
|
||||||
|
return { id, label, role, status, detail };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function collabStateTone(statusText) {
|
||||||
|
const normalized = String(statusText || '').toLowerCase();
|
||||||
|
if (!normalized) return 'pending';
|
||||||
|
if (/(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';
|
||||||
|
}
|
||||||
|
|
||||||
|
function collabStateLabel(statusText) {
|
||||||
|
const normalized = String(statusText || '').trim();
|
||||||
|
if (!normalized) return '等待中';
|
||||||
|
const lower = normalized.toLowerCase();
|
||||||
|
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 '进行中';
|
||||||
|
if (/(idle|pending|queued|waiting)/.test(lower)) return '等待中';
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCollabAgentToolElement(tool) {
|
||||||
|
const data = normalizeCollabAgentData(tool);
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'tool-call-content collab-agent-content';
|
||||||
|
|
||||||
|
const stack = document.createElement('div');
|
||||||
|
stack.className = 'collab-agent-stack';
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'collab-agent-header';
|
||||||
|
|
||||||
|
const titleWrap = document.createElement('div');
|
||||||
|
titleWrap.className = 'collab-agent-title-wrap';
|
||||||
|
|
||||||
|
const kicker = document.createElement('div');
|
||||||
|
kicker.className = 'collab-agent-kicker';
|
||||||
|
kicker.textContent = 'Codex App 子代理';
|
||||||
|
titleWrap.appendChild(kicker);
|
||||||
|
|
||||||
|
const title = document.createElement('div');
|
||||||
|
title.className = 'collab-agent-title';
|
||||||
|
title.textContent = data.tool || '协作任务';
|
||||||
|
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);
|
||||||
|
header.appendChild(titleWrap);
|
||||||
|
|
||||||
|
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);
|
||||||
|
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 item = document.createElement('div');
|
||||||
|
item.className = 'collab-agent-item';
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
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);
|
||||||
|
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) {
|
||||||
|
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 已复制');
|
||||||
|
});
|
||||||
|
item.appendChild(footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
stack.appendChild(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (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.title = `复制子代理线程 ID\n${threadId}`;
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
copyTextToClipboard(threadId, '子代理线程 ID 已复制');
|
||||||
|
});
|
||||||
|
threads.appendChild(btn);
|
||||||
|
});
|
||||||
|
stack.appendChild(threads);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!promptText && stateEntries.length === 0 && (!Array.isArray(data.receiverThreadIds) || data.receiverThreadIds.length === 0)) {
|
||||||
|
const empty = document.createElement('div');
|
||||||
|
empty.className = 'tool-call-empty';
|
||||||
|
empty.textContent = tool.done ? '子代理调用已结束,未返回结构化状态。' : '等待子代理状态…';
|
||||||
|
stack.appendChild(empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.appendChild(stack);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
function isGroupableToolCall(node) {
|
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');
|
||||||
}
|
}
|
||||||
@@ -3508,8 +3817,10 @@
|
|||||||
renderEpoch++;
|
renderEpoch++;
|
||||||
const epoch = renderEpoch;
|
const epoch = renderEpoch;
|
||||||
messagesDiv.innerHTML = '';
|
messagesDiv.innerHTML = '';
|
||||||
|
clearUserMessageIndex();
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
messagesDiv.innerHTML = buildWelcomeMarkup(currentAgent);
|
messagesDiv.innerHTML = buildWelcomeMarkup(currentAgent);
|
||||||
|
updateUserOutlinePanel();
|
||||||
renderPendingNotes({ scroll: false });
|
renderPendingNotes({ scroll: false });
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
return;
|
return;
|
||||||
@@ -3518,6 +3829,7 @@
|
|||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
messages.forEach((message) => frag.appendChild(buildMsgElement(message)));
|
messages.forEach((message) => frag.appendChild(buildMsgElement(message)));
|
||||||
messagesDiv.appendChild(frag);
|
messagesDiv.appendChild(frag);
|
||||||
|
updateUserOutlinePanel();
|
||||||
renderPendingNotes({ scroll: false });
|
renderPendingNotes({ scroll: false });
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
return;
|
return;
|
||||||
@@ -3540,6 +3852,7 @@
|
|||||||
const frag0 = document.createDocumentFragment();
|
const frag0 = document.createDocumentFragment();
|
||||||
for (let i = batches[0][0]; i < batches[0][1]; i++) frag0.appendChild(buildMsgElement(messages[i]));
|
for (let i = batches[0][0]; i < batches[0][1]; i++) frag0.appendChild(buildMsgElement(messages[i]));
|
||||||
messagesDiv.appendChild(frag0);
|
messagesDiv.appendChild(frag0);
|
||||||
|
updateUserOutlinePanel();
|
||||||
renderPendingNotes({ scroll: false });
|
renderPendingNotes({ scroll: false });
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
@@ -3556,6 +3869,7 @@
|
|||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
for (let i = start; i < end; i++) frag.appendChild(buildMsgElement(messages[i]));
|
for (let i = start; i < end; i++) frag.appendChild(buildMsgElement(messages[i]));
|
||||||
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
|
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
|
||||||
|
updateUserOutlinePanel();
|
||||||
// Compensate scrollTop so visible area stays unchanged
|
// Compensate scrollTop so visible area stays unchanged
|
||||||
messagesDiv.scrollTop = prevScrollTop + (messagesDiv.scrollHeight - prevHeight);
|
messagesDiv.scrollTop = prevScrollTop + (messagesDiv.scrollHeight - prevHeight);
|
||||||
updateScrollbar();
|
updateScrollbar();
|
||||||
@@ -3573,12 +3887,14 @@
|
|||||||
messages.forEach((m) => frag.appendChild(buildMsgElement(m)));
|
messages.forEach((m) => frag.appendChild(buildMsgElement(m)));
|
||||||
if (!preserveScroll) {
|
if (!preserveScroll) {
|
||||||
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
|
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
|
||||||
|
updateUserOutlinePanel();
|
||||||
if (!skipScrollbar) updateScrollbar();
|
if (!skipScrollbar) updateScrollbar();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const prevHeight = messagesDiv.scrollHeight;
|
const prevHeight = messagesDiv.scrollHeight;
|
||||||
const prevScrollTop = messagesDiv.scrollTop;
|
const prevScrollTop = messagesDiv.scrollTop;
|
||||||
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
|
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
|
||||||
|
updateUserOutlinePanel();
|
||||||
messagesDiv.scrollTop = prevScrollTop + (messagesDiv.scrollHeight - prevHeight);
|
messagesDiv.scrollTop = prevScrollTop + (messagesDiv.scrollHeight - prevHeight);
|
||||||
if (!skipScrollbar) updateScrollbar();
|
if (!skipScrollbar) updateScrollbar();
|
||||||
}
|
}
|
||||||
@@ -3754,6 +4070,10 @@
|
|||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (kind === 'collab_agent_tool_call') {
|
||||||
|
return createCollabAgentToolElement(tool);
|
||||||
|
}
|
||||||
|
|
||||||
if (effectiveName === 'AskUserQuestion') {
|
if (effectiveName === 'AskUserQuestion') {
|
||||||
const questions = extractAskUserQuestions(effectiveInput);
|
const questions = extractAskUserQuestions(effectiveInput);
|
||||||
if (questions.length > 0) {
|
if (questions.length > 0) {
|
||||||
@@ -4691,7 +5011,11 @@
|
|||||||
function submitUserMessage(text, attachments = []) {
|
function submitUserMessage(text, attachments = []) {
|
||||||
const welcome = messagesDiv.querySelector('.welcome-msg');
|
const welcome = messagesDiv.querySelector('.welcome-msg');
|
||||||
if (welcome) welcome.remove();
|
if (welcome) welcome.remove();
|
||||||
messagesDiv.appendChild(createMsgElement('user', text, attachments));
|
const messageId = createLocalId('user');
|
||||||
|
const element = createMsgElement('user', text, attachments, { messageId });
|
||||||
|
messagesDiv.appendChild(element);
|
||||||
|
registerUserMessage(messageId, element, text);
|
||||||
|
updateUserOutlinePanel();
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
send({ type: 'message', text, attachments, sessionId: currentSessionId, mode: currentMode, agent: currentAgent });
|
send({ type: 'message', text, attachments, sessionId: currentSessionId, mode: currentMode, agent: currentAgent });
|
||||||
@@ -4730,7 +5054,11 @@
|
|||||||
appendError('Codex App 运行中暂不支持 slash 指令插入。');
|
appendError('Codex App 运行中暂不支持 slash 指令插入。');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
messagesDiv.appendChild(createMsgElement('user', text, []));
|
const messageId = createLocalId('user');
|
||||||
|
const element = createMsgElement('user', text, [], { messageId });
|
||||||
|
messagesDiv.appendChild(element);
|
||||||
|
registerUserMessage(messageId, element, text);
|
||||||
|
updateUserOutlinePanel();
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
send({ type: 'message', text, sessionId: currentSessionId, mode: currentMode, agent: currentAgent });
|
send({ type: 'message', text, sessionId: currentSessionId, mode: currentMode, agent: currentAgent });
|
||||||
msgInput.value = '';
|
msgInput.value = '';
|
||||||
@@ -4827,6 +5155,20 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userOutlineBtn && userOutlinePanel) {
|
||||||
|
userOutlineBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleUserOutlinePanel();
|
||||||
|
});
|
||||||
|
userOutlinePanel.addEventListener('click', (e) => {
|
||||||
|
const target = e.target instanceof HTMLElement ? e.target.closest('.user-outline-item') : null;
|
||||||
|
if (!target) return;
|
||||||
|
const anchorId = target.getAttribute('data-target') || '';
|
||||||
|
closeUserOutlinePanel();
|
||||||
|
scrollToMessage(anchorId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Split new-chat button
|
// Split new-chat button
|
||||||
newChatBtn.addEventListener('click', () => showNewSessionModal());
|
newChatBtn.addEventListener('click', () => showNewSessionModal());
|
||||||
newChatArrow.addEventListener('click', (e) => {
|
newChatArrow.addEventListener('click', (e) => {
|
||||||
@@ -4854,6 +5196,11 @@
|
|||||||
e.target !== chatAgentBtn) {
|
e.target !== chatAgentBtn) {
|
||||||
closeAgentMenu();
|
closeAgentMenu();
|
||||||
}
|
}
|
||||||
|
if (userOutlinePanel && !userOutlinePanel.hidden &&
|
||||||
|
!userOutlinePanel.contains(e.target) &&
|
||||||
|
e.target !== userOutlineBtn) {
|
||||||
|
closeUserOutlinePanel();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
sendBtn.addEventListener('click', sendMessage);
|
sendBtn.addEventListener('click', sendMessage);
|
||||||
if (noteModeBtn) {
|
if (noteModeBtn) {
|
||||||
|
|||||||
@@ -61,20 +61,15 @@
|
|||||||
<button id="menu-btn" class="menu-btn" title="菜单">☰</button>
|
<button id="menu-btn" class="menu-btn" title="菜单">☰</button>
|
||||||
<span id="chat-title" class="chat-title">新会话</span>
|
<span id="chat-title" class="chat-title">新会话</span>
|
||||||
<button id="chat-session-id-btn" class="chat-session-id-btn" type="button" title="复制当前会话 ID" hidden>ID</button>
|
<button id="chat-session-id-btn" class="chat-session-id-btn" type="button" title="复制当前会话 ID" hidden>ID</button>
|
||||||
|
<div class="chat-agent-picker">
|
||||||
<button id="chat-agent-btn" class="chat-agent-btn" type="button" aria-haspopup="menu" aria-expanded="false">Claude</button>
|
<button id="chat-agent-btn" class="chat-agent-btn" type="button" aria-haspopup="menu" aria-expanded="false">Claude</button>
|
||||||
<div id="chat-agent-menu" class="chat-agent-menu" hidden>
|
<div id="chat-agent-menu" class="chat-agent-menu" hidden>
|
||||||
<button type="button" class="chat-agent-option active" data-agent="claude">Claude</button>
|
<button type="button" class="chat-agent-option active" data-agent="claude">Claude</button>
|
||||||
<button type="button" class="chat-agent-option" data-agent="codex">Codex</button>
|
<button type="button" class="chat-agent-option" data-agent="codex">Codex</button>
|
||||||
<button type="button" class="chat-agent-option" data-agent="codexapp">Codex App</button>
|
<button type="button" class="chat-agent-option" data-agent="codexapp">Codex App</button>
|
||||||
</div>
|
</div>
|
||||||
<span id="chat-runtime-state" class="chat-runtime-state" hidden>运行中</span>
|
</div>
|
||||||
<button id="chat-cwd" class="chat-cwd" type="button" hidden></button>
|
<button id="chat-cwd" class="chat-cwd" type="button" hidden></button>
|
||||||
<select id="mode-select" class="mode-select" title="权限模式">
|
|
||||||
<option value="yolo">YOLO</option>
|
|
||||||
<option value="default">默认</option>
|
|
||||||
<option value="plan">Plan</option>
|
|
||||||
</select>
|
|
||||||
<span id="cost-display" class="cost-display" hidden></span>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="messages-wrap">
|
<div class="messages-wrap">
|
||||||
@@ -94,6 +89,19 @@
|
|||||||
<div id="cmd-menu" class="cmd-menu" hidden></div>
|
<div id="cmd-menu" class="cmd-menu" hidden></div>
|
||||||
|
|
||||||
<div class="input-area">
|
<div class="input-area">
|
||||||
|
<div class="chat-controls" aria-label="会话控制">
|
||||||
|
<span id="chat-runtime-state" class="chat-runtime-state" hidden>运行中</span>
|
||||||
|
<select id="mode-select" class="mode-select" title="权限模式">
|
||||||
|
<option value="yolo">YOLO</option>
|
||||||
|
<option value="default">默认</option>
|
||||||
|
<option value="plan">Plan</option>
|
||||||
|
</select>
|
||||||
|
<div class="user-outline-anchor">
|
||||||
|
<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>
|
||||||
|
<span id="cost-display" class="cost-display" hidden></span>
|
||||||
|
</div>
|
||||||
<div id="attachment-tray" class="attachment-tray" hidden></div>
|
<div id="attachment-tray" class="attachment-tray" hidden></div>
|
||||||
<div id="pending-notes-tray" class="pending-notes-tray" hidden></div>
|
<div id="pending-notes-tray" class="pending-notes-tray" hidden></div>
|
||||||
<div class="input-wrapper">
|
<div class="input-wrapper">
|
||||||
|
|||||||
361
public/style.css
361
public/style.css
@@ -887,10 +887,19 @@ body.session-loading-active {
|
|||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
.chat-controls {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto 8px;
|
||||||
|
}
|
||||||
.menu-btn {
|
.menu-btn {
|
||||||
display: none;
|
display: none;
|
||||||
background: none;
|
background: none;
|
||||||
@@ -919,7 +928,7 @@ body.session-loading-active {
|
|||||||
.chat-title:hover { background: var(--bg-tertiary); }
|
.chat-title:hover { background: var(--bg-tertiary); }
|
||||||
.chat-session-id-btn {
|
.chat-session-id-btn {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
max-width: 112px;
|
max-width: 120px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -937,6 +946,19 @@ body.session-loading-active {
|
|||||||
background: rgba(91, 126, 161, 0.16);
|
background: rgba(91, 126, 161, 0.16);
|
||||||
border-color: rgba(91, 126, 161, 0.34);
|
border-color: rgba(91, 126, 161, 0.34);
|
||||||
}
|
}
|
||||||
|
.chat-session-id-btn:disabled,
|
||||||
|
.chat-agent-btn:disabled,
|
||||||
|
.chat-cwd:disabled,
|
||||||
|
.mode-select:disabled,
|
||||||
|
.user-outline-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.chat-agent-picker {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.chat-agent-btn {
|
.chat-agent-btn {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -964,8 +986,8 @@ body.session-loading-active {
|
|||||||
}
|
}
|
||||||
.chat-agent-menu {
|
.chat-agent-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% - 6px);
|
top: calc(100% + 6px);
|
||||||
right: 16px;
|
left: 0;
|
||||||
min-width: 148px;
|
min-width: 148px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
@@ -1027,6 +1049,78 @@ body.session-loading-active {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
.cost-display:empty { display: none; }
|
.cost-display:empty { display: none; }
|
||||||
|
.user-outline-btn {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid rgba(91, 126, 161, 0.22);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(91, 126, 161, 0.08);
|
||||||
|
color: var(--info);
|
||||||
|
padding: 4px 10px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.user-outline-anchor {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.user-outline-btn:hover {
|
||||||
|
background: rgba(91, 126, 161, 0.16);
|
||||||
|
border-color: rgba(91, 126, 161, 0.34);
|
||||||
|
}
|
||||||
|
.user-outline-panel {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: calc(100% + 8px);
|
||||||
|
width: min(360px, calc(100vw - 24px));
|
||||||
|
max-height: min(48vh, 360px);
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 252, 248, 0.98);
|
||||||
|
box-shadow: 0 12px 28px rgba(45, 31, 20, 0.08);
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 70;
|
||||||
|
}
|
||||||
|
.user-outline-empty {
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.user-outline-item {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 24px minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.user-outline-item:hover {
|
||||||
|
background: var(--accent-light);
|
||||||
|
}
|
||||||
|
.user-outline-index {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.user-outline-text {
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mode selector */
|
/* Mode selector */
|
||||||
.mode-select {
|
.mode-select {
|
||||||
@@ -1203,6 +1297,7 @@ body.session-loading-active {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.msg-bubble {
|
.msg-bubble {
|
||||||
|
position: relative;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
@@ -1334,6 +1429,32 @@ body.session-loading-active {
|
|||||||
background: var(--bg-bubble-user);
|
background: var(--bg-bubble-user);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-bottom-right-radius: 4px;
|
border-bottom-right-radius: 4px;
|
||||||
|
padding-right: 42px;
|
||||||
|
}
|
||||||
|
.msg-copy-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
color: rgba(255, 255, 255, 0.86);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease, background 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
.msg.user:hover .msg-copy-btn,
|
||||||
|
.msg.user:focus-within .msg-copy-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.msg-copy-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-color: rgba(255, 255, 255, 0.28);
|
||||||
}
|
}
|
||||||
.msg.user.cross-conversation .msg-avatar {
|
.msg.user.cross-conversation .msg-avatar {
|
||||||
background: var(--info);
|
background: var(--info);
|
||||||
@@ -1625,6 +1746,11 @@ body.session-loading-active {
|
|||||||
.tool-call.codex-file-change {
|
.tool-call.codex-file-change {
|
||||||
border-color: rgba(93, 138, 84, 0.24);
|
border-color: rgba(93, 138, 84, 0.24);
|
||||||
}
|
}
|
||||||
|
.tool-call.codex-collab-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 summary {
|
.tool-call summary {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -1729,6 +1855,14 @@ body.session-loading-active {
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
.tool-call-content.collab-agent-content {
|
||||||
|
font-family: inherit;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: normal;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(252, 253, 255, 0.96), rgba(242, 247, 252, 0.98));
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
.tool-call-content.todo-list-content .todo-list-container {
|
.tool-call-content.todo-list-content .todo-list-container {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -2354,13 +2488,17 @@ body.session-loading-active {
|
|||||||
.option-picker { left: 10px; right: 10px; transform: none; min-width: auto; bottom: 72px; }
|
.option-picker { left: 10px; right: 10px; transform: none; min-width: auto; bottom: 72px; }
|
||||||
.chat-header {
|
.chat-header {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
.chat-title {
|
.chat-title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
.chat-controls {
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
.chat-session-id-btn {
|
.chat-session-id-btn {
|
||||||
max-width: 82px;
|
max-width: 92px;
|
||||||
padding-left: 7px;
|
padding-left: 7px;
|
||||||
padding-right: 7px;
|
padding-right: 7px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
@@ -2369,7 +2507,7 @@ body.session-loading-active {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
.chat-agent-menu {
|
.chat-agent-menu {
|
||||||
right: 10px;
|
left: 0;
|
||||||
min-width: 138px;
|
min-width: 138px;
|
||||||
}
|
}
|
||||||
.chat-runtime-state {
|
.chat-runtime-state {
|
||||||
@@ -2377,13 +2515,24 @@ body.session-loading-active {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
.chat-cwd {
|
.chat-cwd {
|
||||||
max-width: 92px;
|
max-width: 98px;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
.mode-select {
|
.mode-select {
|
||||||
padding: 4px 20px 4px 8px;
|
padding: 4px 20px 4px 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
.user-outline-btn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.user-outline-text {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.user-outline-panel {
|
||||||
|
right: 0;
|
||||||
|
width: min(320px, calc(100vw - 20px));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
@@ -2406,10 +2555,55 @@ body.session-loading-active {
|
|||||||
.new-chat-arrow { min-height: 44px; }
|
.new-chat-arrow { min-height: 44px; }
|
||||||
.new-chat-arrow { width: 48px; }
|
.new-chat-arrow { width: 48px; }
|
||||||
.chat-cwd {
|
.chat-cwd {
|
||||||
|
max-width: 76px;
|
||||||
|
padding-left: 6px;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
.chat-controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.chat-cwd,
|
||||||
|
.mode-select,
|
||||||
|
.user-outline-btn,
|
||||||
|
.chat-runtime-state {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.chat-header {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.chat-title {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.chat-session-id-btn {
|
||||||
max-width: 72px;
|
max-width: 72px;
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
}
|
}
|
||||||
|
.chat-agent-btn {
|
||||||
|
max-width: 96px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
.chat-agent-menu {
|
||||||
|
left: 0;
|
||||||
|
right: auto;
|
||||||
|
}
|
||||||
|
.user-outline-panel {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: auto;
|
||||||
|
max-height: min(44vh, 300px);
|
||||||
|
bottom: calc(100% + 6px);
|
||||||
|
}
|
||||||
|
.msg-copy-btn {
|
||||||
|
opacity: 1;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Utility === */
|
/* === Utility === */
|
||||||
@@ -3360,6 +3554,150 @@ html[data-theme='coolvibe'] .settings-back:hover {
|
|||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collab-agent-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.collab-agent-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.collab-agent-title-wrap {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.collab-agent-kicker {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--info);
|
||||||
|
}
|
||||||
|
.collab-agent-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.collab-agent-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.collab-agent-overall-status,
|
||||||
|
.collab-agent-item-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.collab-agent-overall-status.running,
|
||||||
|
.collab-agent-item-status.running {
|
||||||
|
background: rgba(232, 190, 92, 0.16);
|
||||||
|
color: #9a6f14;
|
||||||
|
}
|
||||||
|
.collab-agent-overall-status.done,
|
||||||
|
.collab-agent-item-status.done {
|
||||||
|
background: rgba(93, 138, 84, 0.14);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
.collab-agent-overall-status.error,
|
||||||
|
.collab-agent-item-status.error {
|
||||||
|
background: rgba(192, 85, 58, 0.14);
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
.collab-agent-overall-status.pending,
|
||||||
|
.collab-agent-item-status.pending {
|
||||||
|
background: rgba(91, 126, 161, 0.12);
|
||||||
|
color: var(--info);
|
||||||
|
}
|
||||||
|
.collab-agent-prompt {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid rgba(91, 126, 161, 0.16);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.collab-agent-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
.collab-agent-item-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.collab-agent-item-label {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
.collab-agent-item-role {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--info);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.collab-agent-item-detail {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.collab-agent-item-footer {
|
||||||
|
align-self: flex-start;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.collab-agent-item-footer:hover {
|
||||||
|
color: var(--info);
|
||||||
|
}
|
||||||
|
.collab-agent-threads {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.collab-agent-thread-chip {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid rgba(91, 126, 161, 0.18);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
color: var(--info);
|
||||||
|
padding: 4px 10px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.collab-agent-thread-chip:hover {
|
||||||
|
background: rgba(91, 126, 161, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
/* Todo List */
|
/* Todo List */
|
||||||
.todo-list-container {
|
.todo-list-container {
|
||||||
margin: 12px 0;
|
margin: 12px 0;
|
||||||
@@ -3400,6 +3738,13 @@ html[data-theme='coolvibe'] .settings-back:hover {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.collab-agent-header,
|
||||||
|
.collab-agent-item-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
.import-item-btn:hover { background: var(--accent-hover); }
|
.import-item-btn:hover { background: var(--accent-hover); }
|
||||||
|
|
||||||
/* === File Browser === */
|
/* === File Browser === */
|
||||||
|
|||||||
@@ -161,6 +161,69 @@ function completeTurn(thread, turnId, text, status = 'completed') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (/subagent|collab/i.test(text)) {
|
||||||
|
send({
|
||||||
|
method: 'item/started',
|
||||||
|
params: {
|
||||||
|
threadId: thread.id,
|
||||||
|
turnId,
|
||||||
|
startedAtMs: Date.now(),
|
||||||
|
item: {
|
||||||
|
id: 'tool-collab',
|
||||||
|
type: 'collabAgentToolCall',
|
||||||
|
tool: 'spawn_agent',
|
||||||
|
prompt: '整理前端改动并回报状态',
|
||||||
|
receiverThreadIds: ['child-thread-a', 'child-thread-b'],
|
||||||
|
agentsStates: {
|
||||||
|
'child-thread-a': {
|
||||||
|
name: '实现代理',
|
||||||
|
role: 'frontend',
|
||||||
|
status: 'in_progress',
|
||||||
|
summary: '正在补充结构化渲染与样式',
|
||||||
|
},
|
||||||
|
'child-thread-b': {
|
||||||
|
name: '验证代理',
|
||||||
|
role: 'qa',
|
||||||
|
status: 'completed',
|
||||||
|
summary: '已完成基础事件链检查',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: 'inProgress',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
send({
|
||||||
|
method: 'item/completed',
|
||||||
|
params: {
|
||||||
|
threadId: thread.id,
|
||||||
|
turnId,
|
||||||
|
completedAtMs: Date.now(),
|
||||||
|
item: {
|
||||||
|
id: 'tool-collab',
|
||||||
|
type: 'collabAgentToolCall',
|
||||||
|
tool: 'spawn_agent',
|
||||||
|
prompt: '整理前端改动并回报状态',
|
||||||
|
receiverThreadIds: ['child-thread-a', 'child-thread-b'],
|
||||||
|
agentsStates: {
|
||||||
|
'child-thread-a': {
|
||||||
|
name: '实现代理',
|
||||||
|
role: 'frontend',
|
||||||
|
status: 'completed',
|
||||||
|
summary: '结构化渲染已完成',
|
||||||
|
},
|
||||||
|
'child-thread-b': {
|
||||||
|
name: '验证代理',
|
||||||
|
role: 'qa',
|
||||||
|
status: 'completed',
|
||||||
|
summary: '事件链与持久化检查通过',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: 'completed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
send({
|
send({
|
||||||
method: 'thread/tokenUsage/updated',
|
method: 'thread/tokenUsage/updated',
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -811,6 +811,17 @@ async function main() {
|
|||||||
assert(/"hasCcwebMcpConfig": true/.test(codexAppDynamicTool.result || ''), 'Codex App thread/start should pass ccweb MCP config');
|
assert(/"hasCcwebMcpConfig": true/.test(codexAppDynamicTool.result || ''), 'Codex App thread/start should pass ccweb MCP config');
|
||||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({ type: 'message', text: 'codexapp subagent prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||||
|
const codexAppCollabTool = await nextMessage(messages, ws, (msg) => msg.type === 'tool_end' && msg.sessionId === codexAppSession.sessionId && msg.toolUseId === 'tool-collab');
|
||||||
|
assert(codexAppCollabTool.kind === 'collab_agent_tool_call', 'Codex App should surface collab agent tool calls');
|
||||||
|
assert(/child-thread-a/.test(codexAppCollabTool.result || ''), 'Codex App collab tool should include child thread ids');
|
||||||
|
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||||
|
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
|
||||||
|
const hasCollabTool = storedCodexApp.messages
|
||||||
|
.flatMap((message) => Array.isArray(message.toolCalls) ? message.toolCalls : [])
|
||||||
|
.some((tool) => tool.kind === 'collab_agent_tool_call');
|
||||||
|
assert(hasCollabTool, 'Codex App collab tool should be persisted into session history');
|
||||||
|
|
||||||
ws.send(JSON.stringify({ type: 'message', text: 'codexapp collaboration plan probe', sessionId: codexAppSession.sessionId, mode: 'plan', agent: 'codexapp' }));
|
ws.send(JSON.stringify({ type: 'message', text: 'codexapp collaboration plan probe', sessionId: codexAppSession.sessionId, mode: 'plan', agent: 'codexapp' }));
|
||||||
const codexAppPlanCollab = await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppSession.sessionId && /collaboration mode:/.test(msg.text || ''));
|
const codexAppPlanCollab = await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppSession.sessionId && /collaboration mode:/.test(msg.text || ''));
|
||||||
assert(/"mode":"plan"/.test(codexAppPlanCollab.text || ''), 'Codex App Plan mode should pass plan collaboration mode');
|
assert(/"mode":"plan"/.test(codexAppPlanCollab.text || ''), 'Codex App Plan mode should pass plan collaboration mode');
|
||||||
|
|||||||
Reference in New Issue
Block a user