@tonyclaw/agent-inspector 2.0.3 → 2.0.5
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-D5A4bTfV.js → CompareDrawer-3nRwtk8J.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-CbW5VRER.js +101 -0
- package/.output/public/assets/ReplayDialog-Cl62N9PI.js +1 -0
- package/.output/public/assets/RequestAnatomy-DgQWGvjs.js +1 -0
- package/.output/public/assets/ResponseView-Cvc-ct4E.js +1 -0
- package/.output/public/assets/StreamingChunkSequence-BCQaCAIe.js +1 -0
- package/.output/public/assets/_sessionId-CcD_aLGq.js +1 -0
- package/.output/public/assets/index-B_dffD3u.js +1 -0
- package/.output/public/assets/index-CX796gvi.css +1 -0
- package/.output/public/assets/{json-viewer-BbU0n8eM.js → json-viewer-IXejqXB0.js} +1 -1
- package/.output/public/assets/{main-CZT_F-gu.js → main-2NlGzgOe.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +181 -114
- package/.output/server/{_sessionId-B-s9P7fJ.mjs → _sessionId-DWCTasJU.mjs} +3 -3
- package/.output/server/_ssr/{CompareDrawer-C08L3UOO.mjs → CompareDrawer-DhrN1uC2.mjs} +6 -6
- package/.output/server/_ssr/{ProxyViewerContainer-CMWl3Ijy.mjs → ProxyViewerContainer-DRl51s_n.mjs} +910 -186
- package/.output/server/_ssr/{ReplayDialog-CPDo9_G5.mjs → ReplayDialog-BQT_ygxC.mjs} +240 -14
- package/.output/server/_ssr/{RequestAnatomy-D9wt_K1E.mjs → RequestAnatomy-DS2tZOgq.mjs} +5 -5
- package/.output/server/_ssr/{ResponseView-DXaL7nY3.mjs → ResponseView-e0kL2C3x.mjs} +25 -21
- package/.output/server/_ssr/{StreamingChunkSequence-B_hudZyb.mjs → StreamingChunkSequence-BJG-m7xs.mjs} +3 -3
- package/.output/server/_ssr/{index-CuE_BN86.mjs → index-Dea3OeRw.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-Ci6kkjde.mjs → json-viewer-DDU55MLK.mjs} +3 -3
- package/.output/server/_ssr/{router-BemxgIg7.mjs → router-Dl7oh0zx.mjs} +164 -82
- package/.output/server/_tanstack-start-manifest_v-m-FJNBVf.mjs +4 -0
- package/.output/server/index.mjs +70 -70
- package/package.json +1 -1
- package/src/components/OnboardingBanner.tsx +11 -19
- package/src/components/ProxyViewer.tsx +26 -16
- package/src/components/ProxyViewerContainer.tsx +2 -1
- package/src/components/providers/ProviderCard.tsx +6 -20
- package/src/components/providers/SettingsDialog.tsx +140 -3
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +731 -72
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +16 -0
- package/src/components/proxy-viewer/CompareDrawer.tsx +4 -2
- package/src/components/proxy-viewer/ConversationGroup.tsx +12 -0
- package/src/components/proxy-viewer/ConversationHeader.tsx +6 -6
- package/src/components/proxy-viewer/LogEntry.tsx +5 -5
- package/src/components/proxy-viewer/LogEntryHeader.tsx +21 -36
- package/src/components/proxy-viewer/ReplayDialog.tsx +190 -8
- package/src/components/proxy-viewer/ResponseView.tsx +4 -8
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +37 -17
- package/src/components/proxy-viewer/TurnGroup.tsx +18 -2
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +2 -2
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +6 -12
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +2 -2
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +10 -14
- package/src/components/proxy-viewer/replayComparison.ts +131 -0
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +64 -22
- package/src/components/proxy-viewer/viewerState.ts +14 -2
- package/src/knowledge/candidateStore.ts +32 -1
- package/src/lib/runtimeConfig.ts +6 -0
- package/src/lib/timeDisplay.ts +22 -0
- package/src/lib/useOnboarding.ts +2 -0
- package/src/lib/useStripConfig.ts +16 -0
- package/src/proxy/config.ts +3 -0
- package/src/routes/api/config.ts +5 -1
- package/src/routes/api/knowledge.candidates.$candidateId.ts +50 -0
- package/src/routes/api/knowledge.sessions.$sessionId.candidates.ts +12 -2
- package/.output/public/assets/ProxyViewerContainer-Da0jpBkp.js +0 -101
- package/.output/public/assets/ReplayDialog-CxUk_TF0.js +0 -1
- package/.output/public/assets/RequestAnatomy-DIlzjgjJ.js +0 -1
- package/.output/public/assets/ResponseView-DQCuKJ1G.js +0 -1
- package/.output/public/assets/StreamingChunkSequence-DHk4SGGL.js +0 -1
- package/.output/public/assets/_sessionId-dY1TTl7N.js +0 -1
- package/.output/public/assets/index-D7wwbwly.css +0 -1
- package/.output/public/assets/index-FqQZbfl2.js +0 -1
- package/.output/server/_tanstack-start-manifest_v--L1_b4sd.mjs +0 -4
|
@@ -1,32 +1,52 @@
|
|
|
1
1
|
import { type JSX } from "react";
|
|
2
|
-
import { ChevronRight, Wrench } from "lucide-react";
|
|
2
|
+
import { Check, ChevronRight, Copy, Wrench } from "lucide-react";
|
|
3
3
|
import type { ToolTraceEvent } from "./viewerState";
|
|
4
|
+
import { useCopyFeedback } from "./useCopyFeedback";
|
|
4
5
|
|
|
5
6
|
type ToolTraceEventsProps = {
|
|
6
7
|
events: ToolTraceEvent[];
|
|
7
8
|
};
|
|
8
9
|
|
|
10
|
+
function ToolTraceEventRow({ event }: { event: ToolTraceEvent }): JSX.Element {
|
|
11
|
+
const argumentCopy = useCopyFeedback(event.argumentsText);
|
|
12
|
+
const canCopyArguments = event.argumentsText !== null;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
key={event.id}
|
|
17
|
+
className="group/tool-trace flex min-w-0 items-center gap-2 rounded-md border border-border/70 bg-muted/20 px-2.5 py-1.5 text-xs"
|
|
18
|
+
>
|
|
19
|
+
<Wrench className="size-3.5 shrink-0 text-sky-400/70" />
|
|
20
|
+
<span className="font-mono font-semibold text-foreground/80">{event.name}</span>
|
|
21
|
+
{event.argumentsPreview !== null && (
|
|
22
|
+
<>
|
|
23
|
+
<ChevronRight className="size-3 shrink-0 text-muted-foreground/60" />
|
|
24
|
+
<span className="min-w-0 truncate font-mono text-muted-foreground">
|
|
25
|
+
{event.argumentsPreview}
|
|
26
|
+
</span>
|
|
27
|
+
</>
|
|
28
|
+
)}
|
|
29
|
+
{canCopyArguments && (
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
className="ml-auto inline-flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground opacity-0 transition-opacity hover:bg-background/80 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/tool-trace:opacity-100 group-focus-within/tool-trace:opacity-100"
|
|
33
|
+
onClick={argumentCopy.copy}
|
|
34
|
+
aria-label={argumentCopy.copied ? "Copied tool arguments" : "Copy tool arguments"}
|
|
35
|
+
title={argumentCopy.copied ? "Copied tool arguments" : "Copy tool arguments"}
|
|
36
|
+
>
|
|
37
|
+
{argumentCopy.copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
|
38
|
+
</button>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
9
44
|
export function ToolTraceEvents({ events }: ToolTraceEventsProps): JSX.Element | null {
|
|
10
45
|
if (events.length === 0) return null;
|
|
11
46
|
return (
|
|
12
47
|
<div className="mx-3 mb-2 grid gap-1.5">
|
|
13
48
|
{events.map((event) => (
|
|
14
|
-
<
|
|
15
|
-
key={event.id}
|
|
16
|
-
className="flex min-w-0 items-center gap-2 rounded-md border border-amber-500/20 bg-amber-500/5 px-2.5 py-1.5 text-xs"
|
|
17
|
-
>
|
|
18
|
-
<Wrench className="size-3.5 shrink-0 text-amber-400" />
|
|
19
|
-
<span className="font-mono font-semibold text-amber-300">{event.name}</span>
|
|
20
|
-
<span className="font-mono text-muted-foreground">#{event.logId}</span>
|
|
21
|
-
{event.argumentsPreview !== null && (
|
|
22
|
-
<>
|
|
23
|
-
<ChevronRight className="size-3 shrink-0 text-muted-foreground/60" />
|
|
24
|
-
<span className="min-w-0 truncate font-mono text-muted-foreground">
|
|
25
|
-
{event.argumentsPreview}
|
|
26
|
-
</span>
|
|
27
|
-
</>
|
|
28
|
-
)}
|
|
29
|
-
</div>
|
|
49
|
+
<ToolTraceEventRow key={event.id} event={event} />
|
|
30
50
|
))}
|
|
31
51
|
</div>
|
|
32
52
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type JSX, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { AlertTriangle, ChevronRight, Clock, Zap } from "lucide-react";
|
|
3
3
|
import { isTurnBoundary } from "../../lib/stopReason";
|
|
4
|
+
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
4
5
|
import { cn, formatTokens } from "../../lib/utils";
|
|
5
6
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
6
7
|
import { getCrabVariant } from "../ui/crab-variants";
|
|
@@ -26,6 +27,7 @@ type TurnGroupProps = {
|
|
|
26
27
|
onCompareWithPrevious: (log: CapturedLog) => void;
|
|
27
28
|
comparisonPredecessors: Map<number, CapturedLog>;
|
|
28
29
|
turnIndex?: number;
|
|
30
|
+
timeDisplayFormat: TimeDisplayFormat;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export const TurnGroup = memo(function TurnGroup({
|
|
@@ -37,6 +39,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
37
39
|
onCompareWithPrevious,
|
|
38
40
|
comparisonPredecessors,
|
|
39
41
|
turnIndex = 0,
|
|
42
|
+
timeDisplayFormat,
|
|
40
43
|
}: TurnGroupProps): JSX.Element {
|
|
41
44
|
const lastIdx = entries.length - 1;
|
|
42
45
|
const lastStop = entries[lastIdx]?.stopReason ?? null;
|
|
@@ -134,15 +137,27 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
134
137
|
};
|
|
135
138
|
}, []);
|
|
136
139
|
|
|
140
|
+
const firstLogId = entries[0]?.log.id ?? turnIndex;
|
|
141
|
+
const turnLabel = `Turn ${String(turnIndex + 1)}`;
|
|
142
|
+
|
|
137
143
|
return (
|
|
138
144
|
<div
|
|
139
145
|
ref={containerRef}
|
|
140
|
-
|
|
146
|
+
tabIndex={collapsed ? undefined : 0}
|
|
147
|
+
role={collapsed ? undefined : "group"}
|
|
148
|
+
aria-label={collapsed ? undefined : turnLabel}
|
|
149
|
+
data-nav-id={collapsed ? undefined : `turn-${String(firstLogId)}`}
|
|
150
|
+
className={cn(
|
|
151
|
+
"border rounded-lg",
|
|
152
|
+
isPending ? "border-amber-500/10" : "border-transparent",
|
|
153
|
+
!collapsed &&
|
|
154
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
|
|
155
|
+
)}
|
|
141
156
|
>
|
|
142
157
|
{collapsed ? (
|
|
143
158
|
/* ---- Collapsed: dual-crab (+ summary card for multi-log turns) ---- */
|
|
144
159
|
<div
|
|
145
|
-
data-nav-id={`turn-collapsed-${
|
|
160
|
+
data-nav-id={`turn-collapsed-${String(firstLogId)}`}
|
|
146
161
|
data-nav-action="expand"
|
|
147
162
|
role="button"
|
|
148
163
|
tabIndex={0}
|
|
@@ -331,6 +346,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
331
346
|
viewMode={viewMode}
|
|
332
347
|
strip={strip}
|
|
333
348
|
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
349
|
+
timeDisplayFormat={timeDisplayFormat}
|
|
334
350
|
cacheTrend={cacheTrends?.get(log.id) ?? null}
|
|
335
351
|
onCompareWithPrevious={
|
|
336
352
|
comparisonPredecessors.has(log.id) ? onCompareWithPrevious : undefined
|
|
@@ -7,7 +7,7 @@ const ROLE_COLOR_CLASSES: Record<AnatomyRole, string> = {
|
|
|
7
7
|
system: "bg-sky-500/70",
|
|
8
8
|
user: "bg-emerald-500/70",
|
|
9
9
|
assistant: "bg-violet-500/70",
|
|
10
|
-
tool: "bg-
|
|
10
|
+
tool: "bg-sky-400/55",
|
|
11
11
|
tools: "bg-slate-500/70",
|
|
12
12
|
};
|
|
13
13
|
|
|
@@ -15,7 +15,7 @@ const ROLE_FOCUS_RING: Record<AnatomyRole, string> = {
|
|
|
15
15
|
system: "focus-visible:ring-sky-300",
|
|
16
16
|
user: "focus-visible:ring-emerald-300",
|
|
17
17
|
assistant: "focus-visible:ring-violet-300",
|
|
18
|
-
tool: "focus-visible:ring-
|
|
18
|
+
tool: "focus-visible:ring-sky-300",
|
|
19
19
|
tools: "focus-visible:ring-slate-300",
|
|
20
20
|
};
|
|
21
21
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Brain, ChevronDown, ChevronRight, Terminal } from "lucide-react";
|
|
2
2
|
import { type JSX, memo, useState } from "react";
|
|
3
|
-
import ReactMarkdown from "react-markdown";
|
|
4
3
|
import type { ResponseContentBlockType } from "../../../../proxy/schemas";
|
|
4
|
+
import { AnswerMarkdown } from "../../AnswerMarkdown";
|
|
5
5
|
import { Badge } from "../../../ui/badge";
|
|
6
6
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../ui/collapsible";
|
|
7
7
|
import { JsonViewer } from "../../../ui/json-viewer";
|
|
@@ -30,9 +30,7 @@ function SystemReminderBlock({ text }: { text: string }): JSX.Element {
|
|
|
30
30
|
</CollapsibleTrigger>
|
|
31
31
|
<CollapsibleContent>
|
|
32
32
|
<div className="pl-4 pt-1">
|
|
33
|
-
<
|
|
34
|
-
<ReactMarkdown>{text}</ReactMarkdown>
|
|
35
|
-
</div>
|
|
33
|
+
<AnswerMarkdown text={text} />
|
|
36
34
|
</div>
|
|
37
35
|
</CollapsibleContent>
|
|
38
36
|
</Collapsible>
|
|
@@ -50,11 +48,7 @@ export const TextBlock = memo(function TextBlock({ text }: { text: string }): JS
|
|
|
50
48
|
return (
|
|
51
49
|
<div className="space-y-2">
|
|
52
50
|
{thinking !== null && <ThinkingBlock thinking={thinking} />}
|
|
53
|
-
{remainingText.length > 0 &&
|
|
54
|
-
<div className="prose prose-sm dark:prose-invert max-w-none [&_pre]:bg-muted [&_pre]:text-foreground [&_code]:text-[0.8em] [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1">
|
|
55
|
-
<ReactMarkdown>{remainingText}</ReactMarkdown>
|
|
56
|
-
</div>
|
|
57
|
-
)}
|
|
51
|
+
{remainingText.length > 0 && <AnswerMarkdown text={remainingText} />}
|
|
58
52
|
{thinking === null && remainingText.length === 0 && (
|
|
59
53
|
<p className="text-xs text-muted-foreground italic">Empty text block</p>
|
|
60
54
|
)}
|
|
@@ -113,9 +107,9 @@ export const ToolUseBlock = memo(function ToolUseBlock({
|
|
|
113
107
|
|
|
114
108
|
return (
|
|
115
109
|
<Collapsible open={open} onOpenChange={setOpen}>
|
|
116
|
-
<div className="border-l-2 border-
|
|
117
|
-
<CollapsibleTrigger className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-
|
|
118
|
-
<Terminal className="size-3.5 text-
|
|
110
|
+
<div className="border-l-2 border-sky-400/25 my-1">
|
|
111
|
+
<CollapsibleTrigger className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-sky-400/[0.04] transition-colors rounded-r-sm group">
|
|
112
|
+
<Terminal className="size-3.5 text-sky-400/70 shrink-0" />
|
|
119
113
|
<Badge variant="outline" className="text-[10px] font-mono px-1.5 py-0 h-4">
|
|
120
114
|
{name}
|
|
121
115
|
</Badge>
|
|
@@ -38,14 +38,14 @@ export const StructuredResponseViewAnthropic = memo(function StructuredResponseV
|
|
|
38
38
|
response.usage.cache_creation_input_tokens !== null &&
|
|
39
39
|
response.usage.cache_creation_input_tokens > 0 && (
|
|
40
40
|
<span className="font-mono tabular-nums text-emerald-400">
|
|
41
|
-
Cache +{formatTokens(response.usage.cache_creation_input_tokens)}
|
|
41
|
+
KV Cache +{formatTokens(response.usage.cache_creation_input_tokens)}
|
|
42
42
|
</span>
|
|
43
43
|
)}
|
|
44
44
|
{response.usage.cache_read_input_tokens !== undefined &&
|
|
45
45
|
response.usage.cache_read_input_tokens !== null &&
|
|
46
46
|
response.usage.cache_read_input_tokens > 0 && (
|
|
47
47
|
<span className="font-mono tabular-nums text-purple-400">
|
|
48
|
-
Cache ~{formatTokens(response.usage.cache_read_input_tokens)}
|
|
48
|
+
KV Cache ~{formatTokens(response.usage.cache_read_input_tokens)}
|
|
49
49
|
</span>
|
|
50
50
|
)}
|
|
51
51
|
</span>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { StopCircle, Terminal, Zap } from "lucide-react";
|
|
2
2
|
import { memo, useState, type JSX } from "react";
|
|
3
|
-
import ReactMarkdown from "react-markdown";
|
|
4
3
|
import type { OpenAIResponse, OpenAIToolCall } from "../../../../proxy/schemas";
|
|
5
4
|
import { formatTokens } from "../../../../lib/utils";
|
|
5
|
+
import { AnswerMarkdown } from "../../AnswerMarkdown";
|
|
6
6
|
import { Badge } from "../../../ui/badge";
|
|
7
7
|
import { JsonViewer } from "../../../ui/json-viewer";
|
|
8
8
|
import { safeJsonValue } from "../../../ui/json-viewer-bulk";
|
|
@@ -29,8 +29,8 @@ function parseToolArguments(raw: string | undefined): unknown {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** One collapsible tool_use row, mirroring the Anthropic ToolUseBlock visual
|
|
32
|
-
* treatment (Terminal icon,
|
|
33
|
-
* scrollable JsonViewer). */
|
|
32
|
+
* treatment (Terminal icon, soft tool accent, name as a Badge, JSON input in
|
|
33
|
+
* a scrollable JsonViewer). */
|
|
34
34
|
function OpenAIToolCallBlock({ call }: { call: OpenAIToolCall }): JSX.Element {
|
|
35
35
|
const [open, setOpen] = useState(false);
|
|
36
36
|
const name = call.function.name ?? "(unnamed tool)";
|
|
@@ -38,9 +38,9 @@ function OpenAIToolCallBlock({ call }: { call: OpenAIToolCall }): JSX.Element {
|
|
|
38
38
|
|
|
39
39
|
return (
|
|
40
40
|
<Collapsible open={open} onOpenChange={setOpen}>
|
|
41
|
-
<div className="border-l-2 border-
|
|
42
|
-
<CollapsibleTrigger className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-
|
|
43
|
-
<Terminal className="size-3.5 text-
|
|
41
|
+
<div className="border-l-2 border-sky-400/25 my-1">
|
|
42
|
+
<CollapsibleTrigger className="flex items-center gap-1.5 px-3 py-1 w-full text-left cursor-pointer hover:bg-sky-400/[0.04] transition-colors rounded-r-sm group">
|
|
43
|
+
<Terminal className="size-3.5 text-sky-400/70 shrink-0" />
|
|
44
44
|
<Badge variant="outline" className="text-[10px] font-mono px-1.5 py-0 h-4">
|
|
45
45
|
{name}
|
|
46
46
|
</Badge>
|
|
@@ -133,11 +133,7 @@ export const OpenAIResponseView = memo(function OpenAIResponseView({
|
|
|
133
133
|
<div className="space-y-2">
|
|
134
134
|
{/* Show thinking from tags only if no reasoning_content field */}
|
|
135
135
|
{thinking !== null && !hasReasoningField && <ThinkingBlock thinking={thinking} />}
|
|
136
|
-
{remainingText.length > 0 &&
|
|
137
|
-
<div className="prose prose-sm dark:prose-invert max-w-none [&_pre]:bg-muted [&_pre]:text-foreground [&_code]:text-[0.8em] [&_p]:my-1 [&_ul]:my-1 [&_ol]:my-1">
|
|
138
|
-
<ReactMarkdown>{remainingText}</ReactMarkdown>
|
|
139
|
-
</div>
|
|
140
|
-
)}
|
|
136
|
+
{remainingText.length > 0 && <AnswerMarkdown text={remainingText} />}
|
|
141
137
|
</div>
|
|
142
138
|
);
|
|
143
139
|
})()}
|
|
@@ -146,10 +142,10 @@ export const OpenAIResponseView = memo(function OpenAIResponseView({
|
|
|
146
142
|
<OpenAIToolCallBlock key={call.id ?? `tc-${i}`} call={call} />
|
|
147
143
|
))}
|
|
148
144
|
{message?.function_call !== null && message?.function_call !== undefined && (
|
|
149
|
-
<div className="border border-
|
|
150
|
-
<div className="text-xs text-
|
|
145
|
+
<div className="border border-sky-400/20 rounded-md p-3 bg-muted/20">
|
|
146
|
+
<div className="text-xs text-sky-400/80 font-mono mb-1">function_call</div>
|
|
151
147
|
<div className="font-mono text-xs">
|
|
152
|
-
<span className="text-
|
|
148
|
+
<span className="text-foreground/80">{message.function_call.name}</span>
|
|
153
149
|
<span className="text-muted-foreground">({message.function_call.arguments})</span>
|
|
154
150
|
</div>
|
|
155
151
|
</div>
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export type ReplayMetricValue = number | boolean | null;
|
|
2
|
+
|
|
3
|
+
export type ReplayMetrics = {
|
|
4
|
+
status: number | null;
|
|
5
|
+
elapsedMs: number | null;
|
|
6
|
+
inputTokens: number | null;
|
|
7
|
+
outputTokens: number | null;
|
|
8
|
+
responseBytes: number | null;
|
|
9
|
+
streaming: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ReplayMetricComparison = {
|
|
13
|
+
id: "status" | "elapsed" | "input" | "output" | "bytes" | "streaming";
|
|
14
|
+
label: string;
|
|
15
|
+
original: ReplayMetricValue;
|
|
16
|
+
replay: ReplayMetricValue;
|
|
17
|
+
delta: number | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type JsonObject = Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
function isJsonObject(value: unknown): value is JsonObject {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function byteLength(value: string | null | undefined): number | null {
|
|
27
|
+
if (value === null || value === undefined) return null;
|
|
28
|
+
return new TextEncoder().encode(value).length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readRequestModel(body: string): string | null {
|
|
32
|
+
try {
|
|
33
|
+
const parsed: unknown = JSON.parse(body);
|
|
34
|
+
if (!isJsonObject(parsed)) return null;
|
|
35
|
+
const model = parsed["model"];
|
|
36
|
+
return typeof model === "string" && model.length > 0 ? model : null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function replaceRequestModel(
|
|
43
|
+
body: string,
|
|
44
|
+
model: string,
|
|
45
|
+
): { body: string; error: string | null } {
|
|
46
|
+
try {
|
|
47
|
+
const parsed: unknown = JSON.parse(body);
|
|
48
|
+
if (!isJsonObject(parsed)) {
|
|
49
|
+
return { body, error: "Request body must be a JSON object." };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
body: JSON.stringify({ ...parsed, model }, null, 2),
|
|
53
|
+
error: null,
|
|
54
|
+
};
|
|
55
|
+
} catch {
|
|
56
|
+
return { body, error: "Request body must be valid JSON before changing the replay model." };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildReplayMetrics(input: {
|
|
61
|
+
status: number | null | undefined;
|
|
62
|
+
elapsedMs: number | null | undefined;
|
|
63
|
+
inputTokens: number | null | undefined;
|
|
64
|
+
outputTokens: number | null | undefined;
|
|
65
|
+
responseText: string | null | undefined;
|
|
66
|
+
streaming: boolean | null | undefined;
|
|
67
|
+
}): ReplayMetrics {
|
|
68
|
+
return {
|
|
69
|
+
status: input.status ?? null,
|
|
70
|
+
elapsedMs: input.elapsedMs ?? null,
|
|
71
|
+
inputTokens: input.inputTokens ?? null,
|
|
72
|
+
outputTokens: input.outputTokens ?? null,
|
|
73
|
+
responseBytes: byteLength(input.responseText),
|
|
74
|
+
streaming: input.streaming === true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function numericDelta(original: number | null, replay: number | null): number | null {
|
|
79
|
+
if (original === null || replay === null) return null;
|
|
80
|
+
return replay - original;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function buildReplayComparisons(
|
|
84
|
+
original: ReplayMetrics,
|
|
85
|
+
replay: ReplayMetrics,
|
|
86
|
+
): ReplayMetricComparison[] {
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
id: "status",
|
|
90
|
+
label: "Status",
|
|
91
|
+
original: original.status,
|
|
92
|
+
replay: replay.status,
|
|
93
|
+
delta: numericDelta(original.status, replay.status),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "elapsed",
|
|
97
|
+
label: "Elapsed",
|
|
98
|
+
original: original.elapsedMs,
|
|
99
|
+
replay: replay.elapsedMs,
|
|
100
|
+
delta: numericDelta(original.elapsedMs, replay.elapsedMs),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "input",
|
|
104
|
+
label: "Input",
|
|
105
|
+
original: original.inputTokens,
|
|
106
|
+
replay: replay.inputTokens,
|
|
107
|
+
delta: numericDelta(original.inputTokens, replay.inputTokens),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "output",
|
|
111
|
+
label: "Output",
|
|
112
|
+
original: original.outputTokens,
|
|
113
|
+
replay: replay.outputTokens,
|
|
114
|
+
delta: numericDelta(original.outputTokens, replay.outputTokens),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "bytes",
|
|
118
|
+
label: "Bytes",
|
|
119
|
+
original: original.responseBytes,
|
|
120
|
+
replay: replay.responseBytes,
|
|
121
|
+
delta: numericDelta(original.responseBytes, replay.responseBytes),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "streaming",
|
|
125
|
+
label: "Stream",
|
|
126
|
+
original: original.streaming,
|
|
127
|
+
replay: replay.streaming,
|
|
128
|
+
delta: null,
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
}
|
|
@@ -5,6 +5,11 @@ const NAV_ACTION_ATTR = "data-nav-action";
|
|
|
5
5
|
|
|
6
6
|
export type NavAction = "toggle" | "expand" | "collapse";
|
|
7
7
|
|
|
8
|
+
type KeyboardNavigationOptions = {
|
|
9
|
+
/** When true, Arrow/WASD navigation works even when focus is on the page body. */
|
|
10
|
+
pageWide?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
8
13
|
function findNavItems(container: HTMLElement): HTMLElement[] {
|
|
9
14
|
return Array.from(container.querySelectorAll<HTMLElement>(`[${NAV_ATTR}]`));
|
|
10
15
|
}
|
|
@@ -41,18 +46,40 @@ function safeItemAt(items: HTMLElement[], index: number): HTMLElement | null {
|
|
|
41
46
|
|
|
42
47
|
function isEditableTarget(target: HTMLElement): boolean {
|
|
43
48
|
const tag = target.tagName;
|
|
44
|
-
if (tag === "INPUT" || tag === "TEXTAREA") return true;
|
|
49
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
45
50
|
if (target.isContentEditable) return true;
|
|
46
51
|
return false;
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
function isInteractiveTarget(target: HTMLElement): boolean {
|
|
55
|
+
const tag = target.tagName;
|
|
56
|
+
if (tag === "BUTTON" || tag === "A") return true;
|
|
57
|
+
if (isEditableTarget(target)) return true;
|
|
58
|
+
return target.closest("[role='button'],[role='menuitem'],[role='option']") !== null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isInOpenOverlay(target: HTMLElement): boolean {
|
|
62
|
+
if (target.closest("[role='dialog'],[role='menu'],[role='listbox']") !== null) return true;
|
|
63
|
+
return document.querySelector("[role='dialog'],[role='menu'],[role='listbox']") !== null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hasActiveTextSelection(): boolean {
|
|
67
|
+
const selection = window.getSelection();
|
|
68
|
+
if (selection === null) return false;
|
|
69
|
+
return selection.type === "Range" && selection.toString().length > 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isLetterNavigationKey(event: KeyboardEvent, key: "W" | "A" | "S" | "D"): boolean {
|
|
73
|
+
return event.key === key && event.shiftKey;
|
|
74
|
+
}
|
|
75
|
+
|
|
49
76
|
/**
|
|
50
77
|
* Enables keyboard navigation within a log-list container.
|
|
51
78
|
*
|
|
52
79
|
* Navigation keys:
|
|
53
|
-
* - ArrowUp / Shift+W : previous item
|
|
54
|
-
* - ArrowDown / Shift+S : next item
|
|
55
|
-
* - ArrowLeft / Shift+A : collapse or move to
|
|
80
|
+
* - ArrowUp / Shift+W : previous thread/turn/log item
|
|
81
|
+
* - ArrowDown / Shift+S : next thread/turn/log item
|
|
82
|
+
* - ArrowLeft / Shift+A : collapse or move to previous item
|
|
56
83
|
* - ArrowRight / Shift+D : expand or move to next item
|
|
57
84
|
* - Space : toggle expand/collapse
|
|
58
85
|
*
|
|
@@ -66,17 +93,20 @@ export function useKeyboardNavigation(
|
|
|
66
93
|
/** Ref to the outer focus-receiving wrapper (the one with tabIndex).
|
|
67
94
|
* Defaults to `containerRef` when omitted. */
|
|
68
95
|
wrapperRef?: React.RefObject<HTMLElement | null>,
|
|
96
|
+
options: KeyboardNavigationOptions = {},
|
|
69
97
|
): void {
|
|
70
98
|
const rootRef = wrapperRef ?? containerRef;
|
|
99
|
+
const pageWide = options.pageWide === true;
|
|
71
100
|
|
|
72
101
|
const handleFocusContainer = useCallback(
|
|
73
102
|
(e: FocusEvent) => {
|
|
74
103
|
const container = containerRef.current;
|
|
104
|
+
const root = rootRef.current;
|
|
75
105
|
if (!container) return;
|
|
106
|
+
if (!root) return;
|
|
76
107
|
const target = e.target;
|
|
77
108
|
if (!isFocusTarget(target)) return;
|
|
78
|
-
if (target !==
|
|
79
|
-
if (!container.contains(target)) return;
|
|
109
|
+
if (target !== root) return;
|
|
80
110
|
const items = findNavItems(container);
|
|
81
111
|
const first = safeItemAt(items, 0);
|
|
82
112
|
if (first !== null) focusAndScroll(first);
|
|
@@ -85,17 +115,27 @@ export function useKeyboardNavigation(
|
|
|
85
115
|
);
|
|
86
116
|
|
|
87
117
|
useEffect(() => {
|
|
88
|
-
const root = rootRef.current;
|
|
89
|
-
const container = containerRef.current;
|
|
90
|
-
if (!root || !container) return;
|
|
91
|
-
|
|
92
118
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
119
|
+
if (e.altKey || e.ctrlKey || e.metaKey) return;
|
|
120
|
+
|
|
121
|
+
const container = containerRef.current;
|
|
122
|
+
if (!container) return;
|
|
123
|
+
|
|
93
124
|
const target = e.target;
|
|
94
125
|
if (!isFocusTarget(target)) return;
|
|
95
126
|
|
|
96
|
-
if (!container.contains(target)) return;
|
|
97
|
-
|
|
98
127
|
if (isEditableTarget(target)) return;
|
|
128
|
+
if (hasActiveTextSelection()) return;
|
|
129
|
+
|
|
130
|
+
const isInsideContainer = container.contains(target);
|
|
131
|
+
if (isInsideContainer && isInteractiveTarget(target) && !target.hasAttribute(NAV_ATTR)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!isInsideContainer) {
|
|
136
|
+
if (!pageWide) return;
|
|
137
|
+
if (isInteractiveTarget(target) || isInOpenOverlay(target)) return;
|
|
138
|
+
}
|
|
99
139
|
|
|
100
140
|
const items = findNavItems(container);
|
|
101
141
|
if (items.length === 0) return;
|
|
@@ -108,7 +148,8 @@ export function useKeyboardNavigation(
|
|
|
108
148
|
switch (e.key) {
|
|
109
149
|
case "ArrowUp":
|
|
110
150
|
case "W": {
|
|
111
|
-
if (e.key === "
|
|
151
|
+
if (e.shiftKey && e.key === "ArrowUp") break;
|
|
152
|
+
if (e.key === "W" && !isLetterNavigationKey(e, "W")) break;
|
|
112
153
|
e.preventDefault();
|
|
113
154
|
const prevIdx =
|
|
114
155
|
currentIdx > 0 ? currentIdx - 1 : currentIdx === -1 ? items.length - 1 : -1;
|
|
@@ -121,7 +162,8 @@ export function useKeyboardNavigation(
|
|
|
121
162
|
}
|
|
122
163
|
case "ArrowDown":
|
|
123
164
|
case "S": {
|
|
124
|
-
if (e.key === "
|
|
165
|
+
if (e.shiftKey && e.key === "ArrowDown") break;
|
|
166
|
+
if (e.key === "S" && !isLetterNavigationKey(e, "S")) break;
|
|
125
167
|
e.preventDefault();
|
|
126
168
|
const nextIdx =
|
|
127
169
|
currentIdx < items.length - 1 ? currentIdx + 1 : currentIdx === -1 ? 0 : -1;
|
|
@@ -134,7 +176,8 @@ export function useKeyboardNavigation(
|
|
|
134
176
|
}
|
|
135
177
|
case "ArrowLeft":
|
|
136
178
|
case "A": {
|
|
137
|
-
if (e.key === "
|
|
179
|
+
if (e.shiftKey && e.key === "ArrowLeft") break;
|
|
180
|
+
if (e.key === "A" && !isLetterNavigationKey(e, "A")) break;
|
|
138
181
|
if (current === null) break;
|
|
139
182
|
e.preventDefault();
|
|
140
183
|
const action = getAction(current);
|
|
@@ -149,7 +192,8 @@ export function useKeyboardNavigation(
|
|
|
149
192
|
}
|
|
150
193
|
case "ArrowRight":
|
|
151
194
|
case "D": {
|
|
152
|
-
if (e.key === "
|
|
195
|
+
if (e.shiftKey && e.key === "ArrowRight") break;
|
|
196
|
+
if (e.key === "D" && !isLetterNavigationKey(e, "D")) break;
|
|
153
197
|
if (current === null) break;
|
|
154
198
|
e.preventDefault();
|
|
155
199
|
const action = getAction(current);
|
|
@@ -179,12 +223,10 @@ export function useKeyboardNavigation(
|
|
|
179
223
|
|
|
180
224
|
document.addEventListener("keydown", handleKeyDown, { capture: true });
|
|
181
225
|
return () => document.removeEventListener("keydown", handleKeyDown, { capture: true });
|
|
182
|
-
}, [containerRef, rootRef]);
|
|
226
|
+
}, [containerRef, pageWide, rootRef]);
|
|
183
227
|
|
|
184
228
|
useEffect(() => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return () => root.removeEventListener("focus", handleFocusContainer);
|
|
189
|
-
}, [handleFocusContainer, rootRef]);
|
|
229
|
+
document.addEventListener("focusin", handleFocusContainer);
|
|
230
|
+
return () => document.removeEventListener("focusin", handleFocusContainer);
|
|
231
|
+
}, [handleFocusContainer]);
|
|
190
232
|
}
|
|
@@ -40,6 +40,7 @@ export type ToolTraceEvent = {
|
|
|
40
40
|
index: number;
|
|
41
41
|
provider: "anthropic" | "openai";
|
|
42
42
|
name: string;
|
|
43
|
+
argumentsText: string | null;
|
|
43
44
|
argumentsPreview: string | null;
|
|
44
45
|
};
|
|
45
46
|
|
|
@@ -118,6 +119,13 @@ function previewValue(value: unknown): string | null {
|
|
|
118
119
|
: normalized;
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
function copyValue(value: unknown): string | null {
|
|
123
|
+
if (value === undefined || value === null) return null;
|
|
124
|
+
if (typeof value === "string") return value.length > 0 ? value : null;
|
|
125
|
+
const raw = JSON.stringify(value, null, 2);
|
|
126
|
+
return raw === undefined || raw.length === 0 ? null : raw;
|
|
127
|
+
}
|
|
128
|
+
|
|
121
129
|
function extractAnthropicToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
122
130
|
const parsed = parseJsonResponse(log.responseText);
|
|
123
131
|
const content = safeGetOwnProperty(parsed, "content");
|
|
@@ -129,13 +137,15 @@ function extractAnthropicToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
|
129
137
|
if (type !== "tool_use") continue;
|
|
130
138
|
const name = safeGetOwnProperty(block, "name");
|
|
131
139
|
if (typeof name !== "string" || name.length === 0) continue;
|
|
140
|
+
const input = safeGetOwnProperty(block, "input");
|
|
132
141
|
events.push({
|
|
133
142
|
id: `${String(log.id)}-anthropic-tool-${String(events.length)}`,
|
|
134
143
|
logId: log.id,
|
|
135
144
|
index: events.length,
|
|
136
145
|
provider: "anthropic",
|
|
137
146
|
name,
|
|
138
|
-
|
|
147
|
+
argumentsText: copyValue(input),
|
|
148
|
+
argumentsPreview: previewValue(input),
|
|
139
149
|
});
|
|
140
150
|
}
|
|
141
151
|
return events;
|
|
@@ -155,13 +165,15 @@ function extractOpenAIToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
|
155
165
|
const fn = safeGetOwnProperty(call, "function");
|
|
156
166
|
const name = safeGetOwnProperty(fn, "name");
|
|
157
167
|
if (typeof name !== "string" || name.length === 0) continue;
|
|
168
|
+
const args = safeGetOwnProperty(fn, "arguments");
|
|
158
169
|
events.push({
|
|
159
170
|
id: `${String(log.id)}-openai-tool-${String(events.length)}`,
|
|
160
171
|
logId: log.id,
|
|
161
172
|
index: events.length,
|
|
162
173
|
provider: "openai",
|
|
163
174
|
name,
|
|
164
|
-
|
|
175
|
+
argumentsText: copyValue(args),
|
|
176
|
+
argumentsPreview: previewValue(args),
|
|
165
177
|
});
|
|
166
178
|
}
|
|
167
179
|
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import type { KnowledgeCandidate } from "./types";
|
|
1
|
+
import type { KnowledgeCandidate, KnowledgeCandidateType } from "./types";
|
|
2
|
+
import { redactCandidate } from "./redactor";
|
|
3
|
+
|
|
4
|
+
export type CandidateDraftUpdate = {
|
|
5
|
+
type?: KnowledgeCandidateType;
|
|
6
|
+
title?: string;
|
|
7
|
+
content?: string;
|
|
8
|
+
tags?: string[];
|
|
9
|
+
};
|
|
2
10
|
|
|
3
11
|
const candidates = new Map<string, KnowledgeCandidate>();
|
|
4
12
|
|
|
@@ -58,6 +66,29 @@ export function markCandidateFailed(id: string, error: string): KnowledgeCandida
|
|
|
58
66
|
return updated;
|
|
59
67
|
}
|
|
60
68
|
|
|
69
|
+
export function updateCandidateDraft(
|
|
70
|
+
id: string,
|
|
71
|
+
updates: CandidateDraftUpdate,
|
|
72
|
+
): KnowledgeCandidate | null {
|
|
73
|
+
const existing = candidates.get(id);
|
|
74
|
+
if (existing === undefined) return null;
|
|
75
|
+
if (existing.status === "promoted") return null;
|
|
76
|
+
|
|
77
|
+
const updated = redactCandidate({
|
|
78
|
+
...existing,
|
|
79
|
+
type: updates.type ?? existing.type,
|
|
80
|
+
title: updates.title ?? existing.title,
|
|
81
|
+
content: updates.content ?? existing.content,
|
|
82
|
+
tags: updates.tags ?? existing.tags,
|
|
83
|
+
status: "draft",
|
|
84
|
+
error: null,
|
|
85
|
+
openClawMemoryId: null,
|
|
86
|
+
updatedAt: new Date().toISOString(),
|
|
87
|
+
});
|
|
88
|
+
candidates.set(id, updated);
|
|
89
|
+
return updated;
|
|
90
|
+
}
|
|
91
|
+
|
|
61
92
|
export function _resetCandidatesForTests(): void {
|
|
62
93
|
candidates.clear();
|
|
63
94
|
}
|