feat: auto-fold tool calls into single group after 5 items

This commit is contained in:
cc-dan
2026-03-11 12:13:46 +00:00
parent b2dbacb870
commit 7b24704c4d
3 changed files with 132 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
# 更新记录
- **v1.2.6**
- 工具调用折叠显示长任务中散落的工具调用块达到5个时自动折叠入唯一父节点之后每满5个再次移入同一父节点两级结构输出结束后剩余散落项也一并收入总数不超过5个则完整显示不折叠。
- 新增编辑模板弹窗「获取上游模型列表」:通过 `/v1/models` 端点拉取可用模型,填充到四个模型输入框的下拉建议列表,支持自定义端点地址。
- 修改密码改为按钮+弹窗模式:设置面板中密码修改从内联表单改为独立弹窗,成功后自动关闭。
- 子弹窗关闭按钮样式适配:编辑模板和修改密码弹窗的关闭按钮统一为与主面板一致的风格。

View File

@@ -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;

View File

@@ -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;