Skip to content

Commit 48b10d8

Browse files
committed
Add tool call detail views
1 parent ecf22bf commit 48b10d8

15 files changed

Lines changed: 814 additions & 64 deletions

File tree

docs/codex-session-format.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,9 @@ Observed output/end payload types:
9696
- `tool_search_output`
9797
- `patch_apply_end`
9898

99-
AgentMeter stores the tool name, status, input/output previews, error preview,
100-
and duration when both start and end timestamps are available.
99+
AgentMeter stores the tool name, call id, status, input/output previews, error
100+
preview, start/end raw event links, and duration when both start and end
101+
timestamps are available.
101102

102103
## Timing
103104

docs/data-model.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,10 @@ Fields:
145145
- `input_summary`
146146
- `output_summary`
147147
- `error`
148+
- `call_id`
148149
- `raw_event_id`
150+
- `raw_start_event_id`
151+
- `raw_end_event_id`
149152

150153
MVP statistics:
151154

frontend/src/api.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export interface ModelCall {
118118

119119
export interface ToolCall {
120120
id: number
121+
sessionId: number
121122
startedAt: string
122123
endedAt: string
123124
durationMs: number
@@ -126,7 +127,25 @@ export interface ToolCall {
126127
inputSummary: string
127128
outputSummary: string
128129
error: string
130+
callId?: string
129131
rawEventId: number
132+
rawStartEventId?: number
133+
rawEndEventId?: number
134+
rawEventLine?: number
135+
rawStartEventLine?: number
136+
rawEndEventLine?: number
137+
rawStartEventType?: string
138+
rawEndEventType?: string
139+
rawStartEventSummary?: string
140+
rawEndEventSummary?: string
141+
rawStartEventJson?: string
142+
rawEndEventJson?: string
143+
sessionKey?: string
144+
codexSessionId?: string
145+
projectPath?: string
146+
agentKind?: string
147+
agentName?: string
148+
rawSourcePath?: string
130149
}
131150

132151
export interface SessionDetail {
@@ -186,6 +205,12 @@ export interface SessionFilters {
186205
offset?: number
187206
}
188207

208+
export interface ToolCallFilters {
209+
tool?: string
210+
limit?: number
211+
offset?: number
212+
}
213+
189214
async function request<T>(path: string, init?: RequestInit): Promise<T> {
190215
const response = await fetch(path, {
191216
headers: { 'Content-Type': 'application/json', ...(init?.headers || {}) },
@@ -216,6 +241,13 @@ export const api = {
216241
},
217242
getSessionDetail: (id: number) => request<SessionDetail>(`/api/sessions/${id}`),
218243
getTools: () => request<ToolStat[]>('/api/tools'),
244+
listToolCalls: (filters: ToolCallFilters = {}) => {
245+
const params = new URLSearchParams()
246+
if (filters.tool) params.set('tool', filters.tool)
247+
if (filters.limit) params.set('limit', String(filters.limit))
248+
if (filters.offset) params.set('offset', String(filters.offset))
249+
return request<ToolCall[]>(`/api/tool-calls?${params}`)
250+
},
219251
getPricingModels: () => request<PricingModel[]>('/api/pricing')
220252
}
221253

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import AButton from 'ant-design-vue/es/button'
4+
import ADrawer from 'ant-design-vue/es/drawer'
5+
import ATag from 'ant-design-vue/es/tag'
6+
import Typography from 'ant-design-vue/es/typography'
7+
import { ArrowRightOutlined } from '@ant-design/icons-vue'
8+
import { formatDateTime, formatDuration, formatNumber, shortPath, type ToolCall } from '../api'
9+
10+
const ATypographyParagraph = Typography.Paragraph
11+
const ATypographyText = Typography.Text
12+
13+
const props = withDefaults(
14+
defineProps<{
15+
open: boolean
16+
call: ToolCall | null
17+
showSessionLink?: boolean
18+
}>(),
19+
{ showSessionLink: true }
20+
)
21+
22+
const emit = defineEmits<{
23+
close: []
24+
openSession: [id: number]
25+
}>()
26+
27+
const drawerTitle = computed(() => {
28+
if (!props.call?.toolName) return 'Tool Call Details'
29+
return `${props.call.toolName} Details`
30+
})
31+
32+
function normalizedStatus(status?: string) {
33+
return (status || 'unknown').toLowerCase()
34+
}
35+
36+
function statusClass(status?: string) {
37+
const normalized = normalizedStatus(status)
38+
if (['completed', 'ok', 'indexed', 'success'].includes(normalized)) return 'status-ok'
39+
if (['pending', 'warning', 'scanning', 'unknown', 'started'].includes(normalized)) return 'status-warning'
40+
return 'status-error'
41+
}
42+
43+
function statusColor(status?: string) {
44+
const normalized = normalizedStatus(status)
45+
if (['completed', 'ok', 'indexed', 'success'].includes(normalized)) return 'success'
46+
if (normalized === 'scanning') return 'processing'
47+
if (['pending', 'warning', 'unknown', 'started'].includes(normalized)) return 'warning'
48+
return 'error'
49+
}
50+
51+
function sessionName(call: ToolCall) {
52+
return call.sessionKey || call.codexSessionId || `#${call.sessionId}`
53+
}
54+
55+
function hasText(value?: string) {
56+
return Boolean(value && value.trim())
57+
}
58+
59+
function formatLine(value?: number) {
60+
return value ? formatNumber(value) : '-'
61+
}
62+
63+
function openSession(call: ToolCall) {
64+
emit('openSession', call.sessionId)
65+
}
66+
67+
function hasDistinctEndRaw(call: ToolCall) {
68+
return hasText(call.rawEndEventJson) && call.rawEndEventJson !== call.rawStartEventJson
69+
}
70+
</script>
71+
72+
<template>
73+
<a-drawer class="tool-call-drawer" :open="props.open" :width="720" placement="right" @close="emit('close')">
74+
<template #title>{{ drawerTitle }}</template>
75+
76+
<template v-if="props.call">
77+
<div class="tool-detail-summary">
78+
<div class="tool-detail-heading">
79+
<div class="metric-label">Tool</div>
80+
<div class="summary-title">{{ props.call.toolName || 'unknown' }}</div>
81+
<div class="summary-meta">
82+
<a-tag class="status-tag call-status-tag" :class="statusClass(props.call.status)" :color="statusColor(props.call.status)">
83+
{{ props.call.status || 'unknown' }}
84+
</a-tag>
85+
<span class="summary-chip mono">#{{ formatNumber(props.call.id) }}</span>
86+
<span v-if="props.call.callId" class="summary-chip mono">{{ props.call.callId }}</span>
87+
</div>
88+
</div>
89+
<a-button v-if="props.showSessionLink && props.call.sessionId" @click="openSession(props.call)">
90+
<template #icon>
91+
<ArrowRightOutlined />
92+
</template>
93+
Session
94+
</a-button>
95+
</div>
96+
97+
<div class="metadata-grid tool-detail-grid">
98+
<div class="metadata-item">
99+
<div class="metadata-label">Started</div>
100+
<div class="metadata-value">{{ formatDateTime(props.call.startedAt) }}</div>
101+
</div>
102+
<div class="metadata-item">
103+
<div class="metadata-label">Ended</div>
104+
<div class="metadata-value">{{ formatDateTime(props.call.endedAt) }}</div>
105+
</div>
106+
<div class="metadata-item">
107+
<div class="metadata-label">Duration</div>
108+
<div class="metadata-value number-cell">{{ formatDuration(props.call.durationMs) }}</div>
109+
</div>
110+
<div class="metadata-item">
111+
<div class="metadata-label">Session</div>
112+
<div class="metadata-value mono">{{ sessionName(props.call) }}</div>
113+
</div>
114+
<div class="metadata-item">
115+
<div class="metadata-label">Agent</div>
116+
<div class="metadata-value">{{ props.call.agentName || props.call.agentKind || '-' }}</div>
117+
</div>
118+
<div class="metadata-item">
119+
<div class="metadata-label">Raw Events</div>
120+
<div class="metadata-value mono">
121+
{{ formatLine(props.call.rawStartEventLine || props.call.rawEventLine) }} -> {{ formatLine(props.call.rawEndEventLine) }}
122+
</div>
123+
</div>
124+
<div class="metadata-item is-wide">
125+
<div class="metadata-label">Project</div>
126+
<a-typography-text class="metadata-value detail-path" :ellipsis="{ tooltip: props.call.projectPath }">
127+
{{ props.call.projectPath || '-' }}
128+
</a-typography-text>
129+
</div>
130+
<div class="metadata-item is-wide">
131+
<div class="metadata-label">Raw Source</div>
132+
<a-typography-text class="metadata-value detail-path mono" :ellipsis="{ tooltip: props.call.rawSourcePath }">
133+
{{ props.call.rawSourcePath ? shortPath(props.call.rawSourcePath) : '-' }}
134+
</a-typography-text>
135+
</div>
136+
</div>
137+
138+
<section class="detail-section">
139+
<div class="metadata-label">Input</div>
140+
<a-typography-paragraph class="detail-pre mono" copyable>
141+
{{ props.call.inputSummary || '-' }}
142+
</a-typography-paragraph>
143+
</section>
144+
145+
<section class="detail-section">
146+
<div class="metadata-label">Output</div>
147+
<a-typography-paragraph class="detail-pre mono" copyable>
148+
{{ props.call.outputSummary || '-' }}
149+
</a-typography-paragraph>
150+
</section>
151+
152+
<section v-if="props.call.error" class="detail-section">
153+
<div class="metadata-label">Error</div>
154+
<a-typography-paragraph class="detail-pre detail-pre-error mono" copyable>
155+
{{ props.call.error }}
156+
</a-typography-paragraph>
157+
</section>
158+
159+
<details v-if="hasText(props.call.rawStartEventJson)" class="raw-detail" open>
160+
<summary>
161+
Start raw event
162+
<span class="muted mono">line {{ formatLine(props.call.rawStartEventLine || props.call.rawEventLine) }} · {{ props.call.rawStartEventType || '-' }}</span>
163+
</summary>
164+
<div v-if="props.call.rawStartEventSummary" class="raw-detail-summary">{{ props.call.rawStartEventSummary }}</div>
165+
<a-typography-paragraph class="detail-pre raw-json mono" copyable>
166+
{{ props.call.rawStartEventJson }}
167+
</a-typography-paragraph>
168+
</details>
169+
170+
<details v-if="hasDistinctEndRaw(props.call)" class="raw-detail">
171+
<summary>
172+
End raw event
173+
<span class="muted mono">line {{ formatLine(props.call.rawEndEventLine) }} · {{ props.call.rawEndEventType || '-' }}</span>
174+
</summary>
175+
<div v-if="props.call.rawEndEventSummary" class="raw-detail-summary">{{ props.call.rawEndEventSummary }}</div>
176+
<a-typography-paragraph class="detail-pre raw-json mono" copyable>
177+
{{ props.call.rawEndEventJson }}
178+
</a-typography-paragraph>
179+
</details>
180+
181+
<div v-if="!hasText(props.call.rawStartEventJson) && !hasText(props.call.rawEndEventJson)" class="metadata-item">
182+
<div class="metadata-label">Raw Event</div>
183+
<div class="metadata-value">No raw event recorded</div>
184+
</div>
185+
</template>
186+
</a-drawer>
187+
</template>

frontend/src/styles.css

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,6 +1442,80 @@ body {
14421442
font-variant-numeric: tabular-nums;
14431443
}
14441444

1445+
.tool-detail-summary {
1446+
display: flex;
1447+
align-items: flex-start;
1448+
justify-content: space-between;
1449+
gap: 12px;
1450+
margin-bottom: 14px;
1451+
padding-bottom: 14px;
1452+
border-bottom: 1px solid var(--am-border-subtle);
1453+
}
1454+
1455+
.tool-detail-heading {
1456+
min-width: 0;
1457+
}
1458+
1459+
.tool-detail-grid {
1460+
margin-bottom: 14px;
1461+
}
1462+
1463+
.detail-section {
1464+
margin-top: 12px;
1465+
}
1466+
1467+
.detail-pre {
1468+
max-height: 240px;
1469+
margin: 5px 0 0 !important;
1470+
padding: 10px 11px;
1471+
overflow: auto;
1472+
color: var(--am-text-soft);
1473+
font-size: 12px;
1474+
line-height: 18px;
1475+
white-space: pre-wrap;
1476+
word-break: break-word;
1477+
background: var(--am-surface-subtle);
1478+
border: 1px solid var(--am-border-subtle);
1479+
border-radius: var(--am-radius-sm);
1480+
}
1481+
1482+
.detail-pre-error {
1483+
color: var(--am-danger);
1484+
background: var(--am-danger-soft);
1485+
border-color: #fecaca;
1486+
}
1487+
1488+
.raw-detail {
1489+
margin-top: 12px;
1490+
padding: 9px 10px;
1491+
background: #ffffff;
1492+
border: 1px solid var(--am-border-subtle);
1493+
border-radius: var(--am-radius-sm);
1494+
}
1495+
1496+
.raw-detail summary {
1497+
cursor: pointer;
1498+
color: var(--am-text-soft);
1499+
font-size: 12px;
1500+
font-weight: 700;
1501+
}
1502+
1503+
.raw-detail summary span {
1504+
margin-left: 6px;
1505+
font-weight: 500;
1506+
}
1507+
1508+
.raw-detail-summary {
1509+
margin-top: 8px;
1510+
color: var(--am-muted);
1511+
font-size: 12px;
1512+
line-height: 18px;
1513+
}
1514+
1515+
.raw-json {
1516+
max-height: 340px;
1517+
}
1518+
14451519
.pricing-source-date {
14461520
margin-top: 2px;
14471521
font-size: 11px;

0 commit comments

Comments
 (0)