@tonyclaw/agent-inspector 2.1.15 → 2.1.16

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 (65) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-BcEd6V-V.js → CompareDrawer-Ccrrcx1j.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +106 -0
  4. package/.output/public/assets/{ReplayDialog-BNpC0548.js → ReplayDialog-C7axhr-l.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-Ds1uRLVB.js → RequestAnatomy-WOptg8j_.js} +1 -1
  6. package/.output/public/assets/{ResponseView-7KPVqKl5.js → ResponseView-CSAcxh_M.js} +2 -2
  7. package/.output/public/assets/{StreamingChunkSequence-BHQT261s.js → StreamingChunkSequence-DJS5KhPx.js} +1 -1
  8. package/.output/public/assets/_sessionId-DUSalzKH.js +1 -0
  9. package/.output/public/assets/index-BPpA21dY.css +1 -0
  10. package/.output/public/assets/index-Bt0Az2I2.js +1 -0
  11. package/.output/public/assets/{index-DjKt8XKe.js → index-DMPNh46t.js} +1 -1
  12. package/.output/public/assets/{json-viewer-CkCu-rka.js → json-viewer-inX5QSa3.js} +1 -1
  13. package/.output/public/assets/{main-DpD1N0S8.js → main-aCAKYGSD.js} +2 -2
  14. package/.output/server/_libs/lucide-react.mjs +207 -171
  15. package/.output/server/{_sessionId-DF9Sy8cP.mjs → _sessionId-C0fhvQnn.mjs} +2 -2
  16. package/.output/server/_ssr/{CompareDrawer-BoxztaO7.mjs → CompareDrawer-CO-Ti5sg.mjs} +3 -3
  17. package/.output/server/_ssr/{ProxyViewerContainer-CRBkqFlJ.mjs → ProxyViewerContainer-N4J7uBCX.mjs} +807 -386
  18. package/.output/server/_ssr/{ReplayDialog-Cc1dyDuK.mjs → ReplayDialog-J1VxC0In.mjs} +4 -4
  19. package/.output/server/_ssr/{RequestAnatomy-CMGSsz5Z.mjs → RequestAnatomy-DBH-F3rq.mjs} +3 -3
  20. package/.output/server/_ssr/{ResponseView-Cp10DM1D.mjs → ResponseView-DLFwV7Hd.mjs} +3 -3
  21. package/.output/server/_ssr/{StreamingChunkSequence-B1VGxy3A.mjs → StreamingChunkSequence-BywmuND0.mjs} +3 -3
  22. package/.output/server/_ssr/{index-47XVPghS.mjs → index-CStymIc0.mjs} +2 -2
  23. package/.output/server/_ssr/index.mjs +2 -2
  24. package/.output/server/_ssr/{json-viewer-zDE2rrmJ.mjs → json-viewer-DlQu0N1x.mjs} +3 -3
  25. package/.output/server/_ssr/{router-DVeuZFqI.mjs → router-CQ4Zxhmr.mjs} +112 -46
  26. package/.output/server/{_tanstack-start-manifest_v-Bp8JxtPW.mjs → _tanstack-start-manifest_v-BHeJBGfo.mjs} +1 -1
  27. package/.output/server/index.mjs +72 -72
  28. package/package.json +1 -1
  29. package/src/components/ProxyViewer.tsx +146 -105
  30. package/src/components/ProxyViewerContainer.tsx +77 -67
  31. package/src/components/alerts/AlertsDialog.tsx +2 -1
  32. package/src/components/clients/ClientLogo.tsx +1 -1
  33. package/src/components/groups/GroupsDialog.tsx +19 -1
  34. package/src/components/providers/ProviderCard.tsx +1 -1
  35. package/src/components/providers/ProviderForm.tsx +18 -18
  36. package/src/components/providers/ProviderLogo.tsx +6 -1
  37. package/src/components/providers/ProvidersPanel.tsx +6 -6
  38. package/src/components/providers/SettingsDialog.tsx +13 -6
  39. package/src/components/proxy-viewer/AgentTraceSummary.tsx +6 -6
  40. package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
  41. package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
  42. package/src/components/proxy-viewer/ConversationHeader.tsx +44 -32
  43. package/src/components/proxy-viewer/LogEntry.tsx +21 -17
  44. package/src/components/proxy-viewer/LogEntryHeader.tsx +38 -45
  45. package/src/components/proxy-viewer/ProviderLogoStack.tsx +2 -2
  46. package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
  47. package/src/components/proxy-viewer/ThreadConnector.tsx +4 -4
  48. package/src/components/proxy-viewer/TurnGroup.tsx +16 -11
  49. package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
  50. package/src/components/proxy-viewer/viewerState.ts +0 -4
  51. package/src/components/ui/icon-trigger.ts +2 -0
  52. package/src/components/ui/transient-toast.tsx +104 -0
  53. package/src/lib/sessionInfoContract.ts +4 -0
  54. package/src/mcp/server.ts +71 -27
  55. package/src/mcp/toolHandlers.ts +23 -4
  56. package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
  57. package/src/proxy/sessionInfo.ts +31 -2
  58. package/src/proxy/sessionSupervisor.ts +20 -1
  59. package/src/proxy/store.ts +1 -1
  60. package/src/routes/api/providers.$providerId.test.log.ts +8 -4
  61. package/styles/globals.css +180 -180
  62. package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +0 -106
  63. package/.output/public/assets/_sessionId-DWePGjnS.js +0 -1
  64. package/.output/public/assets/index-CI1-G8ua.js +0 -1
  65. package/.output/public/assets/index-DdhFqPsI.css +0 -1
@@ -101,7 +101,7 @@ function ToolRow({ tool, called }: { tool: RequestToolDefinition; called: boolea
101
101
 
102
102
  return (
103
103
  <Collapsible open={open} onOpenChange={setOpen}>
104
- <div className="rounded-md border border-white/10 bg-black/20 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]">
104
+ <div className="border border-border bg-card shadow-sm rounded-[8px]">
105
105
  <div className="flex min-w-0 items-center gap-2 px-2.5 py-2">
106
106
  <button
107
107
  type="button"
@@ -140,7 +140,7 @@ function ToolRow({ tool, called }: { tool: RequestToolDefinition; called: boolea
140
140
  </button>
141
141
  <button
142
142
  type="button"
143
- className="inline-flex size-7 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
143
+ className="border border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
144
144
  onClick={nameCopy.copy}
145
145
  aria-label={nameCopy.copied ? "Copied tool name" : "Copy tool name"}
146
146
  >
@@ -153,13 +153,13 @@ function ToolRow({ tool, called }: { tool: RequestToolDefinition; called: boolea
153
153
  </div>
154
154
  <CollapsibleContent>
155
155
  {open && (
156
- <div className="space-y-2 border-t border-white/10 bg-white/[0.02] px-2.5 py-2">
156
+ <div className="space-y-2 border-t border-white/10 bg-white/[0.025] px-2.5 py-2">
157
157
  {tool.description !== null && (
158
158
  <p className="text-xs leading-relaxed text-muted-foreground">{tool.description}</p>
159
159
  )}
160
160
  <RequiredParameters names={tool.requiredParameters} />
161
161
  {tool.schema !== null && (
162
- <ScrollArea className="max-h-64 rounded-md border border-white/10 bg-[#06080b] p-2">
162
+ <ScrollArea className="border border-border bg-card shadow-sm max-h-64 rounded-[8px] p-2">
163
163
  <Suspense
164
164
  fallback={<div className="text-xs text-muted-foreground">Loading JSON...</div>}
165
165
  >
@@ -203,7 +203,7 @@ export const RequestTools = memo(function RequestTools({
203
203
  const hiddenPreviewCount = summary.tools.length - previewTools.length;
204
204
 
205
205
  return (
206
- <section className="mb-3 overflow-hidden rounded-md border border-white/10 bg-[#080b0f] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]">
206
+ <section className="border border-border bg-card shadow-sm mb-3 overflow-hidden rounded-[8px]">
207
207
  <div className="flex min-w-0 flex-wrap items-center gap-2 px-3 py-2">
208
208
  <div className="flex min-w-0 flex-1 items-center gap-2">
209
209
  <Wrench className="size-4 shrink-0 text-sky-300/80" />
@@ -242,7 +242,7 @@ export const RequestTools = memo(function RequestTools({
242
242
  type="button"
243
243
  variant="ghost"
244
244
  size="sm"
245
- className="h-7 border border-white/10 bg-black/20 px-2 text-muted-foreground hover:border-cyan-300/30 hover:bg-cyan-400/10 hover:text-cyan-100"
245
+ className="h-7 px-2 text-muted-foreground hover:text-cyan-100"
246
246
  onClick={() => setExpanded((value) => !value)}
247
247
  aria-expanded={expanded}
248
248
  >
@@ -251,7 +251,7 @@ export const RequestTools = memo(function RequestTools({
251
251
  </Button>
252
252
  </div>
253
253
 
254
- <div className="flex min-w-0 flex-wrap gap-1.5 border-t border-white/10 bg-black/10 px-3 py-2">
254
+ <div className="flex min-w-0 flex-wrap gap-1.5 border-t border-white/10 bg-white/[0.025] px-3 py-2">
255
255
  {previewTools.map((tool) => (
256
256
  <Badge
257
257
  key={tool.name}
@@ -272,8 +272,8 @@ export const RequestTools = memo(function RequestTools({
272
272
  </div>
273
273
 
274
274
  {expanded && (
275
- <div className="space-y-2 border-t border-white/10 bg-black/15 px-3 py-3">
276
- <label className="flex h-8 items-center gap-2 rounded-md border border-white/10 bg-[#06080b] px-2 text-xs">
275
+ <div className="space-y-2 border-t border-white/10 bg-black/10 px-3 py-3">
276
+ <label className="border border-input bg-background hover:bg-accent hover:text-accent-foreground flex h-8 items-center gap-2 rounded-md px-2 text-xs">
277
277
  <Search className="size-3.5 shrink-0 text-muted-foreground" />
278
278
  <input
279
279
  value={query}
@@ -91,9 +91,9 @@ export function ThreadConnector({
91
91
  <span title="Start and end of turn">
92
92
  <FusedCrab
93
93
  className={cn(
94
- "size-3.5 text-[#80FF00]",
94
+ "size-3.5 text-cyan-300/90",
95
95
  "animate-crab-settle",
96
- "drop-shadow-[0_0_4px_rgba(128,255,0,0.5)]",
96
+ "drop-shadow-[0_0_4px_rgba(103,232,249,0.35)]",
97
97
  )}
98
98
  />
99
99
  </span>
@@ -111,9 +111,9 @@ export function ThreadConnector({
111
111
  <span title="Start of turn" {...interactiveProps}>
112
112
  <Crab
113
113
  className={cn(
114
- "size-3.5 text-emerald-400",
114
+ "size-3.5 text-cyan-300/85",
115
115
  "animate-crab-appear",
116
- "drop-shadow-[0_0_4px_rgba(52,211,153,0.5)]",
116
+ "drop-shadow-[0_0_4px_rgba(103,232,249,0.32)]",
117
117
  )}
118
118
  />
119
119
  </span>
@@ -48,6 +48,8 @@ export type TurnGroupProps = {
48
48
  turnIndex?: number;
49
49
  timeDisplayFormat: TimeDisplayFormat;
50
50
  pendingFocusRequest?: PendingTurnFocusRequest;
51
+ /** Show client PID/project metadata on each log row. */
52
+ showLogClientMetadata?: boolean;
51
53
  };
52
54
 
53
55
  export const TurnGroup = memo(function TurnGroup({
@@ -61,6 +63,7 @@ export const TurnGroup = memo(function TurnGroup({
61
63
  turnIndex = 0,
62
64
  timeDisplayFormat,
63
65
  pendingFocusRequest,
66
+ showLogClientMetadata = true,
64
67
  }: TurnGroupProps): JSX.Element {
65
68
  const lastIdx = entries.length - 1;
66
69
  const lastStop = entries[lastIdx]?.stopReason ?? null;
@@ -70,7 +73,7 @@ export const TurnGroup = memo(function TurnGroup({
70
73
  const collapsible = isTurnCollapsible(entries.length, isComplete, isPending);
71
74
  const [collapsed, setCollapsed] = useState(false);
72
75
 
73
- // Auto-collapse when the turn finishes (transitions from incomplete complete)
76
+ // Auto-collapse when the turn finishes (transitions from incomplete to complete)
74
77
  const prevCompleteRef = useRef(false);
75
78
  useEffect(() => {
76
79
  if (!collapsible) {
@@ -146,7 +149,7 @@ export const TurnGroup = memo(function TurnGroup({
146
149
  const StartCrab = useMemo(() => getCrabVariant(entries[0]?.log.id ?? 0), [entries]);
147
150
  const EndCrab = useMemo(() => getCrabVariant(entries[lastIdx]?.log.id ?? 0), [entries, lastIdx]);
148
151
 
149
- const bgClass = turnIndex % 2 === 0 ? "bg-[#070a0f]" : "bg-[#090d12]";
152
+ const bgClass = turnIndex % 2 === 0 ? "bg-white/[0.012]" : "bg-cyan-300/[0.012]";
150
153
  const aggregateIsSlow =
151
154
  aggregate.maxElapsed !== null &&
152
155
  slowResponseThresholdSeconds > 0 &&
@@ -160,7 +163,7 @@ export const TurnGroup = memo(function TurnGroup({
160
163
  return events;
161
164
  }, [entries]);
162
165
 
163
- // ResizeObserver re-render connectors when any LogEntry height changes
166
+ // ResizeObserver re-renders connectors when any LogEntry height changes
164
167
  const [layoutVersion, setLayoutVersion] = useState(0);
165
168
  const containerRef = useRef<HTMLDivElement>(null);
166
169
  useEffect(() => {
@@ -197,7 +200,7 @@ export const TurnGroup = memo(function TurnGroup({
197
200
  aria-label={collapsed ? undefined : turnLabel}
198
201
  data-nav-id={collapsed ? undefined : `turn-${String(firstLogId)}`}
199
202
  className={cn(
200
- "border rounded-lg transition-[border-color,box-shadow]",
203
+ "rounded-[8px] border transition-[border-color,box-shadow]",
201
204
  isPending
202
205
  ? "border-amber-300/15 shadow-[0_0_0_1px_rgba(252,211,77,0.04)]"
203
206
  : "border-transparent",
@@ -234,7 +237,7 @@ export const TurnGroup = memo(function TurnGroup({
234
237
  <span
235
238
  role="button"
236
239
  tabIndex={0}
237
- title="Start of turn click to expand"
240
+ title="Start of turn - click to expand"
238
241
  className="cursor-pointer"
239
242
  onClick={(e) => {
240
243
  e.stopPropagation();
@@ -249,8 +252,8 @@ export const TurnGroup = memo(function TurnGroup({
249
252
  >
250
253
  <StartCrab
251
254
  className={cn(
252
- "size-3.5 text-emerald-400",
253
- "animate-crab-appear drop-shadow-[0_0_4px_rgba(52,211,153,0.5)]",
255
+ "size-3.5 text-cyan-300/85",
256
+ "animate-crab-appear drop-shadow-[0_0_4px_rgba(103,232,249,0.32)]",
254
257
  )}
255
258
  />
256
259
  </span>
@@ -262,7 +265,7 @@ export const TurnGroup = memo(function TurnGroup({
262
265
  <span
263
266
  role="button"
264
267
  tabIndex={0}
265
- title="End of Turn click to expand"
268
+ title="End of Turn - click to expand"
266
269
  className="cursor-pointer"
267
270
  onClick={(e) => {
268
271
  e.stopPropagation();
@@ -284,11 +287,12 @@ export const TurnGroup = memo(function TurnGroup({
284
287
  </span>
285
288
  </div>
286
289
 
287
- {/* Summary content hidden for single-log turns. */}
290
+ {/* Summary content - hidden for single-log turns. */}
288
291
  {entries.length > 1 && (
289
292
  <div
290
293
  className={cn(
291
294
  "flex-1 min-w-0 mb-0.5 rounded-lg border border-cyan-300/10 py-1.5 px-3 flex items-center gap-3 text-xs shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
295
+ "border border-border bg-card shadow-sm",
292
296
  bgClass,
293
297
  )}
294
298
  >
@@ -312,7 +316,7 @@ export const TurnGroup = memo(function TurnGroup({
312
316
  {entries.length} request{entries.length > 1 ? "s" : ""}
313
317
  </span>
314
318
 
315
- {/* Elapsed slowest single request in the turn (not the sum) */}
319
+ {/* Elapsed - slowest single request in the turn (not the sum) */}
316
320
  {aggregate.maxElapsed !== null && (
317
321
  <TooltipProvider>
318
322
  <Tooltip>
@@ -389,7 +393,7 @@ export const TurnGroup = memo(function TurnGroup({
389
393
  collapsible={collapsible && isTurnStart}
390
394
  onToggle={toggleCollapse}
391
395
  />
392
- <div className={cn("flex-1 min-w-0 mb-0.5 rounded-lg", bgClass)}>
396
+ <div className={cn("flex-1 min-w-0 mb-0.5 rounded-[8px]", bgClass)}>
393
397
  <LogEntry
394
398
  log={log}
395
399
  displayNumber={entry.sessionLogNumber}
@@ -398,6 +402,7 @@ export const TurnGroup = memo(function TurnGroup({
398
402
  slowResponseThresholdSeconds={slowResponseThresholdSeconds}
399
403
  timeDisplayFormat={timeDisplayFormat}
400
404
  cacheTrend={cacheTrends?.get(log.id) ?? null}
405
+ showClientMetadata={showLogClientMetadata}
401
406
  onCompareWithPrevious={
402
407
  comparisonPredecessors.has(log.id) ? onCompareWithPrevious : undefined
403
408
  }
@@ -21,6 +21,8 @@ type TurnGroupListProps = {
21
21
  onCompareWithPrevious: (log: CapturedLog) => void;
22
22
  comparisonPredecessors: Map<number, CapturedLog>;
23
23
  timeDisplayFormat: TimeDisplayFormat;
24
+ /** Show client PID/project metadata on each log row. */
25
+ showLogClientMetadata?: boolean;
24
26
  };
25
27
 
26
28
  type PendingVirtualFocusRequest = PendingTurnFocusRequest & {
@@ -73,6 +75,7 @@ function renderTurnGroup(
73
75
  comparisonPredecessors={props.comparisonPredecessors}
74
76
  turnIndex={turnGroup.turnIndex}
75
77
  pendingFocusRequest={pendingFocusRequest}
78
+ showLogClientMetadata={props.showLogClientMetadata}
76
79
  />
77
80
  );
78
81
  }
@@ -69,10 +69,6 @@ function responseMayContainToolTrace(log: CapturedLog, format: ResolvedLogFormat
69
69
  }
70
70
  }
71
71
 
72
- export function shouldRenderConversationContent(standalone: boolean, expanded: boolean): boolean {
73
- return standalone || expanded;
74
- }
75
-
76
72
  export function isTurnCollapsible(
77
73
  entryCount: number,
78
74
  isComplete: boolean,
@@ -0,0 +1,2 @@
1
+ export const INSPECTOR_ICON_TRIGGER_CLASS =
2
+ "relative size-8 rounded-md border border-input bg-background text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring/50";
@@ -0,0 +1,104 @@
1
+ import { AlertTriangle, CheckCircle2, X } from "lucide-react";
2
+ import { useCallback, useEffect, useState, type JSX } from "react";
3
+
4
+ import { cn } from "../../lib/utils";
5
+
6
+ export const TRANSIENT_TOAST_AUTO_DISMISS_MS = 3000;
7
+
8
+ export type TransientToastTone = "error" | "success";
9
+
10
+ export type TransientToastInput = {
11
+ message: string;
12
+ tone?: TransientToastTone;
13
+ };
14
+
15
+ export type TransientToastNotice = {
16
+ id: number;
17
+ message: string;
18
+ tone: TransientToastTone;
19
+ };
20
+
21
+ export function createNextTransientToast(
22
+ previous: TransientToastNotice | null,
23
+ input: TransientToastInput,
24
+ ): TransientToastNotice {
25
+ return {
26
+ id: (previous?.id ?? 0) + 1,
27
+ message: input.message,
28
+ tone: input.tone ?? "error",
29
+ };
30
+ }
31
+
32
+ export function useTransientToast(): {
33
+ notice: TransientToastNotice | null;
34
+ showToast: (input: TransientToastInput) => void;
35
+ dismissToast: () => void;
36
+ } {
37
+ const [notice, setNotice] = useState<TransientToastNotice | null>(null);
38
+
39
+ const showToast = useCallback((input: TransientToastInput) => {
40
+ setNotice((previous) => createNextTransientToast(previous, input));
41
+ }, []);
42
+
43
+ const dismissToast = useCallback(() => {
44
+ setNotice(null);
45
+ }, []);
46
+
47
+ useEffect(() => {
48
+ if (notice === null) return undefined;
49
+ const timeout = window.setTimeout(() => {
50
+ setNotice(null);
51
+ }, TRANSIENT_TOAST_AUTO_DISMISS_MS);
52
+
53
+ return () => {
54
+ window.clearTimeout(timeout);
55
+ };
56
+ }, [notice]);
57
+
58
+ return { notice, showToast, dismissToast };
59
+ }
60
+
61
+ export function TransientToast({
62
+ notice,
63
+ onDismiss,
64
+ }: {
65
+ notice: TransientToastNotice | null;
66
+ onDismiss: () => void;
67
+ }): JSX.Element | null {
68
+ if (notice === null) return null;
69
+
70
+ const isError = notice.tone === "error";
71
+ const Icon = isError ? AlertTriangle : CheckCircle2;
72
+
73
+ return (
74
+ <div
75
+ aria-atomic="true"
76
+ aria-live="polite"
77
+ className="pointer-events-none fixed top-4 right-4 z-[100] w-[min(calc(100vw-2rem),26rem)]"
78
+ role="status"
79
+ >
80
+ <div
81
+ key={notice.id}
82
+ className={cn(
83
+ "pointer-events-auto flex items-start gap-2 rounded-md border px-3 py-2 text-sm shadow-[0_18px_50px_rgba(0,0,0,0.45)] backdrop-blur",
84
+ isError
85
+ ? "border-red-400/30 bg-[#16090b]/95 text-red-100"
86
+ : "border-cyan-300/25 bg-[#071318]/95 text-cyan-100",
87
+ )}
88
+ >
89
+ <Icon
90
+ className={cn("mt-0.5 size-4 shrink-0", isError ? "text-red-300" : "text-cyan-200")}
91
+ />
92
+ <div className="min-w-0 flex-1 break-words leading-5">{notice.message}</div>
93
+ <button
94
+ type="button"
95
+ className="inline-flex size-6 shrink-0 items-center justify-center rounded-md text-current/70 transition-colors hover:bg-white/10 hover:text-current"
96
+ aria-label="Dismiss notification"
97
+ onClick={onDismiss}
98
+ >
99
+ <X className="size-3.5" />
100
+ </button>
101
+ </div>
102
+ </div>
103
+ );
104
+ }
@@ -32,6 +32,9 @@ export const SessionLogSummarySchema = z.object({
32
32
  cacheRead: z.number().nullable(),
33
33
  }),
34
34
  sessionId: z.string().nullable(),
35
+ userAgent: z.string().nullable().optional(),
36
+ clientPid: z.number().nullable().optional(),
37
+ clientProjectFolder: z.string().nullable().optional(),
35
38
  });
36
39
 
37
40
  export const SessionInfoSchema = z.object({
@@ -56,6 +59,7 @@ export const SessionInfoSchema = z.object({
56
59
  tokenUsage: SessionTokenUsageSchema,
57
60
  lastLogId: z.number().nullable(),
58
61
  lastModel: z.string().nullable(),
62
+ clientUserAgent: z.string().nullable().optional(),
59
63
  clientPid: z.number().nullable(),
60
64
  clientProjectFolder: z.string().nullable(),
61
65
  inspectorPath: z.string(),
package/src/mcp/server.ts CHANGED
@@ -185,7 +185,7 @@ Parameters:
185
185
  Returns: array of "thick summary" objects, each containing:
186
186
  - id, timestamp, provider, model, apiFormat, method, path, status, isStreaming,
187
187
  hasError, latencyMs, tokens { input, output, cacheCreate, cacheRead },
188
- sessionId, clientPid,
188
+ sessionId, userAgent, clientPid, clientCwd, clientProjectFolder,
189
189
  - lastUserMessagePreview (string|null, ≤500 chars) — first text block of the last user message, parsed via the format-specific schema (NOT a raw body slice)
190
190
  - responsePreview (string|null, ≤500 chars) — first text block of the assistant response
191
191
  - hasChunks, hasRawRequestBody (booleans)
@@ -195,6 +195,72 @@ Either preview field is null when the body is unknown format, unparseable, or th
195
195
  const PROVIDER_WRITE_WARNING =
196
196
  "⚠ This tool mutates provider configuration. Confirm with the user before invoking.";
197
197
 
198
+ const AddProviderInputSchema = z
199
+ .object({
200
+ name: z.string().min(1).describe("Provider display name."),
201
+ apiKey: z.string().min(1).describe("Plaintext provider API key."),
202
+ format: z
203
+ .enum(["anthropic", "openai"])
204
+ .optional()
205
+ .describe("Primary API family. Use openai for Chat Completions or Responses providers."),
206
+ model: z
207
+ .string()
208
+ .min(1)
209
+ .optional()
210
+ .describe("Legacy single-model input. Prefer models for new clients."),
211
+ models: z.array(z.string().min(1)).min(1).optional().describe("Provider model names."),
212
+ anthropicBaseUrl: z
213
+ .string()
214
+ .optional()
215
+ .describe("Anthropic-compatible base or exact /v1/messages endpoint."),
216
+ openaiBaseUrl: z
217
+ .string()
218
+ .optional()
219
+ .describe("OpenAI Chat Completions base or exact /v1/chat/completions endpoint."),
220
+ openaiResponsesBaseUrl: z
221
+ .string()
222
+ .optional()
223
+ .describe(
224
+ "OpenAI Responses base or exact /v1/responses endpoint. MiniMax default endpoint is https://api.minimaxi.com/v1/responses.",
225
+ ),
226
+ authHeader: z.enum(["bearer", "x-api-key"]).optional(),
227
+ apiDocsUrl: z.string().optional(),
228
+ })
229
+ .refine(
230
+ (input) => input.model !== undefined || (input.models !== undefined && input.models.length > 0),
231
+ { message: "Provide models or legacy model." },
232
+ );
233
+
234
+ const UpdateProviderInputSchema = z.object({
235
+ id: z.string().describe("The provider id to update."),
236
+ name: z.string().min(1).optional(),
237
+ apiKey: z.string().min(1).optional(),
238
+ format: z.enum(["anthropic", "openai"]).optional(),
239
+ model: z.string().min(1).optional().describe("Legacy single-model input. Prefer models."),
240
+ models: z.array(z.string().min(1)).optional().describe("Replace provider model names."),
241
+ baseUrl: z
242
+ .string()
243
+ .min(1)
244
+ .optional()
245
+ .describe("Legacy route URL retained for old configs; prefer format-specific URL fields."),
246
+ authHeader: z.enum(["bearer", "x-api-key"]).optional(),
247
+ anthropicBaseUrl: z
248
+ .string()
249
+ .optional()
250
+ .describe("Anthropic-compatible base or exact /v1/messages endpoint."),
251
+ openaiBaseUrl: z
252
+ .string()
253
+ .optional()
254
+ .describe("OpenAI Chat Completions base or exact /v1/chat/completions endpoint."),
255
+ openaiResponsesBaseUrl: z
256
+ .string()
257
+ .optional()
258
+ .describe(
259
+ "OpenAI Responses base or exact /v1/responses endpoint. MiniMax default endpoint is https://api.minimaxi.com/v1/responses.",
260
+ ),
261
+ apiDocsUrl: z.string().optional(),
262
+ });
263
+
198
264
  const LogResourceListResponseSchema = z.object({
199
265
  logs: z.array(z.object({ id: z.number().int().positive() })),
200
266
  });
@@ -1049,18 +1115,8 @@ function registerTools(server: McpServer): void {
1049
1115
  "inspector_add_provider",
1050
1116
  {
1051
1117
  title: "Add a new LLM provider",
1052
- description: `${PROVIDER_WRITE_WARNING}\n\nPersists a new provider to <dataDir>/providers.json. Required fields: name, apiKey, format (anthropic|openai), model. Optional: baseUrl, authHeader (default: bearer), apiDocsUrl.`,
1053
- inputSchema: z.object({
1054
- name: z.string().min(1),
1055
- apiKey: z.string().min(1),
1056
- format: z.enum(["anthropic", "openai"]),
1057
- model: z.string().min(1),
1058
- anthropicBaseUrl: z.string().optional(),
1059
- openaiBaseUrl: z.string().optional(),
1060
- openaiResponsesBaseUrl: z.string().optional(),
1061
- authHeader: z.enum(["bearer", "x-api-key"]).optional(),
1062
- apiDocsUrl: z.string().optional(),
1063
- }),
1118
+ description: `${PROVIDER_WRITE_WARNING}\n\nPersists a new provider to <dataDir>/providers.json. Required fields: name, apiKey, and models (or legacy model). Chat Completions and Responses can use different upstream URLs: set openaiBaseUrl for /v1/chat/completions and openaiResponsesBaseUrl for /v1/responses. The URL builder accepts either a base URL such as https://api.minimaxi.com or an exact endpoint such as https://api.minimaxi.com/v1/responses.`,
1119
+ inputSchema: AddProviderInputSchema,
1064
1120
  },
1065
1121
  (provider) => safeWriteCall("inspector_add_provider", () => addProviderImpl(callApi, provider)),
1066
1122
  );
@@ -1071,20 +1127,8 @@ function registerTools(server: McpServer): void {
1071
1127
  title: "Update an existing provider",
1072
1128
  description: `${PROVIDER_WRITE_WARNING} Updating with nonsense values effectively soft-deletes a provider. Confirm with the user before invoking.
1073
1129
 
1074
- PATCH-style update: only the fields you supply are changed. Returns the updated ProviderConfig.`,
1075
- inputSchema: z.object({
1076
- id: z.string().describe("The provider id to update."),
1077
- name: z.string().min(1).optional(),
1078
- apiKey: z.string().min(1).optional(),
1079
- format: z.enum(["anthropic", "openai"]).optional(),
1080
- model: z.string().min(1).optional(),
1081
- baseUrl: z.string().min(1).optional(),
1082
- authHeader: z.enum(["bearer", "x-api-key"]).optional(),
1083
- anthropicBaseUrl: z.string().optional(),
1084
- openaiBaseUrl: z.string().optional(),
1085
- openaiResponsesBaseUrl: z.string().optional(),
1086
- apiDocsUrl: z.string().optional(),
1087
- }),
1130
+ PATCH-style update: only the fields you supply are changed. Use models to update the configured model list. For OpenAI-compatible providers, openaiBaseUrl targets /v1/chat/completions and openaiResponsesBaseUrl targets /v1/responses, and those endpoints may be different.`,
1131
+ inputSchema: UpdateProviderInputSchema,
1088
1132
  },
1089
1133
  (input) => safeWriteCall("inspector_update_provider", () => updateProviderImpl(callApi, input)),
1090
1134
  );
@@ -175,7 +175,10 @@ export function buildLogSummary(log: CapturedLog) {
175
175
  cacheRead: log.cacheReadInputTokens,
176
176
  },
177
177
  sessionId: log.sessionId,
178
+ userAgent: log.userAgent ?? null,
178
179
  clientPid: log.clientPid ?? null,
180
+ clientCwd: log.clientCwd ?? null,
181
+ clientProjectFolder: log.clientProjectFolder ?? null,
179
182
  lastUserMessagePreview: extractLastUserMessagePreview(log),
180
183
  responsePreview: extractResponsePreview(log),
181
184
  hasChunks:
@@ -715,8 +718,9 @@ export async function replayLogImpl(callApi: CallApiFn, args: ReplayLogArgs): Pr
715
718
  export type AddProviderInput = {
716
719
  name: string;
717
720
  apiKey: string;
718
- format: "anthropic" | "openai";
719
- model: string;
721
+ format?: "anthropic" | "openai";
722
+ model?: string;
723
+ models?: string[];
720
724
  anthropicBaseUrl?: string;
721
725
  openaiBaseUrl?: string;
722
726
  openaiResponsesBaseUrl?: string;
@@ -724,14 +728,23 @@ export type AddProviderInput = {
724
728
  apiDocsUrl?: string;
725
729
  };
726
730
 
731
+ function normalizedProviderModels(input: { model?: string; models?: readonly string[] }): string[] {
732
+ const modelList = input.models ?? [];
733
+ const models = modelList.map((model) => model.trim()).filter((model) => model !== "");
734
+ if (models.length > 0) return models;
735
+ const model = input.model?.trim();
736
+ return model !== undefined && model !== "" ? [model] : [];
737
+ }
738
+
727
739
  export async function addProviderImpl(
728
740
  callApi: CallApiFn,
729
741
  provider: AddProviderInput,
730
742
  ): Promise<ToolResult> {
743
+ const models = normalizedProviderModels(provider);
731
744
  const res = await callApi("/api/providers", {
732
745
  method: "POST",
733
746
  headers: { "content-type": "application/json" },
734
- body: JSON.stringify(provider),
747
+ body: JSON.stringify({ ...provider, models }),
735
748
  });
736
749
  if (!res.ok) return toolError(`POST /api/providers returned ${res.status}`);
737
750
  return textJson(await res.json());
@@ -743,6 +756,7 @@ export type UpdateProviderInput = {
743
756
  apiKey?: string;
744
757
  format?: "anthropic" | "openai";
745
758
  model?: string;
759
+ models?: string[];
746
760
  baseUrl?: string;
747
761
  authHeader?: "bearer" | "x-api-key";
748
762
  anthropicBaseUrl?: string;
@@ -756,10 +770,15 @@ export async function updateProviderImpl(
756
770
  input: UpdateProviderInput,
757
771
  ): Promise<ToolResult> {
758
772
  const { id, ...patch } = input;
773
+ const models =
774
+ patch.models !== undefined || patch.model !== undefined
775
+ ? normalizedProviderModels(patch)
776
+ : undefined;
777
+ const body = models === undefined ? patch : { ...patch, models };
759
778
  const res = await callApi(`/api/providers/${encodeURIComponent(id)}`, {
760
779
  method: "PUT",
761
780
  headers: { "content-type": "application/json" },
762
- body: JSON.stringify(patch),
781
+ body: JSON.stringify(body),
763
782
  });
764
783
  if (!res.ok) return toolError(`PUT /api/providers/${id} returned ${res.status}`);
765
784
  return textJson(await res.json());
@@ -14,10 +14,6 @@ export const alibabaProvider: ProviderAdapter = {
14
14
  matches(model: string): boolean {
15
15
  // Normalize: lowercase, replace whitespace with hyphens
16
16
  const m = model.toLowerCase().replace(/\s+/g, "-");
17
- // Match glm-5* and glm-5.1* models
18
- if (m.startsWith("glm-5")) {
19
- return true;
20
- }
21
17
  // Match qwen models (qwen3.6-plus, qwen3.7-max, Qwen3.6 Plus, etc.)
22
18
  if (m.startsWith("qwen")) {
23
19
  return true;
@@ -65,6 +65,9 @@ export function buildSessionLogSummary(log: CapturedLog): SessionLogSummary {
65
65
  cacheRead: log.cacheReadInputTokens,
66
66
  },
67
67
  sessionId: normalizeString(log.sessionId),
68
+ userAgent: normalizeString(log.userAgent),
69
+ clientPid: log.clientPid ?? null,
70
+ clientProjectFolder: normalizeString(log.clientProjectFolder),
68
71
  };
69
72
  }
70
73
 
@@ -118,6 +121,30 @@ function collectSortedValues(values: readonly (string | null)[]): string[] {
118
121
  return [...set].sort();
119
122
  }
120
123
 
124
+ function firstSummaryUserAgent(summaries: readonly SessionLogSummary[]): string | null {
125
+ for (const summary of summaries) {
126
+ const value = normalizeString(summary.userAgent);
127
+ if (value !== null) return value;
128
+ }
129
+ return null;
130
+ }
131
+
132
+ function firstSummaryClientPid(summaries: readonly SessionLogSummary[]): number | null {
133
+ for (const summary of summaries) {
134
+ const value = summary.clientPid;
135
+ if (value !== null && value !== undefined) return value;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ function firstSummaryProjectFolder(summaries: readonly SessionLogSummary[]): string | null {
141
+ for (const summary of summaries) {
142
+ const value = normalizeString(summary.clientProjectFolder);
143
+ if (value !== null) return value;
144
+ }
145
+ return null;
146
+ }
147
+
121
148
  function countCompletedLogs(summaries: readonly SessionLogSummary[]): number {
122
149
  let count = 0;
123
150
  for (const summary of summaries) {
@@ -215,8 +242,10 @@ export function buildSessionInfo(input: BuildSessionInfoInput): SessionInfo {
215
242
  tokenUsage,
216
243
  lastLogId: input.snapshot?.lastLogId ?? latestLog?.id ?? null,
217
244
  lastModel: normalizeString(input.snapshot?.lastModel) ?? latestLog?.model ?? null,
218
- clientPid: input.snapshot?.clientPid ?? null,
219
- clientProjectFolder: normalizeString(input.snapshot?.clientProjectFolder),
245
+ clientUserAgent: firstSummaryUserAgent(summaries),
246
+ clientPid: input.snapshot?.clientPid ?? firstSummaryClientPid(summaries),
247
+ clientProjectFolder:
248
+ normalizeString(input.snapshot?.clientProjectFolder) ?? firstSummaryProjectFolder(summaries),
220
249
  inspectorPath,
221
250
  inspectorUrl: buildAbsoluteUrl(input.baseUrl, inspectorPath),
222
251
  apiPath,