diff --git a/src/renderer/components/chat-area.ts b/src/renderer/components/chat-area.ts
index dd26cf9..50017d4 100644
--- a/src/renderer/components/chat-area.ts
+++ b/src/renderer/components/chat-area.ts
@@ -157,22 +157,6 @@ export function appendMessageDOM(msg: ChatMessage, index: number): void {
contentHtml = contentHtml.replace('
';
- for (const tc of msg.toolCalls) {
- toolHtml += renderToolCallCard(tc);
- }
- toolHtml += '
';
- // 插入到 msg-content 之前
- const contentIdx = contentHtml.indexOf('');
- if (contentIdx >= 0) {
- contentHtml = contentHtml.slice(0, contentIdx) + toolHtml + contentHtml.slice(contentIdx);
- } else {
- contentHtml = toolHtml + contentHtml;
- }
- }
-
const stats: string[] = [];
const statClasses: string[] = [];
if (msg.timestamp) { stats.push(formatTime(msg.timestamp)); statClasses.push(''); }
diff --git a/src/renderer/components/input-area.ts b/src/renderer/components/input-area.ts
index e9bdc85..5b8ee8b 100644
--- a/src/renderer/components/input-area.ts
+++ b/src/renderer/components/input-area.ts
@@ -12,6 +12,7 @@ import {
appendToolCallCardToPlaceholder, updateToolCallCardInPlaceholder
} from './chat-area.js';
import { showToast } from './toast.js';
+import { addToolCard, updateToolCard } from './workspace-panel.js';
import { ChatDB } from '../db/chat-db.js';
import { OllamaAPI } from '../api/ollama.js';
import { runAgentLoop } from '../services/agent-engine.js';
@@ -288,19 +289,19 @@ async function handleRetry(): Promise
{
onThinking: (thinking) => updateLastAssistantMessage('', thinking, null),
onContent: (content) => updateLastAssistantMessage(content, null, null),
onToolCallStart: (call) => {
- appendToolCallCardToPlaceholder({
+ addToolCard({
name: call.function.name, arguments: call.function.arguments,
result: null, status: 'running', timestamp: Date.now()
});
},
onToolCallResult: (name, result, call) => {
- updateToolCallCardInPlaceholder({
+ updateToolCard({
name, arguments: call.function.arguments,
result, status: result.success ? 'success' : 'error', timestamp: Date.now()
});
},
onToolCallError: (name, error, call) => {
- updateToolCallCardInPlaceholder({
+ updateToolCard({
name, arguments: call.function.arguments,
result: { success: false, error }, status: 'error', timestamp: Date.now()
});
@@ -890,19 +891,19 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
updateLastAssistantMessage(assistantContent, thinkContent || null, null);
},
onToolCallStart: (call) => {
- appendToolCallCardToPlaceholder({
+ addToolCard({
name: call.function.name, arguments: call.function.arguments,
result: null, status: 'running', timestamp: Date.now()
});
},
onToolCallResult: (name, result, call) => {
- updateToolCallCardInPlaceholder({
+ updateToolCard({
name, arguments: call.function.arguments,
result, status: result.success ? 'success' : 'error', timestamp: Date.now()
});
},
onToolCallError: (name, error, call) => {
- updateToolCallCardInPlaceholder({
+ updateToolCard({
name, arguments: call.function.arguments,
result: { success: false, error }, status: 'error', timestamp: Date.now()
});
diff --git a/src/renderer/components/workspace-panel.ts b/src/renderer/components/workspace-panel.ts
index 05823a3..5a9d8a4 100644
--- a/src/renderer/components/workspace-panel.ts
+++ b/src/renderer/components/workspace-panel.ts
@@ -5,6 +5,15 @@
import { logInfo, logError, logDebug } from '../services/log-service.js';
+// ── 工具卡片类型 ──
+interface ToolCallRecord {
+ name: string;
+ arguments: Record;
+ result: Record | null;
+ status: string;
+ timestamp: number;
+}
+
// ── 状态 ──
interface TerminalSession {
id: string;
@@ -30,7 +39,7 @@ interface FileNode {
}
let panelVisible = true; // 常驻显示,不可关闭
-let activeTab: 'terminal' | 'files' = 'terminal';
+let activeTab: 'terminal' | 'tools' | 'files' = 'terminal';
let terminal: TerminalSession | null = null; // 唯一终端
let workspaceDir = '';
let currentFileDir = '';
@@ -38,6 +47,10 @@ let fileTree: FileNode[] = [];
let previewFile: { name: string; content: string; path: string } | null = null;
let _counter = 0;
+/** 工具调用卡片列表 */
+let toolCards: ToolCallRecord[] = [];
+let toolAutoScroll = true;
+
/** 当前正在运行的 AI 命令(用于用户手动终止时通知 AI) */
let currentAiCommand: string | null = null;
@@ -206,6 +219,7 @@ export async function initWorkspacePanel(): Promise {
function bindEvents(): void {
// Tab 切换
document.querySelector('#wsTabTerminal')?.addEventListener('click', () => switchTab('terminal'));
+ document.querySelector('#wsTabTools')?.addEventListener('click', () => switchTab('tools'));
document.querySelector('#wsTabFiles')?.addEventListener('click', () => switchTab('files'));
// 停止按钮
@@ -214,6 +228,9 @@ function bindEvents(): void {
// 清空按钮
document.querySelector('#wsClearBtn')?.addEventListener('click', clearTerminal);
+ // 工具清空按钮
+ document.querySelector('#wsToolsClearBtn')?.addEventListener('click', clearToolCards);
+
// 文件刷新
document.querySelector('#wsFileRefreshBtn')?.addEventListener('click', () => {
loadFiles(currentFileDir || workspaceDir);
@@ -235,7 +252,7 @@ function bindEvents(): void {
}
// ── 面板控制 ──(常驻显示,无需切换)
-function switchTab(tab: 'terminal' | 'files'): void {
+function switchTab(tab: 'terminal' | 'tools' | 'files'): void {
activeTab = tab;
renderPanel();
@@ -438,23 +455,25 @@ export function renderPanel(): void {
if (!panel) return;
const termTab = document.querySelector('#wsTabTerminal') as HTMLElement;
+ const toolsTab = document.querySelector('#wsTabTools') as HTMLElement;
const filesTab = document.querySelector('#wsTabFiles') as HTMLElement;
- if (termTab) {
- termTab.classList.toggle('active', activeTab === 'terminal');
- }
- if (filesTab) {
- filesTab.classList.toggle('active', activeTab === 'files');
- }
+ if (termTab) termTab.classList.toggle('active', activeTab === 'terminal');
+ if (toolsTab) toolsTab.classList.toggle('active', activeTab === 'tools');
+ if (filesTab) filesTab.classList.toggle('active', activeTab === 'files');
const termContent = document.querySelector('#wsTerminalContent') as HTMLElement;
+ const toolsContent = document.querySelector('#wsToolsContent') as HTMLElement;
const filesContent = document.querySelector('#wsFilesContent') as HTMLElement;
if (termContent) termContent.style.display = activeTab === 'terminal' ? '' : 'none';
+ if (toolsContent) toolsContent.style.display = activeTab === 'tools' ? '' : 'none';
if (filesContent) filesContent.style.display = activeTab === 'files' ? '' : 'none';
if (activeTab === 'terminal') {
renderTerminal();
+ } else if (activeTab === 'tools') {
+ renderToolCalls();
} else {
renderFileList();
}
@@ -617,6 +636,197 @@ function formatSize(bytes: number): string {
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
+// ── 工具调用管理 ──
+
+/** 添加工具调用卡片(由外部调用) */
+export function addToolCard(tc: ToolCallRecord): void {
+ toolCards.push(tc);
+ // 如果是 run_command,自动切到终端 tab
+ if (tc.name === 'run_command') {
+ switchToTab('terminal');
+ } else {
+ switchToTab('tools');
+ }
+ renderToolCalls();
+ updateToolsHint();
+}
+
+/** 更新工具调用卡片状态(由外部调用) */
+export function updateToolCard(tc: ToolCallRecord): void {
+ // 找到最后一个同名且 running 的卡片
+ for (let i = toolCards.length - 1; i >= 0; i--) {
+ if (toolCards[i].name === tc.name && toolCards[i].status === 'running') {
+ toolCards[i] = tc;
+ break;
+ }
+ }
+ renderToolCalls();
+ updateToolsHint();
+}
+
+function clearToolCards(): void {
+ toolCards = [];
+ renderToolCalls();
+ updateToolsHint();
+}
+
+function updateToolsHint(): void {
+ const hint = document.querySelector('#wsToolsHint') as HTMLElement;
+ if (!hint) return;
+ const running = toolCards.filter(c => c.status === 'running');
+ if (running.length > 0) {
+ hint.textContent = `⏳ 执行中: ${running.map(c => getToolDisplayName(c.name)).join(', ')}`;
+ hint.classList.add('running');
+ } else {
+ const total = toolCards.length;
+ const success = toolCards.filter(c => c.status === 'success').length;
+ const error = toolCards.filter(c => c.status === 'error').length;
+ hint.textContent = total > 0 ? `共 ${total} 个工具调用(✅${success} ❌${error})` : '等待工具调用...';
+ hint.classList.remove('running');
+ }
+}
+
+function renderToolCalls(): void {
+ const container = document.querySelector('#wsToolsOutput') as HTMLElement;
+ if (!container) return;
+
+ if (toolCards.length === 0) {
+ container.innerHTML = '等待工具调用...
';
+ return;
+ }
+
+ let html = '';
+ for (const tc of toolCards) {
+ html += renderToolCard(tc);
+ }
+ container.innerHTML = html;
+
+ if (toolAutoScroll) {
+ container.scrollTop = container.scrollHeight;
+ }
+
+ container.onscroll = () => {
+ toolAutoScroll = container.scrollHeight - container.scrollTop - container.clientHeight < 50;
+ };
+}
+
+function getToolDisplayName(name: string): string {
+ const names: Record = {
+ read_file: '读取文件', write_file: '写入文件', list_directory: '列出目录',
+ search_files: '搜索文件', create_directory: '创建目录', delete_file: '删除文件', run_command: '执行命令',
+ move_file: '移动文件', copy_file: '复制文件', web_fetch: '网页抓取', append_file: '追加文件',
+ edit_file: '编辑文件', get_file_info: '文件信息', tree: '目录树', download_file: '下载文件',
+ diff_files: '文件对比', replace_in_files: '批量替换', read_multiple_files: '批量读取',
+ git: 'Git 操作', compress: '压缩/解压', web_search: '联网搜索',
+ memory_search: '搜索记忆', memory_add: '添加记忆', memory_replace: '替换记忆', memory_remove: '删除记忆',
+ skill_list: '技能列表', skill_view: '查看技能',
+ session_list: '会话列表', session_read: '读取会话', spawn_task: '子代理委派',
+ browser_open: '打开网页', browser_screenshot: '浏览器截图', browser_evaluate: '执行JS', browser_extract: '提取内容',
+ browser_click: '点击元素', browser_type: '输入文本', browser_scroll: '滚动页面', browser_close: '关闭浏览器'
+ };
+ return names[name] || name;
+}
+
+function getToolIcon(name: string): string {
+ const icons: Record = {
+ read_file: '📄', write_file: '✏️', list_directory: '📁',
+ search_files: '🔍', create_directory: '📂', delete_file: '🗑️', run_command: '💻',
+ move_file: '📦', copy_file: '📋', web_fetch: '🌐', append_file: '➕',
+ edit_file: '✂️', get_file_info: 'ℹ️', tree: '🌳', download_file: '⬇️',
+ diff_files: '🔀', replace_in_files: '🔄', read_multiple_files: '📚',
+ git: '🔖', compress: '🗜️', web_search: '🔍',
+ memory_search: '🧠', memory_add: '💾', memory_replace: '✏️', memory_remove: '🗑️',
+ skill_list: '🎯', skill_view: '👁️',
+ session_list: '📋', session_read: '📖', spawn_task: '🤖',
+ browser_open: '🌐', browser_screenshot: '📸', browser_evaluate: '⚡', browser_extract: '📰',
+ browser_click: '👆', browser_type: '⌨️', browser_scroll: '↕️', browser_close: '❌'
+ };
+ return icons[name] || '🔧';
+}
+
+function escapeHtml(text: string): string {
+ return text
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+function renderToolCard(tc: ToolCallRecord): string {
+ const icon = getToolIcon(tc.name);
+ const name = getToolDisplayName(tc.name);
+ const statusLabels: Record = {
+ pending: '⏳ 等待确认', running: '🔄 执行中', success: '✅ 完成', error: '❌ 失败', cancelled: '🚫 已取消'
+ };
+ const status = statusLabels[tc.status] || tc.status;
+ const args = tc.arguments || {};
+
+ let paramsHtml = '';
+ if (args.path) paramsHtml += `路径: ${escapeHtml(String(args.path))}
`;
+ if (args.command) paramsHtml += `命令: ${escapeHtml(String(args.command))}
`;
+ if (args.query) paramsHtml += `查询: ${escapeHtml(String(args.query))}
`;
+ if (args.url) paramsHtml += `URL: ${escapeHtml(String(args.url))}
`;
+ if (args.source) paramsHtml += `源: ${escapeHtml(String(args.source))}
`;
+ if (args.destination) paramsHtml += `目标: ${escapeHtml(String(args.destination))}
`;
+ if (args.selector) paramsHtml += `选择器: ${escapeHtml(String(args.selector))}
`;
+ if (args.action) paramsHtml += `操作: ${escapeHtml(String(args.action))}
`;
+
+ let resultHtml = '';
+ if (tc.result) {
+ const r = tc.result as Record;
+ if (r.success) {
+ if (tc.name === 'run_command') {
+ const stdout = String(r.stdout || '').slice(0, 500);
+ const stderr = String(r.stderr || '').slice(0, 300);
+ if (stdout) resultHtml += `${escapeHtml(stdout)}${String(r.stdout || '').length > 500 ? '\n...' : ''}`;
+ if (stderr) resultHtml += `${escapeHtml(stderr)}`;
+ resultHtml += `exit: ${r.exitCode || 0}
`;
+ } else if (tc.name === 'web_search' && r.results) {
+ const results = r.results as Array<{ title: string; url: string }>;
+ resultHtml = results.slice(0, 5).map((item, i) =>
+ `[${i + 1}] ${escapeHtml(item.title)}
${escapeHtml(item.url)}
`
+ ).join('');
+ resultHtml += `共 ${r.total || results.length} 条结果
`;
+ } else if (tc.name === 'read_file' && r.content) {
+ const content = String(r.content);
+ const lines = content.split('\n');
+ const preview = lines.slice(0, 15).join('\n');
+ resultHtml = `${escapeHtml(preview)}${lines.length > 15 ? `\n... (共 ${lines.length} 行)` : ''}`;
+ } else if (tc.name === 'list_directory' && r.entries) {
+ const entries = r.entries as Array<{ name: string; type: string }>;
+ resultHtml = entries.slice(0, 20).map(e =>
+ `${e.type === 'directory' ? '📁' : '📄'} ${escapeHtml(e.name)}
`
+ ).join('');
+ if (entries.length > 20) resultHtml += `... 共 ${entries.length} 项
`;
+ } else if (tc.name === 'write_file') {
+ resultHtml = `✅ 已写入 ${escapeHtml(String(r.path || ''))}
`;
+ } else if (tc.name === 'delete_file') {
+ resultHtml = `✅ 已删除 ${escapeHtml(String(r.path || ''))}
`;
+ } else if (tc.name === 'create_directory') {
+ resultHtml = `✅ 已创建 ${escapeHtml(String(r.path || ''))}
`;
+ } else {
+ // 通用成功显示
+ const summary = JSON.stringify(r).slice(0, 300);
+ resultHtml = `${escapeHtml(summary)}${JSON.stringify(r).length > 300 ? '\n...' : ''}`;
+ }
+ } else {
+ resultHtml = `${escapeHtml(String(r.error || '未知错误'))}
`;
+ }
+ }
+
+ const statusClass = tc.status === 'running' ? 'tool-call-running' : tc.status === 'error' ? 'tool-call-error' : '';
+
+ return ``;
+}
+
// ── 外部调用接口 ──
/** 获取工作空间目录 */
@@ -624,3 +834,9 @@ export function getWorkspaceDirPath(): string {
return workspaceDir;
}
+/** 外部自动切换 tab */
+export function switchToTab(tab: 'terminal' | 'tools' | 'files'): void {
+ activeTab = tab;
+ renderPanel();
+}
+
diff --git a/src/renderer/index.html b/src/renderer/index.html
index 2507a36..3e38db1 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -170,6 +170,7 @@
🖥️ 工作空间
+
@@ -195,6 +196,22 @@
+
+
diff --git a/src/renderer/styles/style.css b/src/renderer/styles/style.css
index 8fe4d6a..5233dc8 100644
--- a/src/renderer/styles/style.css
+++ b/src/renderer/styles/style.css
@@ -2351,6 +2351,142 @@ html, body {
font-family: var(--font);
}
+/* 工具调用 Tab */
+.ws-tools-output {
+ flex: 1;
+ overflow-y: auto;
+ padding: 10px 14px;
+ font-size: 12px;
+ line-height: 1.6;
+ color: var(--text-primary);
+ background: #2D2016;
+}
+
+.ws-tools-placeholder {
+ color: rgba(245, 240, 232, 0.4);
+ text-align: center;
+ padding: 40px 20px;
+}
+
+.ws-tools-output .tool-call-card {
+ background: rgba(245, 240, 232, 0.06);
+ border: 1px solid rgba(245, 240, 232, 0.1);
+ border-radius: 8px;
+ margin-bottom: 8px;
+ padding: 10px 12px;
+ font-family: var(--font);
+}
+
+.ws-tools-output .tool-call-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-bottom: 6px;
+}
+
+.ws-tools-output .tool-call-icon {
+ font-size: 14px;
+}
+
+.ws-tools-output .tool-call-name {
+ font-size: 13px;
+ font-weight: 600;
+ color: #FAF7F2;
+ flex: 1;
+}
+
+.ws-tools-output .tool-call-status {
+ font-size: 11px;
+ color: rgba(245, 240, 232, 0.6);
+}
+
+.ws-tools-output .tool-call-params {
+ margin-bottom: 6px;
+ padding-left: 20px;
+}
+
+.ws-tools-output .tool-param {
+ font-size: 11px;
+ color: rgba(245, 240, 232, 0.7);
+ margin-bottom: 2px;
+}
+
+.ws-tools-output .tool-param code {
+ color: #D4A03C;
+ background: rgba(212, 160, 60, 0.15);
+ padding: 1px 4px;
+ border-radius: 3px;
+ font-family: var(--font-mono);
+ font-size: 11px;
+}
+
+.ws-tools-output .tool-call-result {
+ border-top: 1px solid rgba(245, 240, 232, 0.08);
+ padding-top: 6px;
+}
+
+.ws-tools-output .tool-result-content {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ line-height: 1.5;
+ color: rgba(245, 240, 232, 0.8);
+ background: rgba(0, 0, 0, 0.2);
+ padding: 6px 8px;
+ border-radius: 4px;
+ overflow-x: auto;
+ white-space: pre-wrap;
+ word-break: break-all;
+ margin: 0;
+}
+
+.ws-tools-output .tool-result-stderr {
+ color: #ff8a8a;
+}
+
+.ws-tools-output .tool-result-entry {
+ font-size: 11px;
+ color: rgba(245, 240, 232, 0.75);
+ padding: 2px 0;
+}
+
+.ws-tools-output .tool-result-entry code {
+ color: #6db5f5;
+ font-family: var(--font-mono);
+ font-size: 10px;
+}
+
+.ws-tools-output .tool-result-meta {
+ font-size: 10px;
+ color: rgba(245, 240, 232, 0.4);
+ margin-top: 4px;
+}
+
+.ws-tools-output .tool-result-error {
+ color: #ff8a8a;
+ font-size: 12px;
+}
+
+.ws-tools-output .tool-call-running {
+ border-color: rgba(232, 115, 74, 0.4);
+}
+
+.ws-tools-output .tool-call-error {
+ border-color: rgba(255, 138, 138, 0.3);
+}
+
+.ws-tools-hint {
+ font-size: 11px;
+ color: rgba(245, 240, 232, 0.4);
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.ws-tools-hint.running {
+ color: #E8734A;
+}
+
.ws-term-line {
min-height: 1.6em;
}
@@ -2582,6 +2718,7 @@ html, body {
/* ── 滚动条 ── */
.ws-term-output::-webkit-scrollbar,
+.ws-tools-output::-webkit-scrollbar,
.ws-file-list::-webkit-scrollbar,
.ws-preview-content::-webkit-scrollbar,
.ws-term-tab-bar::-webkit-scrollbar {
@@ -2589,12 +2726,14 @@ html, body {
}
.ws-term-output::-webkit-scrollbar-track,
+.ws-tools-output::-webkit-scrollbar-track,
.ws-file-list::-webkit-scrollbar-track,
.ws-preview-content::-webkit-scrollbar-track {
background: transparent;
}
-.ws-term-output::-webkit-scrollbar-thumb {
+.ws-term-output::-webkit-scrollbar-thumb,
+.ws-tools-output::-webkit-scrollbar-thumb {
background: rgba(245, 240, 232, 0.15);
border-radius: 3px;
}
@@ -2605,7 +2744,8 @@ html, body {
border-radius: 3px;
}
-.ws-term-output::-webkit-scrollbar-thumb:hover {
+.ws-term-output::-webkit-scrollbar-thumb:hover,
+.ws-tools-output::-webkit-scrollbar-thumb:hover {
background: rgba(245, 240, 232, 0.25);
}