feat: 工具卡片从聊天区域迁移到工作空间面板
- 工作空间新增「🔧 工具」页签,专门展示工具调用卡片
- 工具执行时自动切换到工具页签
- run_command 执行时自动切换到终端页签
- AI 回复消息不再显示工具卡片,保持聊天区域简洁
- 工具页签支持清空、自动滚动、状态统计
This commit is contained in:
@@ -157,22 +157,6 @@ export function appendMessageDOM(msg: ChatMessage, index: number): void {
|
|||||||
contentHtml = contentHtml.replace('<div class="msg-content"></div>', '');
|
contentHtml = contentHtml.replace('<div class="msg-content"></div>', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// v5.1.2 工具卡片移至文字上方(先展示工具执行过程,再展示最终回答)
|
|
||||||
if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
|
|
||||||
let toolHtml = '<div class="tool-calls-container">';
|
|
||||||
for (const tc of msg.toolCalls) {
|
|
||||||
toolHtml += renderToolCallCard(tc);
|
|
||||||
}
|
|
||||||
toolHtml += '</div>';
|
|
||||||
// 插入到 msg-content 之前
|
|
||||||
const contentIdx = contentHtml.indexOf('<div class="msg-content">');
|
|
||||||
if (contentIdx >= 0) {
|
|
||||||
contentHtml = contentHtml.slice(0, contentIdx) + toolHtml + contentHtml.slice(contentIdx);
|
|
||||||
} else {
|
|
||||||
contentHtml = toolHtml + contentHtml;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats: string[] = [];
|
const stats: string[] = [];
|
||||||
const statClasses: string[] = [];
|
const statClasses: string[] = [];
|
||||||
if (msg.timestamp) { stats.push(formatTime(msg.timestamp)); statClasses.push(''); }
|
if (msg.timestamp) { stats.push(formatTime(msg.timestamp)); statClasses.push(''); }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
appendToolCallCardToPlaceholder, updateToolCallCardInPlaceholder
|
appendToolCallCardToPlaceholder, updateToolCallCardInPlaceholder
|
||||||
} from './chat-area.js';
|
} from './chat-area.js';
|
||||||
import { showToast } from './toast.js';
|
import { showToast } from './toast.js';
|
||||||
|
import { addToolCard, updateToolCard } from './workspace-panel.js';
|
||||||
import { ChatDB } from '../db/chat-db.js';
|
import { ChatDB } from '../db/chat-db.js';
|
||||||
import { OllamaAPI } from '../api/ollama.js';
|
import { OllamaAPI } from '../api/ollama.js';
|
||||||
import { runAgentLoop } from '../services/agent-engine.js';
|
import { runAgentLoop } from '../services/agent-engine.js';
|
||||||
@@ -288,19 +289,19 @@ async function handleRetry(): Promise<void> {
|
|||||||
onThinking: (thinking) => updateLastAssistantMessage('', thinking, null),
|
onThinking: (thinking) => updateLastAssistantMessage('', thinking, null),
|
||||||
onContent: (content) => updateLastAssistantMessage(content, null, null),
|
onContent: (content) => updateLastAssistantMessage(content, null, null),
|
||||||
onToolCallStart: (call) => {
|
onToolCallStart: (call) => {
|
||||||
appendToolCallCardToPlaceholder({
|
addToolCard({
|
||||||
name: call.function.name, arguments: call.function.arguments,
|
name: call.function.name, arguments: call.function.arguments,
|
||||||
result: null, status: 'running', timestamp: Date.now()
|
result: null, status: 'running', timestamp: Date.now()
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onToolCallResult: (name, result, call) => {
|
onToolCallResult: (name, result, call) => {
|
||||||
updateToolCallCardInPlaceholder({
|
updateToolCard({
|
||||||
name, arguments: call.function.arguments,
|
name, arguments: call.function.arguments,
|
||||||
result, status: result.success ? 'success' : 'error', timestamp: Date.now()
|
result, status: result.success ? 'success' : 'error', timestamp: Date.now()
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onToolCallError: (name, error, call) => {
|
onToolCallError: (name, error, call) => {
|
||||||
updateToolCallCardInPlaceholder({
|
updateToolCard({
|
||||||
name, arguments: call.function.arguments,
|
name, arguments: call.function.arguments,
|
||||||
result: { success: false, error }, status: 'error', timestamp: Date.now()
|
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);
|
updateLastAssistantMessage(assistantContent, thinkContent || null, null);
|
||||||
},
|
},
|
||||||
onToolCallStart: (call) => {
|
onToolCallStart: (call) => {
|
||||||
appendToolCallCardToPlaceholder({
|
addToolCard({
|
||||||
name: call.function.name, arguments: call.function.arguments,
|
name: call.function.name, arguments: call.function.arguments,
|
||||||
result: null, status: 'running', timestamp: Date.now()
|
result: null, status: 'running', timestamp: Date.now()
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onToolCallResult: (name, result, call) => {
|
onToolCallResult: (name, result, call) => {
|
||||||
updateToolCallCardInPlaceholder({
|
updateToolCard({
|
||||||
name, arguments: call.function.arguments,
|
name, arguments: call.function.arguments,
|
||||||
result, status: result.success ? 'success' : 'error', timestamp: Date.now()
|
result, status: result.success ? 'success' : 'error', timestamp: Date.now()
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onToolCallError: (name, error, call) => {
|
onToolCallError: (name, error, call) => {
|
||||||
updateToolCallCardInPlaceholder({
|
updateToolCard({
|
||||||
name, arguments: call.function.arguments,
|
name, arguments: call.function.arguments,
|
||||||
result: { success: false, error }, status: 'error', timestamp: Date.now()
|
result: { success: false, error }, status: 'error', timestamp: Date.now()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,15 @@
|
|||||||
|
|
||||||
import { logInfo, logError, logDebug } from '../services/log-service.js';
|
import { logInfo, logError, logDebug } from '../services/log-service.js';
|
||||||
|
|
||||||
|
// ── 工具卡片类型 ──
|
||||||
|
interface ToolCallRecord {
|
||||||
|
name: string;
|
||||||
|
arguments: Record<string, unknown>;
|
||||||
|
result: Record<string, unknown> | null;
|
||||||
|
status: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 状态 ──
|
// ── 状态 ──
|
||||||
interface TerminalSession {
|
interface TerminalSession {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -30,7 +39,7 @@ interface FileNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let panelVisible = true; // 常驻显示,不可关闭
|
let panelVisible = true; // 常驻显示,不可关闭
|
||||||
let activeTab: 'terminal' | 'files' = 'terminal';
|
let activeTab: 'terminal' | 'tools' | 'files' = 'terminal';
|
||||||
let terminal: TerminalSession | null = null; // 唯一终端
|
let terminal: TerminalSession | null = null; // 唯一终端
|
||||||
let workspaceDir = '';
|
let workspaceDir = '';
|
||||||
let currentFileDir = '';
|
let currentFileDir = '';
|
||||||
@@ -38,6 +47,10 @@ let fileTree: FileNode[] = [];
|
|||||||
let previewFile: { name: string; content: string; path: string } | null = null;
|
let previewFile: { name: string; content: string; path: string } | null = null;
|
||||||
let _counter = 0;
|
let _counter = 0;
|
||||||
|
|
||||||
|
/** 工具调用卡片列表 */
|
||||||
|
let toolCards: ToolCallRecord[] = [];
|
||||||
|
let toolAutoScroll = true;
|
||||||
|
|
||||||
/** 当前正在运行的 AI 命令(用于用户手动终止时通知 AI) */
|
/** 当前正在运行的 AI 命令(用于用户手动终止时通知 AI) */
|
||||||
let currentAiCommand: string | null = null;
|
let currentAiCommand: string | null = null;
|
||||||
|
|
||||||
@@ -206,6 +219,7 @@ export async function initWorkspacePanel(): Promise<void> {
|
|||||||
function bindEvents(): void {
|
function bindEvents(): void {
|
||||||
// Tab 切换
|
// Tab 切换
|
||||||
document.querySelector('#wsTabTerminal')?.addEventListener('click', () => switchTab('terminal'));
|
document.querySelector('#wsTabTerminal')?.addEventListener('click', () => switchTab('terminal'));
|
||||||
|
document.querySelector('#wsTabTools')?.addEventListener('click', () => switchTab('tools'));
|
||||||
document.querySelector('#wsTabFiles')?.addEventListener('click', () => switchTab('files'));
|
document.querySelector('#wsTabFiles')?.addEventListener('click', () => switchTab('files'));
|
||||||
|
|
||||||
// 停止按钮
|
// 停止按钮
|
||||||
@@ -214,6 +228,9 @@ function bindEvents(): void {
|
|||||||
// 清空按钮
|
// 清空按钮
|
||||||
document.querySelector('#wsClearBtn')?.addEventListener('click', clearTerminal);
|
document.querySelector('#wsClearBtn')?.addEventListener('click', clearTerminal);
|
||||||
|
|
||||||
|
// 工具清空按钮
|
||||||
|
document.querySelector('#wsToolsClearBtn')?.addEventListener('click', clearToolCards);
|
||||||
|
|
||||||
// 文件刷新
|
// 文件刷新
|
||||||
document.querySelector('#wsFileRefreshBtn')?.addEventListener('click', () => {
|
document.querySelector('#wsFileRefreshBtn')?.addEventListener('click', () => {
|
||||||
loadFiles(currentFileDir || workspaceDir);
|
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;
|
activeTab = tab;
|
||||||
renderPanel();
|
renderPanel();
|
||||||
|
|
||||||
@@ -438,23 +455,25 @@ export function renderPanel(): void {
|
|||||||
if (!panel) return;
|
if (!panel) return;
|
||||||
|
|
||||||
const termTab = document.querySelector('#wsTabTerminal') as HTMLElement;
|
const termTab = document.querySelector('#wsTabTerminal') as HTMLElement;
|
||||||
|
const toolsTab = document.querySelector('#wsTabTools') as HTMLElement;
|
||||||
const filesTab = document.querySelector('#wsTabFiles') as HTMLElement;
|
const filesTab = document.querySelector('#wsTabFiles') as HTMLElement;
|
||||||
|
|
||||||
if (termTab) {
|
if (termTab) termTab.classList.toggle('active', activeTab === 'terminal');
|
||||||
termTab.classList.toggle('active', activeTab === 'terminal');
|
if (toolsTab) toolsTab.classList.toggle('active', activeTab === 'tools');
|
||||||
}
|
if (filesTab) filesTab.classList.toggle('active', activeTab === 'files');
|
||||||
if (filesTab) {
|
|
||||||
filesTab.classList.toggle('active', activeTab === 'files');
|
|
||||||
}
|
|
||||||
|
|
||||||
const termContent = document.querySelector('#wsTerminalContent') as HTMLElement;
|
const termContent = document.querySelector('#wsTerminalContent') as HTMLElement;
|
||||||
|
const toolsContent = document.querySelector('#wsToolsContent') as HTMLElement;
|
||||||
const filesContent = document.querySelector('#wsFilesContent') as HTMLElement;
|
const filesContent = document.querySelector('#wsFilesContent') as HTMLElement;
|
||||||
|
|
||||||
if (termContent) termContent.style.display = activeTab === 'terminal' ? '' : 'none';
|
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 (filesContent) filesContent.style.display = activeTab === 'files' ? '' : 'none';
|
||||||
|
|
||||||
if (activeTab === 'terminal') {
|
if (activeTab === 'terminal') {
|
||||||
renderTerminal();
|
renderTerminal();
|
||||||
|
} else if (activeTab === 'tools') {
|
||||||
|
renderToolCalls();
|
||||||
} else {
|
} else {
|
||||||
renderFileList();
|
renderFileList();
|
||||||
}
|
}
|
||||||
@@ -617,6 +636,197 @@ function formatSize(bytes: number): string {
|
|||||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
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 = '<div class="ws-tools-placeholder">等待工具调用...</div>';
|
||||||
|
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<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderToolCard(tc: ToolCallRecord): string {
|
||||||
|
const icon = getToolIcon(tc.name);
|
||||||
|
const name = getToolDisplayName(tc.name);
|
||||||
|
const statusLabels: Record<string, string> = {
|
||||||
|
pending: '⏳ 等待确认', running: '🔄 执行中', success: '✅ 完成', error: '❌ 失败', cancelled: '🚫 已取消'
|
||||||
|
};
|
||||||
|
const status = statusLabels[tc.status] || tc.status;
|
||||||
|
const args = tc.arguments || {};
|
||||||
|
|
||||||
|
let paramsHtml = '';
|
||||||
|
if (args.path) paramsHtml += `<div class="tool-param">路径: <code>${escapeHtml(String(args.path))}</code></div>`;
|
||||||
|
if (args.command) paramsHtml += `<div class="tool-param">命令: <code>${escapeHtml(String(args.command))}</code></div>`;
|
||||||
|
if (args.query) paramsHtml += `<div class="tool-param">查询: <code>${escapeHtml(String(args.query))}</code></div>`;
|
||||||
|
if (args.url) paramsHtml += `<div class="tool-param">URL: <code>${escapeHtml(String(args.url))}</code></div>`;
|
||||||
|
if (args.source) paramsHtml += `<div class="tool-param">源: <code>${escapeHtml(String(args.source))}</code></div>`;
|
||||||
|
if (args.destination) paramsHtml += `<div class="tool-param">目标: <code>${escapeHtml(String(args.destination))}</code></div>`;
|
||||||
|
if (args.selector) paramsHtml += `<div class="tool-param">选择器: <code>${escapeHtml(String(args.selector))}</code></div>`;
|
||||||
|
if (args.action) paramsHtml += `<div class="tool-param">操作: <code>${escapeHtml(String(args.action))}</code></div>`;
|
||||||
|
|
||||||
|
let resultHtml = '';
|
||||||
|
if (tc.result) {
|
||||||
|
const r = tc.result as Record<string, unknown>;
|
||||||
|
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 += `<pre class="tool-result-content">${escapeHtml(stdout)}${String(r.stdout || '').length > 500 ? '\n...' : ''}</pre>`;
|
||||||
|
if (stderr) resultHtml += `<pre class="tool-result-content tool-result-stderr">${escapeHtml(stderr)}</pre>`;
|
||||||
|
resultHtml += `<div class="tool-result-meta">exit: ${r.exitCode || 0}</div>`;
|
||||||
|
} 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) =>
|
||||||
|
`<div class="tool-result-entry">[${i + 1}] ${escapeHtml(item.title)}<br><code>${escapeHtml(item.url)}</code></div>`
|
||||||
|
).join('');
|
||||||
|
resultHtml += `<div class="tool-result-meta">共 ${r.total || results.length} 条结果</div>`;
|
||||||
|
} 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 = `<pre class="tool-result-content">${escapeHtml(preview)}${lines.length > 15 ? `\n... (共 ${lines.length} 行)` : ''}</pre>`;
|
||||||
|
} 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 =>
|
||||||
|
`<div class="tool-result-entry">${e.type === 'directory' ? '📁' : '📄'} ${escapeHtml(e.name)}</div>`
|
||||||
|
).join('');
|
||||||
|
if (entries.length > 20) resultHtml += `<div class="tool-result-entry">... 共 ${entries.length} 项</div>`;
|
||||||
|
} else if (tc.name === 'write_file') {
|
||||||
|
resultHtml = `<div class="tool-result-entry">✅ 已写入 ${escapeHtml(String(r.path || ''))}</div>`;
|
||||||
|
} else if (tc.name === 'delete_file') {
|
||||||
|
resultHtml = `<div class="tool-result-entry">✅ 已删除 ${escapeHtml(String(r.path || ''))}</div>`;
|
||||||
|
} else if (tc.name === 'create_directory') {
|
||||||
|
resultHtml = `<div class="tool-result-entry">✅ 已创建 ${escapeHtml(String(r.path || ''))}</div>`;
|
||||||
|
} else {
|
||||||
|
// 通用成功显示
|
||||||
|
const summary = JSON.stringify(r).slice(0, 300);
|
||||||
|
resultHtml = `<pre class="tool-result-content">${escapeHtml(summary)}${JSON.stringify(r).length > 300 ? '\n...' : ''}</pre>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resultHtml = `<div class="tool-result-error">${escapeHtml(String(r.error || '未知错误'))}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusClass = tc.status === 'running' ? 'tool-call-running' : tc.status === 'error' ? 'tool-call-error' : '';
|
||||||
|
|
||||||
|
return `<div class="tool-call-card ${statusClass}" data-tool-name="${escapeHtml(tc.name)}">
|
||||||
|
<div class="tool-call-header">
|
||||||
|
<span class="tool-call-icon">${icon}</span>
|
||||||
|
<span class="tool-call-name">${escapeHtml(name)}</span>
|
||||||
|
<span class="tool-call-status">${status}</span>
|
||||||
|
</div>
|
||||||
|
${paramsHtml ? `<div class="tool-call-params">${paramsHtml}</div>` : ''}
|
||||||
|
${resultHtml ? `<div class="tool-call-result">${resultHtml}</div>` : ''}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 外部调用接口 ──
|
// ── 外部调用接口 ──
|
||||||
|
|
||||||
/** 获取工作空间目录 */
|
/** 获取工作空间目录 */
|
||||||
@@ -624,3 +834,9 @@ export function getWorkspaceDirPath(): string {
|
|||||||
return workspaceDir;
|
return workspaceDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 外部自动切换 tab */
|
||||||
|
export function switchToTab(tab: 'terminal' | 'tools' | 'files'): void {
|
||||||
|
activeTab = tab;
|
||||||
|
renderPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,7 @@
|
|||||||
<span class="ws-title">🖥️ 工作空间</span>
|
<span class="ws-title">🖥️ 工作空间</span>
|
||||||
<div class="ws-tabs">
|
<div class="ws-tabs">
|
||||||
<button class="ws-header-tab active" id="wsTabTerminal">💻 命令行</button>
|
<button class="ws-header-tab active" id="wsTabTerminal">💻 命令行</button>
|
||||||
|
<button class="ws-header-tab" id="wsTabTools">🔧 工具</button>
|
||||||
<button class="ws-header-tab" id="wsTabFiles">📁 文件</button>
|
<button class="ws-header-tab" id="wsTabFiles">📁 文件</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,6 +196,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 工具调用 Tab -->
|
||||||
|
<div class="ws-content" id="wsToolsContent" style="display:none;">
|
||||||
|
<div class="ws-tools-output" id="wsToolsOutput"></div>
|
||||||
|
<div class="ws-term-toolbar">
|
||||||
|
<span class="ws-tools-hint" id="wsToolsHint">等待工具调用...</span>
|
||||||
|
<div class="ws-term-toolbar-btns">
|
||||||
|
<button class="ws-toolbar-btn" id="wsToolsClearBtn" title="清空">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 文件 Tab -->
|
<!-- 文件 Tab -->
|
||||||
<div class="ws-content" id="wsFilesContent" style="display:none;">
|
<div class="ws-content" id="wsFilesContent" style="display:none;">
|
||||||
<div class="ws-file-toolbar">
|
<div class="ws-file-toolbar">
|
||||||
|
|||||||
@@ -2351,6 +2351,142 @@ html, body {
|
|||||||
font-family: var(--font);
|
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 {
|
.ws-term-line {
|
||||||
min-height: 1.6em;
|
min-height: 1.6em;
|
||||||
}
|
}
|
||||||
@@ -2582,6 +2718,7 @@ html, body {
|
|||||||
/* ── 滚动条 ── */
|
/* ── 滚动条 ── */
|
||||||
|
|
||||||
.ws-term-output::-webkit-scrollbar,
|
.ws-term-output::-webkit-scrollbar,
|
||||||
|
.ws-tools-output::-webkit-scrollbar,
|
||||||
.ws-file-list::-webkit-scrollbar,
|
.ws-file-list::-webkit-scrollbar,
|
||||||
.ws-preview-content::-webkit-scrollbar,
|
.ws-preview-content::-webkit-scrollbar,
|
||||||
.ws-term-tab-bar::-webkit-scrollbar {
|
.ws-term-tab-bar::-webkit-scrollbar {
|
||||||
@@ -2589,12 +2726,14 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ws-term-output::-webkit-scrollbar-track,
|
.ws-term-output::-webkit-scrollbar-track,
|
||||||
|
.ws-tools-output::-webkit-scrollbar-track,
|
||||||
.ws-file-list::-webkit-scrollbar-track,
|
.ws-file-list::-webkit-scrollbar-track,
|
||||||
.ws-preview-content::-webkit-scrollbar-track {
|
.ws-preview-content::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
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);
|
background: rgba(245, 240, 232, 0.15);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
@@ -2605,7 +2744,8 @@ html, body {
|
|||||||
border-radius: 3px;
|
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);
|
background: rgba(245, 240, 232, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user