@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.
Files changed (54) hide show
  1. package/.output/cli.js +1 -0
  2. package/.output/nitro.json +1 -1
  3. package/.output/public/assets/index-BmkN9DxE.js +107 -0
  4. package/.output/public/assets/index-DPe3eOih.css +1 -0
  5. package/.output/public/assets/{main-BLYgekFx.js → main-BjnjXVBU.js} +1 -1
  6. package/.output/server/_libs/diff.mjs +2 -2
  7. package/.output/server/_ssr/{index-P66uoVEU.mjs → index-BIOEVAzU.mjs} +783 -588
  8. package/.output/server/_ssr/index.mjs +2 -2
  9. package/.output/server/_ssr/{router-DpLCKk51.mjs → router-THS9ptvu.mjs} +439 -177
  10. package/.output/server/{_tanstack-start-manifest_v-C9Wq6YdJ.mjs → _tanstack-start-manifest_v-BYhN7q_z.mjs} +1 -1
  11. package/.output/server/index.mjs +31 -31
  12. package/README.md +200 -113
  13. package/package.json +1 -1
  14. package/src/cli.ts +1 -0
  15. package/src/components/ProxyViewer.tsx +77 -85
  16. package/src/components/ProxyViewerContainer.tsx +148 -76
  17. package/src/components/providers/ImportWizardDialog.tsx +27 -3
  18. package/src/components/proxy-viewer/CompareDrawer.tsx +17 -4
  19. package/src/components/proxy-viewer/ConversationGroup.tsx +15 -47
  20. package/src/components/proxy-viewer/ConversationHeader.tsx +58 -5
  21. package/src/components/proxy-viewer/LogEntry.tsx +297 -329
  22. package/src/components/proxy-viewer/LogEntryHeader.tsx +126 -137
  23. package/src/components/proxy-viewer/ResponseView.tsx +14 -34
  24. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +3 -3
  25. package/src/components/proxy-viewer/TurnGroup.tsx +25 -21
  26. package/src/components/proxy-viewer/diff/DiffView.tsx +5 -3
  27. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +13 -9
  28. package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +3 -3
  29. package/src/components/proxy-viewer/formats/index.tsx +19 -10
  30. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +7 -3
  31. package/src/components/proxy-viewer/log-formats/anthropic.ts +48 -0
  32. package/src/components/proxy-viewer/log-formats/index.ts +23 -0
  33. package/src/components/proxy-viewer/log-formats/openai.ts +40 -0
  34. package/src/components/proxy-viewer/log-formats/types.ts +33 -0
  35. package/src/components/proxy-viewer/log-formats/unknown.ts +14 -0
  36. package/src/components/proxy-viewer/viewerState.ts +58 -0
  37. package/src/components/ui/json-viewer.tsx +3 -3
  38. package/src/lib/objectUtils.ts +22 -0
  39. package/src/proxy/claudeCodeStrip.ts +5 -8
  40. package/src/proxy/formats/index.ts +1 -1
  41. package/src/proxy/formats/registry.ts +9 -0
  42. package/src/proxy/handler.ts +2 -8
  43. package/src/proxy/logIndex.ts +58 -43
  44. package/src/proxy/logger.ts +51 -27
  45. package/src/proxy/openaiOrphanToolStrip.ts +11 -17
  46. package/src/proxy/providerImporters.ts +245 -19
  47. package/src/proxy/providers.ts +20 -7
  48. package/src/proxy/schemas.ts +5 -9
  49. package/src/proxy/socketTracker.ts +109 -78
  50. package/src/proxy/store.ts +68 -83
  51. package/src/routes/api/logs.ts +31 -2
  52. package/styles/globals.css +22 -0
  53. package/.output/public/assets/index-CMuJQyt1.js +0 -105
  54. 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(standalone);
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 stopReasons = useMemo(() => group.logs.map((log) => extractStopReason(log)), [group.logs]);
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-4">
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
- logs={tg.logs}
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 { ChevronDown, ChevronRight, Clock, Loader2, MessageSquare, User, Zap } from "lucide-react";
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-muted/30",
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.sessionId !== null && log.sessionId !== "") {
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 pid = log.clientPid !== null ? `PID:${log.clientPid}` : "unknown";
185
- const folder = log.clientProjectFolder !== null ? log.clientProjectFolder : "no-folder";
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