@tonyclaw/agent-inspector 2.1.14 → 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-Ccrrcx1j.js +1 -0
- package/.output/public/assets/ProxyViewerContainer-DLqClc9A.js +106 -0
- package/.output/public/assets/ReplayDialog-C7axhr-l.js +1 -0
- package/.output/public/assets/{RequestAnatomy-BNahe83D.js → RequestAnatomy-WOptg8j_.js} +1 -1
- package/.output/public/assets/{ResponseView-DSOnGqi6.js → ResponseView-CSAcxh_M.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-BEKTDklB.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-DWOkqdCa.js → index-DMPNh46t.js} +1 -1
- package/.output/public/assets/{json-viewer-C2JpgcW0.js → json-viewer-inX5QSa3.js} +1 -1
- package/.output/public/assets/{main-CSONBwwn.js → main-aCAKYGSD.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +224 -194
- package/.output/server/{_sessionId-CPAa37n5.mjs → _sessionId-C0fhvQnn.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-ceW5VxMo.mjs → CompareDrawer-CO-Ti5sg.mjs} +36 -15
- package/.output/server/_ssr/{ProxyViewerContainer-CDfEE_w-.mjs → ProxyViewerContainer-N4J7uBCX.mjs} +1153 -494
- package/.output/server/_ssr/{ReplayDialog-V0s_eEbR.mjs → ReplayDialog-J1VxC0In.mjs} +14 -7
- package/.output/server/_ssr/{RequestAnatomy-f1ccwR9d.mjs → RequestAnatomy-DBH-F3rq.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-BIRrqG4H.mjs → ResponseView-DLFwV7Hd.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-V3JFjCgX.mjs → StreamingChunkSequence-BywmuND0.mjs} +3 -3
- package/.output/server/_ssr/{index-DsykulzS.mjs → index-CStymIc0.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-Dcnm0Ivf.mjs → json-viewer-DlQu0N1x.mjs} +3 -3
- package/.output/server/_ssr/{router-4bdm6Mt2.mjs → router-CQ4Zxhmr.mjs} +118 -47
- package/.output/server/{_tanstack-start-manifest_v-Xp4CO64V.mjs → _tanstack-start-manifest_v-BHeJBGfo.mjs} +1 -1
- package/.output/server/index.mjs +75 -75
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +174 -107
- package/src/components/ProxyViewerContainer.tsx +77 -67
- package/src/components/alerts/AlertsDialog.tsx +2 -1
- package/src/components/clients/ClientLogo.tsx +132 -0
- package/src/components/groups/GroupsDialog.tsx +37 -14
- 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 +62 -29
- package/src/components/proxy-viewer/CompareDrawer.tsx +38 -8
- 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 +65 -61
- package/src/components/proxy-viewer/LogEntry.tsx +31 -18
- package/src/components/proxy-viewer/LogEntryHeader.tsx +42 -49
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +60 -0
- package/src/components/proxy-viewer/ReplayDialog.tsx +8 -2
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +9 -9
- package/src/components/proxy-viewer/ThreadConnector.tsx +22 -11
- package/src/components/proxy-viewer/TurnGroup.tsx +30 -36
- package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
- package/src/components/proxy-viewer/viewerState.ts +5 -6
- package/src/components/ui/icon-trigger.ts +2 -0
- package/src/components/ui/transient-toast.tsx +104 -0
- package/src/lib/sessionInfoContract.ts +5 -0
- package/src/lib/stopReason.ts +48 -16
- 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 +36 -3
- 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/CompareDrawer-DjgjIFx7.js +0 -1
- package/.output/public/assets/ProxyViewerContainer-CWUQZLYy.js +0 -106
- package/.output/public/assets/ReplayDialog-CU0Tbb2c.js +0 -1
- package/.output/public/assets/_sessionId-Cif8JZdn.js +0 -1
- package/.output/public/assets/index-D_WfwzUi.js +0 -1
- package/.output/public/assets/index-DtLuQrs0.css +0 -1
|
@@ -14,8 +14,8 @@ import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
|
14
14
|
import { cn, formatTokens } from "../../lib/utils";
|
|
15
15
|
import type { CapturedLog } from "../../contracts";
|
|
16
16
|
import { getCrabVariant } from "../ui/crab-variants";
|
|
17
|
-
import { ProviderLogo, detectProvider, type Provider } from "../providers/ProviderLogo";
|
|
18
17
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
|
|
18
|
+
import { ROW_CHEVRON_SLOT_CLASS } from "./ProviderLogoStack";
|
|
19
19
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
20
20
|
import { LogEntry } from "./LogEntry";
|
|
21
21
|
import {
|
|
@@ -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) {
|
|
@@ -142,21 +145,11 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
142
145
|
};
|
|
143
146
|
}, [entries, lastIdx]);
|
|
144
147
|
|
|
145
|
-
// Unique providers across all entries (for collapsed model logos)
|
|
146
|
-
const uniqueProviders = useMemo(() => {
|
|
147
|
-
const seen = new Set<Provider>();
|
|
148
|
-
for (const e of entries) {
|
|
149
|
-
const p = detectProvider(e.log.model);
|
|
150
|
-
if (p !== "unknown") seen.add(p);
|
|
151
|
-
}
|
|
152
|
-
return [...seen];
|
|
153
|
-
}, [entries]);
|
|
154
|
-
|
|
155
148
|
// Crab variant creators for the dual-crab collapsed layout
|
|
156
149
|
const StartCrab = useMemo(() => getCrabVariant(entries[0]?.log.id ?? 0), [entries]);
|
|
157
150
|
const EndCrab = useMemo(() => getCrabVariant(entries[lastIdx]?.log.id ?? 0), [entries, lastIdx]);
|
|
158
151
|
|
|
159
|
-
const bgClass = turnIndex % 2 === 0 ? "bg-[
|
|
152
|
+
const bgClass = turnIndex % 2 === 0 ? "bg-white/[0.012]" : "bg-cyan-300/[0.012]";
|
|
160
153
|
const aggregateIsSlow =
|
|
161
154
|
aggregate.maxElapsed !== null &&
|
|
162
155
|
slowResponseThresholdSeconds > 0 &&
|
|
@@ -170,7 +163,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
170
163
|
return events;
|
|
171
164
|
}, [entries]);
|
|
172
165
|
|
|
173
|
-
// ResizeObserver
|
|
166
|
+
// ResizeObserver re-renders connectors when any LogEntry height changes
|
|
174
167
|
const [layoutVersion, setLayoutVersion] = useState(0);
|
|
175
168
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
176
169
|
useEffect(() => {
|
|
@@ -207,7 +200,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
207
200
|
aria-label={collapsed ? undefined : turnLabel}
|
|
208
201
|
data-nav-id={collapsed ? undefined : `turn-${String(firstLogId)}`}
|
|
209
202
|
className={cn(
|
|
210
|
-
"
|
|
203
|
+
"rounded-[8px] border transition-[border-color,box-shadow]",
|
|
211
204
|
isPending
|
|
212
205
|
? "border-amber-300/15 shadow-[0_0_0_1px_rgba(252,211,77,0.04)]"
|
|
213
206
|
: "border-transparent",
|
|
@@ -244,7 +237,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
244
237
|
<span
|
|
245
238
|
role="button"
|
|
246
239
|
tabIndex={0}
|
|
247
|
-
title="Start of turn
|
|
240
|
+
title="Start of turn - click to expand"
|
|
248
241
|
className="cursor-pointer"
|
|
249
242
|
onClick={(e) => {
|
|
250
243
|
e.stopPropagation();
|
|
@@ -259,8 +252,8 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
259
252
|
>
|
|
260
253
|
<StartCrab
|
|
261
254
|
className={cn(
|
|
262
|
-
"size-3.5 text-
|
|
263
|
-
"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)]",
|
|
264
257
|
)}
|
|
265
258
|
/>
|
|
266
259
|
</span>
|
|
@@ -272,7 +265,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
272
265
|
<span
|
|
273
266
|
role="button"
|
|
274
267
|
tabIndex={0}
|
|
275
|
-
title="End of Turn
|
|
268
|
+
title="End of Turn - click to expand"
|
|
276
269
|
className="cursor-pointer"
|
|
277
270
|
onClick={(e) => {
|
|
278
271
|
e.stopPropagation();
|
|
@@ -294,17 +287,28 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
294
287
|
</span>
|
|
295
288
|
</div>
|
|
296
289
|
|
|
297
|
-
{/* Summary content
|
|
290
|
+
{/* Summary content - hidden for single-log turns. */}
|
|
298
291
|
{entries.length > 1 && (
|
|
299
292
|
<div
|
|
300
293
|
className={cn(
|
|
301
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",
|
|
302
296
|
bgClass,
|
|
303
297
|
)}
|
|
304
298
|
>
|
|
299
|
+
<span className={ROW_CHEVRON_SLOT_CLASS}>
|
|
300
|
+
<ChevronRight className="size-4 shrink-0" />
|
|
301
|
+
</span>
|
|
302
|
+
|
|
305
303
|
{/* ID range */}
|
|
306
|
-
<span
|
|
307
|
-
|
|
304
|
+
<span
|
|
305
|
+
className="text-blue-400/80 font-mono font-semibold tabular-nums shrink-0"
|
|
306
|
+
title={`Log IDs ${String(entries[0]?.log.id ?? "?")} ~ ${String(
|
|
307
|
+
entries[lastIdx]?.log.id ?? "?",
|
|
308
|
+
)}`}
|
|
309
|
+
>
|
|
310
|
+
#{entries[0]?.sessionLogNumber ?? "?"} ~ #
|
|
311
|
+
{entries[lastIdx]?.sessionLogNumber ?? "?"}
|
|
308
312
|
</span>
|
|
309
313
|
|
|
310
314
|
{/* Request count */}
|
|
@@ -312,16 +316,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
312
316
|
{entries.length} request{entries.length > 1 ? "s" : ""}
|
|
313
317
|
</span>
|
|
314
318
|
|
|
315
|
-
{/*
|
|
316
|
-
{uniqueProviders.length > 0 && (
|
|
317
|
-
<span className="flex items-center gap-0.5 shrink-0">
|
|
318
|
-
{uniqueProviders.map((p) => (
|
|
319
|
-
<ProviderLogo key={p} provider={p} className="size-4" />
|
|
320
|
-
))}
|
|
321
|
-
</span>
|
|
322
|
-
)}
|
|
323
|
-
|
|
324
|
-
{/* Elapsed — slowest single request in the turn (not the sum) */}
|
|
319
|
+
{/* Elapsed - slowest single request in the turn (not the sum) */}
|
|
325
320
|
{aggregate.maxElapsed !== null && (
|
|
326
321
|
<TooltipProvider>
|
|
327
322
|
<Tooltip>
|
|
@@ -368,9 +363,6 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
368
363
|
|
|
369
364
|
{/* Spacer */}
|
|
370
365
|
<span className="flex-1 min-w-0" />
|
|
371
|
-
|
|
372
|
-
{/* Expand chevron */}
|
|
373
|
-
<ChevronRight className="size-4 text-muted-foreground shrink-0" />
|
|
374
366
|
</div>
|
|
375
367
|
)}
|
|
376
368
|
</div>
|
|
@@ -401,14 +393,16 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
401
393
|
collapsible={collapsible && isTurnStart}
|
|
402
394
|
onToggle={toggleCollapse}
|
|
403
395
|
/>
|
|
404
|
-
<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)}>
|
|
405
397
|
<LogEntry
|
|
406
398
|
log={log}
|
|
399
|
+
displayNumber={entry.sessionLogNumber}
|
|
407
400
|
viewMode={viewMode}
|
|
408
401
|
strip={strip}
|
|
409
402
|
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
410
403
|
timeDisplayFormat={timeDisplayFormat}
|
|
411
404
|
cacheTrend={cacheTrends?.get(log.id) ?? null}
|
|
405
|
+
showClientMetadata={showLogClientMetadata}
|
|
412
406
|
onCompareWithPrevious={
|
|
413
407
|
comparisonPredecessors.has(log.id) ? onCompareWithPrevious : undefined
|
|
414
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
|
}
|
|
@@ -6,6 +6,7 @@ import { resolveLogFormat } from "./log-formats";
|
|
|
6
6
|
export type TurnEntry = {
|
|
7
7
|
log: CapturedLog;
|
|
8
8
|
stopReason: StopReason;
|
|
9
|
+
sessionLogNumber: number;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
export type TurnGroupData = {
|
|
@@ -68,10 +69,6 @@ function responseMayContainToolTrace(log: CapturedLog, format: ResolvedLogFormat
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
export function shouldRenderConversationContent(standalone: boolean, expanded: boolean): boolean {
|
|
72
|
-
return standalone || expanded;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
72
|
export function isTurnCollapsible(
|
|
76
73
|
entryCount: number,
|
|
77
74
|
isComplete: boolean,
|
|
@@ -85,8 +82,10 @@ export function buildTurnGroups(logs: CapturedLog[]): TurnGroupData[] {
|
|
|
85
82
|
let entries: TurnEntry[] = [];
|
|
86
83
|
let turnIndex = 0;
|
|
87
84
|
|
|
88
|
-
for (
|
|
89
|
-
|
|
85
|
+
for (let index = 0; index < logs.length; index += 1) {
|
|
86
|
+
const log = logs[index];
|
|
87
|
+
if (log === undefined) continue;
|
|
88
|
+
entries.push({ log, stopReason: extractStopReason(log), sessionLogNumber: index + 1 });
|
|
90
89
|
const current = entries[entries.length - 1];
|
|
91
90
|
if (current !== undefined && isTurnBoundary(current.stopReason)) {
|
|
92
91
|
groups.push({ entries, turnIndex });
|
|
@@ -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
|
+
}
|
|
@@ -10,6 +10,7 @@ export const SessionTokenUsageSchema = z.object({
|
|
|
10
10
|
|
|
11
11
|
export const SessionLogSummarySchema = z.object({
|
|
12
12
|
id: z.number().int().positive(),
|
|
13
|
+
sessionLogNumber: z.number().int().positive().optional(),
|
|
13
14
|
timestamp: z.string(),
|
|
14
15
|
provider: z.string().nullable(),
|
|
15
16
|
model: z.string().nullable(),
|
|
@@ -31,6 +32,9 @@ export const SessionLogSummarySchema = z.object({
|
|
|
31
32
|
cacheRead: z.number().nullable(),
|
|
32
33
|
}),
|
|
33
34
|
sessionId: z.string().nullable(),
|
|
35
|
+
userAgent: z.string().nullable().optional(),
|
|
36
|
+
clientPid: z.number().nullable().optional(),
|
|
37
|
+
clientProjectFolder: z.string().nullable().optional(),
|
|
34
38
|
});
|
|
35
39
|
|
|
36
40
|
export const SessionInfoSchema = z.object({
|
|
@@ -55,6 +59,7 @@ export const SessionInfoSchema = z.object({
|
|
|
55
59
|
tokenUsage: SessionTokenUsageSchema,
|
|
56
60
|
lastLogId: z.number().nullable(),
|
|
57
61
|
lastModel: z.string().nullable(),
|
|
62
|
+
clientUserAgent: z.string().nullable().optional(),
|
|
58
63
|
clientPid: z.number().nullable(),
|
|
59
64
|
clientProjectFolder: z.string().nullable(),
|
|
60
65
|
inspectorPath: z.string(),
|
package/src/lib/stopReason.ts
CHANGED
|
@@ -1,11 +1,41 @@
|
|
|
1
1
|
import type { CapturedLog } from "../contracts";
|
|
2
2
|
|
|
3
|
-
export type StopReason = "end_turn" | "tool_use" | "stop" | null;
|
|
3
|
+
export type StopReason = "end_turn" | "tool_use" | "stop" | "length" | null;
|
|
4
4
|
|
|
5
5
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
6
6
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
function normalizeAnthropicStopReason(value: string): StopReason {
|
|
10
|
+
switch (value) {
|
|
11
|
+
case "end_turn":
|
|
12
|
+
case "tool_use":
|
|
13
|
+
return value;
|
|
14
|
+
default:
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeOpenAIFinishReason(value: string): StopReason {
|
|
20
|
+
switch (value) {
|
|
21
|
+
case "stop":
|
|
22
|
+
case "length":
|
|
23
|
+
return value;
|
|
24
|
+
default:
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isResponsesLengthLimitReason(value: string): boolean {
|
|
30
|
+
switch (value) {
|
|
31
|
+
case "max_output_tokens":
|
|
32
|
+
case "max_tokens":
|
|
33
|
+
return true;
|
|
34
|
+
default:
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
9
39
|
/**
|
|
10
40
|
* Extracts the stop/finish reason from a captured log's response text.
|
|
11
41
|
* Detects the response body shape (Anthropic or OpenAI) independently of
|
|
@@ -26,21 +56,16 @@ export function extractStopReason(log: CapturedLog): StopReason {
|
|
|
26
56
|
|
|
27
57
|
// Anthropic shape: { stop_reason: "end_turn" | "tool_use" | ... }
|
|
28
58
|
if (typeof json.stop_reason === "string") {
|
|
29
|
-
|
|
30
|
-
return json.stop_reason;
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
59
|
+
return normalizeAnthropicStopReason(json.stop_reason);
|
|
33
60
|
}
|
|
34
61
|
|
|
35
|
-
// OpenAI shape: { choices: [{ finish_reason: "stop" | ... }] }
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
) {
|
|
43
|
-
return "stop";
|
|
62
|
+
// OpenAI Chat shape: { choices: [{ finish_reason: "stop" | "length" | ... }] }
|
|
63
|
+
if (Array.isArray(json.choices)) {
|
|
64
|
+
for (const choice of json.choices) {
|
|
65
|
+
if (!isRecord(choice) || typeof choice.finish_reason !== "string") continue;
|
|
66
|
+
const reason = normalizeOpenAIFinishReason(choice.finish_reason);
|
|
67
|
+
if (reason !== null) return reason;
|
|
68
|
+
}
|
|
44
69
|
}
|
|
45
70
|
|
|
46
71
|
// OpenAI Responses shape: { status: "completed", output: [...] }
|
|
@@ -55,6 +80,13 @@ export function extractStopReason(log: CapturedLog): StopReason {
|
|
|
55
80
|
}
|
|
56
81
|
}
|
|
57
82
|
|
|
83
|
+
if (json.status === "incomplete" && isRecord(json.incomplete_details)) {
|
|
84
|
+
const reason = json.incomplete_details.reason;
|
|
85
|
+
if (typeof reason === "string" && isResponsesLengthLimitReason(reason)) {
|
|
86
|
+
return "length";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
58
90
|
return null;
|
|
59
91
|
} catch {
|
|
60
92
|
return null;
|
|
@@ -63,8 +95,8 @@ export function extractStopReason(log: CapturedLog): StopReason {
|
|
|
63
95
|
|
|
64
96
|
/**
|
|
65
97
|
* Returns true when the stop reason indicates the assistant completed its
|
|
66
|
-
* turn
|
|
98
|
+
* turn, including responses truncated by output length limits.
|
|
67
99
|
*/
|
|
68
100
|
export function isTurnBoundary(stopReason: StopReason): boolean {
|
|
69
|
-
return stopReason === "end_turn" || stopReason === "stop";
|
|
101
|
+
return stopReason === "end_turn" || stopReason === "stop" || stopReason === "length";
|
|
70
102
|
}
|
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;
|