@tonyclaw/llm-inspector 1.14.8 → 1.14.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,51 +38,51 @@ const assets = {
38
38
  "/assets/alibaba-TTwafVwX.svg": {
39
39
  "type": "image/svg+xml",
40
40
  "etag": '"171b-6dyV5K8QjiaY35sN9qNprh9zDIs"',
41
- "mtime": "2026-06-11T12:14:10.996Z",
41
+ "mtime": "2026-06-11T13:44:56.125Z",
42
42
  "size": 5915,
43
43
  "path": "../public/assets/alibaba-TTwafVwX.svg"
44
44
  },
45
- "/assets/index-vP91146S.css": {
45
+ "/assets/index-bqeypwJB.css": {
46
46
  "type": "text/css; charset=utf-8",
47
- "etag": '"14a58-Ts4ulNygZtoNVfw4qwf2tNfAXoU"',
48
- "mtime": "2026-06-11T12:14:10.997Z",
49
- "size": 84568,
50
- "path": "../public/assets/index-vP91146S.css"
51
- },
52
- "/assets/zhipuai-BPNAnxo-.svg": {
53
- "type": "image/svg+xml",
54
- "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
55
- "mtime": "2026-06-11T12:14:10.996Z",
56
- "size": 11256,
57
- "path": "../public/assets/zhipuai-BPNAnxo-.svg"
47
+ "etag": '"14d2d-fciDL/LvvG4KiFnlq0WNx0FqBmw"',
48
+ "mtime": "2026-06-11T13:44:56.125Z",
49
+ "size": 85293,
50
+ "path": "../public/assets/index-bqeypwJB.css"
58
51
  },
59
52
  "/assets/minimax-BPMzvuL-.jpeg": {
60
53
  "type": "image/jpeg",
61
54
  "etag": '"1b06-IwivU89ko5UTMUM1/t7hn4sQK9A"',
62
- "mtime": "2026-06-11T12:14:10.996Z",
55
+ "mtime": "2026-06-11T13:44:56.125Z",
63
56
  "size": 6918,
64
57
  "path": "../public/assets/minimax-BPMzvuL-.jpeg"
65
58
  },
66
- "/assets/main-CJ4MreBr.js": {
59
+ "/assets/zhipuai-BPNAnxo-.svg": {
60
+ "type": "image/svg+xml",
61
+ "etag": '"2bf8-hNaLCTi89nOFCsIIfWpP/jrfo0s"',
62
+ "mtime": "2026-06-11T13:44:56.122Z",
63
+ "size": 11256,
64
+ "path": "../public/assets/zhipuai-BPNAnxo-.svg"
65
+ },
66
+ "/assets/main-C8OUJKbz.js": {
67
67
  "type": "text/javascript; charset=utf-8",
68
- "etag": '"50599-xS7/i8WCDvqJMx9qZW3sZ2pNsvE"',
69
- "mtime": "2026-06-11T12:14:10.997Z",
68
+ "etag": '"50599-x4LjDXM7jigK2tdjHCfXl0STwVQ"',
69
+ "mtime": "2026-06-11T13:44:56.125Z",
70
70
  "size": 329113,
71
- "path": "../public/assets/main-CJ4MreBr.js"
71
+ "path": "../public/assets/main-C8OUJKbz.js"
72
72
  },
73
73
  "/assets/qwen-CONDcHqt.png": {
74
74
  "type": "image/png",
75
75
  "etag": '"572c3-cdJAPaHdOvFCGzuaQjagdgOu6XE"',
76
- "mtime": "2026-06-11T12:14:10.996Z",
76
+ "mtime": "2026-06-11T13:44:56.125Z",
77
77
  "size": 357059,
78
78
  "path": "../public/assets/qwen-CONDcHqt.png"
79
79
  },
80
- "/assets/index-CdnotuLh.js": {
80
+ "/assets/index-Dv-dj1xH.js": {
81
81
  "type": "text/javascript; charset=utf-8",
82
- "etag": '"95a74-8hHRRtv/cPIlS0z++Wpqpv18rpY"',
83
- "mtime": "2026-06-11T12:14:10.999Z",
84
- "size": 612980,
85
- "path": "../public/assets/index-CdnotuLh.js"
82
+ "etag": '"9600f-3dnGI92f5ekK1BS9qp5iLg0Map4"',
83
+ "mtime": "2026-06-11T13:44:56.127Z",
84
+ "size": 614415,
85
+ "path": "../public/assets/index-Dv-dj1xH.js"
86
86
  }
87
87
  };
88
88
  function readAsset(id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonyclaw/llm-inspector",
3
- "version": "1.14.8",
3
+ "version": "1.14.9",
4
4
  "type": "module",
5
5
  "description": "LLM API proxy inspector — captures and displays requests/responses from AI coding tools in a web UI",
6
6
  "license": "MIT",
@@ -1,6 +1,7 @@
1
1
  import { type JSX, useCallback, useEffect, useMemo, useState, useRef } from "react";
2
2
  import { useVirtualizer } from "@tanstack/react-virtual";
3
3
  import { Download, GitBranch, LayoutGrid, List } from "lucide-react";
4
+ import { cn } from "../lib/utils";
4
5
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./ui/tooltip";
5
6
  import type { CapturedLog } from "../proxy/schemas";
6
7
  import { exportLogsAsZip } from "../lib/export-logs";
@@ -156,6 +157,18 @@ export function ProxyViewer({
156
157
  const cacheTrends = useMemo(() => computeCacheTrends(groups), [groups]);
157
158
  const stopReasons = useMemo(() => logs.map((log) => extractStopReason(log)), [logs]);
158
159
 
160
+ const turnIndices = useMemo(() => {
161
+ const indices: number[] = [];
162
+ let turn = 0;
163
+ for (let i = 0; i < stopReasons.length; i++) {
164
+ if (i > 0 && (stopReasons[i - 1] === "end_turn" || stopReasons[i - 1] === "stop")) {
165
+ turn++;
166
+ }
167
+ indices.push(turn);
168
+ }
169
+ return indices;
170
+ }, [stopReasons]);
171
+
159
172
  // Determine what items to render (groups or individual logs)
160
173
  const renderGroups = logs.length > 0 && groupedView && groups.length > 1;
161
174
 
@@ -402,8 +415,18 @@ export function ProxyViewer({
402
415
  isPending={log.responseStatus === null}
403
416
  isFirst={idx === 0}
404
417
  isLast={idx === logs.length - 1}
418
+ isTurnStart={
419
+ idx === 0 ||
420
+ stopReasons[idx - 1] === "end_turn" ||
421
+ stopReasons[idx - 1] === "stop"
422
+ }
405
423
  />
406
- <div className="flex-1 min-w-0 mb-2">
424
+ <div
425
+ className={cn(
426
+ "flex-1 min-w-0 mb-2 rounded-lg",
427
+ (turnIndices[idx] ?? 0) % 2 === 0 ? "bg-muted/10" : "bg-muted/25",
428
+ )}
429
+ >
407
430
  <LogEntry
408
431
  log={log}
409
432
  viewMode={viewMode}
@@ -2,6 +2,7 @@ import { useState, memo, useMemo, useEffect } from "react";
2
2
  import type { JSX } from "react";
3
3
  import type { CapturedLog } from "../../proxy/schemas";
4
4
  import { extractStopReason } from "../../lib/stopReason";
5
+ import { cn } from "../../lib/utils";
5
6
  import {
6
7
  ConversationHeader,
7
8
  getConversationId,
@@ -68,6 +69,19 @@ export const ConversationGroup = memo(function ({
68
69
  // Pre-compute stop reasons for each log — used by ThreadConnector
69
70
  const stopReasons = useMemo(() => group.logs.map((log) => extractStopReason(log)), [group.logs]);
70
71
 
72
+ // Compute turn indices for alternating background colours
73
+ const turnIndices = useMemo(() => {
74
+ const indices: number[] = [];
75
+ let turn = 0;
76
+ for (let i = 0; i < stopReasons.length; i++) {
77
+ if (i > 0 && (stopReasons[i - 1] === "end_turn" || stopReasons[i - 1] === "stop")) {
78
+ turn++;
79
+ }
80
+ indices.push(turn);
81
+ }
82
+ return indices;
83
+ }, [stopReasons]);
84
+
71
85
  const displayId =
72
86
  group.conversationId.startsWith("PID:") || group.conversationId.includes("|")
73
87
  ? group.conversationId
@@ -89,6 +103,7 @@ export const ConversationGroup = memo(function ({
89
103
  onToggle={() => setExpanded(!expanded)}
90
104
  hideApiFormat={mixed}
91
105
  isLoading={isLoading}
106
+ userAgent={group.logs[0]?.userAgent ?? null}
92
107
  viewMode={groupViewMode}
93
108
  onToggleViewMode={() => setGroupViewMode((prev) => (prev === "thread" ? "flat" : "thread"))}
94
109
  />
@@ -100,7 +115,6 @@ export const ConversationGroup = memo(function ({
100
115
  key={log.id}
101
116
  log={log}
102
117
  viewMode={viewMode}
103
- suppressApiFormatBadge={!mixed}
104
118
  strip={strip}
105
119
  cacheTrend={cacheTrends?.get(log.id) ?? null}
106
120
  onCompareWithPrevious={() => onCompareWithPrevious(log)}
@@ -111,26 +125,35 @@ export const ConversationGroup = memo(function ({
111
125
 
112
126
  {expanded && groupViewMode === "thread" && (
113
127
  <div className="ml-3">
114
- {group.logs.map((log, idx) => (
115
- <div key={log.id} className="flex items-stretch">
116
- <ThreadConnector
117
- stopReason={stopReasons[idx] ?? null}
118
- isPending={log.responseStatus === null}
119
- isFirst={idx === 0}
120
- isLast={idx === group.logs.length - 1}
121
- />
122
- <div className="flex-1 min-w-0 mb-2">
123
- <LogEntry
124
- log={log}
125
- viewMode={viewMode}
126
- suppressApiFormatBadge={!mixed}
127
- strip={strip}
128
- cacheTrend={cacheTrends?.get(log.id) ?? null}
129
- onCompareWithPrevious={() => onCompareWithPrevious(log)}
128
+ {group.logs.map((log, idx) => {
129
+ const isTurnStart =
130
+ idx === 0 || stopReasons[idx - 1] === "end_turn" || stopReasons[idx - 1] === "stop";
131
+ return (
132
+ <div key={log.id} className="flex items-stretch">
133
+ <ThreadConnector
134
+ stopReason={stopReasons[idx] ?? null}
135
+ isPending={log.responseStatus === null}
136
+ isFirst={idx === 0}
137
+ isLast={idx === group.logs.length - 1}
138
+ isTurnStart={isTurnStart}
130
139
  />
140
+ <div
141
+ className={cn(
142
+ "flex-1 min-w-0 mb-2 rounded-lg",
143
+ (turnIndices[idx] ?? 0) % 2 === 0 ? "bg-muted/10" : "bg-muted/25",
144
+ )}
145
+ >
146
+ <LogEntry
147
+ log={log}
148
+ viewMode={viewMode}
149
+ strip={strip}
150
+ cacheTrend={cacheTrends?.get(log.id) ?? null}
151
+ onCompareWithPrevious={() => onCompareWithPrevious(log)}
152
+ />
153
+ </div>
131
154
  </div>
132
- </div>
133
- ))}
155
+ );
156
+ })}
134
157
  </div>
135
158
  )}
136
159
  </div>
@@ -5,6 +5,7 @@ import {
5
5
  GitBranch,
6
6
  Loader2,
7
7
  MessageSquare,
8
+ User,
8
9
  Zap,
9
10
  } from "lucide-react";
10
11
  import type { JSX } from "react";
@@ -40,6 +41,8 @@ export type ConversationHeaderProps = {
40
41
  viewMode?: ViewMode;
41
42
  /** Toggle between flat and thread display modes for this group. */
42
43
  onToggleViewMode?: () => void;
44
+ /** User-Agent string from the first log in the group. */
45
+ userAgent?: string | null;
43
46
  };
44
47
 
45
48
  function formatTimestamp(iso: string): string {
@@ -61,6 +64,7 @@ export function ConversationHeader({
61
64
  isLoading = false,
62
65
  viewMode,
63
66
  onToggleViewMode,
67
+ userAgent,
64
68
  }: ConversationHeaderProps): JSX.Element {
65
69
  return (
66
70
  <div
@@ -123,6 +127,17 @@ export function ConversationHeader({
123
127
  : conversationId}
124
128
  </span>
125
129
 
130
+ {/* User-Agent */}
131
+ {userAgent !== null && userAgent !== undefined && userAgent !== "" && (
132
+ <span
133
+ className="flex items-center gap-1 text-muted-foreground text-xs shrink-0"
134
+ title={userAgent}
135
+ >
136
+ <User className="size-3" />
137
+ <span className="font-mono tabular-nums truncate max-w-[120px]">{userAgent}</span>
138
+ </span>
139
+ )}
140
+
126
141
  {/* API Format Badge */}
127
142
  {!hideApiFormat && (
128
143
  <Badge
@@ -2,7 +2,13 @@ import { Check, Copy, GitCompareArrows, RotateCcw } from "lucide-react";
2
2
  import type { JSX } from "react";
3
3
  import { useMemo, useState, memo } from "react";
4
4
  import { cn } from "../../lib/utils";
5
- import { type CapturedLog, parseRequest } from "../../proxy/schemas";
5
+ import {
6
+ type CapturedLog,
7
+ parseRequest,
8
+ InspectorResponseSchema,
9
+ parseOpenAIResponse,
10
+ OpenAIRequestSchema,
11
+ } from "../../proxy/schemas";
6
12
  import { stripClaudeCodeBillingHeader } from "../../proxy/claudeCodeStrip";
7
13
  import { Button } from "../ui/button";
8
14
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
@@ -18,10 +24,7 @@ import type { CacheTrendEntry } from "./cacheTrend";
18
24
  export type LogEntryProps = {
19
25
  log: CapturedLog;
20
26
  viewMode?: "simple" | "full";
21
- /** Suppress the API format badge when log is displayed within a group */
22
- suppressApiFormatBadge?: boolean;
23
- /**
24
- * Live "strip Claude Code billing header" flag, sourced once at the viewer
27
+ /** Live "strip Claude Code billing header" flag, sourced once at the viewer
25
28
  * container. Hoisted out of `LogEntry` so a single SWR subscription serves
26
29
  * the whole virtualized list (N logs == N subscriptions is the previous
27
30
  * cost).
@@ -146,7 +149,6 @@ function DiffToggleButton({
146
149
  export const LogEntry = memo(function ({
147
150
  log,
148
151
  viewMode = "simple",
149
- suppressApiFormatBadge = false,
150
152
  strip,
151
153
  cacheTrend = null,
152
154
  onCompareWithPrevious,
@@ -158,7 +160,63 @@ export const LogEntry = memo(function ({
158
160
  const [replayOpen, setReplayOpen] = useState<boolean>(false);
159
161
  const [headersDiff, setHeadersDiff] = useState<boolean>(false);
160
162
  const [requestDiff, setRequestDiff] = useState<boolean>(false);
161
- const parsedRequest = useMemo(() => parseRequest(log.rawRequestBody), [log.rawRequestBody]);
163
+ const messageCount = useMemo(() => {
164
+ if (log.rawRequestBody === null) return null;
165
+ if (log.apiFormat === "anthropic") {
166
+ const parsed = parseRequest(log.rawRequestBody);
167
+ if (parsed !== null) return parsed.messages.length;
168
+ } else if (log.apiFormat === "openai") {
169
+ try {
170
+ const result = OpenAIRequestSchema.safeParse(JSON.parse(log.rawRequestBody));
171
+ if (result.success) return result.data.messages.length;
172
+ } catch {
173
+ // ignore
174
+ }
175
+ }
176
+ return null;
177
+ }, [log.rawRequestBody, log.apiFormat]);
178
+ const toolCount = useMemo(() => {
179
+ if (log.rawRequestBody === null) return null;
180
+ if (log.apiFormat === "anthropic") {
181
+ const parsed = parseRequest(log.rawRequestBody);
182
+ if (parsed !== null && parsed.tools !== undefined && parsed.tools.length > 0) {
183
+ return parsed.tools.length;
184
+ }
185
+ } else if (log.apiFormat === "openai") {
186
+ try {
187
+ const result = OpenAIRequestSchema.safeParse(JSON.parse(log.rawRequestBody));
188
+ if (result.success && result.data.tools !== undefined && result.data.tools.length > 0) {
189
+ return result.data.tools.length;
190
+ }
191
+ } catch {
192
+ // ignore
193
+ }
194
+ }
195
+ return null;
196
+ }, [log.rawRequestBody, log.apiFormat]);
197
+ const responseToolNames = useMemo(() => {
198
+ if (log.responseText === null) return null;
199
+ if (log.apiFormat === "openai") {
200
+ const parsed = parseOpenAIResponse(log.responseText);
201
+ if (parsed !== null) {
202
+ const toolCalls = parsed.choices[0]?.message?.tool_calls;
203
+ if (toolCalls !== undefined && toolCalls !== null && toolCalls.length > 0) {
204
+ return toolCalls.map((tc) => tc.function?.name ?? "?").filter((n) => n !== "");
205
+ }
206
+ }
207
+ } else if (log.apiFormat === "anthropic") {
208
+ try {
209
+ const result = InspectorResponseSchema.safeParse(JSON.parse(log.responseText));
210
+ if (result.success) {
211
+ const names = result.data.content.filter((c) => c.type === "tool_use").map((c) => c.name);
212
+ if (names.length > 0) return names;
213
+ }
214
+ } catch {
215
+ // JSON parse failed, ignore
216
+ }
217
+ }
218
+ return null;
219
+ }, [log.responseText, log.apiFormat]);
162
220
  const strippedRequestBody = useMemo(() => {
163
221
  if (!strip || log.apiFormat !== "anthropic" || log.rawRequestBody === null) {
164
222
  return null;
@@ -208,10 +266,11 @@ export const LogEntry = memo(function ({
208
266
  <div className="border border-border rounded-lg mb-3 overflow-hidden">
209
267
  <LogEntryHeader
210
268
  log={log}
211
- parsedRequest={parsedRequest}
269
+ messageCount={messageCount}
270
+ toolCount={toolCount}
212
271
  expanded={expanded}
213
272
  onToggle={() => setExpanded(!expanded)}
214
- suppressApiFormatBadge={suppressApiFormatBadge}
273
+ responseToolNames={responseToolNames}
215
274
  cacheTrend={cacheTrend}
216
275
  />
217
276
 
@@ -9,14 +9,13 @@ import {
9
9
  Loader2,
10
10
  MessageSquare,
11
11
  Radio,
12
- User,
13
12
  Wrench,
14
13
  Zap,
15
14
  } from "lucide-react";
16
15
  import type { JSX } from "react";
17
16
  import { memo } from "react";
18
17
  import { cn, formatTokens, getStatusCategory, type StatusCategory } from "../../lib/utils";
19
- import type { CapturedLog, InspectorRequest } from "../../proxy/schemas";
18
+ import type { CapturedLog } from "../../proxy/schemas";
20
19
  import { Badge } from "../ui/badge";
21
20
  import { ProviderLogo, detectProvider } from "../providers/ProviderLogo";
22
21
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
@@ -56,13 +55,15 @@ function CacheTrendIndicator({ trend }: { trend: CacheTrend | null }): JSX.Eleme
56
55
 
57
56
  export type LogEntryHeaderProps = {
58
57
  log: CapturedLog;
59
- parsedRequest: InspectorRequest | null;
58
+ /** Number of messages in the request (supports both Anthropic and OpenAI formats). */
59
+ messageCount?: number | null;
60
+ /** Number of tools defined in the request (supports both Anthropic and OpenAI formats). */
61
+ toolCount?: number | null;
60
62
  expanded: boolean;
61
63
  onToggle: () => void;
62
- /** Suppress the API format badge when log is displayed within a group */
63
- suppressApiFormatBadge?: boolean;
64
- /**
65
- * Per-log cache token trend (creation + read) relative to the previous log
64
+ /** Tool call names extracted from the model response (e.g., ["read_file", "grep"]). */
65
+ responseToolNames?: string[] | null;
66
+ /** Per-log cache token trend (creation + read) relative to the previous log
66
67
  * in the same conversation group. When `undefined` or a field is `null`,
67
68
  * the corresponding cache span renders as it did before — no arrow.
68
69
  */
@@ -71,23 +72,17 @@ export type LogEntryHeaderProps = {
71
72
 
72
73
  export const LogEntryHeader = memo(function ({
73
74
  log,
74
- parsedRequest,
75
+ messageCount = null,
76
+ toolCount = null,
75
77
  expanded,
76
78
  onToggle,
77
- suppressApiFormatBadge = false,
79
+ responseToolNames = null,
78
80
  cacheTrend = null,
79
81
  }: LogEntryHeaderProps): JSX.Element {
80
82
  const statusCategory = getStatusCategory(log.responseStatus);
81
83
 
82
84
  const hasTokens = log.inputTokens !== null || log.outputTokens !== null;
83
85
 
84
- const messageCount = parsedRequest !== null ? parsedRequest.messages.length : null;
85
-
86
- const toolCount =
87
- parsedRequest !== null && parsedRequest.tools !== undefined && parsedRequest.tools.length > 0
88
- ? parsedRequest.tools.length
89
- : null;
90
-
91
86
  return (
92
87
  <div
93
88
  role="button"
@@ -124,50 +119,38 @@ export const LogEntryHeader = memo(function ({
124
119
  </TooltipProvider>
125
120
  )}
126
121
 
127
- {/* API Format Badge */}
128
- {!suppressApiFormatBadge && (
129
- <Badge
130
- variant="outline"
131
- className={cn(
132
- "text-[10px] px-1.5 py-0 h-5 font-mono shrink-0",
133
- log.apiFormat === "openai" && "border-blue-500/40 text-blue-400",
134
- log.apiFormat === "anthropic" && "border-orange-500/40 text-orange-400",
135
- log.apiFormat === "unknown" && "border-muted text-muted-foreground",
136
- )}
137
- >
138
- {log.apiFormat === "anthropic"
139
- ? "Anthropic"
140
- : log.apiFormat === "openai"
141
- ? "OpenAI"
142
- : "Unknown"}
143
- </Badge>
144
- )}
145
-
146
- {/* Response Status */}
147
- {statusCategory === "server_error" ? (
148
- <Badge variant="destructive" className="text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums">
149
- {log.responseStatus}
150
- </Badge>
151
- ) : statusCategory === "pending" ? (
152
- <Badge
153
- variant="outline"
154
- className={cn(
155
- "text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums",
156
- STATUS_BADGE_CLASSES[statusCategory],
157
- )}
158
- >
159
- <Loader2 className="size-3 animate-spin" />
160
- </Badge>
161
- ) : (
162
- <Badge
163
- variant="outline"
164
- className={cn(
165
- "text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums",
166
- STATUS_BADGE_CLASSES[statusCategory],
122
+ {/* Response Status only shown for non-200 or pending */}
123
+ {statusCategory !== "success" && (
124
+ <>
125
+ {statusCategory === "server_error" ? (
126
+ <Badge
127
+ variant="destructive"
128
+ className="text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums"
129
+ >
130
+ {log.responseStatus}
131
+ </Badge>
132
+ ) : statusCategory === "pending" ? (
133
+ <Badge
134
+ variant="outline"
135
+ className={cn(
136
+ "text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums",
137
+ STATUS_BADGE_CLASSES[statusCategory],
138
+ )}
139
+ >
140
+ <Loader2 className="size-3 animate-spin" />
141
+ </Badge>
142
+ ) : (
143
+ <Badge
144
+ variant="outline"
145
+ className={cn(
146
+ "text-[10px] px-1.5 py-0 h-5 font-mono tabular-nums",
147
+ STATUS_BADGE_CLASSES[statusCategory],
148
+ )}
149
+ >
150
+ {log.responseStatus}
151
+ </Badge>
167
152
  )}
168
- >
169
- {log.responseStatus}
170
- </Badge>
153
+ </>
171
154
  )}
172
155
 
173
156
  {/* Elapsed time */}
@@ -257,6 +240,23 @@ export const LogEntryHeader = memo(function ({
257
240
  </TooltipProvider>
258
241
  )}
259
242
 
243
+ {/* Response tool calls — tool names the model requested to invoke */}
244
+ {responseToolNames !== null && responseToolNames.length > 0 && (
245
+ <TooltipProvider>
246
+ <Tooltip>
247
+ <TooltipTrigger asChild>
248
+ <span className="flex items-center gap-1 text-amber-400/80 text-xs shrink-0">
249
+ <Wrench className="size-3" />
250
+ <span className="font-mono tabular-nums truncate max-w-[160px]">
251
+ {responseToolNames.join(", ")}
252
+ </span>
253
+ </span>
254
+ </TooltipTrigger>
255
+ <TooltipContent>Tools called by model: {responseToolNames.join(", ")}</TooltipContent>
256
+ </Tooltip>
257
+ </TooltipProvider>
258
+ )}
259
+
260
260
  {/* Origin */}
261
261
  {log.origin !== null && (
262
262
  <span
@@ -270,19 +270,6 @@ export const LogEntryHeader = memo(function ({
270
270
  </span>
271
271
  )}
272
272
 
273
- {/* User-Agent */}
274
- {log.userAgent !== null && (
275
- <span
276
- className="flex items-center gap-1 text-muted-foreground text-xs shrink-0"
277
- title={`User-Agent: ${log.userAgent}`}
278
- >
279
- <User className="size-3" />
280
- <span className="font-mono tabular-nums truncate max-w-[150px]" title={log.userAgent}>
281
- {log.userAgent}
282
- </span>
283
- </span>
284
- )}
285
-
286
273
  {/* Client info (PID + project folder) */}
287
274
  {(log.clientPid !== null || log.clientProjectFolder !== null) && (
288
275
  <TooltipProvider>