Skip to content

Commit fd11902

Browse files
committed
feat(tool-execution): enhance logging with statistics and performance insights
- Add view mode toggle (logs/stats) to ToolCallLogContent component - Implement statistics calculation with tool execution metrics - Add performance insights tracking for execution analysis - Extract duration calculation to separate variable for clarity - Enhance log data structure with success and error fields - Add new icons (BarChart3, List, AlertTriangle, Clock, Zap) for UI enhancements - Include stats and insights in exported log data - Refactor component layout with improved spacing and organization - Update JSDoc comments to reflect new functionality - Improve code formatting and consistency throughout
1 parent 90d3041 commit fd11902

File tree

3 files changed

+375
-139
lines changed

3 files changed

+375
-139
lines changed

src/renderer/agent/services/ToolExecutionService.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export class ToolExecutionService {
9292
fullPath = filePath.startsWith(workspacePath) ? filePath : `${workspacePath}/${filePath}`
9393
originalContent = await api.file.read(fullPath)
9494
store.addSnapshotToCurrentCheckpoint(fullPath, originalContent)
95-
95+
9696
// 启动流式编辑追踪
9797
streamingEditId = streamingEditService.startEdit(fullPath, originalContent || '')
9898
logger.agent.debug(`[ToolExecutionService] Started streaming edit for ${fullPath}, editId: ${streamingEditId}`)
@@ -105,12 +105,17 @@ export class ToolExecutionService {
105105
// 结束性能监控
106106
performanceMonitor.end(timerName, result.success)
107107

108+
// 计算执行时间
109+
const duration = Date.now() - startTime
110+
108111
// 记录执行日志
109112
useStore.getState().addToolCallLog({
110113
type: 'response',
111114
toolName: name,
112115
data: { success: result.success, result: result.result?.slice?.(0, 500), error: result.error },
113-
duration: Date.now() - startTime
116+
duration,
117+
success: result.success,
118+
error: result.error,
114119
})
115120

116121
// 更新工具状态
Lines changed: 229 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,135 +1,249 @@
11
/**
22
* 工具调用日志内容组件
3-
* 用于在 BottomBarPopover 中显示
3+
* 包含日志列表和统计视图
44
*/
55

6-
import { useState } from 'react'
7-
import { Trash2, Download, Copy, Check, ChevronDown, ChevronRight } from 'lucide-react'
6+
import { useState, useMemo } from 'react'
7+
import {
8+
Trash2,
9+
Download,
10+
Copy,
11+
Check,
12+
ChevronDown,
13+
ChevronRight,
14+
BarChart3,
15+
List,
16+
AlertTriangle,
17+
Clock,
18+
Zap,
19+
} from 'lucide-react'
820
import { Button } from '../ui'
921
import { JsonHighlight } from '@/renderer/utils/jsonHighlight'
1022
import { useStore } from '@/renderer/store'
1123

1224
interface ToolCallLogContentProps {
13-
language?: 'en' | 'zh'
25+
language?: 'en' | 'zh'
1426
}
1527

28+
type ViewMode = 'logs' | 'stats'
29+
1630
export default function ToolCallLogContent({ language = 'zh' }: ToolCallLogContentProps) {
17-
const { toolCallLogs: logs, clearToolCallLogs } = useStore()
18-
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
19-
const [filter, setFilter] = useState<'all' | 'request' | 'response'>('all')
20-
const [copiedId, setCopiedId] = useState<string | null>(null)
21-
22-
const toggleExpand = (id: string) => {
23-
const newExpanded = new Set(expandedIds)
24-
if (newExpanded.has(id)) {
25-
newExpanded.delete(id)
26-
} else {
27-
newExpanded.add(id)
28-
}
29-
setExpandedIds(newExpanded)
30-
}
31+
const { toolCallLogs: logs, clearToolCallLogs, getToolStats, getPerformanceInsights } = useStore()
32+
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
33+
const [filter, setFilter] = useState<'all' | 'request' | 'response'>('all')
34+
const [copiedId, setCopiedId] = useState<string | null>(null)
35+
const [viewMode, setViewMode] = useState<ViewMode>('logs')
3136

32-
const handleCopy = async (id: string, data: unknown) => {
33-
await navigator.clipboard.writeText(JSON.stringify(data, null, 2))
34-
setCopiedId(id)
35-
setTimeout(() => setCopiedId(null), 2000)
36-
}
37+
const stats = useMemo(() => getToolStats(), [logs])
38+
const insights = useMemo(() => getPerformanceInsights(), [logs])
3739

38-
const handleExport = () => {
39-
const blob = new Blob([JSON.stringify(logs, null, 2)], { type: 'application/json' })
40-
const url = URL.createObjectURL(blob)
41-
const a = document.createElement('a')
42-
a.href = url
43-
a.download = `tool-logs-${new Date().toISOString().slice(0, 10)}.json`
44-
a.click()
45-
URL.revokeObjectURL(url)
40+
const toggleExpand = (id: string) => {
41+
const newExpanded = new Set(expandedIds)
42+
if (newExpanded.has(id)) {
43+
newExpanded.delete(id)
44+
} else {
45+
newExpanded.add(id)
4646
}
47+
setExpandedIds(newExpanded)
48+
}
4749

48-
const filteredLogs = filter === 'all' ? logs : logs.filter(log => log.type === filter)
49-
50-
return (
51-
<div className="h-full flex flex-col">
52-
{/* 工具栏 */}
53-
<div className="flex items-center gap-2 px-2 py-1.5 border-b border-border-subtle bg-surface/30">
54-
<select
55-
value={filter}
56-
onChange={e => setFilter(e.target.value as 'all' | 'request' | 'response')}
57-
className="px-1.5 py-0.5 text-[10px] bg-surface border border-border-subtle rounded text-text-secondary outline-none focus:border-accent/50"
58-
>
59-
<option value="all">{language === 'zh' ? '全部' : 'All'}</option>
60-
<option value="request">{language === 'zh' ? '请求' : 'Req'}</option>
61-
<option value="response">{language === 'zh' ? '响应' : 'Res'}</option>
62-
</select>
63-
<div className="flex-1" />
64-
<Button
65-
variant="ghost"
66-
size="sm"
67-
onClick={handleExport}
68-
className="h-6 px-1.5 text-[10px] gap-1 text-text-muted hover:text-text-primary"
69-
title={language === 'zh' ? '导出日志' : 'Export Logs'}
70-
>
71-
<Download className="w-3 h-3" />
72-
<span className="hidden sm:inline">{language === 'zh' ? '导出' : 'Export'}</span>
73-
</Button>
74-
<Button
75-
variant="ghost"
76-
size="sm"
77-
onClick={clearToolCallLogs}
78-
className="h-6 px-1.5 text-[10px] gap-1 text-text-muted hover:text-red-400 hover:bg-red-500/10"
79-
title={language === 'zh' ? '清除日志' : 'Clear Logs'}
80-
>
81-
<Trash2 className="w-3 h-3" />
82-
<span className="hidden sm:inline">{language === 'zh' ? '清除' : 'Clear'}</span>
83-
</Button>
84-
</div>
50+
const handleCopy = async (id: string, data: unknown) => {
51+
await navigator.clipboard.writeText(JSON.stringify(data, null, 2))
52+
setCopiedId(id)
53+
setTimeout(() => setCopiedId(null), 2000)
54+
}
55+
56+
const handleExport = () => {
57+
const exportData = { logs, stats, insights, exportedAt: new Date().toISOString() }
58+
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
59+
const url = URL.createObjectURL(blob)
60+
const a = document.createElement('a')
61+
a.href = url
62+
a.download = `tool-logs-${new Date().toISOString().slice(0, 10)}.json`
63+
a.click()
64+
URL.revokeObjectURL(url)
65+
}
66+
67+
const filteredLogs = filter === 'all' ? logs : logs.filter((log) => log.type === filter)
68+
const t = (zh: string, en: string) => (language === 'zh' ? zh : en)
69+
70+
71+
return (
72+
<div className="h-full flex flex-col">
73+
{/* 工具栏 */}
74+
<div className="flex items-center gap-1.5 px-2 py-1.5 border-b border-border-subtle bg-surface/30">
75+
{/* 视图切换 */}
76+
<div className="flex bg-surface/50 rounded p-0.5">
77+
<button
78+
onClick={() => setViewMode('logs')}
79+
className={`px-1.5 py-0.5 text-[10px] rounded transition-colors ${
80+
viewMode === 'logs' ? 'bg-accent/20 text-accent' : 'text-text-muted hover:text-text-primary'
81+
}`}
82+
title={t('日志', 'Logs')}
83+
>
84+
<List className="w-3 h-3" />
85+
</button>
86+
<button
87+
onClick={() => setViewMode('stats')}
88+
className={`px-1.5 py-0.5 text-[10px] rounded transition-colors ${
89+
viewMode === 'stats' ? 'bg-accent/20 text-accent' : 'text-text-muted hover:text-text-primary'
90+
}`}
91+
title={t('统计', 'Stats')}
92+
>
93+
<BarChart3 className="w-3 h-3" />
94+
</button>
95+
</div>
96+
97+
{viewMode === 'logs' && (
98+
<select
99+
value={filter}
100+
onChange={(e) => setFilter(e.target.value as 'all' | 'request' | 'response')}
101+
className="px-1.5 py-0.5 text-[10px] bg-surface border border-border-subtle rounded text-text-secondary outline-none focus:border-accent/50"
102+
>
103+
<option value="all">{t('全部', 'All')}</option>
104+
<option value="request">{t('请求', 'Req')}</option>
105+
<option value="response">{t('响应', 'Res')}</option>
106+
</select>
107+
)}
108+
109+
<div className="flex-1" />
110+
111+
<Button variant="ghost" size="sm" onClick={handleExport}
112+
className="h-6 px-1.5 text-[10px] gap-1 text-text-muted hover:text-text-primary" title={t('导出', 'Export')}>
113+
<Download className="w-3 h-3" />
114+
</Button>
115+
<Button variant="ghost" size="sm" onClick={clearToolCallLogs}
116+
className="h-6 px-1.5 text-[10px] gap-1 text-text-muted hover:text-red-400 hover:bg-red-500/10" title={t('清除', 'Clear')}>
117+
<Trash2 className="w-3 h-3" />
118+
</Button>
119+
</div>
120+
121+
{/* 内容区域 */}
122+
<div className="flex-1 overflow-auto">
123+
{viewMode === 'logs' ? (
124+
<LogsView logs={filteredLogs} expandedIds={expandedIds} toggleExpand={toggleExpand}
125+
handleCopy={handleCopy} copiedId={copiedId} language={language} />
126+
) : (
127+
<StatsView stats={stats} insights={insights} language={language} />
128+
)}
129+
</div>
130+
</div>
131+
)
132+
}
133+
134+
135+
// 日志列表视图
136+
function LogsView({ logs, expandedIds, toggleExpand, handleCopy, copiedId, language }: {
137+
logs: import('@/renderer/store/slices/logSlice').ToolCallLogEntry[]
138+
expandedIds: Set<string>
139+
toggleExpand: (id: string) => void
140+
handleCopy: (id: string, data: unknown) => void
141+
copiedId: string | null
142+
language?: string
143+
}) {
144+
const t = (zh: string, en: string) => (language === 'zh' ? zh : en)
145+
146+
if (logs.length === 0) {
147+
return <div className="flex items-center justify-center h-full text-text-muted text-xs">{t('暂无日志', 'No logs')}</div>
148+
}
85149

86-
{/* 日志列表 */}
87-
<div className="flex-1 overflow-auto">
88-
{filteredLogs.length === 0 ? (
89-
<div className="flex items-center justify-center h-full text-text-muted text-xs">
90-
{language === 'zh' ? '暂无日志' : 'No logs'}
91-
</div>
92-
) : (
93-
<div className="divide-y divide-border-subtle">
94-
{filteredLogs.map(log => (
95-
<div key={log.id}>
96-
<button
97-
onClick={() => toggleExpand(log.id)}
98-
className="w-full flex items-center gap-1.5 px-2 py-1.5 hover:bg-surface/50 text-left"
99-
>
100-
{expandedIds.has(log.id)
101-
? <ChevronDown className="w-3 h-3 text-text-muted" />
102-
: <ChevronRight className="w-3 h-3 text-text-muted" />
103-
}
104-
<span className={`px-1 py-0.5 text-[9px] rounded font-medium ${log.type === 'request' ? 'bg-blue-500/20 text-blue-400' : 'bg-green-500/20 text-green-400'
105-
}`}>
106-
{log.type === 'request' ? 'REQ' : 'RES'}
107-
</span>
108-
<span className="text-[10px] font-medium text-text-primary truncate flex-1">{log.toolName}</span>
109-
{log.duration && <span className="text-[9px] text-text-muted">{log.duration}ms</span>}
110-
</button>
111-
112-
{expandedIds.has(log.id) && (
113-
<div className="relative px-2 pb-2">
114-
<button
115-
onClick={() => handleCopy(log.id, log.data)}
116-
className="absolute top-1 right-2 p-0.5 hover:bg-surface rounded"
117-
>
118-
{copiedId === log.id
119-
? <Check className="w-3 h-3 text-green-400" />
120-
: <Copy className="w-3 h-3 text-text-muted" />
121-
}
122-
</button>
123-
<div className="bg-surface/50 rounded p-1.5 overflow-auto max-h-32">
124-
<JsonHighlight data={log.data} maxHeight="max-h-28" />
125-
</div>
126-
</div>
127-
)}
128-
</div>
129-
))}
130-
</div>
131-
)}
150+
return (
151+
<div className="divide-y divide-border-subtle">
152+
{logs.map((log) => (
153+
<div key={log.id}>
154+
<button onClick={() => toggleExpand(log.id)}
155+
className="w-full flex items-center gap-1.5 px-2 py-1.5 hover:bg-surface/50 text-left">
156+
{expandedIds.has(log.id) ? <ChevronDown className="w-3 h-3 text-text-muted" /> : <ChevronRight className="w-3 h-3 text-text-muted" />}
157+
<span className={`px-1 py-0.5 text-[9px] rounded font-medium ${log.type === 'request' ? 'bg-blue-500/20 text-blue-400' : 'bg-green-500/20 text-green-400'}`}>
158+
{log.type === 'request' ? 'REQ' : 'RES'}
159+
</span>
160+
<span className="text-[10px] font-medium text-text-primary truncate flex-1">{log.toolName}</span>
161+
{log.success === false && <AlertTriangle className="w-3 h-3 text-red-400" />}
162+
{log.duration && <span className="text-[9px] text-text-muted">{log.duration}ms</span>}
163+
</button>
164+
165+
{expandedIds.has(log.id) && (
166+
<div className="relative px-2 pb-2">
167+
<button onClick={() => handleCopy(log.id, log.data)} className="absolute top-1 right-2 p-0.5 hover:bg-surface rounded">
168+
{copiedId === log.id ? <Check className="w-3 h-3 text-green-400" /> : <Copy className="w-3 h-3 text-text-muted" />}
169+
</button>
170+
<div className="bg-surface/50 rounded p-1.5 overflow-auto max-h-32">
171+
<JsonHighlight data={log.data} maxHeight="max-h-28" />
172+
</div>
132173
</div>
174+
)}
175+
</div>
176+
))}
177+
</div>
178+
)
179+
}
180+
181+
182+
// 统计视图
183+
function StatsView({ stats, insights, language }: {
184+
stats: import('@/renderer/store/slices/logSlice').ToolStats[]
185+
insights: import('@/renderer/store/slices/logSlice').PerformanceInsight[]
186+
language?: string
187+
}) {
188+
const t = (zh: string, en: string) => (language === 'zh' ? zh : en)
189+
190+
if (stats.length === 0) {
191+
return <div className="flex items-center justify-center h-full text-text-muted text-xs">{t('暂无统计数据', 'No statistics')}</div>
192+
}
193+
194+
return (
195+
<div className="p-2 space-y-3">
196+
{/* 性能洞察 */}
197+
{insights.length > 0 && (
198+
<div className="space-y-1">
199+
<div className="text-[10px] font-medium text-text-muted uppercase tracking-wide">{t('性能洞察', 'Insights')}</div>
200+
<div className="space-y-1">
201+
{insights.slice(0, 3).map((insight, i) => (
202+
<div key={i} className={`flex items-center gap-2 px-2 py-1 rounded text-[10px] ${
203+
insight.severity === 'critical' ? 'bg-red-500/10 text-red-400' :
204+
insight.severity === 'warning' ? 'bg-yellow-500/10 text-yellow-400' : 'bg-blue-500/10 text-blue-400'
205+
}`}>
206+
{insight.type === 'slow_tool' && <Clock className="w-3 h-3" />}
207+
{insight.type === 'high_failure' && <AlertTriangle className="w-3 h-3" />}
208+
{insight.type === 'frequent_tool' && <Zap className="w-3 h-3" />}
209+
<span className="font-medium">{insight.toolName}</span>
210+
<span className="text-[9px] opacity-80">{language === 'zh' ? insight.messageZh : insight.message}</span>
211+
</div>
212+
))}
213+
</div>
214+
</div>
215+
)}
216+
217+
{/* 工具统计表 */}
218+
<div className="space-y-1">
219+
<div className="text-[10px] font-medium text-text-muted uppercase tracking-wide">{t('工具统计', 'Tool Stats')}</div>
220+
<div className="bg-surface/30 rounded border border-border-subtle overflow-hidden">
221+
<table className="w-full text-[10px]">
222+
<thead>
223+
<tr className="bg-surface/50 text-text-muted">
224+
<th className="text-left px-2 py-1 font-medium">{t('工具', 'Tool')}</th>
225+
<th className="text-right px-2 py-1 font-medium">{t('调用', 'Calls')}</th>
226+
<th className="text-right px-2 py-1 font-medium">{t('成功率', 'Rate')}</th>
227+
<th className="text-right px-2 py-1 font-medium">{t('平均', 'Avg')}</th>
228+
</tr>
229+
</thead>
230+
<tbody className="divide-y divide-border-subtle">
231+
{stats.slice(0, 8).map((stat) => (
232+
<tr key={stat.toolName} className="hover:bg-surface/30">
233+
<td className="px-2 py-1 text-text-primary truncate max-w-[100px]" title={stat.toolName}>{stat.toolName}</td>
234+
<td className="px-2 py-1 text-right text-text-secondary">{stat.totalCalls}</td>
235+
<td className="px-2 py-1 text-right">
236+
<span className={stat.successRate >= 0.9 ? 'text-green-400' : stat.successRate >= 0.7 ? 'text-yellow-400' : 'text-red-400'}>
237+
{Math.round(stat.successRate * 100)}%
238+
</span>
239+
</td>
240+
<td className="px-2 py-1 text-right text-text-muted">{Math.round(stat.avgDuration)}ms</td>
241+
</tr>
242+
))}
243+
</tbody>
244+
</table>
133245
</div>
134-
)
246+
</div>
247+
</div>
248+
)
135249
}

0 commit comments

Comments
 (0)