@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BcEd6V-V.js → CompareDrawer-Ccrrcx1j.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +106 -0
- package/.output/public/assets/{ReplayDialog-BNpC0548.js → ReplayDialog-C7axhr-l.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-Ds1uRLVB.js → RequestAnatomy-WOptg8j_.js} +1 -1
- package/.output/public/assets/{ResponseView-7KPVqKl5.js → ResponseView-CSAcxh_M.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-BHQT261s.js → StreamingChunkSequence-DJS5KhPx.js} +1 -1
- package/.output/public/assets/_sessionId-DUSalzKH.js +1 -0
- package/.output/public/assets/index-BPpA21dY.css +1 -0
- package/.output/public/assets/index-Bt0Az2I2.js +1 -0
- package/.output/public/assets/{index-DjKt8XKe.js → index-DMPNh46t.js} +1 -1
- package/.output/public/assets/{json-viewer-CkCu-rka.js → json-viewer-inX5QSa3.js} +1 -1
- package/.output/public/assets/{main-DpD1N0S8.js → main-aCAKYGSD.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +207 -171
- package/.output/server/{_sessionId-DF9Sy8cP.mjs → _sessionId-C0fhvQnn.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-BoxztaO7.mjs → CompareDrawer-CO-Ti5sg.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-CRBkqFlJ.mjs → ProxyViewerContainer-N4J7uBCX.mjs} +807 -386
- package/.output/server/_ssr/{ReplayDialog-Cc1dyDuK.mjs → ReplayDialog-J1VxC0In.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-CMGSsz5Z.mjs → RequestAnatomy-DBH-F3rq.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-Cp10DM1D.mjs → ResponseView-DLFwV7Hd.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-B1VGxy3A.mjs → StreamingChunkSequence-BywmuND0.mjs} +3 -3
- package/.output/server/_ssr/{index-47XVPghS.mjs → index-CStymIc0.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-zDE2rrmJ.mjs → json-viewer-DlQu0N1x.mjs} +3 -3
- package/.output/server/_ssr/{router-DVeuZFqI.mjs → router-CQ4Zxhmr.mjs} +112 -46
- package/.output/server/{_tanstack-start-manifest_v-Bp8JxtPW.mjs → _tanstack-start-manifest_v-BHeJBGfo.mjs} +1 -1
- package/.output/server/index.mjs +72 -72
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +146 -105
- package/src/components/ProxyViewerContainer.tsx +77 -67
- package/src/components/alerts/AlertsDialog.tsx +2 -1
- package/src/components/clients/ClientLogo.tsx +1 -1
- package/src/components/groups/GroupsDialog.tsx +19 -1
- package/src/components/providers/ProviderCard.tsx +1 -1
- package/src/components/providers/ProviderForm.tsx +18 -18
- package/src/components/providers/ProviderLogo.tsx +6 -1
- package/src/components/providers/ProvidersPanel.tsx +6 -6
- package/src/components/providers/SettingsDialog.tsx +13 -6
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +6 -6
- package/src/components/proxy-viewer/ConversationGroup.tsx +43 -32
- package/src/components/proxy-viewer/ConversationGroupList.tsx +452 -89
- package/src/components/proxy-viewer/ConversationHeader.tsx +44 -32
- package/src/components/proxy-viewer/LogEntry.tsx +21 -17
- package/src/components/proxy-viewer/LogEntryHeader.tsx +38 -45
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +2 -2
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
- package/src/components/proxy-viewer/ThreadConnector.tsx +4 -4
- package/src/components/proxy-viewer/TurnGroup.tsx +16 -11
- package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
- package/src/components/proxy-viewer/viewerState.ts +0 -4
- package/src/components/ui/icon-trigger.ts +2 -0
- package/src/components/ui/transient-toast.tsx +104 -0
- package/src/lib/sessionInfoContract.ts +4 -0
- package/src/mcp/server.ts +71 -27
- package/src/mcp/toolHandlers.ts +23 -4
- package/src/proxy/formats/openai/alibabaProvider.ts +0 -4
- package/src/proxy/sessionInfo.ts +31 -2
- package/src/proxy/sessionSupervisor.ts +20 -1
- package/src/proxy/store.ts +1 -1
- package/src/routes/api/providers.$providerId.test.log.ts +8 -4
- package/styles/globals.css +180 -180
- package/.output/public/assets/ProxyViewerContainer-h851qWNp.js +0 -106
- package/.output/public/assets/_sessionId-DWePGjnS.js +0 -1
- package/.output/public/assets/index-CI1-G8ua.js +0 -1
- 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="
|
|
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:
|
|
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.
|
|
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-
|
|
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="
|
|
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
|
|
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-
|
|
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/
|
|
276
|
-
<label className="flex h-8 items-center gap-2 rounded-md
|
|
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-
|
|
94
|
+
"size-3.5 text-cyan-300/90",
|
|
95
95
|
"animate-crab-settle",
|
|
96
|
-
"drop-shadow-[0_0_4px_rgba(
|
|
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-
|
|
114
|
+
"size-3.5 text-cyan-300/85",
|
|
115
115
|
"animate-crab-appear",
|
|
116
|
-
"drop-shadow-[0_0_4px_rgba(
|
|
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
|
|
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-[
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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-
|
|
253
|
-
"animate-crab-appear drop-shadow-[0_0_4px_rgba(
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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,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,
|
|
1053
|
-
inputSchema:
|
|
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.
|
|
1075
|
-
inputSchema:
|
|
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
|
);
|
package/src/mcp/toolHandlers.ts
CHANGED
|
@@ -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
|
|
719
|
-
model
|
|
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(
|
|
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;
|
package/src/proxy/sessionInfo.ts
CHANGED
|
@@ -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
|
-
|
|
219
|
-
|
|
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,
|