fix: 修复工具增强显示的多个问题

1. read_file 工具支持三种模式显示
   - read 模式:显示完整文件内容
   - search 模式:显示搜索匹配项和行号
   - extract 模式:显示提取的多个片段

2. edit_file 工具兼容新旧数据格式
   - 支持 old_string/new_string 格式(当前后端使用)
   - 支持 replacements 数组格式(为未来扩展预留)

3. write_file 工具显示写入内容
   - 显示完整的写入内容
   - 使用绿色+号样式(与 edit_file 一致)
   - 支持显示追加模式标识

4. 极简模式工具图标匹配修复
   - 修复所有工具图标都显示为 run_command 的问题
   - 使用与堆叠块模式相同的图标获取逻辑
   - 正确显示每个工具对应的图标

5. 新增样式支持
   - 搜索匹配项样式
   - 提取片段样式
   - 移除彩色侧边框,保持简洁

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
JOJO 2026-04-04 15:37:38 +08:00
parent 6697da9581
commit a030ff954b
3 changed files with 309 additions and 33 deletions

View File

@ -34,9 +34,11 @@
class="step-item"
>
<div class="step-timeline">
<svg class="step-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<g v-html="getStepIcon(step)"></g>
</svg>
<span
class="step-icon icon icon-sm"
:style="getStepIconStyle(step)"
aria-hidden="true"
></span>
<div class="step-line"></div>
</div>
<div class="step-content">
@ -249,8 +251,14 @@ const toggleExpand = (groupId: string) => {
}
};
const getStepIcon = (step: Step) => {
return step.type === 'thinking' ? getIconPath('brain') : getIconPath('terminal');
const getStepIconStyle = (step: Step) => {
if (step.type === 'thinking') {
return props.iconStyle ? props.iconStyle('brain') : {};
} else if (step.type === 'tool' && step.action.tool) {
const iconKey = props.getToolIcon ? props.getToolIcon(step.action.tool) : 'terminal';
return props.iconStyle ? props.iconStyle(iconKey) : {};
}
return props.iconStyle ? props.iconStyle('terminal') : {};
};
const getToolName = (action: Action) => {
@ -322,15 +330,6 @@ const escapeHtml = (text: string) => {
return div.innerHTML;
};
const getIconPath = (name: string) => {
const icons: Record<string, string> = {
brain: '<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"></path><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"></path><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"></path><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"></path><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"></path><path d="M3.477 10.896a4 4 0 0 1 .585-.396"></path><path d="M19.938 10.5a4 4 0 0 1 .585.396"></path><path d="M6 18a4 4 0 0 1-1.967-.516"></path><path d="M19.967 17.484A4 4 0 0 1 18 18"></path>',
terminal: '<polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line>',
search: '<circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path>'
};
return icons[name] || icons.brain;
};
watch(() => props.actions, () => {
//
}, { deep: true });
@ -465,7 +464,6 @@ watch(() => props.actions, () => {
}
.step-icon {
color: var(--claude-text-secondary);
flex-shrink: 0;
}

View File

@ -244,27 +244,49 @@ function renderCreateFile(result: any, args: any): string {
}
function renderWriteFile(result: any, args: any): string {
const path = args.path || result.path || '';
const path = args.file_path || args.path || result.path || '';
const status = result.success ? '✓ 已写入' : '✗ 失败';
const content = args.content || '';
const isAppend = args.append || false;
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
if (isAppend) {
html += `<div><strong>模式:</strong>追加</div>`;
}
html += '</div>';
//
if (content) {
html += '<div class="tool-result-diff scrollable">';
const lines = content.split('\n');
lines.forEach((line: string) => {
html += `<div class="diff-line diff-add">+ ${escapeHtml(line)}</div>`;
});
html += '</div>';
}
return html;
}
function renderEditFile(result: any, args: any): string {
const path = args.path || result.path || '';
const path = args.file_path || args.path || result.path || '';
const status = result.success ? '✓ 已编辑' : '✗ 失败';
//
// 1. args.replacements
// 2. args.old_string args.new_string
const replacements = args.replacements || [];
let oldString = args.old_string || '';
let newString = args.new_string || '';
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += '</div>';
// replacements 使
if (replacements.length > 0) {
html += '<div class="tool-result-diff scrollable">';
replacements.forEach((rep: any, idx: number) => {
@ -289,6 +311,30 @@ function renderEditFile(result: any, args: any): string {
});
html += '</div>';
}
// 使old_string/new_string
else if (oldString || newString) {
// \n
oldString = oldString.replace(/\\n/g, '\n');
newString = newString.replace(/\\n/g, '\n');
html += '<div class="tool-result-diff scrollable">';
if (oldString) {
const oldLines = oldString.split('\n');
oldLines.forEach((line: string) => {
html += `<div class="diff-line diff-remove">- ${escapeHtml(line)}</div>`;
});
}
if (newString) {
const newLines = newString.split('\n');
newLines.forEach((line: string) => {
html += `<div class="diff-line diff-add">+ ${escapeHtml(line)}</div>`;
});
}
html += '</div>';
}
return html;
}
@ -324,7 +370,7 @@ function renderReadFile(result: any, args: any): string {
const path = args.path || result.path || '';
const status = result.success ? '✓ 成功' : '✗ 失败';
const size = result.size || result.file_size || 0;
const content = result.content || result.text || '';
const readType = result.type || 'read';
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
@ -332,12 +378,69 @@ function renderReadFile(result: any, args: any): string {
if (size > 0) {
html += `<div><strong>大小:</strong>${formatBytes(size)}</div>`;
}
if (readType !== 'read') {
html += `<div><strong>模式:</strong>${escapeHtml(readType)}</div>`;
}
html += '</div>';
if (content) {
html += '<div class="tool-result-content scrollable">';
html += `<pre>${escapeHtml(content)}</pre>`;
html += '</div>';
//
if (readType === 'read') {
const content = result.content || result.text || '';
if (content) {
html += '<div class="tool-result-content scrollable">';
html += `<pre>${escapeHtml(content)}</pre>`;
html += '</div>';
}
}
//
else if (readType === 'search') {
const matches = result.matches || [];
const query = result.query || args.query || '';
if (query) {
html += '<div class="tool-result-meta">';
html += `<div><strong>搜索关键词:</strong>${escapeHtml(query)}</div>`;
html += `<div><strong>匹配数量:</strong>${matches.length}</div>`;
html += '</div>';
}
if (matches.length > 0) {
html += '<div class="tool-result-content scrollable">';
matches.forEach((match: any, idx: number) => {
if (idx > 0) html += '<div class="search-match-separator">⋯</div>';
html += '<div class="search-match-item">';
html += `<div class="search-match-line">行 ${match.line_number || '?'}</div>`;
html += `<pre>${escapeHtml(match.snippet || match.content || '')}</pre>`;
html += '</div>';
});
html += '</div>';
} else {
html += '<div class="tool-result-empty">未找到匹配内容</div>';
}
}
//
else if (readType === 'extract') {
const segments = result.segments || [];
if (segments.length > 0) {
html += '<div class="tool-result-meta">';
html += `<div><strong>提取片段数:</strong>${segments.length}</div>`;
html += '</div>';
html += '<div class="tool-result-content scrollable">';
segments.forEach((segment: any, idx: number) => {
if (idx > 0) html += '<div class="segment-separator">⋯</div>';
html += '<div class="segment-item">';
const startLine = segment.start_line || segment.line_start || '?';
const endLine = segment.end_line || segment.line_end || '?';
html += `<div class="segment-range">行 ${startLine}-${endLine}</div>`;
html += `<pre>${escapeHtml(segment.content || segment.text || '')}</pre>`;
html += '</div>';
});
html += '</div>';
} else {
html += '<div class="tool-result-empty">未提取到内容</div>';
}
}
return html;
@ -883,4 +986,76 @@ function renderEasterEgg(result: any, args: any): string {
font-size: 14px;
line-height: 1.6;
}
/* 搜索匹配项 */
.search-match-separator {
text-align: center;
color: rgba(0, 0, 0, 0.3);
margin: 12px 0;
font-size: 16px;
}
.search-match-item {
margin-bottom: 12px;
padding: 8px;
background: rgba(0, 0, 0, 0.02);
border-radius: 6px;
}
:root[data-theme='dark'] .search-match-item {
background: rgba(255, 255, 255, 0.05);
}
.search-match-line {
font-size: 11px;
color: rgba(0, 0, 0, 0.5);
margin-bottom: 4px;
font-weight: 600;
}
:root[data-theme='dark'] .search-match-line {
color: rgba(255, 255, 255, 0.5);
}
.search-match-item pre {
margin: 0;
font-size: 12px;
line-height: 1.5;
}
/* 提取片段 */
.segment-separator {
text-align: center;
color: rgba(0, 0, 0, 0.3);
margin: 12px 0;
font-size: 16px;
}
.segment-item {
margin-bottom: 12px;
padding: 8px;
background: rgba(0, 0, 0, 0.02);
border-radius: 6px;
}
:root[data-theme='dark'] .segment-item {
background: rgba(255, 255, 255, 0.05);
}
.segment-range {
font-size: 11px;
color: rgba(0, 0, 0, 0.5);
margin-bottom: 4px;
font-weight: 600;
}
:root[data-theme='dark'] .segment-range {
color: rgba(255, 255, 255, 0.5);
}
.segment-item pre {
margin: 0;
font-size: 12px;
line-height: 1.5;
}
</style>

View File

@ -189,33 +189,79 @@ function renderCreateFile(result: any, args: any): string {
}
function renderWriteFile(result: any, args: any): string {
const path = args.path || result.path || '';
const path = args.file_path || args.path || result.path || '';
const status = result.success ? '✓ 已写入' : '✗ 失败';
const content = args.content || '';
const isAppend = args.append || false;
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
if (isAppend) {
html += `<div><strong>模式:</strong>追加</div>`;
}
html += '</div>';
// 显示写入的内容
if (content) {
html += '<div class="tool-result-diff scrollable">';
const lines = content.split('\n');
lines.forEach((line: string) => {
html += `<div class="diff-line diff-add">+ ${escapeHtml(line)}</div>`;
});
html += '</div>';
}
return html;
}
function renderEditFile(result: any, args: any): string {
const path = args.file_path || args.path || result.path || '';
const status = result.success ? '✓ 已编辑' : '✗ 失败';
// 兼容两种数据格式:
// 1. 新格式args.replacements 数组
// 2. 旧格式args.old_string 和 args.new_string
const replacements = args.replacements || [];
let oldString = args.old_string || '';
let newString = args.new_string || '';
// 处理转义的 \n
oldString = oldString.replace(/\\n/g, '\n');
newString = newString.replace(/\\n/g, '\n');
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
html += `<div><strong>状态:</strong>${status}</div>`;
html += '</div>';
if (oldString || newString) {
// 如果有 replacements 数组,使用新格式
if (replacements.length > 0) {
html += '<div class="tool-result-diff scrollable">';
replacements.forEach((rep: any, idx: number) => {
if (idx > 0) html += '<div class="diff-separator">⋮</div>';
const oldText = rep.old_text || rep.old || '';
const newText = rep.new_text || rep.new || '';
if (oldText) {
const oldLines = oldText.split('\n');
oldLines.forEach((line: string) => {
html += `<div class="diff-line diff-remove">- ${escapeHtml(line)}</div>`;
});
}
if (newText) {
const newLines = newText.split('\n');
newLines.forEach((line: string) => {
html += `<div class="diff-line diff-add">+ ${escapeHtml(line)}</div>`;
});
}
});
html += '</div>';
}
// 否则使用旧格式old_string/new_string
else if (oldString || newString) {
// 处理转义的 \n
oldString = oldString.replace(/\\n/g, '\n');
newString = newString.replace(/\\n/g, '\n');
html += '<div class="tool-result-diff scrollable">';
if (oldString) {
@ -269,7 +315,7 @@ function renderReadFile(result: any, args: any): string {
const path = args.path || result.path || '';
const status = result.success ? '✓ 成功' : '✗ 失败';
const size = result.size || result.file_size || 0;
const content = result.content || result.text || '';
const readType = result.type || 'read';
let html = '<div class="tool-result-meta">';
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
@ -277,12 +323,69 @@ function renderReadFile(result: any, args: any): string {
if (size > 0) {
html += `<div><strong>大小:</strong>${formatBytes(size)}</div>`;
}
if (readType !== 'read') {
html += `<div><strong>模式:</strong>${escapeHtml(readType)}</div>`;
}
html += '</div>';
if (content) {
html += '<div class="tool-result-content scrollable">';
html += `<pre>${escapeHtml(content)}</pre>`;
html += '</div>';
// 处理普通读取模式
if (readType === 'read') {
const content = result.content || result.text || '';
if (content) {
html += '<div class="tool-result-content scrollable">';
html += `<pre>${escapeHtml(content)}</pre>`;
html += '</div>';
}
}
// 处理搜索模式
else if (readType === 'search') {
const matches = result.matches || [];
const query = result.query || args.query || '';
if (query) {
html += '<div class="tool-result-meta">';
html += `<div><strong>搜索关键词:</strong>${escapeHtml(query)}</div>`;
html += `<div><strong>匹配数量:</strong>${matches.length}</div>`;
html += '</div>';
}
if (matches.length > 0) {
html += '<div class="tool-result-content scrollable">';
matches.forEach((match: any, idx: number) => {
if (idx > 0) html += '<div class="search-match-separator">⋯</div>';
html += '<div class="search-match-item">';
html += `<div class="search-match-line">行 ${match.line_number || '?'}</div>`;
html += `<pre>${escapeHtml(match.snippet || match.content || '')}</pre>`;
html += '</div>';
});
html += '</div>';
} else {
html += '<div class="tool-result-empty">未找到匹配内容</div>';
}
}
// 处理提取模式
else if (readType === 'extract') {
const segments = result.segments || [];
if (segments.length > 0) {
html += '<div class="tool-result-meta">';
html += `<div><strong>提取片段数:</strong>${segments.length}</div>`;
html += '</div>';
html += '<div class="tool-result-content scrollable">';
segments.forEach((segment: any, idx: number) => {
if (idx > 0) html += '<div class="segment-separator">⋯</div>';
html += '<div class="segment-item">';
const startLine = segment.start_line || segment.line_start || '?';
const endLine = segment.end_line || segment.line_end || '?';
html += `<div class="segment-range">行 ${startLine}-${endLine}</div>`;
html += `<pre>${escapeHtml(segment.content || segment.text || '')}</pre>`;
html += '</div>';
});
html += '</div>';
} else {
html += '<div class="tool-result-empty">未提取到内容</div>';
}
}
return html;