@tonyclaw/llm-inspector 1.15.0 → 1.16.0
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.
- package/.output/cli.js +1 -0
- package/.output/nitro.json +1 -1
- package/.output/public/assets/index-BmkN9DxE.js +107 -0
- package/.output/public/assets/index-DPe3eOih.css +1 -0
- package/.output/public/assets/{main-BLYgekFx.js → main-BjnjXVBU.js} +1 -1
- package/.output/server/_libs/diff.mjs +2 -2
- package/.output/server/_ssr/{index-P66uoVEU.mjs → index-BIOEVAzU.mjs} +783 -588
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-DpLCKk51.mjs → router-THS9ptvu.mjs} +439 -177
- package/.output/server/{_tanstack-start-manifest_v-C9Wq6YdJ.mjs → _tanstack-start-manifest_v-BYhN7q_z.mjs} +1 -1
- package/.output/server/index.mjs +31 -31
- package/README.md +200 -113
- package/package.json +1 -1
- package/src/cli.ts +1 -0
- package/src/components/ProxyViewer.tsx +77 -85
- package/src/components/ProxyViewerContainer.tsx +148 -76
- package/src/components/providers/ImportWizardDialog.tsx +27 -3
- package/src/components/proxy-viewer/CompareDrawer.tsx +17 -4
- package/src/components/proxy-viewer/ConversationGroup.tsx +15 -47
- package/src/components/proxy-viewer/ConversationHeader.tsx +58 -5
- package/src/components/proxy-viewer/LogEntry.tsx +297 -329
- package/src/components/proxy-viewer/LogEntryHeader.tsx +126 -137
- package/src/components/proxy-viewer/ResponseView.tsx +14 -34
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +3 -3
- package/src/components/proxy-viewer/TurnGroup.tsx +25 -21
- package/src/components/proxy-viewer/diff/DiffView.tsx +5 -3
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -9
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +3 -3
- package/src/components/proxy-viewer/formats/index.tsx +19 -10
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +7 -3
- package/src/components/proxy-viewer/log-formats/anthropic.ts +48 -0
- package/src/components/proxy-viewer/log-formats/index.ts +23 -0
- package/src/components/proxy-viewer/log-formats/openai.ts +40 -0
- package/src/components/proxy-viewer/log-formats/types.ts +33 -0
- package/src/components/proxy-viewer/log-formats/unknown.ts +14 -0
- package/src/components/proxy-viewer/viewerState.ts +58 -0
- package/src/components/ui/json-viewer.tsx +3 -3
- package/src/lib/objectUtils.ts +22 -0
- package/src/proxy/claudeCodeStrip.ts +5 -8
- package/src/proxy/formats/index.ts +1 -1
- package/src/proxy/formats/registry.ts +9 -0
- package/src/proxy/handler.ts +2 -8
- package/src/proxy/logIndex.ts +58 -43
- package/src/proxy/logger.ts +51 -27
- package/src/proxy/openaiOrphanToolStrip.ts +11 -17
- package/src/proxy/providerImporters.ts +245 -19
- package/src/proxy/providers.ts +20 -7
- package/src/proxy/schemas.ts +5 -9
- package/src/proxy/socketTracker.ts +109 -78
- package/src/proxy/store.ts +68 -83
- package/src/routes/api/logs.ts +31 -2
- package/styles/globals.css +22 -0
- package/.output/public/assets/index-CMuJQyt1.js +0 -105
- package/.output/public/assets/index-DciyfYBk.css +0 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useState, memo, useMemo } from "react";
|
|
2
2
|
import type { JSX } from "react";
|
|
3
3
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
4
|
-
import { extractStopReason } from "../../lib/stopReason";
|
|
5
4
|
import {
|
|
6
5
|
ConversationHeader,
|
|
7
6
|
getGroupApiFormat,
|
|
@@ -10,6 +9,7 @@ import {
|
|
|
10
9
|
} from "./ConversationHeader";
|
|
11
10
|
import { TurnGroup } from "./TurnGroup";
|
|
12
11
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
12
|
+
import { buildTurnGroups, shouldRenderConversationContent } from "./viewerState";
|
|
13
13
|
|
|
14
14
|
export type ConversationGroupProps = {
|
|
15
15
|
group: ConversationGroupData;
|
|
@@ -23,8 +23,11 @@ export type ConversationGroupProps = {
|
|
|
23
23
|
cacheTrends?: Map<number, CacheTrendEntry>;
|
|
24
24
|
/** Callback to open CompareDrawer with a log and its immediate predecessor. */
|
|
25
25
|
onCompareWithPrevious: (log: CapturedLog) => void;
|
|
26
|
+
comparisonPredecessors: Map<number, CapturedLog>;
|
|
26
27
|
/** When true, skip the group header and render content directly. */
|
|
27
28
|
standalone?: boolean;
|
|
29
|
+
/** Clear all logs that belong to this group. */
|
|
30
|
+
onClearGroup: (ids: number[]) => void;
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
function computeStats(logs: CapturedLog[]): {
|
|
@@ -46,56 +49,19 @@ export const ConversationGroup = memo(function ({
|
|
|
46
49
|
strip,
|
|
47
50
|
cacheTrends,
|
|
48
51
|
onCompareWithPrevious,
|
|
52
|
+
comparisonPredecessors,
|
|
53
|
+
onClearGroup,
|
|
49
54
|
standalone = false,
|
|
50
55
|
}: ConversationGroupProps): JSX.Element {
|
|
51
|
-
const [expanded, setExpanded] = useState(
|
|
52
|
-
const stats = computeStats(group.logs);
|
|
56
|
+
const [expanded, setExpanded] = useState(false);
|
|
57
|
+
const stats = useMemo(() => computeStats(group.logs), [group.logs]);
|
|
53
58
|
const startTime = group.logs[0]?.timestamp ?? new Date().toISOString();
|
|
54
59
|
const endTime = group.logs[group.logs.length - 1]?.timestamp ?? new Date().toISOString();
|
|
55
60
|
const mixed = hasMixedApiFormat(group.logs);
|
|
56
61
|
const isLoading = group.logs.some((log) => log.responseStatus === null);
|
|
57
62
|
|
|
58
63
|
// Pre-compute stop reasons for each log — used by turnIndices
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
// Compute turn indices for alternating background colours
|
|
62
|
-
const turnIndices = useMemo(() => {
|
|
63
|
-
const indices: number[] = [];
|
|
64
|
-
let turn = 0;
|
|
65
|
-
for (let i = 0; i < stopReasons.length; i++) {
|
|
66
|
-
if (i > 0 && (stopReasons[i - 1] === "end_turn" || stopReasons[i - 1] === "stop")) {
|
|
67
|
-
turn++;
|
|
68
|
-
}
|
|
69
|
-
indices.push(turn);
|
|
70
|
-
}
|
|
71
|
-
return indices;
|
|
72
|
-
}, [stopReasons]);
|
|
73
|
-
|
|
74
|
-
// Group logs into turns for collapsible turn display
|
|
75
|
-
const turnGroups = useMemo(() => {
|
|
76
|
-
const groups: { logs: CapturedLog[]; turnIndex: number }[] = [];
|
|
77
|
-
let currentGroup: CapturedLog[] = [];
|
|
78
|
-
let currentTurn = 0;
|
|
79
|
-
|
|
80
|
-
for (let i = 0; i < group.logs.length; i++) {
|
|
81
|
-
const turnIdx = turnIndices[i] ?? 0;
|
|
82
|
-
const log = group.logs[i];
|
|
83
|
-
if (!log) continue;
|
|
84
|
-
if (turnIdx !== currentTurn) {
|
|
85
|
-
if (currentGroup.length > 0) {
|
|
86
|
-
groups.push({ logs: currentGroup, turnIndex: currentTurn });
|
|
87
|
-
}
|
|
88
|
-
currentGroup = [log];
|
|
89
|
-
currentTurn = turnIdx;
|
|
90
|
-
} else {
|
|
91
|
-
currentGroup.push(log);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (currentGroup.length > 0) {
|
|
95
|
-
groups.push({ logs: currentGroup, turnIndex: currentTurn });
|
|
96
|
-
}
|
|
97
|
-
return groups;
|
|
98
|
-
}, [group.logs, turnIndices]);
|
|
64
|
+
const turnGroups = useMemo(() => buildTurnGroups(group.logs), [group.logs]);
|
|
99
65
|
const displayId =
|
|
100
66
|
group.conversationId.startsWith("PID:") || group.conversationId.includes("|")
|
|
101
67
|
? group.conversationId
|
|
@@ -104,7 +70,7 @@ export const ConversationGroup = memo(function ({
|
|
|
104
70
|
: group.conversationId;
|
|
105
71
|
|
|
106
72
|
return (
|
|
107
|
-
<div className="mb-
|
|
73
|
+
<div className="mb-2">
|
|
108
74
|
{!standalone && (
|
|
109
75
|
<ConversationHeader
|
|
110
76
|
conversationId={displayId}
|
|
@@ -119,19 +85,21 @@ export const ConversationGroup = memo(function ({
|
|
|
119
85
|
hideApiFormat={mixed}
|
|
120
86
|
isLoading={isLoading}
|
|
121
87
|
userAgent={group.logs[0]?.userAgent ?? null}
|
|
88
|
+
onClear={() => onClearGroup(group.logs.map((l) => l.id))}
|
|
122
89
|
/>
|
|
123
90
|
)}
|
|
124
91
|
|
|
125
|
-
{expanded && (
|
|
126
|
-
<div>
|
|
92
|
+
{shouldRenderConversationContent(standalone, expanded) && (
|
|
93
|
+
<div className="max-h-[70vh] overflow-y-auto">
|
|
127
94
|
{turnGroups.map((tg) => (
|
|
128
95
|
<TurnGroup
|
|
129
96
|
key={tg.turnIndex}
|
|
130
|
-
|
|
97
|
+
entries={tg.entries}
|
|
131
98
|
viewMode={viewMode}
|
|
132
99
|
strip={strip}
|
|
133
100
|
cacheTrends={cacheTrends}
|
|
134
101
|
onCompareWithPrevious={onCompareWithPrevious}
|
|
102
|
+
comparisonPredecessors={comparisonPredecessors}
|
|
135
103
|
turnIndex={tg.turnIndex}
|
|
136
104
|
/>
|
|
137
105
|
))}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
ChevronDown,
|
|
4
|
+
ChevronRight,
|
|
5
|
+
Clock,
|
|
6
|
+
Loader2,
|
|
7
|
+
MessageSquare,
|
|
8
|
+
Trash2,
|
|
9
|
+
User,
|
|
10
|
+
Zap,
|
|
11
|
+
} from "lucide-react";
|
|
2
12
|
import type { JSX } from "react";
|
|
3
13
|
import { cn, formatTokens } from "../../lib/utils";
|
|
4
14
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
5
15
|
import { Badge } from "../ui/badge";
|
|
16
|
+
import { ConfirmDialog } from "../ui/confirm-dialog";
|
|
6
17
|
|
|
7
18
|
const API_FORMAT_LABELS: Record<"anthropic" | "openai" | "unknown", string> = {
|
|
8
19
|
anthropic: "Anthropic",
|
|
@@ -30,6 +41,9 @@ export type ConversationHeaderProps = {
|
|
|
30
41
|
isLoading?: boolean;
|
|
31
42
|
/** User-Agent string from the first log in the group. */
|
|
32
43
|
userAgent?: string | null;
|
|
44
|
+
/** Clear all logs in this group. After confirmation the parent removes them
|
|
45
|
+
* from the in-memory store; this header is then unmounted. */
|
|
46
|
+
onClear?: () => void;
|
|
33
47
|
};
|
|
34
48
|
|
|
35
49
|
function formatTimestamp(iso: string): string {
|
|
@@ -50,7 +64,16 @@ export function ConversationHeader({
|
|
|
50
64
|
hideApiFormat = false,
|
|
51
65
|
isLoading = false,
|
|
52
66
|
userAgent,
|
|
67
|
+
onClear,
|
|
53
68
|
}: ConversationHeaderProps): JSX.Element {
|
|
69
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
70
|
+
|
|
71
|
+
const handleClearClick = (e: React.MouseEvent | React.KeyboardEvent): void => {
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
if (onClear === undefined) return;
|
|
74
|
+
setConfirmOpen(true);
|
|
75
|
+
};
|
|
76
|
+
|
|
54
77
|
return (
|
|
55
78
|
<div
|
|
56
79
|
role="button"
|
|
@@ -59,10 +82,11 @@ export function ConversationHeader({
|
|
|
59
82
|
"flex items-center gap-3 px-3 py-2 cursor-pointer transition-colors",
|
|
60
83
|
"hover:bg-muted/50",
|
|
61
84
|
"select-none",
|
|
62
|
-
"border border-border rounded-lg mb-2 bg-
|
|
85
|
+
"border border-border rounded-lg mb-2 bg-background sticky top-0 z-10",
|
|
63
86
|
)}
|
|
64
87
|
onClick={onToggle}
|
|
65
88
|
onKeyDown={(e) => {
|
|
89
|
+
if (e.target !== e.currentTarget) return;
|
|
66
90
|
if (e.key === "Enter" || e.key === " ") {
|
|
67
91
|
e.preventDefault();
|
|
68
92
|
onToggle();
|
|
@@ -142,6 +166,31 @@ export function ConversationHeader({
|
|
|
142
166
|
|
|
143
167
|
{/* Spacer */}
|
|
144
168
|
<span className="flex-1 min-w-0" />
|
|
169
|
+
|
|
170
|
+
{/* Per-group Clear — does not toggle the group's expand state */}
|
|
171
|
+
{onClear !== undefined && (
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
onClick={handleClearClick}
|
|
175
|
+
aria-label={`Clear group (${totalCalls} request${totalCalls !== 1 ? "s" : ""})`}
|
|
176
|
+
title="Clear this group"
|
|
177
|
+
className="text-muted-foreground hover:text-foreground transition-colors shrink-0 inline-flex items-center justify-center size-6 rounded hover:bg-muted cursor-pointer"
|
|
178
|
+
>
|
|
179
|
+
<Trash2 className="size-3.5" />
|
|
180
|
+
</button>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
<ConfirmDialog
|
|
184
|
+
open={confirmOpen}
|
|
185
|
+
onOpenChange={setConfirmOpen}
|
|
186
|
+
title="Clear this group?"
|
|
187
|
+
description={`This will remove ${totalCalls} request${totalCalls !== 1 ? "s" : ""} from this conversation. This action cannot be undone.`}
|
|
188
|
+
confirmLabel="Clear"
|
|
189
|
+
variant="destructive"
|
|
190
|
+
onConfirm={() => {
|
|
191
|
+
onClear?.();
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
145
194
|
</div>
|
|
146
195
|
);
|
|
147
196
|
}
|
|
@@ -178,11 +227,15 @@ export function hasMixedApiFormat(logs: CapturedLog[]): boolean {
|
|
|
178
227
|
}
|
|
179
228
|
|
|
180
229
|
export function getConversationId(log: CapturedLog): string {
|
|
181
|
-
if (log.
|
|
230
|
+
if (log.isTest === true) return "provider-test";
|
|
231
|
+
if (log.sessionId !== null && log.sessionId !== "" && log.sessionId !== undefined) {
|
|
182
232
|
return log.sessionId;
|
|
183
233
|
}
|
|
184
|
-
const
|
|
185
|
-
const
|
|
234
|
+
const hasPid = log.clientPid !== null && log.clientPid !== undefined;
|
|
235
|
+
const hasFolder = log.clientProjectFolder !== null && log.clientProjectFolder !== undefined;
|
|
236
|
+
if (!hasPid && !hasFolder) return "default";
|
|
237
|
+
const pid = hasPid ? `PID:${log.clientPid}` : "unknown";
|
|
238
|
+
const folder = hasFolder ? log.clientProjectFolder : "no-folder";
|
|
186
239
|
return `${pid}|${folder}`;
|
|
187
240
|
}
|
|
188
241
|
|