- MinimalBlocks 思考内容(.thinking-content)补全自定义滚动条 - ToolAction 结构化显示三类滚动区(content/diff/code/output pre)统一样式 - 参考 git 状态侧边栏滚动条:10px 圆角胶囊滑块、半透明主题色 - 滚动槽(track)设为透明,贴合工具块灰色底色,避免明显槽体 - 颜色走 --claude-*/--theme-* 变量,三模式自适应 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1541 lines
48 KiB
Vue
1541 lines
48 KiB
Vue
<template>
|
||
<div
|
||
class="collapsible-block tool-block"
|
||
:class="{
|
||
expanded,
|
||
processing: action.tool.status === 'preparing' || action.tool.status === 'running',
|
||
completed: action.tool.status === 'completed'
|
||
}"
|
||
>
|
||
<div class="collapsible-header" @click="$emit('toggle')">
|
||
<div class="arrow"></div>
|
||
<div class="status-icon">
|
||
<span
|
||
class="tool-icon icon icon-md"
|
||
:class="getToolAnimationClass(action.tool)"
|
||
:style="iconStyle(getToolIcon(action.tool))"
|
||
aria-hidden="true"
|
||
></span>
|
||
</div>
|
||
<span class="status-text">{{ getToolStatusText(action.tool) }}</span>
|
||
<span class="tool-desc">{{ getToolDescription(action.tool) }}</span>
|
||
</div>
|
||
<div
|
||
class="collapsible-content"
|
||
:ref="
|
||
(el) =>
|
||
registerCollapseContent &&
|
||
registerCollapseContent(collapseKey || action.tool.id || action.id || 'tool', el)
|
||
"
|
||
>
|
||
<div class="content-inner">
|
||
<div v-if="shouldUseEnhancedDisplay" v-html="renderEnhancedToolResult()"></div>
|
||
<div v-else>
|
||
<pre>{{ JSON.stringify(action.tool.result || action.tool.arguments, null, 2) }}</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="action.tool.status === 'preparing' || action.tool.status === 'running'"
|
||
class="progress-indicator"
|
||
></div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue';
|
||
import { usePersonalizationStore } from '@/stores/personalization';
|
||
|
||
defineOptions({ name: 'ToolAction' });
|
||
|
||
const props = defineProps<{
|
||
action: any;
|
||
expanded: boolean;
|
||
iconStyle: (key: string) => Record<string, string>;
|
||
getToolAnimationClass: (tool: any) => Record<string, unknown>;
|
||
getToolIcon: (tool: any) => string;
|
||
getToolStatusText: (tool: any) => string;
|
||
getToolDescription: (tool: any) => string;
|
||
formatSearchTopic: (filters: Record<string, any>) => string;
|
||
formatSearchTime: (filters: Record<string, any>) => string;
|
||
formatSearchDomains: (filters: Record<string, any>) => string;
|
||
streamingMessage: boolean;
|
||
registerCollapseContent?: (key: string, el: Element | null) => void;
|
||
collapseKey?: string;
|
||
}>();
|
||
|
||
defineEmits<{ (event: 'toggle'): void }>();
|
||
|
||
// 初始化 store
|
||
const personalizationStore = usePersonalizationStore();
|
||
|
||
const shouldUseEnhancedDisplay = computed(() => {
|
||
return personalizationStore.form.enhanced_tool_display && props.action.tool.result;
|
||
});
|
||
|
||
function escapeHtml(text: string): string {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function formatBytes(bytes: number): string {
|
||
if (bytes === 0) return '0 B';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||
}
|
||
|
||
function formatToolStatusLabel(result: any, successLabel: string, failedLabel = '✗ 失败'): string {
|
||
const state = String(result?.status || props.action?.tool?.status || '').toLowerCase();
|
||
if (state === 'awaiting_user_answer') {
|
||
return '等待回答';
|
||
}
|
||
if (state === 'awaiting_approval' || state === 'pending_approval' || state === 'pending') {
|
||
return '待审批';
|
||
}
|
||
if (state === 'rejected') {
|
||
return '✗ 已拒绝';
|
||
}
|
||
if (state === 'cancelled') {
|
||
return '✗ 已取消';
|
||
}
|
||
return result?.success ? successLabel : failedLabel;
|
||
}
|
||
|
||
function renderEnhancedToolResult(): string {
|
||
const tool = props.action.tool;
|
||
const result = tool.result;
|
||
const args = tool.arguments || {};
|
||
const name = tool.name;
|
||
|
||
// 网络检索类
|
||
if (name === 'web_search') {
|
||
return renderWebSearch(result, args);
|
||
} else if (name === 'extract_webpage') {
|
||
return renderExtractWebpage(result, args);
|
||
} else if (name === 'save_webpage') {
|
||
return renderSaveWebpage(result, args);
|
||
}
|
||
|
||
// 文件编辑类
|
||
else if (name === 'create_file' || name === 'create_folder') {
|
||
return renderCreateFile(result, args);
|
||
} else if (name === 'write_file') {
|
||
return renderWriteFile(result, args);
|
||
} else if (name === 'edit_file') {
|
||
return renderEditFile(result, args);
|
||
} else if (name === 'delete_file') {
|
||
return renderDeleteFile(result, args);
|
||
} else if (name === 'rename_file') {
|
||
return renderRenameFile(result, args);
|
||
}
|
||
|
||
// 阅读聚焦 / Skills 类
|
||
else if (name === 'read_file' || name === 'read_skill') {
|
||
return renderReadFile(result, args);
|
||
} else if (name === 'create_skill') {
|
||
return renderCreateSkill(result, args);
|
||
} else if (name === 'vlm_analyze') {
|
||
return renderVlmAnalyze(result, args);
|
||
} else if (name === 'ocr_image') {
|
||
return renderOcrImage(result, args);
|
||
} else if (name === 'view_image') {
|
||
return renderViewImage(result, args);
|
||
}
|
||
|
||
// 终端类
|
||
else if (name === 'terminal_session') {
|
||
return renderTerminalSession(result, args);
|
||
} else if (name === 'terminal_input') {
|
||
return renderTerminalInput(result, args);
|
||
} else if (name === 'terminal_snapshot') {
|
||
return renderTerminalSnapshot(result, args);
|
||
} else if (name === 'sleep') {
|
||
return renderSleep(result, args);
|
||
}
|
||
|
||
// 终端指令类
|
||
else if (name === 'run_command') {
|
||
return renderRunCommand(result, args);
|
||
} else if (name === 'run_python') {
|
||
return renderRunPython(result, args);
|
||
}
|
||
|
||
// 记忆类
|
||
else if (name === 'update_memory') {
|
||
return renderUpdateMemory(result, args);
|
||
} else if (name === 'conversation_search') {
|
||
return renderConversationSearch(result, args);
|
||
} else if (name === 'conversation_review') {
|
||
return renderConversationReview(result, args);
|
||
}
|
||
|
||
// 用户沟通类
|
||
else if (name === 'ask_user') {
|
||
return renderAskUser(result, args);
|
||
}
|
||
|
||
// 个性化管理类
|
||
else if (name === 'manage_personalization') {
|
||
return renderManagePersonalization(result, args);
|
||
}
|
||
|
||
// 待办事项类
|
||
else if (name === 'todo_create') {
|
||
return renderTodoCreate(result, args);
|
||
} else if (name === 'todo_update_task') {
|
||
return renderTodoUpdate(result, args);
|
||
}
|
||
|
||
// 彩蛋类
|
||
else if (name === 'trigger_easter_egg') {
|
||
return renderEasterEgg(result, args);
|
||
}
|
||
|
||
// 默认显示 JSON
|
||
return `<pre>${escapeHtml(JSON.stringify(result || args, null, 2))}</pre>`;
|
||
}
|
||
|
||
// ===== 网络检索类 =====
|
||
function renderWebSearch(result: any, args: any): string {
|
||
const query = result.query || args.query || '';
|
||
const filters = result.filters || {};
|
||
const totalResults = result.total_results || 0;
|
||
const results = result.results || [];
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>搜索内容:</strong>${escapeHtml(query)}</div>`;
|
||
html += `<div><strong>主题:</strong>${escapeHtml(props.formatSearchTopic(filters))}</div>`;
|
||
html += `<div><strong>时间范围:</strong>${escapeHtml(props.formatSearchTime(filters))}</div>`;
|
||
html += `<div><strong>限定网站:</strong>${escapeHtml(props.formatSearchDomains(filters))}</div>`;
|
||
html += `<div><strong>结果数量:</strong>${totalResults}</div>`;
|
||
html += '</div>';
|
||
|
||
if (results.length > 0) {
|
||
html += '<div class="search-result-list">';
|
||
results.forEach((item: any) => {
|
||
html += '<div class="search-result-item">';
|
||
html += `<div class="search-result-title">${escapeHtml(item.title || '无标题')}</div>`;
|
||
html += '<div class="search-result-url">';
|
||
if (item.url) {
|
||
html += `<a href="${escapeHtml(item.url)}" target="_blank">${escapeHtml(item.url)}</a>`;
|
||
} else {
|
||
html += '<span>无可用链接</span>';
|
||
}
|
||
html += '</div></div>';
|
||
});
|
||
html += '</div>';
|
||
} else {
|
||
html += '<div class="tool-result-empty">未返回详细的搜索结果。</div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderExtractWebpage(result: any, args: any): string {
|
||
const url = args.url || result.url || '';
|
||
const status = formatToolStatusLabel(result, '✓ 成功');
|
||
const content = result.content || result.text || '';
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>URL:</strong>${escapeHtml(url)}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += '</div>';
|
||
|
||
if (content) {
|
||
html += '<div class="tool-result-content scrollable">';
|
||
html += `<pre>${escapeHtml(content)}</pre>`;
|
||
html += '</div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderSaveWebpage(result: any, args: any): string {
|
||
const url = args.url || result.url || '';
|
||
const path = result.path || args.save_path || '';
|
||
const status = formatToolStatusLabel(result, '✓ 已保存');
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>URL:</strong>${escapeHtml(url)}</div>`;
|
||
html += `<div><strong>保存路径:</strong>${escapeHtml(path)}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += '</div>';
|
||
|
||
return html;
|
||
}
|
||
|
||
// ===== 文件编辑类 =====
|
||
function renderCreateFile(result: any, args: any): string {
|
||
const path = args.path || result.path || '';
|
||
const status = formatToolStatusLabel(result, '✓ 已创建');
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += '</div>';
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderWriteFile(result: any, args: any): string {
|
||
const path = args.file_path || args.path || result.path || '';
|
||
const status = formatToolStatusLabel(result, '✓ 已写入');
|
||
const content = args.content || '';
|
||
const appendFlag = typeof args.append === 'boolean' ? args.append : null;
|
||
const modeRaw = String(result.mode || '').toLowerCase();
|
||
const writeMode =
|
||
appendFlag === true || modeRaw === 'a'
|
||
? '追加'
|
||
: appendFlag === false || modeRaw === 'w'
|
||
? '覆盖'
|
||
: '无';
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += `<div><strong>模式:</strong>${writeMode}</div>`;
|
||
if (!result.success && result.error) {
|
||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||
}
|
||
html += '</div>';
|
||
|
||
// 显示写入的内容
|
||
if (result.success && content) {
|
||
html += '<div class="tool-result-diff scrollable">';
|
||
const lines = content.split('\n');
|
||
lines.forEach((line: string, idx: number) => {
|
||
html += `<div class="diff-line diff-add"><span class="diff-line-number">${idx + 1}</span><span class="diff-marker">+</span><span class="diff-content">${escapeHtml(line)}</span></div>`;
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderEditFile(result: any, args: any): string {
|
||
const path = args.file_path || args.path || result.path || '';
|
||
const status = formatToolStatusLabel(result, '✓ 已编辑');
|
||
const foundCount =
|
||
typeof result.found_matches === 'number'
|
||
? result.found_matches
|
||
: typeof result.replacements === 'number'
|
||
? result.replacements
|
||
: null;
|
||
const replacedCount = typeof result.replacements === 'number' ? result.replacements : null;
|
||
|
||
const replacements = Array.isArray(args.replacements) ? args.replacements : [];
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += `<div><strong>替换组数:</strong>${(result.replacement_groups ?? replacements.length) || '无'}</div>`;
|
||
html += `<div><strong>找到:</strong>${foundCount ?? '无'}处</div>`;
|
||
html += `<div><strong>替换:</strong>${replacedCount ?? '无'}处</div>`;
|
||
if (!result.success && result.error) {
|
||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||
}
|
||
html += '</div>';
|
||
|
||
if (!result.success) {
|
||
return html;
|
||
}
|
||
|
||
if (replacements.length > 0) {
|
||
html += '<div class="tool-result-diff scrollable">';
|
||
const details = Array.isArray(result.details) ? result.details : [];
|
||
const renderDiffLine = (lineNo: number | null, marker: string, text: string, className = '') => {
|
||
const lineNoText = typeof lineNo === 'number' && Number.isFinite(lineNo) ? String(lineNo) : '';
|
||
html += `<div class="diff-line${className ? ` ${className}` : ''}"><span class="diff-line-number">${escapeHtml(lineNoText)}</span><span class="diff-marker">${escapeHtml(marker)}</span><span class="diff-content">${escapeHtml(text)}</span></div>`;
|
||
};
|
||
const hunks: any[] = [];
|
||
replacements.forEach((rep: any, idx: number) => {
|
||
const oldText = rep.old_string || '';
|
||
const newText = rep.new_string || '';
|
||
const detail = details[idx] || {};
|
||
const contexts = Array.isArray(detail.contexts) ? detail.contexts : [];
|
||
const buildPairRows = (context?: any) => {
|
||
const rows: any[] = [];
|
||
const startLineCandidate = Number(context?.old_start_line ?? detail.matched_lines?.[0]);
|
||
const startLine = Number.isFinite(startLineCandidate) ? startLineCandidate : null;
|
||
if (oldText) {
|
||
const oldLines = oldText.split('\n');
|
||
oldLines.forEach((line: string, lineIdx: number) => {
|
||
rows.push({ lineNo: startLine === null ? null : startLine + lineIdx, marker: '-', text: line, className: 'diff-remove' });
|
||
});
|
||
}
|
||
|
||
if (newText) {
|
||
const newLines = newText.split('\n');
|
||
newLines.forEach((line: string, lineIdx: number) => {
|
||
rows.push({ lineNo: startLine === null ? null : startLine + lineIdx, marker: '+', text: line, className: 'diff-add' });
|
||
});
|
||
}
|
||
return rows;
|
||
};
|
||
|
||
if (contexts.length > 0) {
|
||
contexts.forEach((context: any) => {
|
||
const rows: any[] = [];
|
||
const rawBeforeLines = Array.isArray(context.before_lines) ? context.before_lines : [];
|
||
const rawAfterLines = Array.isArray(context.after_lines) ? context.after_lines : [];
|
||
const beforeLines =
|
||
rawBeforeLines.length >= 2 && rawBeforeLines.every((item: any) => String(item?.text ?? '') === '')
|
||
? []
|
||
:
|
||
rawBeforeLines.length >= 2 && String(rawBeforeLines[0]?.text ?? '') === ''
|
||
? rawBeforeLines.slice(1)
|
||
: rawBeforeLines;
|
||
const afterLines =
|
||
rawAfterLines.length >= 2 && rawAfterLines.every((item: any) => String(item?.text ?? '') === '')
|
||
? []
|
||
:
|
||
rawAfterLines.length >= 2 && String(rawAfterLines[rawAfterLines.length - 1]?.text ?? '') === ''
|
||
? rawAfterLines.slice(0, -1)
|
||
: rawAfterLines;
|
||
beforeLines.forEach((item: any) => {
|
||
rows.push({ lineNo: Number(item.line), marker: ' ', text: String(item.text ?? ''), className: '' });
|
||
});
|
||
rows.push(...buildPairRows(context));
|
||
afterLines.forEach((item: any) => {
|
||
rows.push({ lineNo: Number(item.line), marker: ' ', text: String(item.text ?? ''), className: '' });
|
||
});
|
||
const numberedRows = rows.filter((row) => typeof row.lineNo === 'number' && Number.isFinite(row.lineNo));
|
||
hunks.push({
|
||
rows,
|
||
minLine: numberedRows.length ? Math.min(...numberedRows.map((row) => row.lineNo)) : null,
|
||
maxLine: numberedRows.length ? Math.max(...numberedRows.map((row) => row.lineNo)) : null,
|
||
});
|
||
});
|
||
} else {
|
||
const rows = buildPairRows();
|
||
const numberedRows = rows.filter((row) => typeof row.lineNo === 'number' && Number.isFinite(row.lineNo));
|
||
hunks.push({
|
||
rows,
|
||
minLine: numberedRows.length ? Math.min(...numberedRows.map((row) => row.lineNo)) : null,
|
||
maxLine: numberedRows.length ? Math.max(...numberedRows.map((row) => row.lineNo)) : null,
|
||
});
|
||
}
|
||
});
|
||
const sortedHunks = hunks.sort((a, b) => (a.minLine ?? Number.MAX_SAFE_INTEGER) - (b.minLine ?? Number.MAX_SAFE_INTEGER));
|
||
const mergedHunks: any[] = [];
|
||
sortedHunks.forEach((hunk) => {
|
||
const last = mergedHunks[mergedHunks.length - 1];
|
||
if (last && hunk.minLine !== null && last.maxLine !== null && hunk.minLine <= last.maxLine + 1) {
|
||
last.rows.push(...hunk.rows);
|
||
last.maxLine = Math.max(last.maxLine, hunk.maxLine ?? last.maxLine);
|
||
} else {
|
||
mergedHunks.push({ ...hunk, rows: [...hunk.rows] });
|
||
}
|
||
});
|
||
const markerOrder: Record<string, number> = { ' ': 0, '-': 1, '+': 2 };
|
||
mergedHunks.forEach((hunk, hunkIdx) => {
|
||
if (hunkIdx > 0) renderDiffLine(null, ' ', '⋮', 'diff-separator');
|
||
const changedLines = new Set(
|
||
hunk.rows
|
||
.filter((row: any) => (row.marker === '-' || row.marker === '+') && typeof row.lineNo === 'number')
|
||
.map((row: any) => row.lineNo)
|
||
);
|
||
const seenContextLines = new Set<number>();
|
||
hunk.rows
|
||
.filter((row: any) => !(row.marker === ' ' && changedLines.has(row.lineNo)))
|
||
.filter((row: any) => {
|
||
if (row.marker !== ' ' || typeof row.lineNo !== 'number') return true;
|
||
if (seenContextLines.has(row.lineNo)) return false;
|
||
seenContextLines.add(row.lineNo);
|
||
return true;
|
||
})
|
||
.sort((a: any, b: any) => {
|
||
const lineDelta = (a.lineNo ?? Number.MAX_SAFE_INTEGER) - (b.lineNo ?? Number.MAX_SAFE_INTEGER);
|
||
return lineDelta || (markerOrder[a.marker] ?? 9) - (markerOrder[b.marker] ?? 9);
|
||
})
|
||
.forEach((row: any) => renderDiffLine(row.lineNo, row.marker, row.text, row.className));
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderDeleteFile(result: any, args: any): string {
|
||
const path = args.path || result.path || '';
|
||
const status = formatToolStatusLabel(result, '✓ 已删除');
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += '</div>';
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderRenameFile(result: any, args: any): string {
|
||
const oldPath = args.old_path || args.source || result.old_path || '';
|
||
const newPath = args.new_path || args.destination || result.new_path || '';
|
||
const status = formatToolStatusLabel(result, '✓ 已重命名');
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>路径:</strong>${escapeHtml(oldPath)}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += `<div><strong>重命名:</strong>${escapeHtml(oldPath)} → ${escapeHtml(newPath)}</div>`;
|
||
html += '</div>';
|
||
|
||
return html;
|
||
}
|
||
|
||
// ===== 阅读聚焦类 =====
|
||
function renderReadFile(result: any, args: any): string {
|
||
const path = args.path || result.path || '';
|
||
const status = formatToolStatusLabel(result, '✓ 成功');
|
||
const size = result.size || result.file_size || 0;
|
||
const readType = result.type || 'read';
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
if (size > 0) {
|
||
html += `<div><strong>大小:</strong>${formatBytes(size)}</div>`;
|
||
}
|
||
if (readType !== 'read') {
|
||
html += `<div><strong>模式:</strong>${escapeHtml(readType)}</div>`;
|
||
}
|
||
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;
|
||
}
|
||
|
||
function renderVlmAnalyze(result: any, args: any): string {
|
||
const path = args.image_path || args.path || result.path || '';
|
||
const status = formatToolStatusLabel(result, '✓ 成功');
|
||
const analysis = result.analysis || result.result || '';
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += '</div>';
|
||
|
||
if (analysis) {
|
||
html += '<div class="tool-result-content scrollable">';
|
||
html += '<div class="content-label">分析结果:</div>';
|
||
html += `<pre>${escapeHtml(analysis)}</pre>`;
|
||
html += '</div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderOcrImage(result: any, args: any): string {
|
||
const path = args.image_path || args.path || result.path || '';
|
||
const status = formatToolStatusLabel(result, '✓ 成功');
|
||
const size = result.size || result.file_size || 0;
|
||
const text = result.text || result.ocr_text || '';
|
||
const imageUrl = result.image_url || result.url || '';
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
if (size > 0) {
|
||
html += `<div><strong>大小:</strong>${formatBytes(size)}</div>`;
|
||
}
|
||
html += '</div>';
|
||
|
||
if (imageUrl) {
|
||
html += `<div class="tool-result-image"><img src="${escapeHtml(imageUrl)}" alt="OCR图片" style="max-width: 100%; height: auto;" /></div>`;
|
||
}
|
||
|
||
if (text) {
|
||
html += '<div class="tool-result-content scrollable">';
|
||
html += '<div class="content-label">识别文本:</div>';
|
||
html += `<pre>${escapeHtml(text)}</pre>`;
|
||
html += '</div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderViewImage(result: any, args: any): string {
|
||
const path = args.image_path || args.path || result.path || '';
|
||
const status = formatToolStatusLabel(result, '✓ 成功');
|
||
const size = result.size || result.file_size || 0;
|
||
const imageUrl = result.image_url || result.url || '';
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>路径:</strong>${escapeHtml(path)}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
if (size > 0) {
|
||
html += `<div><strong>大小:</strong>${formatBytes(size)}</div>`;
|
||
}
|
||
html += '</div>';
|
||
|
||
if (imageUrl) {
|
||
html += `<div class="tool-result-image"><img src="${escapeHtml(imageUrl)}" alt="查看图片" style="max-width: 100%; height: auto;" /></div>`;
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
// ===== 终端类 =====
|
||
function renderTerminalSession(result: any, args: any): string {
|
||
const operation = args.operation || args.action || 'start';
|
||
const sessionName = args.session_name || args.name || result.session_name || '';
|
||
const status = formatToolStatusLabel(result, '✓ 成功');
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>操作:</strong>${escapeHtml(operation)}</div>`;
|
||
html += `<div><strong>终端名:</strong>${escapeHtml(sessionName)}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += '</div>';
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderTerminalInput(result: any, args: any): string {
|
||
const command = args.command || args.input || '';
|
||
const timeout = args.timeout || 30;
|
||
const output = result.output || result.result || '';
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>指令:</strong>${escapeHtml(command)}</div>`;
|
||
html += `<div><strong>超时时间:</strong>${timeout}秒</div>`;
|
||
html += '</div>';
|
||
|
||
if (output) {
|
||
html += '<div class="tool-result-content scrollable">';
|
||
html += '<div class="content-label">输出:</div>';
|
||
html += `<pre>${escapeHtml(output)}</pre>`;
|
||
html += '</div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderTerminalSnapshot(result: any, args: any): string {
|
||
const sessionName = args.session_name || args.name || result.session_name || '';
|
||
const status = formatToolStatusLabel(result, '✓ 成功');
|
||
const startLine = args.start_line || 0;
|
||
const endLine = args.end_line || 0;
|
||
const content = result.content || result.output || '';
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += `<div><strong>终端名:</strong>${escapeHtml(sessionName)}</div>`;
|
||
if (startLine || endLine) {
|
||
html += `<div><strong>行范围:</strong>${startLine} - ${endLine}</div>`;
|
||
}
|
||
html += '</div>';
|
||
|
||
if (content) {
|
||
html += '<div class="tool-result-content scrollable">';
|
||
html += '<div class="content-label">获取的内容:</div>';
|
||
html += `<pre>${escapeHtml(content)}</pre>`;
|
||
html += '</div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderSleep(result: any, args: any): string {
|
||
const seconds = args.seconds || args.duration || 0;
|
||
const status = formatToolStatusLabel(result, '✓ 完成');
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>等待时间:</strong>${seconds}秒</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += '</div>';
|
||
|
||
return html;
|
||
}
|
||
|
||
// ===== 终端指令类 =====
|
||
function renderRunCommand(result: any, args: any): string {
|
||
const command = args.command || '';
|
||
const timeout = args.timeout || 30;
|
||
const output = result.output || result.stdout || '';
|
||
const exitCode = result.exit_code !== undefined ? result.exit_code : result.returncode;
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>指令:</strong>${escapeHtml(command)}</div>`;
|
||
html += `<div><strong>超时时间:</strong>${timeout}秒</div>`;
|
||
if (exitCode !== undefined) {
|
||
html += `<div><strong>退出码:</strong>${exitCode}</div>`;
|
||
}
|
||
html += '</div>';
|
||
|
||
if (output) {
|
||
html += '<div class="tool-result-content scrollable">';
|
||
html += '<div class="content-label">输出:</div>';
|
||
html += `<pre>${escapeHtml(output)}</pre>`;
|
||
html += '</div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderRunPython(result: any, args: any): string {
|
||
const code = result.code || args.code || '';
|
||
const output = result.output || '';
|
||
|
||
let html = '<div class="code-block">';
|
||
html += '<div class="code-label">代码:</div>';
|
||
html += `<pre><code class="language-python">${escapeHtml(code)}</code></pre>`;
|
||
html += '</div>';
|
||
|
||
if (output) {
|
||
html += '<div class="output-block">';
|
||
html += '<div class="output-label">输出:</div>';
|
||
html += `<pre>${escapeHtml(output)}</pre>`;
|
||
html += '</div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
// ===== 记忆类 =====
|
||
function renderUpdateMemory(result: any, args: any): string {
|
||
const operations = args.operations || [];
|
||
const added = operations.filter((op: any) => op.action === 'add' || op.action === 'create');
|
||
const updated = operations.filter((op: any) => op.action === 'update' || op.action === 'modify');
|
||
const deleted = operations.filter((op: any) => op.action === 'delete' || op.action === 'remove');
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>操作:</strong>添加 ${added.length} 条 | 更新 ${updated.length} 条 | 删除 ${deleted.length} 条</div>`;
|
||
html += '</div>';
|
||
|
||
html += '<div class="tool-result-content scrollable">';
|
||
|
||
if (added.length > 0) {
|
||
html += '<div class="memory-section">';
|
||
added.forEach((op: any) => {
|
||
html += `<div class="memory-item memory-add">+ ${escapeHtml(op.content || op.text || '')}</div>`;
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
if (updated.length > 0) {
|
||
html += '<div class="memory-section">';
|
||
updated.forEach((op: any) => {
|
||
html += `<div class="memory-item memory-update">✏️ ${escapeHtml(op.content || op.text || '')}</div>`;
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
if (deleted.length > 0) {
|
||
html += '<div class="memory-section">';
|
||
deleted.forEach((op: any) => {
|
||
html += `<div class="memory-item memory-delete">- ${escapeHtml(op.content || op.text || '')}</div>`;
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
html += '</div>';
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderConversationSearch(result: any, args: any): string {
|
||
const results = Array.isArray(result?.results) ? result.results : [];
|
||
const keywords = Array.isArray(result?.keywords)
|
||
? result.keywords
|
||
: (Array.isArray(args?.keywords) ? args.keywords : []);
|
||
const keywordText = keywords.length
|
||
? keywords.join(' / ')
|
||
: (result?.query || args?.query || '(未指定)');
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>关键词:</strong>${escapeHtml(keywordText)}</div>`;
|
||
html += `<div><strong>日期范围:</strong>${escapeHtml(result?.start_date || args?.start_date || '不限')} ~ ${escapeHtml(result?.end_date || args?.end_date || '不限')}</div>`;
|
||
html += '<div><strong>范围:</strong>当前工作区历史对话(已排除当前对话)</div>';
|
||
html += `<div><strong>结果数量:</strong>${results.length}</div>`;
|
||
html += '</div>';
|
||
if (!results.length) {
|
||
html += '<div class="tool-result-empty">未找到匹配对话。</div>';
|
||
return html;
|
||
}
|
||
html += '<div class="search-result-list">';
|
||
results.forEach((item: any) => {
|
||
html += '<div class="search-result-item">';
|
||
html += `<div class="search-result-title">${escapeHtml(item.title || '未命名对话')}</div>`;
|
||
html += `<div><strong>ID:</strong>${escapeHtml(item.id || '')}</div>`;
|
||
html += `<div><strong>规模:</strong>${escapeHtml(String(item.total_messages || 0))} 条消息,${escapeHtml(String(item.total_tools || 0))} 个工具</div>`;
|
||
if (item.first_user_message) {
|
||
html += `<div><strong>首条用户消息:</strong>${escapeHtml(item.first_user_message)}</div>`;
|
||
}
|
||
if (item.updated_at) {
|
||
html += `<div><strong>更新时间:</strong>${escapeHtml(item.updated_at)}</div>`;
|
||
}
|
||
html += '</div>';
|
||
});
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
function renderConversationReview(result: any, args: any): string {
|
||
const status = formatToolStatusLabel(result, result?.mode === 'read' ? '✓ 已返回' : '✓ 已生成');
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>对话 ID:</strong>${escapeHtml(result?.conversation_id || args?.conversation_id || '')}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += `<div><strong>模式:</strong>${escapeHtml(result?.mode || args?.mode || '')}</div>`;
|
||
if (result?.title) {
|
||
html += `<div><strong>标题:</strong>${escapeHtml(result.title)}</div>`;
|
||
}
|
||
if (result?.path) {
|
||
html += `<div><strong>回顾文件:</strong>${escapeHtml(result.path)}</div>`;
|
||
}
|
||
if (result?.char_count !== undefined) {
|
||
html += `<div><strong>字符数:</strong>${escapeHtml(String(result.char_count))}</div>`;
|
||
}
|
||
html += '</div>';
|
||
if (!result?.success && result?.error) {
|
||
html += `<div class="tool-result-error">${escapeHtml(result.error)}</div>`;
|
||
}
|
||
if (result?.too_long && result?.path) {
|
||
html += '<div class="tool-result-warning">内容太长,已保存到文件,请分段或查找阅读。</div>';
|
||
}
|
||
if (result?.content) {
|
||
html += '<div class="tool-result-content scrollable">';
|
||
html += '<div class="content-label">回顾内容:</div>';
|
||
html += `<pre>${escapeHtml(result.content)}</pre>`;
|
||
html += '</div>';
|
||
}
|
||
return html;
|
||
}
|
||
|
||
function renderCreateSkill(result: any, args: any): string {
|
||
const status = formatToolStatusLabel(result, '✓ 已归档');
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
if (result?.skill_name) {
|
||
html += `<div><strong>Skill:</strong>${escapeHtml(result.skill_name)}</div>`;
|
||
}
|
||
html += '</div>';
|
||
if (!result?.success && result?.error) {
|
||
html += `<div class="tool-result-error">${escapeHtml(result.error)}</div>`;
|
||
}
|
||
if (result?.sync_warning) {
|
||
html += `<div class="tool-result-warning">${escapeHtml(result.sync_warning)}</div>`;
|
||
}
|
||
return html;
|
||
}
|
||
|
||
function formatPersonalizationFieldLabel(field: string): string {
|
||
const labelMap: Record<string, string> = {
|
||
enabled: '个性化功能',
|
||
self_identify: 'AI 自称',
|
||
user_name: '用户称呼',
|
||
profession: '用户职业',
|
||
tone: '交流语气',
|
||
considerations: '注意事项',
|
||
theme: '主题配色',
|
||
communication_style: '交流风格'
|
||
};
|
||
return labelMap[field] || field;
|
||
}
|
||
|
||
function formatPersonalizationFieldValue(field: string, value: any): string {
|
||
if (value === null || value === undefined || value === '') {
|
||
return '未设置';
|
||
}
|
||
|
||
if (field === 'enabled') {
|
||
return value ? '开启' : '关闭';
|
||
}
|
||
if (field === 'theme') {
|
||
const themeMap: Record<string, string> = {
|
||
classic: 'classic(经典)',
|
||
light: 'light(明亮)',
|
||
dark: 'dark(暗黑)'
|
||
};
|
||
return themeMap[String(value)] || String(value);
|
||
}
|
||
if (field === 'communication_style') {
|
||
const styleMap: Record<string, string> = {
|
||
default: 'default(标准 AI 风格)',
|
||
human_like: 'human_like(拟人聊天风格)'
|
||
};
|
||
return styleMap[String(value)] || String(value);
|
||
}
|
||
if (field === 'considerations') {
|
||
if (!Array.isArray(value) || value.length === 0) {
|
||
return '未设置';
|
||
}
|
||
return value.map((item) => String(item)).join('、');
|
||
}
|
||
|
||
return String(value);
|
||
}
|
||
|
||
|
||
function renderAskUser(result: any, args: any): string {
|
||
const question = args.question || result.question || '';
|
||
const context = args.context || result.context || '';
|
||
const status = formatToolStatusLabel(result, '✓ 已回答');
|
||
const answerText = result.status === 'answered' ? String(result.answer_text || result.message || '').trim() : '';
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>状态:</strong>${escapeHtml(status)}</div>`;
|
||
if (question) {
|
||
html += `<div><strong>问题:</strong>${escapeHtml(String(question))}</div>`;
|
||
}
|
||
if (context) {
|
||
html += `<div><strong>说明:</strong>${escapeHtml(String(context))}</div>`;
|
||
}
|
||
html += '</div>';
|
||
|
||
const options = Array.isArray(args.options) ? args.options : [];
|
||
if (options.length > 0) {
|
||
html += '<div class="tool-result-content">';
|
||
html += '<div class="tool-result-meta">';
|
||
html += '<div><strong>提供的选项:</strong></div>';
|
||
options.forEach((option: any, idx: number) => {
|
||
const label = String(option?.label || option?.id || `选项 ${idx + 1}`);
|
||
const desc = String(option?.description || '').trim();
|
||
html += `<div>${idx + 1}. ${escapeHtml(label)}${desc ? ` — ${escapeHtml(desc)}` : ''}</div>`;
|
||
});
|
||
html += '</div></div>';
|
||
}
|
||
|
||
if (answerText) {
|
||
html += '<div class="tool-result-content">';
|
||
html += '<div class="tool-result-meta">';
|
||
answerText.split('\n').map((line) => line.trim()).filter(Boolean).forEach((line) => {
|
||
const separator = line.indexOf(':');
|
||
if (separator > 0) {
|
||
const label = line.slice(0, separator + 1);
|
||
const value = line.slice(separator + 1);
|
||
html += `<div><strong>${escapeHtml(label)}</strong>${escapeHtml(value)}</div>`;
|
||
} else {
|
||
html += `<div><strong>用户回答:</strong>${escapeHtml(line)}</div>`;
|
||
}
|
||
});
|
||
html += '</div></div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderManagePersonalization(result: any, args: any): string {
|
||
const action = String(args.action || result.action || 'read');
|
||
const status = formatToolStatusLabel(
|
||
result,
|
||
action === 'update' ? '✓ 已更新' : '✓ 已读取'
|
||
);
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>操作:</strong>${escapeHtml(action === 'update' ? '更新配置' : '读取配置')}</div>`;
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
|
||
if (!result?.success) {
|
||
if (result?.error) {
|
||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||
}
|
||
if (Array.isArray(result?.validation_errors) && result.validation_errors.length > 0) {
|
||
html += `<div><strong>校验:</strong>${escapeHtml(result.validation_errors.join(';'))}</div>`;
|
||
}
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
if (action === 'update') {
|
||
const field = String(result.updated_field || args.field || '');
|
||
const newValue = result.updated_value ?? args.value;
|
||
|
||
if (field) {
|
||
html += `<div><strong>修改项:</strong>${escapeHtml(formatPersonalizationFieldLabel(field))}</div>`;
|
||
html += `<div><strong>修改后:</strong>${escapeHtml(formatPersonalizationFieldValue(field, newValue))}</div>`;
|
||
}
|
||
html += '</div>';
|
||
|
||
return html;
|
||
}
|
||
html += '</div>';
|
||
|
||
return html;
|
||
}
|
||
|
||
// ===== 待办事项类 =====
|
||
function renderTodoCreate(result: any, args: any): string {
|
||
const status = formatToolStatusLabel(result, '✓ 已创建');
|
||
const title = args.title || result.title || '';
|
||
const tasks = args.tasks || result.tasks || [];
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += `<div><strong>标题:</strong>${escapeHtml(title)}</div>`;
|
||
html += '</div>';
|
||
|
||
if (tasks.length > 0) {
|
||
html += '<div class="tool-result-content">';
|
||
html += '<div class="todo-list">';
|
||
tasks.forEach((task: any) => {
|
||
const taskText = typeof task === 'string' ? task : task.text || task.title || '';
|
||
html += `<div class="todo-item"><input type="checkbox" disabled /> ${escapeHtml(taskText)}</div>`;
|
||
});
|
||
html += '</div>';
|
||
html += '</div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
function renderTodoUpdate(result: any, args: any): string {
|
||
const status = formatToolStatusLabel(result, '✓ 已更新');
|
||
const title = args.title || result.title || '';
|
||
const tasks = result.tasks || args.tasks || [];
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += `<div><strong>标题:</strong>${escapeHtml(title)}</div>`;
|
||
html += '</div>';
|
||
|
||
if (tasks.length > 0) {
|
||
html += '<div class="tool-result-content">';
|
||
html += '<div class="todo-list">';
|
||
tasks.forEach((task: any) => {
|
||
const taskText = typeof task === 'string' ? task : task.text || task.title || '';
|
||
const completed = typeof task === 'object' && (task.completed || task.done || task.checked);
|
||
html += `<div class="todo-item"><input type="checkbox" ${completed ? 'checked' : ''} disabled /> ${escapeHtml(taskText)}</div>`;
|
||
});
|
||
html += '</div>';
|
||
html += '</div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
// ===== 彩蛋类 =====
|
||
function renderEasterEgg(result: any, args: any): string {
|
||
const status = formatToolStatusLabel(result, '✓ 已触发');
|
||
const type = result.type || args.type || '';
|
||
const content = result.content || result.message || '';
|
||
|
||
let html = '<div class="tool-result-meta">';
|
||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||
html += `<div><strong>类型:</strong>${escapeHtml(type)}</div>`;
|
||
html += '</div>';
|
||
|
||
if (content) {
|
||
html += '<div class="tool-result-content">';
|
||
html += `<div class="easter-egg-content">${escapeHtml(content)}</div>`;
|
||
html += '</div>';
|
||
}
|
||
|
||
return html;
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
/* 工具增强显示样式 - 不使用 scoped 以便应用到 v-html 内容 */
|
||
.tool-result-meta {
|
||
margin-bottom: 12px;
|
||
padding: 8px 12px;
|
||
background: rgba(0, 0, 0, 0.02);
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.tool-result-meta > div {
|
||
margin: 4px 0;
|
||
}
|
||
|
||
.tool-result-content {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.tool-result-content.scrollable {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
border-radius: 6px;
|
||
padding: 8px;
|
||
background: rgba(0, 0, 0, 0.01);
|
||
}
|
||
|
||
/* 结构化显示滚动条样式(参考 git 状态侧边栏文件列表,滚动槽透明贴合灰色底色) */
|
||
.tool-result-content.scrollable,
|
||
.tool-result-diff.scrollable,
|
||
.code-block pre,
|
||
.output-block pre {
|
||
scrollbar-width: thin; /* Firefox */
|
||
scrollbar-color: color-mix(in srgb, var(--claude-text-secondary, #6b7280) 55%, transparent)
|
||
transparent;
|
||
}
|
||
|
||
.tool-result-content.scrollable::-webkit-scrollbar,
|
||
.tool-result-diff.scrollable::-webkit-scrollbar,
|
||
.code-block pre::-webkit-scrollbar,
|
||
.output-block pre::-webkit-scrollbar {
|
||
width: 10px;
|
||
height: 10px;
|
||
}
|
||
|
||
.tool-result-content.scrollable::-webkit-scrollbar-track,
|
||
.tool-result-diff.scrollable::-webkit-scrollbar-track,
|
||
.code-block pre::-webkit-scrollbar-track,
|
||
.output-block pre::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
border-radius: 999px;
|
||
margin: 8px;
|
||
}
|
||
|
||
.tool-result-content.scrollable::-webkit-scrollbar-thumb,
|
||
.tool-result-diff.scrollable::-webkit-scrollbar-thumb,
|
||
.code-block pre::-webkit-scrollbar-thumb,
|
||
.output-block pre::-webkit-scrollbar-thumb {
|
||
background: color-mix(in srgb, var(--claude-text-secondary, #6b7280) 55%, transparent);
|
||
border-radius: 999px;
|
||
border: 2px solid transparent;
|
||
background-clip: padding-box;
|
||
}
|
||
|
||
.tool-result-content pre {
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.content-label {
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.tool-result-empty {
|
||
color: rgba(0, 0, 0, 0.5);
|
||
font-style: italic;
|
||
padding: 8px;
|
||
}
|
||
|
||
/* 搜索结果 */
|
||
.search-result-list {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.search-result-item {
|
||
padding: 10px;
|
||
margin-bottom: 8px;
|
||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
border-radius: 6px;
|
||
background: rgba(0, 0, 0, 0.01);
|
||
}
|
||
|
||
/* 暗色模式下的搜索结果 */
|
||
:root[data-theme='dark'] .search-result-item {
|
||
background: #2a2a2a;
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.search-result-title {
|
||
font-weight: 600;
|
||
margin-bottom: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.search-result-url {
|
||
font-size: 12px;
|
||
color: rgba(0, 0, 0, 0.6);
|
||
}
|
||
|
||
:root[data-theme='dark'] .search-result-url {
|
||
color: rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
.search-result-url a {
|
||
color: #0066cc;
|
||
text-decoration: none;
|
||
}
|
||
|
||
:root[data-theme='dark'] .search-result-url a {
|
||
color: #6b9fff;
|
||
}
|
||
|
||
.search-result-url a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* 文件差异 */
|
||
.tool-result-diff {
|
||
margin-top: 12px;
|
||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.tool-result-diff.scrollable {
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
border-radius: 6px;
|
||
padding: 8px;
|
||
background: rgba(0, 0, 0, 0.01);
|
||
}
|
||
|
||
.diff-line {
|
||
display: grid;
|
||
grid-template-columns: 5ch 1ch minmax(0, 1fr);
|
||
column-gap: 8px;
|
||
padding: 2px 4px;
|
||
margin: 1px 0;
|
||
align-items: start;
|
||
}
|
||
|
||
.diff-line-number {
|
||
color: var(--claude-text-tertiary);
|
||
text-align: right;
|
||
user-select: none;
|
||
}
|
||
|
||
.diff-marker {
|
||
color: var(--claude-text-secondary);
|
||
text-align: center;
|
||
user-select: none;
|
||
}
|
||
|
||
.diff-content {
|
||
min-width: 0;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
.diff-remove {
|
||
background: rgba(255, 0, 0, 0.1);
|
||
color: #c00;
|
||
}
|
||
|
||
.diff-add {
|
||
background: rgba(0, 255, 0, 0.1);
|
||
color: #0a0;
|
||
}
|
||
|
||
.diff-separator {
|
||
text-align: left;
|
||
color: var(--claude-text-tertiary);
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.diff-separator .diff-content {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.diff-operation {
|
||
font-weight: 600;
|
||
margin: 8px 0 4px;
|
||
color: rgba(0, 0, 0, 0.7);
|
||
}
|
||
|
||
/* 图片 */
|
||
.tool-result-image {
|
||
margin-top: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
.tool-result-image img {
|
||
max-width: 100%;
|
||
height: auto;
|
||
border-radius: 6px;
|
||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* 代码块 */
|
||
.code-block {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.code-label,
|
||
.output-label {
|
||
font-weight: 600;
|
||
margin-bottom: 6px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.code-block pre,
|
||
.output-block pre {
|
||
margin: 0;
|
||
padding: 12px;
|
||
background: rgba(0, 0, 0, 0.03);
|
||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||
border-radius: 6px;
|
||
overflow-x: auto;
|
||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
/* 暗色模式:run_python 代码/输出统一白字,去除任何描边/阴影观感 */
|
||
:root[data-theme='dark'] .code-block pre,
|
||
:root[data-theme='dark'] .output-block pre {
|
||
color: #ffffff !important;
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border-color: rgba(255, 255, 255, 0.12);
|
||
text-shadow: none !important;
|
||
}
|
||
|
||
:root[data-theme='dark'] .code-block pre code,
|
||
:root[data-theme='dark'] .output-block pre code {
|
||
color: #ffffff !important;
|
||
text-shadow: none !important;
|
||
-webkit-text-fill-color: #ffffff;
|
||
}
|
||
|
||
.output-block {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
/* 记忆 */
|
||
.memory-section {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.memory-item {
|
||
padding: 6px 8px;
|
||
margin: 4px 0;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.memory-add {
|
||
background: rgba(0, 255, 0, 0.1);
|
||
color: #0a0;
|
||
}
|
||
|
||
.memory-update {
|
||
background: rgba(255, 165, 0, 0.1);
|
||
color: #f90;
|
||
}
|
||
|
||
.memory-delete {
|
||
background: rgba(255, 0, 0, 0.1);
|
||
color: #c00;
|
||
}
|
||
|
||
/* 待办事项 */
|
||
.todo-list {
|
||
padding: 8px;
|
||
}
|
||
|
||
.todo-item {
|
||
padding: 6px 0;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.todo-item input[type='checkbox'] {
|
||
margin: 0;
|
||
}
|
||
|
||
/* 彩蛋 */
|
||
.easter-egg-content {
|
||
padding: 12px;
|
||
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1), rgba(255, 105, 180, 0.1));
|
||
border: 1px solid rgba(255, 215, 0, 0.3);
|
||
border-radius: 8px;
|
||
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;
|
||
}
|
||
|
||
/* 个性化管理 */
|
||
.personalization-message {
|
||
padding: 12px;
|
||
background: rgba(0, 0, 0, 0.02);
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
line-height: 1.6;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
:root[data-theme='dark'] .personalization-message {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.personalization-config-title {
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
color: rgba(0, 0, 0, 0.8);
|
||
}
|
||
|
||
:root[data-theme='dark'] .personalization-config-title {
|
||
color: rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
.personalization-config-list {
|
||
display: grid;
|
||
gap: 6px;
|
||
}
|
||
|
||
.config-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 6px 10px;
|
||
background: rgba(0, 0, 0, 0.02);
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
:root[data-theme='dark'] .config-item {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.config-label {
|
||
color: rgba(0, 0, 0, 0.6);
|
||
font-weight: 500;
|
||
}
|
||
|
||
:root[data-theme='dark'] .config-label {
|
||
color: rgba(255, 255, 255, 0.6);
|
||
}
|
||
|
||
.config-value {
|
||
color: rgba(0, 0, 0, 0.9);
|
||
font-weight: 400;
|
||
}
|
||
|
||
:root[data-theme='dark'] .config-value {
|
||
color: rgba(255, 255, 255, 0.9);
|
||
}
|
||
</style>
|