@tonyclaw/agent-inspector 2.0.3 → 2.0.4
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-BCH_fsLm.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-D85_UANk.js +101 -0
- package/.output/public/assets/{ReplayDialog-CxUk_TF0.js → ReplayDialog-DTeaHHit.js} +1 -1
- package/.output/public/assets/RequestAnatomy-DZ8grAih.js +1 -0
- package/.output/public/assets/ResponseView-Cldm6RCi.js +1 -0
- package/.output/public/assets/{StreamingChunkSequence-DHk4SGGL.js → StreamingChunkSequence-3x4p-yT7.js} +1 -1
- package/.output/public/assets/_sessionId-YqWFBu6d.js +1 -0
- package/.output/public/assets/index-BIw2H6jO.js +1 -0
- package/.output/public/assets/index-CobXD0yH.css +1 -0
- package/.output/public/assets/{json-viewer-BbU0n8eM.js → json-viewer-BrzjD7qI.js} +1 -1
- package/.output/public/assets/{main-CZT_F-gu.js → main-mgxeUdZQ.js} +2 -2
- package/.output/server/{_sessionId-B-s9P7fJ.mjs → _sessionId-C4xsxIWm.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-C08L3UOO.mjs → CompareDrawer-DuWEpqQ7.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-CMWl3Ijy.mjs → ProxyViewerContainer-Cckz5qKu.mjs} +167 -87
- package/.output/server/_ssr/{ReplayDialog-CPDo9_G5.mjs → ReplayDialog-BDRcr8E5.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-D9wt_K1E.mjs → RequestAnatomy-BoO2_Ij0.mjs} +4 -4
- package/.output/server/_ssr/{ResponseView-DXaL7nY3.mjs → ResponseView-DZiPBxvO.mjs} +20 -16
- package/.output/server/_ssr/{StreamingChunkSequence-B_hudZyb.mjs → StreamingChunkSequence-D-be7KEL.mjs} +3 -3
- package/.output/server/_ssr/{index-CuE_BN86.mjs → index-5RImHKfu.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-Ci6kkjde.mjs → json-viewer-aJhb93ZK.mjs} +2 -2
- package/.output/server/_ssr/{router-BemxgIg7.mjs → router-Dgkv5nKP.mjs} +22 -14
- package/.output/server/{_tanstack-start-manifest_v--L1_b4sd.mjs → _tanstack-start-manifest_v-B8rrWXjr.mjs} +1 -1
- package/.output/server/index.mjs +61 -61
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +25 -15
- package/src/components/ProxyViewerContainer.tsx +2 -1
- package/src/components/providers/SettingsDialog.tsx +45 -1
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +103 -45
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +16 -0
- 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 +9 -14
- package/src/components/proxy-viewer/ResponseView.tsx +2 -6
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +3 -4
- package/src/components/proxy-viewer/TurnGroup.tsx +4 -0
- 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/openai/ResponseView.tsx +10 -14
- 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/.output/public/assets/ProxyViewerContainer-Da0jpBkp.js +0 -101
- package/.output/public/assets/RequestAnatomy-DIlzjgjJ.js +0 -1
- package/.output/public/assets/ResponseView-DQCuKJ1G.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
|
@@ -6,7 +6,11 @@ import { Button } from "../ui/button";
|
|
|
6
6
|
import { ProvidersPanel } from "./ProvidersPanel";
|
|
7
7
|
import { useProviders } from "../../lib/useProviders";
|
|
8
8
|
import { useStripConfig } from "../../lib/useStripConfig";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
MAX_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
11
|
+
TimeDisplayFormatSchema,
|
|
12
|
+
type TimeDisplayFormat,
|
|
13
|
+
} from "../../lib/runtimeConfig";
|
|
10
14
|
|
|
11
15
|
export function SettingsDialog(): JSX.Element {
|
|
12
16
|
const [open, setOpen] = useState(false);
|
|
@@ -100,9 +104,11 @@ function ProxySettingsTab(): JSX.Element {
|
|
|
100
104
|
const {
|
|
101
105
|
strip,
|
|
102
106
|
slowResponseThresholdSeconds,
|
|
107
|
+
timeDisplayFormat,
|
|
103
108
|
isLoading,
|
|
104
109
|
setStrip,
|
|
105
110
|
setSlowResponseThresholdSeconds,
|
|
111
|
+
setTimeDisplayFormat,
|
|
106
112
|
} = useStripConfig();
|
|
107
113
|
const [error, setError] = useState<string | null>(null);
|
|
108
114
|
const [pending, setPending] = useState(false);
|
|
@@ -137,6 +143,21 @@ function ProxySettingsTab(): JSX.Element {
|
|
|
137
143
|
[setSlowResponseThresholdSeconds],
|
|
138
144
|
);
|
|
139
145
|
|
|
146
|
+
const handleTimeDisplayFormatChange = useCallback(
|
|
147
|
+
async (next: TimeDisplayFormat) => {
|
|
148
|
+
setError(null);
|
|
149
|
+
setPending(true);
|
|
150
|
+
try {
|
|
151
|
+
await setTimeDisplayFormat(next);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
154
|
+
} finally {
|
|
155
|
+
setPending(false);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
[setTimeDisplayFormat],
|
|
159
|
+
);
|
|
160
|
+
|
|
140
161
|
return (
|
|
141
162
|
<div className="space-y-4">
|
|
142
163
|
<div className="space-y-1">
|
|
@@ -196,6 +217,29 @@ function ProxySettingsTab(): JSX.Element {
|
|
|
196
217
|
</div>
|
|
197
218
|
</div>
|
|
198
219
|
|
|
220
|
+
<div className="space-y-1">
|
|
221
|
+
<label htmlFor="time-display-format" className="text-sm font-semibold">
|
|
222
|
+
Time display
|
|
223
|
+
</label>
|
|
224
|
+
<p className="text-xs text-muted-foreground">
|
|
225
|
+
Controls timestamps in session summaries, conversation headers, and log rows.
|
|
226
|
+
</p>
|
|
227
|
+
<select
|
|
228
|
+
id="time-display-format"
|
|
229
|
+
value={timeDisplayFormat}
|
|
230
|
+
disabled={isLoading || pending}
|
|
231
|
+
onChange={(event) => {
|
|
232
|
+
const parsed = TimeDisplayFormatSchema.safeParse(event.currentTarget.value);
|
|
233
|
+
if (!parsed.success) return;
|
|
234
|
+
void handleTimeDisplayFormatChange(parsed.data);
|
|
235
|
+
}}
|
|
236
|
+
className="h-8 rounded-md border border-input bg-background px-2 text-sm disabled:cursor-not-allowed disabled:opacity-50"
|
|
237
|
+
>
|
|
238
|
+
<option value="time">Time only</option>
|
|
239
|
+
<option value="full">Full ISO</option>
|
|
240
|
+
</select>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
199
243
|
{error !== null && <p className="text-xs text-destructive">Failed to save: {error}</p>}
|
|
200
244
|
</div>
|
|
201
245
|
);
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { useCallback, useMemo, useState, type JSX } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AlertTriangle,
|
|
4
|
+
Brain,
|
|
5
|
+
ChevronDown,
|
|
6
|
+
ChevronRight,
|
|
7
|
+
Clock,
|
|
8
|
+
Loader2,
|
|
9
|
+
MessageSquare,
|
|
10
|
+
Wrench,
|
|
11
|
+
Zap,
|
|
12
|
+
} from "lucide-react";
|
|
3
13
|
import { z } from "zod";
|
|
14
|
+
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
15
|
+
import { formatTimestampRange } from "../../lib/timeDisplay";
|
|
4
16
|
import { formatTokens } from "../../lib/utils";
|
|
5
17
|
import { parseJsonResponse, readApiError } from "../../lib/apiClient";
|
|
6
18
|
import { KnowledgeCandidateSchema, type KnowledgeCandidate } from "../../knowledge/types";
|
|
@@ -23,6 +35,8 @@ type AgentTraceSummaryProps = {
|
|
|
23
35
|
logs: CapturedLog[];
|
|
24
36
|
scopeId: string;
|
|
25
37
|
slowResponseThresholdSeconds: number;
|
|
38
|
+
showRollupMetrics: boolean;
|
|
39
|
+
timeDisplayFormat: TimeDisplayFormat;
|
|
26
40
|
};
|
|
27
41
|
|
|
28
42
|
function formatElapsed(ms: number | null): string {
|
|
@@ -31,25 +45,30 @@ function formatElapsed(ms: number | null): string {
|
|
|
31
45
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
32
46
|
}
|
|
33
47
|
|
|
34
|
-
function formatTimeRange(
|
|
48
|
+
function formatTimeRange(
|
|
49
|
+
startedAt: string | null,
|
|
50
|
+
endedAt: string | null,
|
|
51
|
+
timeDisplayFormat: TimeDisplayFormat,
|
|
52
|
+
): string | null {
|
|
35
53
|
if (startedAt === null || endedAt === null) return null;
|
|
36
|
-
|
|
37
|
-
new Date(iso).toLocaleTimeString([], {
|
|
38
|
-
hour: "2-digit",
|
|
39
|
-
minute: "2-digit",
|
|
40
|
-
second: "2-digit",
|
|
41
|
-
});
|
|
42
|
-
return `${format(startedAt)} - ${format(endedAt)}`;
|
|
54
|
+
return formatTimestampRange(startedAt, endedAt, timeDisplayFormat);
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
function
|
|
46
|
-
|
|
57
|
+
function formatCandidateCount(count: number): string {
|
|
58
|
+
return `${String(count)} candidate${count === 1 ? "" : "s"}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getLogAnchor(logId: number): string {
|
|
62
|
+
return `log-${String(logId)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function jumpToLog(logId: number): void {
|
|
66
|
+
const anchor = getLogAnchor(logId);
|
|
67
|
+
const target = document.getElementById(anchor);
|
|
68
|
+
window.history.replaceState(null, "", `#${anchor}`);
|
|
47
69
|
if (!(target instanceof HTMLElement)) return;
|
|
48
70
|
target.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
49
71
|
target.focus({ preventScroll: true });
|
|
50
|
-
if (target.getAttribute("data-nav-action") === "expand") {
|
|
51
|
-
target.click();
|
|
52
|
-
}
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
function CandidateList({ candidates }: { candidates: KnowledgeCandidate[] }): JSX.Element | null {
|
|
@@ -74,14 +93,18 @@ function CandidateList({ candidates }: { candidates: KnowledgeCandidate[] }): JS
|
|
|
74
93
|
</div>
|
|
75
94
|
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
|
76
95
|
{candidate.logIds.map((logId) => (
|
|
77
|
-
<
|
|
96
|
+
<a
|
|
78
97
|
key={logId}
|
|
79
|
-
|
|
80
|
-
onClick={() =>
|
|
81
|
-
|
|
98
|
+
href={`#${getLogAnchor(logId)}`}
|
|
99
|
+
onClick={(event) => {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
jumpToLog(logId);
|
|
102
|
+
}}
|
|
103
|
+
className="rounded border border-blue-400/25 px-1.5 py-0.5 font-mono text-[10px] text-blue-400 underline-offset-2 transition-colors hover:bg-blue-400/10 hover:text-blue-300 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
104
|
+
aria-label={`Jump to evidence log ${String(logId)}`}
|
|
82
105
|
>
|
|
83
106
|
#{logId}
|
|
84
|
-
</
|
|
107
|
+
</a>
|
|
85
108
|
))}
|
|
86
109
|
</div>
|
|
87
110
|
</div>
|
|
@@ -94,19 +117,24 @@ export function AgentTraceSummary({
|
|
|
94
117
|
logs,
|
|
95
118
|
scopeId,
|
|
96
119
|
slowResponseThresholdSeconds,
|
|
120
|
+
showRollupMetrics,
|
|
121
|
+
timeDisplayFormat,
|
|
97
122
|
}: AgentTraceSummaryProps): JSX.Element | null {
|
|
98
123
|
const [candidates, setCandidates] = useState<KnowledgeCandidate[]>([]);
|
|
124
|
+
const [candidatesExpanded, setCandidatesExpanded] = useState(true);
|
|
99
125
|
const [candidateState, setCandidateState] = useState<CandidateLoadState>({
|
|
100
126
|
status: "idle",
|
|
101
127
|
error: null,
|
|
102
128
|
});
|
|
129
|
+
const hasCandidates = candidates.length > 0;
|
|
103
130
|
const summary = useMemo(
|
|
104
131
|
() => buildTraceSummary(logs, slowResponseThresholdSeconds, candidates.length),
|
|
105
132
|
[candidates.length, logs, slowResponseThresholdSeconds],
|
|
106
133
|
);
|
|
134
|
+
const showElapsedSummary = showRollupMetrics || summary.maxElapsedMs !== null;
|
|
107
135
|
const timeRange = useMemo(
|
|
108
|
-
() => formatTimeRange(summary.startedAt, summary.endedAt),
|
|
109
|
-
[summary.endedAt, summary.startedAt],
|
|
136
|
+
() => formatTimeRange(summary.startedAt, summary.endedAt, timeDisplayFormat),
|
|
137
|
+
[summary.endedAt, summary.startedAt, timeDisplayFormat],
|
|
110
138
|
);
|
|
111
139
|
|
|
112
140
|
const createCandidates = useCallback(() => {
|
|
@@ -128,6 +156,7 @@ export function AgentTraceSummary({
|
|
|
128
156
|
}
|
|
129
157
|
const parsed = await parseJsonResponse(response, CandidateResponseSchema);
|
|
130
158
|
setCandidates(parsed.candidates);
|
|
159
|
+
setCandidatesExpanded(parsed.candidates.length > 0);
|
|
131
160
|
setCandidateState({ status: "ready", error: null });
|
|
132
161
|
} catch (error) {
|
|
133
162
|
setCandidateState({
|
|
@@ -143,20 +172,24 @@ export function AgentTraceSummary({
|
|
|
143
172
|
return (
|
|
144
173
|
<section className="mb-2 rounded-lg border border-border bg-muted/10 px-3 py-2">
|
|
145
174
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-xs">
|
|
146
|
-
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
175
|
+
{showRollupMetrics && (
|
|
176
|
+
<span className="inline-flex items-center gap-1.5 font-semibold text-foreground">
|
|
177
|
+
<MessageSquare className="size-3.5 text-blue-400" />
|
|
178
|
+
{summary.llmCallCount} LLM
|
|
179
|
+
</span>
|
|
180
|
+
)}
|
|
150
181
|
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
151
|
-
<Wrench className="size-3.5 text-
|
|
182
|
+
<Wrench className="size-3.5 text-sky-400/70" />
|
|
152
183
|
{summary.toolCallCount} tools
|
|
153
184
|
</span>
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
185
|
+
{showRollupMetrics && (
|
|
186
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
187
|
+
<Zap className="size-3.5 text-emerald-400" />
|
|
188
|
+
<span className="font-mono">
|
|
189
|
+
{formatTokens(summary.totalInputTokens)} / {formatTokens(summary.totalOutputTokens)}
|
|
190
|
+
</span>
|
|
158
191
|
</span>
|
|
159
|
-
|
|
192
|
+
)}
|
|
160
193
|
{(summary.totalCacheCreationInputTokens > 0 || summary.totalCacheReadInputTokens > 0) && (
|
|
161
194
|
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
162
195
|
<Zap className="size-3.5 text-purple-400" />
|
|
@@ -166,16 +199,20 @@ export function AgentTraceSummary({
|
|
|
166
199
|
</span>
|
|
167
200
|
</span>
|
|
168
201
|
)}
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
202
|
+
{showElapsedSummary && (
|
|
203
|
+
<span className="inline-flex items-center gap-1.5 text-muted-foreground">
|
|
204
|
+
<Clock className="size-3.5" />
|
|
205
|
+
{showRollupMetrics && (
|
|
206
|
+
<span className="font-mono">{formatElapsed(summary.totalElapsedMs)}</span>
|
|
207
|
+
)}
|
|
208
|
+
{summary.maxElapsedMs !== null && (
|
|
209
|
+
<span className="font-mono text-muted-foreground/70">
|
|
210
|
+
max {formatElapsed(summary.maxElapsedMs)}
|
|
211
|
+
</span>
|
|
212
|
+
)}
|
|
213
|
+
</span>
|
|
214
|
+
)}
|
|
215
|
+
{showRollupMetrics && timeRange !== null && (
|
|
179
216
|
<span className="font-mono text-muted-foreground/70">{timeRange}</span>
|
|
180
217
|
)}
|
|
181
218
|
{(summary.failedCallCount > 0 ||
|
|
@@ -190,9 +227,11 @@ export function AgentTraceSummary({
|
|
|
190
227
|
</span>
|
|
191
228
|
)}
|
|
192
229
|
<span className="flex-1" />
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
230
|
+
{hasCandidates && (
|
|
231
|
+
<Badge variant="outline" className="h-6 px-2 text-[10px] font-mono">
|
|
232
|
+
{formatCandidateCount(summary.knowledgeCandidateCount)}
|
|
233
|
+
</Badge>
|
|
234
|
+
)}
|
|
196
235
|
<Button
|
|
197
236
|
type="button"
|
|
198
237
|
variant="outline"
|
|
@@ -208,11 +247,30 @@ export function AgentTraceSummary({
|
|
|
208
247
|
)}
|
|
209
248
|
Candidate
|
|
210
249
|
</Button>
|
|
250
|
+
{hasCandidates && (
|
|
251
|
+
<Button
|
|
252
|
+
type="button"
|
|
253
|
+
variant="ghost"
|
|
254
|
+
size="icon"
|
|
255
|
+
className="size-7 text-muted-foreground"
|
|
256
|
+
onClick={() => setCandidatesExpanded((value) => !value)}
|
|
257
|
+
aria-expanded={candidatesExpanded}
|
|
258
|
+
aria-label={
|
|
259
|
+
candidatesExpanded ? "Collapse memory candidates" : "Expand memory candidates"
|
|
260
|
+
}
|
|
261
|
+
>
|
|
262
|
+
{candidatesExpanded ? (
|
|
263
|
+
<ChevronDown className="size-3.5" />
|
|
264
|
+
) : (
|
|
265
|
+
<ChevronRight className="size-3.5" />
|
|
266
|
+
)}
|
|
267
|
+
</Button>
|
|
268
|
+
)}
|
|
211
269
|
</div>
|
|
212
270
|
{candidateState.status === "failed" && (
|
|
213
271
|
<p className="mt-2 text-xs text-destructive">{candidateState.error}</p>
|
|
214
272
|
)}
|
|
215
|
-
<CandidateList candidates={candidates} />
|
|
273
|
+
{candidatesExpanded && <CandidateList candidates={candidates} />}
|
|
216
274
|
</section>
|
|
217
275
|
);
|
|
218
276
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import ReactMarkdown from "react-markdown";
|
|
3
|
+
|
|
4
|
+
const ANSWER_MARKDOWN_CLASS =
|
|
5
|
+
"prose prose-sm dark:prose-invert max-w-none text-[13px] leading-[1.65] " +
|
|
6
|
+
"[&_pre]:bg-muted [&_pre]:text-foreground [&_code]:text-[0.8em] " +
|
|
7
|
+
"[&_p]:my-1 [&_p]:leading-[1.65] [&_ul]:my-1 [&_ol]:my-1 [&_li]:my-0.5 " +
|
|
8
|
+
"[&_li]:leading-[1.6]";
|
|
9
|
+
|
|
10
|
+
export function AnswerMarkdown({ text }: { text: string }): JSX.Element {
|
|
11
|
+
return (
|
|
12
|
+
<div className={ANSWER_MARKDOWN_CLASS}>
|
|
13
|
+
<ReactMarkdown>{text}</ReactMarkdown>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, memo, useMemo } from "react";
|
|
2
2
|
import type { JSX } from "react";
|
|
3
|
+
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
3
4
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
4
5
|
import {
|
|
5
6
|
ConversationHeader,
|
|
@@ -29,6 +30,10 @@ export type ConversationGroupProps = {
|
|
|
29
30
|
comparisonPredecessors: Map<number, CapturedLog>;
|
|
30
31
|
/** When true, skip the group header and render content directly. */
|
|
31
32
|
standalone?: boolean;
|
|
33
|
+
/** When true, the route-level session context already shows rollup metrics. */
|
|
34
|
+
hasPinnedSessionContext?: boolean;
|
|
35
|
+
/** Controls whether timestamps render as compact local time or full ISO strings. */
|
|
36
|
+
timeDisplayFormat: TimeDisplayFormat;
|
|
32
37
|
/** Clear all logs that belong to this group. */
|
|
33
38
|
onClearGroup: (ids: number[]) => void;
|
|
34
39
|
};
|
|
@@ -56,6 +61,8 @@ export const ConversationGroup = memo(function ({
|
|
|
56
61
|
comparisonPredecessors,
|
|
57
62
|
onClearGroup,
|
|
58
63
|
standalone = false,
|
|
64
|
+
hasPinnedSessionContext = false,
|
|
65
|
+
timeDisplayFormat,
|
|
59
66
|
}: ConversationGroupProps): JSX.Element {
|
|
60
67
|
const [expanded, setExpanded] = useState(false);
|
|
61
68
|
const stats = useMemo(() => computeStats(group.logs), [group.logs]);
|
|
@@ -63,6 +70,7 @@ export const ConversationGroup = memo(function ({
|
|
|
63
70
|
const endTime = group.logs[group.logs.length - 1]?.timestamp ?? new Date().toISOString();
|
|
64
71
|
const mixed = hasMixedApiFormat(group.logs);
|
|
65
72
|
const isLoading = group.logs.some((log) => log.responseStatus === null);
|
|
73
|
+
const showTraceRollupMetrics = standalone && !hasPinnedSessionContext;
|
|
66
74
|
|
|
67
75
|
// Pre-compute stop reasons for each log — used by turnIndices
|
|
68
76
|
const turnGroups = useMemo(() => buildTurnGroups(group.logs), [group.logs]);
|
|
@@ -82,6 +90,7 @@ export const ConversationGroup = memo(function ({
|
|
|
82
90
|
hideApiFormat={mixed}
|
|
83
91
|
isLoading={isLoading}
|
|
84
92
|
userAgent={group.logs[0]?.userAgent ?? null}
|
|
93
|
+
timeDisplayFormat={timeDisplayFormat}
|
|
85
94
|
onClear={() => onClearGroup(group.logs.map((l) => l.id))}
|
|
86
95
|
/>
|
|
87
96
|
)}
|
|
@@ -92,6 +101,8 @@ export const ConversationGroup = memo(function ({
|
|
|
92
101
|
logs={group.logs}
|
|
93
102
|
scopeId={group.conversationId}
|
|
94
103
|
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
104
|
+
showRollupMetrics={showTraceRollupMetrics}
|
|
105
|
+
timeDisplayFormat={timeDisplayFormat}
|
|
95
106
|
/>
|
|
96
107
|
{turnGroups.map((tg) => (
|
|
97
108
|
<TurnGroup
|
|
@@ -100,6 +111,7 @@ export const ConversationGroup = memo(function ({
|
|
|
100
111
|
viewMode={viewMode}
|
|
101
112
|
strip={strip}
|
|
102
113
|
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
114
|
+
timeDisplayFormat={timeDisplayFormat}
|
|
103
115
|
cacheTrends={cacheTrends}
|
|
104
116
|
onCompareWithPrevious={onCompareWithPrevious}
|
|
105
117
|
comparisonPredecessors={comparisonPredecessors}
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
} from "lucide-react";
|
|
13
13
|
import type { JSX } from "react";
|
|
14
14
|
import { getSessionPath } from "../../lib/sessionUrl";
|
|
15
|
+
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
16
|
+
import { formatTimestampRange } from "../../lib/timeDisplay";
|
|
15
17
|
import { cn, formatTokens } from "../../lib/utils";
|
|
16
18
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
17
19
|
import { Badge } from "../ui/badge";
|
|
@@ -43,16 +45,13 @@ export type ConversationHeaderProps = {
|
|
|
43
45
|
isLoading?: boolean;
|
|
44
46
|
/** User-Agent string from the first log in the group. */
|
|
45
47
|
userAgent?: string | null;
|
|
48
|
+
/** Controls whether timestamps render as compact local time or full ISO strings. */
|
|
49
|
+
timeDisplayFormat: TimeDisplayFormat;
|
|
46
50
|
/** Clear all logs in this group. After confirmation the parent removes them
|
|
47
51
|
* from the in-memory store; this header is then unmounted. */
|
|
48
52
|
onClear?: () => void;
|
|
49
53
|
};
|
|
50
54
|
|
|
51
|
-
function formatTimestamp(iso: string): string {
|
|
52
|
-
const date = new Date(iso);
|
|
53
|
-
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
55
|
export function ConversationHeader({
|
|
57
56
|
conversationId,
|
|
58
57
|
startTime,
|
|
@@ -66,6 +65,7 @@ export function ConversationHeader({
|
|
|
66
65
|
hideApiFormat = false,
|
|
67
66
|
isLoading = false,
|
|
68
67
|
userAgent,
|
|
68
|
+
timeDisplayFormat,
|
|
69
69
|
onClear,
|
|
70
70
|
}: ConversationHeaderProps): JSX.Element {
|
|
71
71
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
@@ -157,7 +157,7 @@ export function ConversationHeader({
|
|
|
157
157
|
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
158
158
|
<Clock className="size-3" />
|
|
159
159
|
<span className="font-mono tabular-nums">
|
|
160
|
-
{
|
|
160
|
+
{formatTimestampRange(startTime, endTime, timeDisplayFormat)}
|
|
161
161
|
</span>
|
|
162
162
|
</span>
|
|
163
163
|
|
|
@@ -2,6 +2,7 @@ import { GitCompareArrows } from "lucide-react";
|
|
|
2
2
|
import { Suspense, type JSX } from "react";
|
|
3
3
|
import { useMemo, useRef, useState, memo } from "react";
|
|
4
4
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
5
|
+
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
5
6
|
import { stripClaudeCodeBillingHeader } from "../../proxy/claudeCodeStrip";
|
|
6
7
|
import { Button } from "../ui/button";
|
|
7
8
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
@@ -48,6 +49,8 @@ export type LogEntryProps = {
|
|
|
48
49
|
strip: boolean;
|
|
49
50
|
/** Slow-response threshold in seconds. `0` disables the warning indicator. */
|
|
50
51
|
slowResponseThresholdSeconds: number;
|
|
52
|
+
/** Controls whether timestamps render as compact local time or full ISO strings. */
|
|
53
|
+
timeDisplayFormat: TimeDisplayFormat;
|
|
51
54
|
/**
|
|
52
55
|
* Per-log cache token trend, looked up in the viewer-level trend map.
|
|
53
56
|
* `null` (or absent) means the header should render with no arrows.
|
|
@@ -119,6 +122,7 @@ export const LogEntry = memo(function ({
|
|
|
119
122
|
viewMode = "simple",
|
|
120
123
|
strip,
|
|
121
124
|
slowResponseThresholdSeconds,
|
|
125
|
+
timeDisplayFormat,
|
|
122
126
|
cacheTrend = null,
|
|
123
127
|
onCompareWithPrevious,
|
|
124
128
|
}: LogEntryProps): JSX.Element {
|
|
@@ -135,10 +139,6 @@ export const LogEntry = memo(function ({
|
|
|
135
139
|
() => adapter.analyzeRequest(log.rawRequestBody),
|
|
136
140
|
[adapter, log.rawRequestBody],
|
|
137
141
|
);
|
|
138
|
-
const responseAnalysis = useMemo(
|
|
139
|
-
() => adapter.analyzeResponse(log.responseText),
|
|
140
|
-
[adapter, log.responseText],
|
|
141
|
-
);
|
|
142
142
|
const strippedRequestBody = useMemo(() => {
|
|
143
143
|
if (!strip || resolvedFormat !== "anthropic" || log.rawRequestBody === null) {
|
|
144
144
|
return null;
|
|
@@ -296,9 +296,9 @@ export const LogEntry = memo(function ({
|
|
|
296
296
|
toolCount={requestAnalysis.toolCount}
|
|
297
297
|
expanded={expanded}
|
|
298
298
|
onToggle={() => setExpanded(!expanded)}
|
|
299
|
-
responseToolNames={responseAnalysis.toolNames}
|
|
300
299
|
cacheTrend={cacheTrend}
|
|
301
300
|
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
301
|
+
timeDisplayFormat={timeDisplayFormat}
|
|
302
302
|
activeTab={activeTab}
|
|
303
303
|
tabActions={tabActions}
|
|
304
304
|
onReplay={
|
|
@@ -22,7 +22,9 @@ import {
|
|
|
22
22
|
Zap,
|
|
23
23
|
} from "lucide-react";
|
|
24
24
|
import type { JSX, MouseEvent } from "react";
|
|
25
|
-
import { memo
|
|
25
|
+
import { memo } from "react";
|
|
26
|
+
import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
|
|
27
|
+
import { formatTimestamp } from "../../lib/timeDisplay";
|
|
26
28
|
import { cn, formatTokens, getStatusCategory, type StatusCategory } from "../../lib/utils";
|
|
27
29
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
28
30
|
import { Badge } from "../ui/badge";
|
|
@@ -105,8 +107,6 @@ export type LogEntryHeaderProps = {
|
|
|
105
107
|
toolCount?: number | null;
|
|
106
108
|
expanded: boolean;
|
|
107
109
|
onToggle: () => void;
|
|
108
|
-
/** Tool call names extracted from the model response (e.g., ["read_file", "grep"]). */
|
|
109
|
-
responseToolNames?: string[] | null;
|
|
110
110
|
/** Per-log cache token trend (creation + read) relative to the previous log
|
|
111
111
|
* in the same conversation group. When `undefined` or a field is `null`,
|
|
112
112
|
* the corresponding cache span renders as it did before — no arrow.
|
|
@@ -124,6 +124,8 @@ export type LogEntryHeaderProps = {
|
|
|
124
124
|
onReplay?: () => void;
|
|
125
125
|
/** Slow-response threshold in seconds. `0` disables the warning indicator. */
|
|
126
126
|
slowResponseThresholdSeconds?: number;
|
|
127
|
+
/** Controls whether timestamps render as compact local time or full ISO strings. */
|
|
128
|
+
timeDisplayFormat: TimeDisplayFormat;
|
|
127
129
|
};
|
|
128
130
|
|
|
129
131
|
export const LogEntryHeader = memo(function ({
|
|
@@ -132,12 +134,12 @@ export const LogEntryHeader = memo(function ({
|
|
|
132
134
|
toolCount = null,
|
|
133
135
|
expanded,
|
|
134
136
|
onToggle,
|
|
135
|
-
responseToolNames = null,
|
|
136
137
|
cacheTrend = null,
|
|
137
138
|
activeTab,
|
|
138
139
|
tabActions,
|
|
139
140
|
onReplay,
|
|
140
141
|
slowResponseThresholdSeconds = 0,
|
|
142
|
+
timeDisplayFormat,
|
|
141
143
|
}: LogEntryHeaderProps): JSX.Element {
|
|
142
144
|
const statusCategory = getStatusCategory(log.responseStatus);
|
|
143
145
|
const isSlowResponse =
|
|
@@ -146,7 +148,6 @@ export const LogEntryHeader = memo(function ({
|
|
|
146
148
|
log.elapsedMs > slowResponseThresholdSeconds * 1000;
|
|
147
149
|
|
|
148
150
|
const hasTokens = log.inputTokens !== null || log.outputTokens !== null;
|
|
149
|
-
const toolNamesJoined = useMemo(() => responseToolNames?.join(", ") ?? null, [responseToolNames]);
|
|
150
151
|
|
|
151
152
|
return (
|
|
152
153
|
<TooltipProvider>
|
|
@@ -178,7 +179,9 @@ export const LogEntryHeader = memo(function ({
|
|
|
178
179
|
{/* Request start time */}
|
|
179
180
|
<span className="flex items-center gap-1 text-muted-foreground text-xs shrink-0">
|
|
180
181
|
<Clock className="size-3" />
|
|
181
|
-
<span className="font-mono tabular-nums"
|
|
182
|
+
<span className="font-mono tabular-nums" title={log.timestamp}>
|
|
183
|
+
{formatTimestamp(log.timestamp, timeDisplayFormat)}
|
|
184
|
+
</span>
|
|
182
185
|
</span>
|
|
183
186
|
|
|
184
187
|
{/* Model — logo icon only, model name in tooltip */}
|
|
@@ -303,14 +306,6 @@ export const LogEntryHeader = memo(function ({
|
|
|
303
306
|
</span>
|
|
304
307
|
)}
|
|
305
308
|
|
|
306
|
-
{/* Response tool calls — tool names the model requested to invoke */}
|
|
307
|
-
{responseToolNames !== null && responseToolNames.length > 0 && (
|
|
308
|
-
<span className="flex items-center gap-1 text-amber-400/80 text-xs shrink-0">
|
|
309
|
-
<Wrench className="size-3" />
|
|
310
|
-
<span className="font-mono tabular-nums truncate max-w-[160px]">{toolNamesJoined}</span>
|
|
311
|
-
</span>
|
|
312
|
-
)}
|
|
313
|
-
|
|
314
309
|
{/* Origin */}
|
|
315
310
|
{log.origin !== null && (
|
|
316
311
|
<span
|
|
@@ -2,9 +2,9 @@ import { AlertTriangle, Zap } from "lucide-react";
|
|
|
2
2
|
import type { JSX } from "react";
|
|
3
3
|
import { memo } from "react";
|
|
4
4
|
import { useMemo } from "react";
|
|
5
|
-
import ReactMarkdown from "react-markdown";
|
|
6
5
|
import { cn, formatTokens, getStatusCategory, type StatusCategory } from "../../lib/utils";
|
|
7
6
|
import type { RequestFormat } from "../../proxy/schemas";
|
|
7
|
+
import { AnswerMarkdown } from "./AnswerMarkdown";
|
|
8
8
|
import { formatViewFor } from "./formats";
|
|
9
9
|
import { getLogFormatAdapter } from "./log-formats";
|
|
10
10
|
|
|
@@ -61,11 +61,7 @@ function ErrorResponseView({ text }: { text: string }): JSX.Element {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function MarkdownFallbackView({ text }: { text: string }): JSX.Element {
|
|
64
|
-
return
|
|
65
|
-
<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">
|
|
66
|
-
<ReactMarkdown>{text}</ReactMarkdown>
|
|
67
|
-
</div>
|
|
68
|
-
);
|
|
64
|
+
return <AnswerMarkdown text={text} />;
|
|
69
65
|
}
|
|
70
66
|
|
|
71
67
|
export const ResponseView = memo(function ResponseView({
|
|
@@ -13,11 +13,10 @@ export function ToolTraceEvents({ events }: ToolTraceEventsProps): JSX.Element |
|
|
|
13
13
|
{events.map((event) => (
|
|
14
14
|
<div
|
|
15
15
|
key={event.id}
|
|
16
|
-
className="flex min-w-0 items-center gap-2 rounded-md border border-
|
|
16
|
+
className="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"
|
|
17
17
|
>
|
|
18
|
-
<Wrench className="size-3.5 shrink-0 text-
|
|
19
|
-
<span className="font-mono font-semibold text-
|
|
20
|
-
<span className="font-mono text-muted-foreground">#{event.logId}</span>
|
|
18
|
+
<Wrench className="size-3.5 shrink-0 text-sky-400/70" />
|
|
19
|
+
<span className="font-mono font-semibold text-foreground/80">{event.name}</span>
|
|
21
20
|
{event.argumentsPreview !== null && (
|
|
22
21
|
<>
|
|
23
22
|
<ChevronRight className="size-3 shrink-0 text-muted-foreground/60" />
|
|
@@ -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;
|
|
@@ -331,6 +334,7 @@ export const TurnGroup = memo(function TurnGroup({
|
|
|
331
334
|
viewMode={viewMode}
|
|
332
335
|
strip={strip}
|
|
333
336
|
slowResponseThresholdSeconds={slowResponseThresholdSeconds}
|
|
337
|
+
timeDisplayFormat={timeDisplayFormat}
|
|
334
338
|
cacheTrend={cacheTrends?.get(log.id) ?? null}
|
|
335
339
|
onCompareWithPrevious={
|
|
336
340
|
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
|
|