fix: improve codex tool call live updates

This commit is contained in:
shiyue
2026-03-30 10:23:51 +08:00
parent 34e42b3254
commit 2e2dc21047
3 changed files with 228 additions and 30 deletions

View File

@@ -334,6 +334,26 @@ function createAgentRuntime(deps) {
break;
}
case 'item.updated': {
const item = event.item;
if (!item || !item.id || item.type === 'agent_message') break;
const tc = ensureCodexToolCall(entry, item);
const resultText = codexToolResult(item).slice(0, 2000);
tc.done = false;
tc.result = resultText;
wsSend(entry.ws, {
type: 'tool_update',
toolUseId: item.id,
name: tc.name,
input: tc.input,
result: resultText,
kind: tc.kind,
meta: tc.meta,
});
break;
}
case 'item.completed': {
const item = event.item;
if (!item || !item.id) break;
@@ -361,6 +381,7 @@ function createAgentRuntime(deps) {
const resultText = codexToolResult(item).slice(0, 2000);
tc.done = true;
tc.result = resultText;
wsSend(entry.ws, {
type: 'tool_end',
toolUseId: item.id,

View File

@@ -84,6 +84,8 @@
let pendingText = '';
let renderTimer = null;
let activeToolCalls = new Map();
let activeTodoCallTargets = new Map();
let toolDomSeq = 0;
let toolGroupCount = 0; // 当前 .msg-tools 直接子节点数(含已有父目录)
let hasGrouped = false; // 本次输出是否已触发过折叠
let cmdMenuIndex = -1;
@@ -1365,6 +1367,7 @@
pendingAttachments = [];
uploadingAttachments = [];
activeToolCalls.clear();
activeTodoCallTargets.clear();
sendBtn.hidden = false;
abortBtn.hidden = true;
chatTitle.textContent = '新会话';
@@ -1387,6 +1390,7 @@
abortBtn.hidden = true;
pendingText = '';
activeToolCalls.clear();
activeTodoCallTargets.clear();
}
currentSessionId = snapshot.sessionId;
loadedHistorySessionId = snapshot.sessionId;
@@ -1814,6 +1818,27 @@
appendToolCall(msg.toolUseId, msg.name, msg.input, false, msg.kind || null, msg.meta || null);
break;
case 'tool_update':
if (!isGenerating) startGenerating();
if (!activeToolCalls.has(msg.toolUseId)) {
activeToolCalls.set(msg.toolUseId, {
name: msg.name,
input: msg.input,
kind: msg.kind || null,
meta: msg.meta || null,
done: false,
});
appendToolCall(msg.toolUseId, msg.name, msg.input, false, msg.kind || null, msg.meta || null);
}
activeToolCalls.get(msg.toolUseId).done = false;
if (msg.name) activeToolCalls.get(msg.toolUseId).name = msg.name;
if (msg.input !== undefined) activeToolCalls.get(msg.toolUseId).input = msg.input;
if (msg.kind) activeToolCalls.get(msg.toolUseId).kind = msg.kind;
if (msg.meta) activeToolCalls.get(msg.toolUseId).meta = msg.meta;
activeToolCalls.get(msg.toolUseId).result = msg.result;
updateToolCall(msg.toolUseId, msg.result, false);
break;
case 'tool_end':
if (activeToolCalls.has(msg.toolUseId)) {
activeToolCalls.get(msg.toolUseId).done = true;
@@ -1821,7 +1846,7 @@
if (msg.meta) activeToolCalls.get(msg.toolUseId).meta = msg.meta;
activeToolCalls.get(msg.toolUseId).result = msg.result;
}
updateToolCall(msg.toolUseId, msg.result);
updateToolCall(msg.toolUseId, msg.result, true);
break;
case 'cost':
@@ -1880,6 +1905,7 @@
toolGroupCount = 0;
hasGrouped = false;
activeToolCalls.clear();
activeTodoCallTargets.clear();
const toolsDiv = document.querySelector('#streaming-msg .msg-tools');
if (toolsDiv) toolsDiv.innerHTML = '';
}
@@ -1896,8 +1922,8 @@
done: tc.done,
});
appendToolCall(tc.id, tc.name, tc.input, tc.done, tc.kind || null, tc.meta || null);
if (tc.done && tc.result) {
updateToolCall(tc.id, tc.result);
if (tc.result !== undefined && tc.result !== null) {
updateToolCall(tc.id, tc.result, !!tc.done);
}
}
}
@@ -2015,6 +2041,7 @@
pendingText = '';
window.pendingContentBlocks = [];
activeToolCalls.clear();
activeTodoCallTargets.clear();
toolGroupCount = 0;
hasGrouped = false;
sendBtn.hidden = true;
@@ -2062,7 +2089,7 @@
if (hasGrouped) {
const toolsDiv = streamEl.querySelector('.msg-tools');
if (toolsDiv) {
const loose = Array.from(toolsDiv.children).filter(c => c.classList.contains('tool-call'));
const loose = Array.from(toolsDiv.children).filter(isGroupableToolCall);
if (loose.length > 0) {
let group = toolsDiv.querySelector(':scope > .tool-group');
if (!group) {
@@ -2088,6 +2115,7 @@
if (sessionId) currentSessionId = sessionId;
pendingText = '';
activeToolCalls.clear();
activeTodoCallTargets.clear();
toolGroupCount = 0;
hasGrouped = false;
}
@@ -2197,7 +2225,7 @@
}
return;
}
} catch {}
} catch (e) {}
}
// 尝试直接解析 JSON
@@ -2209,7 +2237,7 @@
bubble.appendChild(createTodoListElement(parsed));
return;
}
} catch {}
} catch (e) {}
}
bubble.innerHTML = renderMarkdown(content);
return;
@@ -2270,17 +2298,52 @@
return tool?.kind || tool?.meta?.kind || '';
}
function toolTitle(tool) {
if (tool?.meta?.title) return tool.meta.title;
if (toolKind(tool) === 'file_change') {
const filePath = tool?.meta?.subtitle || tool?.input?.file_path || '';
const action = tool?.input?.new_string && tool?.input?.old_string ? '更新' : '创建';
return filePath ? `${action} ${filePath}` : 'File Change';
function normalizeDisplayPath(filePath) {
const rawPath = typeof filePath === 'string' ? filePath.trim() : '';
if (!rawPath) return '';
const normalizedPath = rawPath.replace(/\\/g, '/');
const normalizedCwd = typeof currentCwd === 'string' ? currentCwd.trim().replace(/\\/g, '/') : '';
if (normalizedCwd && normalizedPath.startsWith(`${normalizedCwd}/`)) {
return normalizedPath.slice(normalizedCwd.length + 1);
}
return normalizedPath;
}
function getFileChangeDisplay(tool) {
const changes = Array.isArray(tool?.input?.changes) ? tool.input.changes : [];
const primaryChange = changes[0] || null;
const rawPath = tool?.meta?.subtitle || primaryChange?.path || tool?.input?.path || tool?.input?.file_path || '';
const filePath = normalizeDisplayPath(rawPath);
const rawKind = primaryChange?.kind || tool?.input?.kind || '';
let action = '修改';
if (rawKind === 'create' || rawKind === 'new') {
action = '创建';
} else if (rawKind === 'delete' || rawKind === 'remove') {
action = '删除';
} else if (rawKind === 'update' || rawKind === 'edit') {
action = '更新';
} else if (tool?.input?.new_string && tool?.input?.old_string) {
action = '更新';
}
return { action, filePath, changeCount: changes.length };
}
function toolTitle(tool) {
if (toolKind(tool) === 'file_change') {
const { action, filePath, changeCount } = getFileChangeDisplay(tool);
if (filePath && changeCount > 1) return `${action} ${filePath}${changeCount} 个文件`;
if (filePath) return `${action} ${filePath}`;
if (changeCount > 1) return `修改 ${changeCount} 个文件`;
return tool?.meta?.title || 'File Change';
}
if (tool?.meta?.title) return tool.meta.title;
return tool?.name || 'Tool';
}
function toolSubtitle(tool) {
if (toolKind(tool) === 'file_change') {
return '';
}
if (tool?.meta?.subtitle) return tool.meta.subtitle;
if (toolKind(tool) === 'command_execution') {
return tool?.input?.command || '';
@@ -2357,6 +2420,33 @@
return section;
}
function isGroupableToolCall(node) {
return !!(node?.classList?.contains('tool-call') && node.dataset.toolKind !== 'todo_list');
}
function rememberToolCallTarget(toolUseId, tool, element) {
if (!element) return;
const entry = activeToolCalls.get(toolUseId);
if (entry) {
entry.domElement = element;
}
if (toolKind(tool) === 'todo_list' && tool?.input?.id) {
activeTodoCallTargets.set(tool.input.id, element);
}
}
function getLatestAssistantToolScope() {
const streamEl = document.getElementById('streaming-msg');
if (streamEl) return streamEl;
const agentSelector = `.msg.assistant.agent-${normalizeAgent(currentAgent)}`;
const assistantMessages = messagesDiv.querySelectorAll(agentSelector);
if (assistantMessages.length > 0) {
return assistantMessages[assistantMessages.length - 1];
}
const fallbackMessages = messagesDiv.querySelectorAll('.msg.assistant');
return fallbackMessages.length > 0 ? fallbackMessages[fallbackMessages.length - 1] : null;
}
function buildMsgElement(m) {
const el = createMsgElement(m.role, m.content, m.attachments || []);
if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {
@@ -2367,7 +2457,7 @@
const details = createToolCallElement(tc.id || `saved-${Math.random().toString(36).slice(2)}`, tc, true);
// 散落的 .tool-call 达到 FOLD_AT 个时,移入唯一 .tool-group
const loose = Array.from(bubble.children).filter(c => c.classList.contains('tool-call'));
const loose = Array.from(bubble.children).filter(isGroupableToolCall);
if (loose.length >= FOLD_AT) {
let group = bubble.querySelector(':scope > .tool-group');
if (!group) {
@@ -2390,7 +2480,7 @@
}
// 结束时若出现过父目录,收尾散落项
if (grouped) {
const loose = Array.from(bubble.children).filter(c => c.classList.contains('tool-call'));
const loose = Array.from(bubble.children).filter(isGroupableToolCall);
if (loose.length > 0) {
const group = bubble.querySelector(':scope > .tool-group');
if (group) {
@@ -2633,6 +2723,23 @@
const effectiveInput = tool.input !== undefined ? tool.input : input;
const effectiveResult = tool.result;
const kind = toolKind(tool);
if (kind === 'todo_list') {
let todoData = effectiveInput;
// 如果有 result 且是字符串,尝试解析
if (effectiveResult && typeof effectiveResult === 'string') {
try {
todoData = JSON.parse(effectiveResult);
} catch (e) {
}
} else {
}
const wrapper = document.createElement('div');
wrapper.className = 'tool-call-content todo-list-content';
wrapper.appendChild(createTodoListElement(todoData && typeof todoData === 'object' ? todoData : { items: [] }));
return wrapper;
}
if (effectiveName === 'AskUserQuestion') {
const questions = extractAskUserQuestions(effectiveInput);
if (questions.length > 0) {
@@ -2693,7 +2800,8 @@
function createToolCallElement(toolUseId, tool, done) {
const details = document.createElement('details');
details.className = 'tool-call';
details.id = `tool-${toolUseId}`;
details.id = `tool-node-${++toolDomSeq}`;
details.dataset.toolUseId = toolUseId ? String(toolUseId) : '';
details.dataset.toolName = tool.name || '';
if (toolKind(tool)) {
details.dataset.toolKind = toolKind(tool);
@@ -2728,12 +2836,24 @@
const tool = { id: toolUseId, name, input, kind, meta, done };
// 如果是 todo_list检查是否已存在相同 id 的 todo_list
if (kind === 'todo_list' && input?.id) {
const existingTodo = findTodoToolCallByTodoId(toolsDiv, input.id);
if (existingTodo) {
const details = createToolCallElement(toolUseId, tool, done);
existingTodo.replaceWith(details);
rememberToolCallTarget(toolUseId, tool, details);
scrollToBottom();
return;
}
}
const details = createToolCallElement(toolUseId, tool, done);
// 折叠策略:只维护唯一一个 .tool-group 父节点
// 散落的 .tool-call 直接子节点达到3个时将它们全部移入父节点之后继续散落再达3个再移入
const FOLD_AT = 3;
const looseBefore = Array.from(toolsDiv.children).filter(c => c.classList.contains('tool-call'));
const looseBefore = Array.from(toolsDiv.children).filter(isGroupableToolCall);
if (looseBefore.length >= FOLD_AT) {
// 确保存在唯一的 .tool-group
let group = toolsDiv.querySelector(':scope > .tool-group');
@@ -2754,6 +2874,7 @@
_refreshGroupSummary(group);
}
toolsDiv.appendChild(details);
rememberToolCallTarget(toolUseId, tool, details);
scrollToBottom();
}
@@ -2764,23 +2885,71 @@
if (summary) summary.textContent = `展开 ${count} 个工具调用`;
}
function updateToolCall(toolUseId, result) {
const el = document.getElementById(`tool-${toolUseId}`);
if (!el) return;
const tool = activeToolCalls.get(toolUseId) || {
function findLatestToolCallElement(root, matcher) {
if (!root || typeof matcher !== 'function') return null;
const allTools = root.querySelectorAll('.tool-call');
for (let i = allTools.length - 1; i >= 0; i--) {
const el = allTools[i];
if (matcher(el)) return el;
}
return null;
}
function findTodoToolCallByTodoId(root, todoId) {
if (!todoId) return null;
return findLatestToolCallElement(root, (el) => {
if (el.dataset.toolKind !== 'todo_list') return false;
const currentTodoId = el.querySelector('.todo-list-container')?.dataset.todoId;
return currentTodoId === todoId;
});
}
function updateToolCall(toolUseId, result, done = true) {
const tool = activeToolCalls.get(toolUseId) || null;
const toolUseIdText = toolUseId ? String(toolUseId) : '';
const scope = getLatestAssistantToolScope();
let el = tool?.domElement && tool.domElement.isConnected ? tool.domElement : null;
if (!el) {
if (tool?.kind === 'todo_list' && tool?.input?.id) {
const markedTodo = activeTodoCallTargets.get(tool.input.id);
if (markedTodo && markedTodo.isConnected) {
el = markedTodo;
}
}
}
if (!el) {
el = findLatestToolCallElement(scope, (candidate) => candidate.dataset.toolUseId === toolUseIdText);
}
if (!el && tool?.kind === 'todo_list' && tool?.input?.id) {
el = findTodoToolCallByTodoId(scope, tool.input.id);
}
if (!el) {
return;
}
const nextTool = tool || {
id: toolUseId,
name: el.dataset.toolName || '',
kind: el.dataset.toolKind || null,
done: true,
done,
};
tool.done = true;
if (result !== undefined) tool.result = result;
nextTool.done = done;
if (result !== undefined) nextTool.result = result;
rememberToolCallTarget(toolUseId, nextTool, el);
const summary = el.querySelector('summary');
if (summary) applyToolSummary(summary, tool, true);
if (tool.name === 'AskUserQuestion') return;
const nextContent = buildToolContentElement(tool);
const content = el.querySelector('.tool-call-content');
if (content) content.replaceWith(nextContent);
if (summary) applyToolSummary(summary, nextTool, done);
if (nextTool.name === 'AskUserQuestion') return;
const nextContent = buildToolContentElement(nextTool);
const content = Array.from(el.children).find((child) => child.tagName !== 'SUMMARY') || null;
if (content) {
content.replaceWith(nextContent);
} else {
el.appendChild(nextContent);
}
}
function getDeleteConfirmMessage(agent) {

View File

@@ -1221,7 +1221,7 @@ body.session-loading-active {
color: var(--text-secondary);
background: var(--bg-secondary);
display: flex;
align-items: flex-start;
align-items: anchor-center;
gap: 8px;
user-select: none;
list-style: none;
@@ -1313,6 +1313,14 @@ body.session-loading-active {
color: var(--text-primary);
background: linear-gradient(180deg, rgba(255, 249, 242, 0.92), rgba(245, 221, 212, 0.32));
}
.tool-call-content.todo-list-content {
font-family: inherit;
white-space: normal;
word-break: normal;
}
.tool-call-content.todo-list-content .todo-list-container {
margin: 0;
}
.tool-call-content.command,
.tool-call-content.file-change {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(250, 246, 240, 0.92));
@@ -1631,7 +1639,7 @@ body.session-loading-active {
}
.input-wrapper {
display: flex;
align-items: flex-end;
align-items: anchor-center;
gap: 8px;
background: #fff;
border: 1px solid var(--border-color);