@tonyclaw/agent-inspector 2.1.15 → 2.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/cli.js +57 -13
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BcEd6V-V.js → CompareDrawer-B9sLBHw5.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-DbWjc_BE.js +106 -0
- package/.output/public/assets/{ReplayDialog-BNpC0548.js → ReplayDialog-D9I9W9n3.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-Ds1uRLVB.js → RequestAnatomy-DGXK_Rii.js} +1 -1
- package/.output/public/assets/{ResponseView-7KPVqKl5.js → ResponseView-df_JzwsS.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-BHQT261s.js → StreamingChunkSequence-i3DM5IKL.js} +1 -1
- package/.output/public/assets/_sessionId-CTpCHdFh.js +1 -0
- package/.output/public/assets/index-Blqvndcn.js +1 -0
- package/.output/public/assets/index-Cs79WRQj.css +1 -0
- package/.output/public/assets/{index-DjKt8XKe.js → index-Dqc2r1ea.js} +1 -1
- package/.output/public/assets/{json-viewer-CkCu-rka.js → json-viewer-D3QWQlSB.js} +1 -1
- package/.output/public/assets/{main-DpD1N0S8.js → main-ZhxhOCmF.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +255 -201
- package/.output/server/{_sessionId-DF9Sy8cP.mjs → _sessionId-YFrP0lZP.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-BoxztaO7.mjs → CompareDrawer-qC3adEeq.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-CRBkqFlJ.mjs → ProxyViewerContainer-DCZIJC2b.mjs} +2732 -1849
- package/.output/server/_ssr/{ReplayDialog-Cc1dyDuK.mjs → ReplayDialog-CvX4XYCi.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-CMGSsz5Z.mjs → RequestAnatomy-HQfrxjO1.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-Cp10DM1D.mjs → ResponseView-QSlSD11e.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-B1VGxy3A.mjs → StreamingChunkSequence-DTAiwlnI.mjs} +3 -3
- package/.output/server/_ssr/{index-47XVPghS.mjs → index-D_EzfXaN.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-zDE2rrmJ.mjs → json-viewer-CrIX0e0Q.mjs} +3 -3
- package/.output/server/_ssr/{router-DVeuZFqI.mjs → router-__o2wrfO.mjs} +505 -136
- package/.output/server/{_tanstack-start-manifest_v-Bp8JxtPW.mjs → _tanstack-start-manifest_v-DbqyLpsI.mjs} +1 -1
- package/.output/server/index.mjs +63 -63
- package/README.md +57 -2
- package/package.json +1 -1
- package/src/cli/startupOutput.ts +16 -0
- package/src/cli.ts +61 -15
- package/src/components/ProxyViewer.tsx +535 -163
- 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 +133 -32
- package/src/components/providers/ProviderForm.tsx +61 -29
- package/src/components/providers/ProviderLogo.tsx +6 -1
- package/src/components/providers/ProvidersPanel.tsx +7 -7
- 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 +153 -120
- package/src/components/proxy-viewer/LogEntry.tsx +130 -43
- package/src/components/proxy-viewer/LogEntryHeader.tsx +357 -354
- 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 +102 -83
- package/src/components/proxy-viewer/TurnGroup.tsx +33 -69
- package/src/components/proxy-viewer/TurnGroupList.tsx +3 -0
- package/src/components/proxy-viewer/bodyHydration.ts +56 -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/providerTestContract.ts +56 -10
- package/src/lib/providerTestPrompt.ts +23 -1
- package/src/lib/sessionInfoContract.ts +4 -0
- package/src/lib/upstreamUrl.ts +105 -1
- package/src/mcp/server.ts +71 -27
- package/src/mcp/toolHandlers.ts +24 -5
- 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 +93 -14
- 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
|
@@ -39,7 +39,7 @@ export function ProviderLogoStack({
|
|
|
39
39
|
<TooltipTrigger asChild>
|
|
40
40
|
<span
|
|
41
41
|
className={cn(
|
|
42
|
-
"inline-flex size-6 shrink-0 items-center justify-center rounded-md
|
|
42
|
+
"border border-border bg-muted/40 inline-flex size-6 shrink-0 items-center justify-center rounded-md",
|
|
43
43
|
index > 0 && "-ml-2",
|
|
44
44
|
)}
|
|
45
45
|
>
|
|
@@ -50,7 +50,7 @@ export function ProviderLogoStack({
|
|
|
50
50
|
</Tooltip>
|
|
51
51
|
))}
|
|
52
52
|
{overflowCount > 0 && (
|
|
53
|
-
<span className="-ml-2 inline-flex h-6 min-w-6 shrink-0 items-center justify-center rounded-md
|
|
53
|
+
<span className="border border-border bg-muted/40 -ml-2 inline-flex h-6 min-w-6 shrink-0 items-center justify-center rounded-md px-1 font-mono text-[10px] text-muted-foreground">
|
|
54
54
|
+{overflowCount}
|
|
55
55
|
</span>
|
|
56
56
|
)}
|
|
@@ -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}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { cn } from "../../lib/utils";
|
|
1
|
+
import type { JSX, KeyboardEvent, MouseEvent } from "react";
|
|
3
2
|
import { isTurnBoundary, type StopReason } from "../../lib/stopReason";
|
|
4
|
-
import {
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
5
4
|
|
|
6
5
|
export type ThreadConnectorProps = {
|
|
7
6
|
stopReason: StopReason;
|
|
@@ -11,13 +10,13 @@ export type ThreadConnectorProps = {
|
|
|
11
10
|
isTurnStart: boolean;
|
|
12
11
|
/** True when this entry is the turn's only request. */
|
|
13
12
|
isOnlyEntry?: boolean;
|
|
14
|
-
/**
|
|
15
|
-
crabIndex?: number;
|
|
16
|
-
/** When true the crab is clickable (collapse / expand a turn). */
|
|
13
|
+
/** When true the timeline marker is clickable (collapse / expand a turn). */
|
|
17
14
|
collapsible?: boolean;
|
|
18
15
|
onToggle?: () => void;
|
|
19
16
|
};
|
|
20
17
|
|
|
18
|
+
type MarkerTone = "start" | "end" | "fused" | "running" | "idle";
|
|
19
|
+
|
|
21
20
|
function boundaryTitle(stopReason: StopReason): string {
|
|
22
21
|
switch (stopReason) {
|
|
23
22
|
case "end_turn":
|
|
@@ -32,104 +31,124 @@ function boundaryTitle(stopReason: StopReason): string {
|
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
function markerTone({
|
|
35
|
+
isFusedBoundary,
|
|
36
|
+
isBoundary,
|
|
37
|
+
isTurnStart,
|
|
38
|
+
isRunning,
|
|
39
|
+
}: {
|
|
40
|
+
isFusedBoundary: boolean;
|
|
41
|
+
isBoundary: boolean;
|
|
42
|
+
isTurnStart: boolean;
|
|
43
|
+
isRunning: boolean;
|
|
44
|
+
}): MarkerTone {
|
|
45
|
+
if (isFusedBoundary) return "fused";
|
|
46
|
+
if (isBoundary) return "end";
|
|
47
|
+
if (isTurnStart) return "start";
|
|
48
|
+
if (isRunning) return "running";
|
|
49
|
+
return "idle";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function markerClass(tone: MarkerTone, interactive: boolean): string {
|
|
53
|
+
const base =
|
|
54
|
+
"inline-flex size-3.5 items-center justify-center rounded-full border transition-colors";
|
|
55
|
+
const cursor = interactive ? "cursor-pointer hover:border-cyan-200/70 hover:bg-cyan-300/20" : "";
|
|
56
|
+
|
|
57
|
+
switch (tone) {
|
|
58
|
+
case "start":
|
|
59
|
+
return cn(base, cursor, "border-cyan-300/50 bg-cyan-300/12 text-cyan-200");
|
|
60
|
+
case "end":
|
|
61
|
+
return cn(base, cursor, "border-amber-300/55 bg-amber-300/12 text-amber-200");
|
|
62
|
+
case "fused":
|
|
63
|
+
return cn(
|
|
64
|
+
base,
|
|
65
|
+
cursor,
|
|
66
|
+
"border-slate-200/50 bg-slate-200/10 text-slate-100 ring-2 ring-cyan-300/15",
|
|
67
|
+
);
|
|
68
|
+
case "running":
|
|
69
|
+
return cn(
|
|
70
|
+
base,
|
|
71
|
+
cursor,
|
|
72
|
+
"border-amber-300/45 bg-amber-300/10 text-amber-200 ring-2 ring-amber-300/10",
|
|
73
|
+
);
|
|
74
|
+
case "idle":
|
|
75
|
+
return cn(base, cursor, "border-muted-foreground/25 bg-muted/30 text-muted-foreground/60");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function markerInnerClass(tone: MarkerTone): string {
|
|
80
|
+
switch (tone) {
|
|
81
|
+
case "running":
|
|
82
|
+
return "size-1.5 rounded-full bg-current animate-pulse";
|
|
83
|
+
case "idle":
|
|
84
|
+
return "size-1 rounded-full bg-current opacity-55";
|
|
85
|
+
case "start":
|
|
86
|
+
case "end":
|
|
87
|
+
case "fused":
|
|
88
|
+
return "size-1.5 rounded-full bg-current";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function markerTitle(tone: MarkerTone, stopReason: StopReason): string {
|
|
93
|
+
switch (tone) {
|
|
94
|
+
case "start":
|
|
95
|
+
return "Start of turn";
|
|
96
|
+
case "end":
|
|
97
|
+
return boundaryTitle(stopReason);
|
|
98
|
+
case "fused":
|
|
99
|
+
return "Single-request turn";
|
|
100
|
+
case "running":
|
|
101
|
+
return "Processing";
|
|
102
|
+
case "idle":
|
|
103
|
+
return "Request in turn";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
42
107
|
export function ThreadConnector({
|
|
43
108
|
stopReason,
|
|
44
109
|
isPending,
|
|
45
110
|
isFirst,
|
|
46
111
|
isTurnStart,
|
|
47
112
|
isOnlyEntry = false,
|
|
48
|
-
crabIndex = 0,
|
|
49
113
|
collapsible = false,
|
|
50
114
|
onToggle,
|
|
51
115
|
}: ThreadConnectorProps): JSX.Element {
|
|
52
116
|
const isBoundary = isTurnBoundary(stopReason);
|
|
53
117
|
const isFusedBoundary = isOnlyEntry && isTurnStart && isBoundary;
|
|
54
118
|
const isRunning = isPending && !isBoundary;
|
|
55
|
-
const
|
|
56
|
-
const
|
|
119
|
+
const tone = markerTone({ isFusedBoundary, isBoundary, isTurnStart, isRunning });
|
|
120
|
+
const interactive = collapsible && onToggle !== undefined;
|
|
121
|
+
|
|
122
|
+
const handleMarkerClick = (event: MouseEvent<HTMLSpanElement>): void => {
|
|
123
|
+
if (!interactive || onToggle === undefined) return;
|
|
124
|
+
event.stopPropagation();
|
|
125
|
+
onToggle();
|
|
126
|
+
};
|
|
57
127
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
onClick: (e: React.MouseEvent) => {
|
|
65
|
-
e.stopPropagation();
|
|
66
|
-
onToggle();
|
|
67
|
-
},
|
|
68
|
-
onKeyDown: (e: React.KeyboardEvent) => {
|
|
69
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
70
|
-
e.preventDefault();
|
|
71
|
-
onToggle();
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
} as const)
|
|
75
|
-
: ({} as const);
|
|
128
|
+
const handleMarkerKeyDown = (event: KeyboardEvent<HTMLSpanElement>): void => {
|
|
129
|
+
if (!interactive || onToggle === undefined) return;
|
|
130
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
onToggle();
|
|
133
|
+
};
|
|
76
134
|
|
|
77
135
|
return (
|
|
78
136
|
<div className="flex flex-col items-center w-6 shrink-0 pt-0.5 pb-0.5">
|
|
79
|
-
{/* Top spacer — crab centre must land on the LogEntry index midline.
|
|
80
|
-
* Index midline from container top = border(1px) + py-1(0.25rem) + ½line(0.5rem)
|
|
81
|
-
* = 1px + 0.75rem. Crab centre = pt(2px) + spacer + 7px(half crab).
|
|
82
|
-
* ∴ spacer = 0.75rem - 8px. (at 16px root: 12-8=4px; 2+4+7=13px). */}
|
|
83
137
|
<div className="flex justify-center h-[calc(0.75rem-8px)]">
|
|
84
138
|
{!isFirst && <div className="w-0.5 bg-muted-foreground/30 h-full" />}
|
|
85
139
|
</div>
|
|
86
140
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)}
|
|
98
|
-
/>
|
|
99
|
-
</span>
|
|
100
|
-
) : isBoundary ? (
|
|
101
|
-
<span title={boundaryTitle(stopReason)} {...interactiveProps}>
|
|
102
|
-
<Crab
|
|
103
|
-
className={cn(
|
|
104
|
-
"size-3.5 text-amber-400",
|
|
105
|
-
"animate-crab-settle",
|
|
106
|
-
"drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]",
|
|
107
|
-
)}
|
|
108
|
-
/>
|
|
109
|
-
</span>
|
|
110
|
-
) : isTurnStart ? (
|
|
111
|
-
<span title="Start of turn" {...interactiveProps}>
|
|
112
|
-
<Crab
|
|
113
|
-
className={cn(
|
|
114
|
-
"size-3.5 text-emerald-400",
|
|
115
|
-
"animate-crab-appear",
|
|
116
|
-
"drop-shadow-[0_0_4px_rgba(52,211,153,0.5)]",
|
|
117
|
-
)}
|
|
118
|
-
/>
|
|
119
|
-
</span>
|
|
120
|
-
) : isRunning ? (
|
|
121
|
-
<span title="Processing…">
|
|
122
|
-
<Crab className={cn("size-3.5 text-amber-300/80", "animate-crab-crawl")} />
|
|
123
|
-
</span>
|
|
124
|
-
) : (
|
|
125
|
-
<span>
|
|
126
|
-
<Crab className="size-3.5 text-muted-foreground/40" />
|
|
127
|
-
</span>
|
|
128
|
-
)}
|
|
141
|
+
<span
|
|
142
|
+
role={interactive ? "button" : undefined}
|
|
143
|
+
tabIndex={interactive ? 0 : undefined}
|
|
144
|
+
title={markerTitle(tone, stopReason)}
|
|
145
|
+
className={markerClass(tone, interactive)}
|
|
146
|
+
onClick={handleMarkerClick}
|
|
147
|
+
onKeyDown={handleMarkerKeyDown}
|
|
148
|
+
>
|
|
149
|
+
<span className={markerInnerClass(tone)} />
|
|
150
|
+
</span>
|
|
129
151
|
|
|
130
|
-
{/* Bottom spacer — flex-1 fills whatever height the LogEntry
|
|
131
|
-
* consumes (header + expanded content). Turn boundaries have
|
|
132
|
-
* no outgoing line. */}
|
|
133
152
|
<div className="flex-1 flex justify-center min-h-0">
|
|
134
153
|
{!isBoundary && (
|
|
135
154
|
<div
|
|
@@ -13,9 +13,7 @@ import { isTurnBoundary } from "../../lib/stopReason";
|
|
|
13
13
|
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
14
14
|
import { cn, formatTokens } from "../../lib/utils";
|
|
15
15
|
import type { CapturedLog } from "../../contracts";
|
|
16
|
-
import { getCrabVariant } from "../ui/crab-variants";
|
|
17
16
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
|
|
18
|
-
import { ROW_CHEVRON_SLOT_CLASS } from "./ProviderLogoStack";
|
|
19
17
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
20
18
|
import { LogEntry } from "./LogEntry";
|
|
21
19
|
import {
|
|
@@ -48,6 +46,8 @@ export type TurnGroupProps = {
|
|
|
48
46
|
turnIndex?: number;
|
|
49
47
|
timeDisplayFormat: TimeDisplayFormat;
|
|
50
48
|
pendingFocusRequest?: PendingTurnFocusRequest;
|
|
49
|
+
/** Show client PID/project metadata on each log row. */
|
|
50
|
+
showLogClientMetadata?: boolean;
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
export const TurnGroup = memo(function TurnGroup({
|
|
@@ -61,6 +61,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
61
61
|
turnIndex = 0,
|
|
62
62
|
timeDisplayFormat,
|
|
63
63
|
pendingFocusRequest,
|
|
64
|
+
showLogClientMetadata = true,
|
|
64
65
|
}: TurnGroupProps): JSX.Element {
|
|
65
66
|
const lastIdx = entries.length - 1;
|
|
66
67
|
const lastStop = entries[lastIdx]?.stopReason ?? null;
|
|
@@ -70,7 +71,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
70
71
|
const collapsible = isTurnCollapsible(entries.length, isComplete, isPending);
|
|
71
72
|
const [collapsed, setCollapsed] = useState(false);
|
|
72
73
|
|
|
73
|
-
// Auto-collapse when the turn finishes (transitions from incomplete
|
|
74
|
+
// Auto-collapse when the turn finishes (transitions from incomplete to complete)
|
|
74
75
|
const prevCompleteRef = useRef(false);
|
|
75
76
|
useEffect(() => {
|
|
76
77
|
if (!collapsible) {
|
|
@@ -142,11 +143,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
142
143
|
};
|
|
143
144
|
}, [entries, lastIdx]);
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
const StartCrab = useMemo(() => getCrabVariant(entries[0]?.log.id ?? 0), [entries]);
|
|
147
|
-
const EndCrab = useMemo(() => getCrabVariant(entries[lastIdx]?.log.id ?? 0), [entries, lastIdx]);
|
|
148
|
-
|
|
149
|
-
const bgClass = turnIndex % 2 === 0 ? "bg-[#070a0f]" : "bg-[#090d12]";
|
|
146
|
+
const bgClass = turnIndex % 2 === 0 ? "bg-white/[0.012]" : "bg-cyan-300/[0.012]";
|
|
150
147
|
const aggregateIsSlow =
|
|
151
148
|
aggregate.maxElapsed !== null &&
|
|
152
149
|
slowResponseThresholdSeconds > 0 &&
|
|
@@ -160,7 +157,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
160
157
|
return events;
|
|
161
158
|
}, [entries]);
|
|
162
159
|
|
|
163
|
-
// ResizeObserver
|
|
160
|
+
// ResizeObserver re-renders connectors when any LogEntry height changes
|
|
164
161
|
const [layoutVersion, setLayoutVersion] = useState(0);
|
|
165
162
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
166
163
|
useEffect(() => {
|
|
@@ -197,7 +194,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
197
194
|
aria-label={collapsed ? undefined : turnLabel}
|
|
198
195
|
data-nav-id={collapsed ? undefined : `turn-${String(firstLogId)}`}
|
|
199
196
|
className={cn(
|
|
200
|
-
"
|
|
197
|
+
"rounded-[8px] border transition-[border-color,box-shadow]",
|
|
201
198
|
isPending
|
|
202
199
|
? "border-amber-300/15 shadow-[0_0_0_1px_rgba(252,211,77,0.04)]"
|
|
203
200
|
: "border-transparent",
|
|
@@ -206,7 +203,6 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
206
203
|
)}
|
|
207
204
|
>
|
|
208
205
|
{collapsed ? (
|
|
209
|
-
/* ---- Collapsed: dual-crab (+ summary card for multi-log turns) ---- */
|
|
210
206
|
<div
|
|
211
207
|
data-nav-id={`turn-collapsed-${String(firstLogId)}`}
|
|
212
208
|
data-nav-action="expand"
|
|
@@ -228,77 +224,38 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
228
224
|
</span>
|
|
229
225
|
</div>
|
|
230
226
|
|
|
231
|
-
|
|
232
|
-
<div className="w-6 shrink-0 flex flex-col items-center pt-0.5 pb-0.5">
|
|
227
|
+
<div className="w-6 shrink-0 flex flex-col items-center pt-1.5 pb-1.5">
|
|
233
228
|
<div className="flex justify-center h-[calc(0.75rem-8px)]" />
|
|
234
|
-
<span
|
|
235
|
-
role="button"
|
|
236
|
-
tabIndex={0}
|
|
237
|
-
title="Start of turn — click to expand"
|
|
238
|
-
className="cursor-pointer"
|
|
239
|
-
onClick={(e) => {
|
|
240
|
-
e.stopPropagation();
|
|
241
|
-
toggleCollapse();
|
|
242
|
-
}}
|
|
243
|
-
onKeyDown={(e) => {
|
|
244
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
245
|
-
e.preventDefault();
|
|
246
|
-
toggleCollapse();
|
|
247
|
-
}
|
|
248
|
-
}}
|
|
249
|
-
>
|
|
250
|
-
<StartCrab
|
|
251
|
-
className={cn(
|
|
252
|
-
"size-3.5 text-emerald-400",
|
|
253
|
-
"animate-crab-appear drop-shadow-[0_0_4px_rgba(52,211,153,0.5)]",
|
|
254
|
-
)}
|
|
255
|
-
/>
|
|
256
|
-
</span>
|
|
257
|
-
|
|
229
|
+
<span className="size-2 rounded-full border border-cyan-300/45 bg-cyan-300/20" />
|
|
258
230
|
<div className="flex-1 flex justify-center min-h-0">
|
|
259
|
-
<div className="w-
|
|
231
|
+
<div className="w-px bg-muted-foreground/25 h-full" />
|
|
260
232
|
</div>
|
|
261
|
-
|
|
262
233
|
<span
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}}
|
|
271
|
-
onKeyDown={(e) => {
|
|
272
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
273
|
-
e.preventDefault();
|
|
274
|
-
toggleCollapse();
|
|
275
|
-
}
|
|
276
|
-
}}
|
|
277
|
-
>
|
|
278
|
-
<EndCrab
|
|
279
|
-
className={cn(
|
|
280
|
-
"size-3.5 text-amber-400",
|
|
281
|
-
"animate-crab-settle drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]",
|
|
282
|
-
)}
|
|
283
|
-
/>
|
|
284
|
-
</span>
|
|
234
|
+
className={cn(
|
|
235
|
+
"size-2 rounded-full border",
|
|
236
|
+
isComplete
|
|
237
|
+
? "border-amber-300/45 bg-amber-300/20"
|
|
238
|
+
: "border-muted-foreground/30 bg-muted/30",
|
|
239
|
+
)}
|
|
240
|
+
/>
|
|
285
241
|
</div>
|
|
286
242
|
|
|
287
|
-
{/* Summary content
|
|
243
|
+
{/* Summary content - hidden for single-log turns. */}
|
|
288
244
|
{entries.length > 1 && (
|
|
289
245
|
<div
|
|
290
246
|
className={cn(
|
|
291
|
-
"flex-1 min-w-0 mb-0.5 rounded-
|
|
247
|
+
"flex-1 min-w-0 mb-0.5 rounded-[8px] py-1.5 px-3 flex items-center gap-3 text-xs shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
|
|
248
|
+
"border border-border bg-card shadow-sm transition-colors hover:border-cyan-200/20",
|
|
292
249
|
bgClass,
|
|
293
250
|
)}
|
|
294
251
|
>
|
|
295
|
-
<span className=
|
|
296
|
-
|
|
252
|
+
<span className="font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/70">
|
|
253
|
+
{turnLabel}
|
|
297
254
|
</span>
|
|
298
255
|
|
|
299
256
|
{/* ID range */}
|
|
300
257
|
<span
|
|
301
|
-
className="text-blue-
|
|
258
|
+
className="text-blue-300/85 font-mono font-semibold tabular-nums shrink-0"
|
|
302
259
|
title={`Log IDs ${String(entries[0]?.log.id ?? "?")} ~ ${String(
|
|
303
260
|
entries[lastIdx]?.log.id ?? "?",
|
|
304
261
|
)}`}
|
|
@@ -312,7 +269,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
312
269
|
{entries.length} request{entries.length > 1 ? "s" : ""}
|
|
313
270
|
</span>
|
|
314
271
|
|
|
315
|
-
{/* Elapsed
|
|
272
|
+
{/* Elapsed - slowest single request in the turn (not the sum) */}
|
|
316
273
|
{aggregate.maxElapsed !== null && (
|
|
317
274
|
<TooltipProvider>
|
|
318
275
|
<Tooltip>
|
|
@@ -359,6 +316,13 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
359
316
|
|
|
360
317
|
{/* Spacer */}
|
|
361
318
|
<span className="flex-1 min-w-0" />
|
|
319
|
+
|
|
320
|
+
<span
|
|
321
|
+
className="border border-input bg-background inline-flex size-7 shrink-0 items-center justify-center rounded-md text-slate-200"
|
|
322
|
+
aria-hidden="true"
|
|
323
|
+
>
|
|
324
|
+
<ChevronRight className="size-4 shrink-0" />
|
|
325
|
+
</span>
|
|
362
326
|
</div>
|
|
363
327
|
)}
|
|
364
328
|
</div>
|
|
@@ -385,11 +349,10 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
385
349
|
isFirst={visibleIdx === 0}
|
|
386
350
|
isTurnStart={isTurnStart}
|
|
387
351
|
isOnlyEntry={isSingleLog}
|
|
388
|
-
crabIndex={log.id % 12}
|
|
389
352
|
collapsible={collapsible && isTurnStart}
|
|
390
353
|
onToggle={toggleCollapse}
|
|
391
354
|
/>
|
|
392
|
-
<div className={cn("flex-1 min-w-0 mb-0.5 rounded-
|
|
355
|
+
<div className={cn("flex-1 min-w-0 mb-0.5 rounded-[8px]", bgClass)}>
|
|
393
356
|
<LogEntry
|
|
394
357
|
log={log}
|
|
395
358
|
displayNumber={entry.sessionLogNumber}
|
|
@@ -398,6 +361,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
398
361
|
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
399
362
|
timeDisplayFormat={timeDisplayFormat}
|
|
400
363
|
cacheTrend={cacheTrends?.get(log.id) ?? null}
|
|
364
|
+
showClientMetadata={showLogClientMetadata}
|
|
401
365
|
onCompareWithPrevious={
|
|
402
366
|
comparisonPredecessors.has(log.id) ? onCompareWithPrevious : undefined
|
|
403
367
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ApiTimeoutError } from "../../lib/apiClient";
|
|
2
|
+
|
|
3
|
+
export type FullBodyHydrationError = {
|
|
4
|
+
title: string;
|
|
5
|
+
detail: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const DEFAULT_FULL_BODY_HYDRATION_ERROR_DETAIL =
|
|
9
|
+
"The compact log metadata is still available. Retry if the local server was restarting or the body store was briefly unavailable.";
|
|
10
|
+
|
|
11
|
+
export function formatBytes(bytes: number): string {
|
|
12
|
+
if (bytes < 1024) return `${bytes.toLocaleString()} B`;
|
|
13
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
14
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
|
|
15
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GiB`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatBodyHydrationFootprint(
|
|
19
|
+
requestBytes: number | null | undefined,
|
|
20
|
+
responseBytes: number | null | undefined,
|
|
21
|
+
): string | null {
|
|
22
|
+
const parts: string[] = [];
|
|
23
|
+
|
|
24
|
+
if (requestBytes !== null && requestBytes !== undefined) {
|
|
25
|
+
parts.push(`Request ${formatBytes(requestBytes)}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (responseBytes !== null && responseBytes !== undefined) {
|
|
29
|
+
parts.push(`Response ${formatBytes(responseBytes)}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return parts.length === 0 ? null : parts.join(" · ");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function describeFullBodyHydrationError(error: unknown): FullBodyHydrationError {
|
|
36
|
+
if (error instanceof ApiTimeoutError) {
|
|
37
|
+
return {
|
|
38
|
+
title: "Full bodies timed out",
|
|
39
|
+
detail:
|
|
40
|
+
"The log store did not answer before the UI timeout. Compact metadata is still available; retry loading the bodies.",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (error instanceof Error) {
|
|
45
|
+
const message = error.message.trim();
|
|
46
|
+
return {
|
|
47
|
+
title: "Full bodies could not be loaded",
|
|
48
|
+
detail: message === "" ? DEFAULT_FULL_BODY_HYDRATION_ERROR_DETAIL : message,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
title: "Full bodies could not be loaded",
|
|
54
|
+
detail: DEFAULT_FULL_BODY_HYDRATION_ERROR_DETAIL,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -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,
|