From 7b24704c4d9c419f340c6fa5e619b6b666bee0c5 Mon Sep 17 00:00:00 2001 From: cc-dan Date: Wed, 11 Mar 2026 12:13:46 +0000 Subject: [PATCH] feat: auto-fold tool calls into single group after 5 items --- CHANGELOG.md | 1 + public/app.js | 99 +++++++++++++++++++++++++++++++++++++++++++++++- public/style.css | 33 ++++++++++++++++ 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36a4c9b..e0dfbf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 更新记录 - **v1.2.6** + - 工具调用折叠显示:长任务中散落的工具调用块达到5个时自动折叠入唯一父节点,之后每满5个再次移入同一父节点(两级结构),输出结束后剩余散落项也一并收入;总数不超过5个则完整显示不折叠。 - 新增编辑模板弹窗「获取上游模型列表」:通过 `/v1/models` 端点拉取可用模型,填充到四个模型输入框的下拉建议列表,支持自定义端点地址。 - 修改密码改为按钮+弹窗模式:设置面板中密码修改从内联表单改为独立弹窗,成功后自动关闭。 - 子弹窗关闭按钮样式适配:编辑模板和修改密码弹窗的关闭按钮统一为与主面板一致的风格。 diff --git a/public/app.js b/public/app.js index 753f113..f7b9387 100644 --- a/public/app.js +++ b/public/app.js @@ -43,6 +43,8 @@ let pendingText = ''; let renderTimer = null; let activeToolCalls = new Map(); + let toolGroupCount = 0; // 当前 .msg-tools 直接子节点数(含已有父目录) + let hasGrouped = false; // 本次输出是否已触发过折叠 let cmdMenuIndex = -1; let currentMode = localStorage.getItem('cc-web-mode') || 'yolo'; let currentModel = 'opus'; @@ -353,6 +355,8 @@ isGenerating = true; pendingText = ''; activeToolCalls.clear(); + toolGroupCount = 0; + hasGrouped = false; sendBtn.hidden = true; abortBtn.hidden = false; // 不禁用输入框,允许用户继续输入(但无法发送) @@ -388,11 +392,39 @@ if (typing) typing.remove(); const streamEl = document.getElementById('streaming-msg'); - if (streamEl) streamEl.removeAttribute('id'); + if (streamEl) { + // 若本轮出现过父目录,把末尾散落的 .tool-call 也一并收入同一父节点 + if (hasGrouped) { + const toolsDiv = streamEl.querySelector('.msg-tools'); + if (toolsDiv) { + const loose = Array.from(toolsDiv.children).filter(c => c.classList.contains('tool-call')); + if (loose.length > 0) { + let group = toolsDiv.querySelector(':scope > .tool-group'); + if (!group) { + group = document.createElement('details'); + group.className = 'tool-group'; + const gs = document.createElement('summary'); + gs.className = 'tool-group-summary'; + group.appendChild(gs); + const inner = document.createElement('div'); + inner.className = 'tool-group-inner'; + group.appendChild(inner); + toolsDiv.insertBefore(group, toolsDiv.firstChild); + } + const inner = group.querySelector('.tool-group-inner'); + loose.forEach(c => inner.appendChild(c)); + _refreshGroupSummary(group); + } + } + } + streamEl.removeAttribute('id'); + } if (sessionId) currentSessionId = sessionId; pendingText = ''; activeToolCalls.clear(); + toolGroupCount = 0; + hasGrouped = false; } // --- Rendering --- @@ -458,6 +490,8 @@ const el = createMsgElement(m.role, m.content); if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) { const bubble = el.querySelector('.msg-bubble'); + const FOLD_AT = 5; + let grouped = false; for (const tc of m.toolCalls) { const details = document.createElement('details'); details.className = 'tool-call'; @@ -468,8 +502,41 @@ details.appendChild(summary); const displayInput = tc.name === 'AskUserQuestion' ? tc.input : (tc.result || tc.input); details.appendChild(buildToolContentElement(tc.name, displayInput)); + + // 散落的 .tool-call 达到 FOLD_AT 个时,移入唯一 .tool-group + const loose = Array.from(bubble.children).filter(c => c.classList.contains('tool-call')); + if (loose.length >= FOLD_AT) { + let group = bubble.querySelector(':scope > .tool-group'); + if (!group) { + group = document.createElement('details'); + group.className = 'tool-group'; + const gs = document.createElement('summary'); + gs.className = 'tool-group-summary'; + group.appendChild(gs); + const inner = document.createElement('div'); + inner.className = 'tool-group-inner'; + group.appendChild(inner); + bubble.insertBefore(group, bubble.firstChild); + grouped = true; + } + const inner = group.querySelector('.tool-group-inner'); + loose.forEach(c => inner.appendChild(c)); + _refreshGroupSummary(group); + } bubble.appendChild(details); } + // 结束时若出现过父目录,收尾散落项 + if (grouped) { + const loose = Array.from(bubble.children).filter(c => c.classList.contains('tool-call')); + if (loose.length > 0) { + const group = bubble.querySelector(':scope > .tool-group'); + if (group) { + const inner = group.querySelector('.tool-group-inner'); + loose.forEach(c => inner.appendChild(c)); + _refreshGroupSummary(group); + } + } + } } return el; } @@ -703,10 +770,40 @@ details.appendChild(summary); details.appendChild(buildToolContentElement(name, input)); + // 折叠策略:只维护唯一一个 .tool-group 父节点 + // 散落的 .tool-call 直接子节点达到5个时,将它们全部移入父节点;之后继续散落,再达5个再移入 + const FOLD_AT = 5; + const looseBefore = Array.from(toolsDiv.children).filter(c => c.classList.contains('tool-call')); + if (looseBefore.length >= FOLD_AT) { + // 确保存在唯一的 .tool-group + let group = toolsDiv.querySelector(':scope > .tool-group'); + if (!group) { + group = document.createElement('details'); + group.className = 'tool-group'; + const gs = document.createElement('summary'); + gs.className = 'tool-group-summary'; + group.appendChild(gs); + const inner = document.createElement('div'); + inner.className = 'tool-group-inner'; + group.appendChild(inner); + toolsDiv.insertBefore(group, toolsDiv.firstChild); + hasGrouped = true; + } + const inner = group.querySelector('.tool-group-inner'); + looseBefore.forEach(c => inner.appendChild(c)); + _refreshGroupSummary(group); + } toolsDiv.appendChild(details); scrollToBottom(); } + function _refreshGroupSummary(group) { + const inner = group.querySelector('.tool-group-inner'); + const count = inner ? inner.childElementCount : 0; + const summary = group.querySelector('.tool-group-summary'); + if (summary) summary.textContent = `展开 ${count} 个工具调用`; + } + function updateToolCall(toolUseId, result) { const el = document.getElementById(`tool-${toolUseId}`); if (!el) return; diff --git a/public/style.css b/public/style.css index 8bba09e..9c9e5fc 100644 --- a/public/style.css +++ b/public/style.css @@ -665,6 +665,39 @@ body { font-family: 'SF Mono', monospace; } +/* Tool group (auto-fold) */ +.tool-group { + margin: 8px 0; + border: 1px solid var(--border-color); + border-radius: 10px; + overflow: hidden; +} +.tool-group-summary { + padding: 8px 12px; + cursor: pointer; + font-size: 12px; + color: var(--text-muted); + background: var(--bg-secondary); + list-style: none; + user-select: none; +} +.tool-group-summary::-webkit-details-marker { display: none; } +.tool-group-summary::before { + content: '▸ '; + font-size: 11px; + transition: transform 0.2s; + display: inline-block; +} +.tool-group[open] > .tool-group-summary::before { transform: rotate(90deg); } +.tool-group-summary:hover { background: var(--bg-tertiary); } +.tool-group-inner { + padding: 4px 8px; + background: var(--bg-primary); +} +.tool-group-inner .tool-call { + margin: 4px 0; +} + /* AskUserQuestion preview */ .ask-user-question { padding: 10px 12px;