@tonyclaw/agent-inspector 2.0.2 → 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-Bp7_x-5N.js → CompareDrawer-BCH_fsLm.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-D85_UANk.js +101 -0
- package/.output/public/assets/{ReplayDialog-DFHCd0yx.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-Bjs4Lqwn.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-6uV_YXws.js → json-viewer-BrzjD7qI.js} +1 -1
- package/.output/public/assets/{main-FSGUGtEL.js → main-mgxeUdZQ.js} +2 -2
- package/.output/server/_libs/lucide-react.mjs +8 -8
- package/.output/server/{_sessionId-_bf9vUww.mjs → _sessionId-C4xsxIWm.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-DIth2DQM.mjs → CompareDrawer-DuWEpqQ7.mjs} +4 -4
- package/.output/server/_ssr/{ProxyViewerContainer-249bTH-T.mjs → ProxyViewerContainer-Cckz5qKu.mjs} +519 -89
- package/.output/server/_ssr/{ReplayDialog-C1aGx0y1.mjs → ReplayDialog-BDRcr8E5.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-D2bCiEJn.mjs → RequestAnatomy-BoO2_Ij0.mjs} +5 -5
- package/.output/server/_ssr/{ResponseView-DP6k4Xs_.mjs → ResponseView-DZiPBxvO.mjs} +21 -17
- package/.output/server/_ssr/{StreamingChunkSequence-HyXZV-b5.mjs → StreamingChunkSequence-D-be7KEL.mjs} +3 -3
- package/.output/server/_ssr/{index-Bt47f9pn.mjs → index-5RImHKfu.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-Co-YRwUP.mjs → json-viewer-aJhb93ZK.mjs} +2 -2
- package/.output/server/_ssr/{router-to_OJirX.mjs → router-Dgkv5nKP.mjs} +38 -99
- package/.output/server/{_tanstack-start-manifest_v-Bd-2YRWo.mjs → _tanstack-start-manifest_v-B8rrWXjr.mjs} +1 -1
- package/.output/server/index.mjs +63 -63
- package/README.md +5 -2
- 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 +276 -0
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +16 -0
- package/src/components/proxy-viewer/ConversationGroup.tsx +18 -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 +32 -0
- package/src/components/proxy-viewer/TurnGroup.tsx +15 -1
- 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/components/proxy-viewer/viewerState.ts +177 -0
- 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/chunkStorage.ts +3 -4
- package/src/proxy/config.ts +3 -0
- package/src/proxy/logger.ts +8 -15
- package/src/proxy/store.ts +8 -16
- package/src/routes/api/config.ts +5 -1
- package/src/routes/api/providers.$providerId.test.log.ts +0 -79
- package/.output/public/assets/ProxyViewerContainer-USuxPy-K.js +0 -101
- package/.output/public/assets/RequestAnatomy-ehyrskxt.js +0 -1
- package/.output/public/assets/ResponseView-BNGyc8e_.js +0 -1
- package/.output/public/assets/_sessionId-D_SeK_qp.js +0 -1
- package/.output/public/assets/index-BGGOWR7A.js +0 -1
- package/.output/public/assets/index-CIL46Z2y.css +0 -1
|
@@ -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>
|
|
@@ -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>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { extractStopReason, isTurnBoundary, type StopReason } from "../../lib/stopReason";
|
|
2
|
+
import { safeGetOwnProperty } from "../../lib/objectUtils";
|
|
2
3
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
3
4
|
import { resolveLogFormat } from "./log-formats";
|
|
4
5
|
|
|
@@ -16,6 +17,34 @@ type ConversationLike = {
|
|
|
16
17
|
logs: CapturedLog[];
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
export type TraceSummary = {
|
|
21
|
+
llmCallCount: number;
|
|
22
|
+
toolCallCount: number;
|
|
23
|
+
failedCallCount: number;
|
|
24
|
+
pendingCallCount: number;
|
|
25
|
+
slowCallCount: number;
|
|
26
|
+
totalInputTokens: number;
|
|
27
|
+
totalOutputTokens: number;
|
|
28
|
+
totalCacheCreationInputTokens: number;
|
|
29
|
+
totalCacheReadInputTokens: number;
|
|
30
|
+
totalElapsedMs: number;
|
|
31
|
+
maxElapsedMs: number | null;
|
|
32
|
+
startedAt: string | null;
|
|
33
|
+
endedAt: string | null;
|
|
34
|
+
knowledgeCandidateCount: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ToolTraceEvent = {
|
|
38
|
+
id: string;
|
|
39
|
+
logId: number;
|
|
40
|
+
index: number;
|
|
41
|
+
provider: "anthropic" | "openai";
|
|
42
|
+
name: string;
|
|
43
|
+
argumentsPreview: string | null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const PREVIEW_LIMIT = 180;
|
|
47
|
+
|
|
19
48
|
export function shouldRenderConversationContent(standalone: boolean, expanded: boolean): boolean {
|
|
20
49
|
return standalone || expanded;
|
|
21
50
|
}
|
|
@@ -64,3 +93,151 @@ export function buildValidPredecessors(groups: ConversationLike[]): Map<number,
|
|
|
64
93
|
|
|
65
94
|
return predecessors;
|
|
66
95
|
}
|
|
96
|
+
|
|
97
|
+
function parseJsonResponse(responseText: string | null): unknown {
|
|
98
|
+
if (responseText === null) return null;
|
|
99
|
+
try {
|
|
100
|
+
const parsed: unknown = JSON.parse(responseText);
|
|
101
|
+
if (typeof parsed === "string") {
|
|
102
|
+
return JSON.parse(parsed);
|
|
103
|
+
}
|
|
104
|
+
return parsed;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function previewValue(value: unknown): string | null {
|
|
111
|
+
if (value === undefined || value === null) return null;
|
|
112
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
113
|
+
if (raw === undefined) return null;
|
|
114
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
115
|
+
if (normalized.length === 0) return null;
|
|
116
|
+
return normalized.length > PREVIEW_LIMIT
|
|
117
|
+
? `${normalized.slice(0, PREVIEW_LIMIT - 1)}...`
|
|
118
|
+
: normalized;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractAnthropicToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
122
|
+
const parsed = parseJsonResponse(log.responseText);
|
|
123
|
+
const content = safeGetOwnProperty(parsed, "content");
|
|
124
|
+
if (!Array.isArray(content)) return [];
|
|
125
|
+
|
|
126
|
+
const events: ToolTraceEvent[] = [];
|
|
127
|
+
for (const block of content) {
|
|
128
|
+
const type = safeGetOwnProperty(block, "type");
|
|
129
|
+
if (type !== "tool_use") continue;
|
|
130
|
+
const name = safeGetOwnProperty(block, "name");
|
|
131
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
132
|
+
events.push({
|
|
133
|
+
id: `${String(log.id)}-anthropic-tool-${String(events.length)}`,
|
|
134
|
+
logId: log.id,
|
|
135
|
+
index: events.length,
|
|
136
|
+
provider: "anthropic",
|
|
137
|
+
name,
|
|
138
|
+
argumentsPreview: previewValue(safeGetOwnProperty(block, "input")),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return events;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractOpenAIToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
145
|
+
const parsed = parseJsonResponse(log.responseText);
|
|
146
|
+
const choices = safeGetOwnProperty(parsed, "choices");
|
|
147
|
+
if (!Array.isArray(choices)) return [];
|
|
148
|
+
|
|
149
|
+
const events: ToolTraceEvent[] = [];
|
|
150
|
+
for (const choice of choices) {
|
|
151
|
+
const message = safeGetOwnProperty(choice, "message");
|
|
152
|
+
const toolCalls = safeGetOwnProperty(message, "tool_calls");
|
|
153
|
+
if (!Array.isArray(toolCalls)) continue;
|
|
154
|
+
for (const call of toolCalls) {
|
|
155
|
+
const fn = safeGetOwnProperty(call, "function");
|
|
156
|
+
const name = safeGetOwnProperty(fn, "name");
|
|
157
|
+
if (typeof name !== "string" || name.length === 0) continue;
|
|
158
|
+
events.push({
|
|
159
|
+
id: `${String(log.id)}-openai-tool-${String(events.length)}`,
|
|
160
|
+
logId: log.id,
|
|
161
|
+
index: events.length,
|
|
162
|
+
provider: "openai",
|
|
163
|
+
name,
|
|
164
|
+
argumentsPreview: previewValue(safeGetOwnProperty(fn, "arguments")),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return events;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function extractToolTraceEvents(log: CapturedLog): ToolTraceEvent[] {
|
|
172
|
+
const format = resolveLogFormat(log);
|
|
173
|
+
switch (format) {
|
|
174
|
+
case "anthropic":
|
|
175
|
+
return extractAnthropicToolTraceEvents(log);
|
|
176
|
+
case "openai":
|
|
177
|
+
return extractOpenAIToolTraceEvents(log);
|
|
178
|
+
case "unknown":
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function buildTraceSummary(
|
|
184
|
+
logs: CapturedLog[],
|
|
185
|
+
slowResponseThresholdSeconds: number,
|
|
186
|
+
knowledgeCandidateCount = 0,
|
|
187
|
+
): TraceSummary {
|
|
188
|
+
let failedCallCount = 0;
|
|
189
|
+
let pendingCallCount = 0;
|
|
190
|
+
let slowCallCount = 0;
|
|
191
|
+
let totalInputTokens = 0;
|
|
192
|
+
let totalOutputTokens = 0;
|
|
193
|
+
let totalCacheCreationInputTokens = 0;
|
|
194
|
+
let totalCacheReadInputTokens = 0;
|
|
195
|
+
let totalElapsedMs = 0;
|
|
196
|
+
let maxElapsedMs: number | null = null;
|
|
197
|
+
let toolCallCount = 0;
|
|
198
|
+
|
|
199
|
+
for (const log of logs) {
|
|
200
|
+
if (log.responseStatus === null) {
|
|
201
|
+
pendingCallCount += 1;
|
|
202
|
+
} else if (log.responseStatus >= 400) {
|
|
203
|
+
failedCallCount += 1;
|
|
204
|
+
}
|
|
205
|
+
if (
|
|
206
|
+
log.elapsedMs !== null &&
|
|
207
|
+
slowResponseThresholdSeconds > 0 &&
|
|
208
|
+
log.elapsedMs > slowResponseThresholdSeconds * 1000
|
|
209
|
+
) {
|
|
210
|
+
slowCallCount += 1;
|
|
211
|
+
}
|
|
212
|
+
if (log.inputTokens !== null) totalInputTokens += log.inputTokens;
|
|
213
|
+
if (log.outputTokens !== null) totalOutputTokens += log.outputTokens;
|
|
214
|
+
if (log.cacheCreationInputTokens !== null) {
|
|
215
|
+
totalCacheCreationInputTokens += log.cacheCreationInputTokens;
|
|
216
|
+
}
|
|
217
|
+
if (log.cacheReadInputTokens !== null) {
|
|
218
|
+
totalCacheReadInputTokens += log.cacheReadInputTokens;
|
|
219
|
+
}
|
|
220
|
+
if (log.elapsedMs !== null) {
|
|
221
|
+
totalElapsedMs += log.elapsedMs;
|
|
222
|
+
maxElapsedMs = maxElapsedMs === null ? log.elapsedMs : Math.max(maxElapsedMs, log.elapsedMs);
|
|
223
|
+
}
|
|
224
|
+
toolCallCount += extractToolTraceEvents(log).length;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
llmCallCount: logs.length,
|
|
229
|
+
toolCallCount,
|
|
230
|
+
failedCallCount,
|
|
231
|
+
pendingCallCount,
|
|
232
|
+
slowCallCount,
|
|
233
|
+
totalInputTokens,
|
|
234
|
+
totalOutputTokens,
|
|
235
|
+
totalCacheCreationInputTokens,
|
|
236
|
+
totalCacheReadInputTokens,
|
|
237
|
+
totalElapsedMs,
|
|
238
|
+
maxElapsedMs,
|
|
239
|
+
startedAt: logs[0]?.timestamp ?? null,
|
|
240
|
+
endedAt: logs[logs.length - 1]?.timestamp ?? null,
|
|
241
|
+
knowledgeCandidateCount,
|
|
242
|
+
};
|
|
243
|
+
}
|
package/src/lib/runtimeConfig.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { z } from "zod";
|
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS = 10;
|
|
4
4
|
export const MAX_SLOW_RESPONSE_THRESHOLD_SECONDS = 600;
|
|
5
|
+
export const DEFAULT_TIME_DISPLAY_FORMAT = "time";
|
|
6
|
+
|
|
7
|
+
export const TimeDisplayFormatSchema = z.enum(["time", "full"]);
|
|
8
|
+
|
|
9
|
+
export type TimeDisplayFormat = z.infer<typeof TimeDisplayFormatSchema>;
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* Schema for the runtime proxy config. Shared between server
|
|
@@ -20,6 +25,7 @@ export const RuntimeConfigSchema = z.object({
|
|
|
20
25
|
.min(0)
|
|
21
26
|
.max(MAX_SLOW_RESPONSE_THRESHOLD_SECONDS)
|
|
22
27
|
.default(DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS),
|
|
28
|
+
timeDisplayFormat: TimeDisplayFormatSchema.default(DEFAULT_TIME_DISPLAY_FORMAT),
|
|
23
29
|
});
|
|
24
30
|
|
|
25
31
|
export type RuntimeConfig = z.infer<typeof RuntimeConfigSchema>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TimeDisplayFormat } from "./runtimeConfig";
|
|
2
|
+
|
|
3
|
+
export function formatTimestamp(iso: string, format: TimeDisplayFormat): string {
|
|
4
|
+
switch (format) {
|
|
5
|
+
case "full":
|
|
6
|
+
return iso;
|
|
7
|
+
case "time":
|
|
8
|
+
return new Date(iso).toLocaleTimeString([], {
|
|
9
|
+
hour: "2-digit",
|
|
10
|
+
minute: "2-digit",
|
|
11
|
+
second: "2-digit",
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatTimestampRange(
|
|
17
|
+
startedAt: string,
|
|
18
|
+
endedAt: string,
|
|
19
|
+
format: TimeDisplayFormat,
|
|
20
|
+
): string {
|
|
21
|
+
return `${formatTimestamp(startedAt, format)} - ${formatTimestamp(endedAt, format)}`;
|
|
22
|
+
}
|
package/src/lib/useOnboarding.ts
CHANGED
|
@@ -2,6 +2,7 @@ import useSWR, { type SWRResponse, useSWRConfig } from "swr";
|
|
|
2
2
|
import { fetchJson } from "./apiClient";
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
5
|
+
DEFAULT_TIME_DISPLAY_FORMAT,
|
|
5
6
|
RuntimeConfigSchema,
|
|
6
7
|
type RuntimeConfig,
|
|
7
8
|
} from "./runtimeConfig";
|
|
@@ -66,6 +67,7 @@ export function useOnboarding(): UseOnboarding {
|
|
|
66
67
|
hasSeenOnboarding: true,
|
|
67
68
|
slowResponseThresholdSeconds:
|
|
68
69
|
response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
70
|
+
timeDisplayFormat: response.data?.timeDisplayFormat ?? DEFAULT_TIME_DISPLAY_FORMAT,
|
|
69
71
|
},
|
|
70
72
|
rollbackOnError: true,
|
|
71
73
|
revalidate: false,
|
|
@@ -2,7 +2,9 @@ import useSWR, { type SWRResponse, useSWRConfig } from "swr";
|
|
|
2
2
|
import { fetchJson } from "./apiClient";
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
5
|
+
DEFAULT_TIME_DISPLAY_FORMAT,
|
|
5
6
|
RuntimeConfigSchema,
|
|
7
|
+
type TimeDisplayFormat,
|
|
6
8
|
type RuntimeConfig,
|
|
7
9
|
} from "./runtimeConfig";
|
|
8
10
|
|
|
@@ -36,10 +38,12 @@ export async function setRuntimeConfig(patch: Partial<RuntimeConfig>): Promise<R
|
|
|
36
38
|
export type UseStripConfig = {
|
|
37
39
|
strip: boolean;
|
|
38
40
|
slowResponseThresholdSeconds: number;
|
|
41
|
+
timeDisplayFormat: TimeDisplayFormat;
|
|
39
42
|
isLoading: boolean;
|
|
40
43
|
isError: boolean;
|
|
41
44
|
setStrip: (next: boolean) => Promise<void>;
|
|
42
45
|
setSlowResponseThresholdSeconds: (next: number) => Promise<void>;
|
|
46
|
+
setTimeDisplayFormat: (next: TimeDisplayFormat) => Promise<void>;
|
|
43
47
|
};
|
|
44
48
|
|
|
45
49
|
/**
|
|
@@ -64,12 +68,14 @@ export function useStripConfig(): UseStripConfig {
|
|
|
64
68
|
const strip = response.data?.stripClaudeCodeBillingHeader ?? false;
|
|
65
69
|
const slowResponseThresholdSeconds =
|
|
66
70
|
response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS;
|
|
71
|
+
const timeDisplayFormat = response.data?.timeDisplayFormat ?? DEFAULT_TIME_DISPLAY_FORMAT;
|
|
67
72
|
|
|
68
73
|
const optimisticConfig = (patch: Partial<RuntimeConfig>): RuntimeConfig => ({
|
|
69
74
|
stripClaudeCodeBillingHeader: response.data?.stripClaudeCodeBillingHeader ?? false,
|
|
70
75
|
hasSeenOnboarding: response.data?.hasSeenOnboarding ?? false,
|
|
71
76
|
slowResponseThresholdSeconds:
|
|
72
77
|
response.data?.slowResponseThresholdSeconds ?? DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
78
|
+
timeDisplayFormat: response.data?.timeDisplayFormat ?? DEFAULT_TIME_DISPLAY_FORMAT,
|
|
73
79
|
...patch,
|
|
74
80
|
});
|
|
75
81
|
|
|
@@ -97,12 +103,22 @@ export function useStripConfig(): UseStripConfig {
|
|
|
97
103
|
);
|
|
98
104
|
};
|
|
99
105
|
|
|
106
|
+
const setTimeDisplayFormat = async (next: TimeDisplayFormat): Promise<void> => {
|
|
107
|
+
await globalMutate(STRIP_CONFIG_SWR_KEY, setRuntimeConfig({ timeDisplayFormat: next }), {
|
|
108
|
+
optimisticData: optimisticConfig({ timeDisplayFormat: next }),
|
|
109
|
+
rollbackOnError: true,
|
|
110
|
+
revalidate: false,
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
100
114
|
return {
|
|
101
115
|
strip,
|
|
102
116
|
slowResponseThresholdSeconds,
|
|
117
|
+
timeDisplayFormat,
|
|
103
118
|
isLoading: response.isLoading,
|
|
104
119
|
isError: response.error !== undefined,
|
|
105
120
|
setStrip,
|
|
106
121
|
setSlowResponseThresholdSeconds,
|
|
122
|
+
setTimeDisplayFormat,
|
|
107
123
|
};
|
|
108
124
|
}
|
|
@@ -30,11 +30,10 @@ const StreamingChunksDataSchema = z.object({
|
|
|
30
30
|
truncated: z.boolean().optional(),
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
const CHUNKS_DIR_ENV = process.env["CHUNKS_DIR"];
|
|
34
|
-
|
|
35
33
|
export function getChunksDir(): string {
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const chunksDirEnv = process.env["CHUNKS_DIR"];
|
|
35
|
+
if (chunksDirEnv !== undefined && chunksDirEnv !== "") {
|
|
36
|
+
return isAbsolute(chunksDirEnv) ? chunksDirEnv : join(getDataDir(), chunksDirEnv);
|
|
38
37
|
}
|
|
39
38
|
return join(getDataDir(), "chunks");
|
|
40
39
|
}
|
package/src/proxy/config.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { logger } from "./logger";
|
|
|
12
12
|
import { getDataDir } from "./dataDir";
|
|
13
13
|
import {
|
|
14
14
|
DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
15
|
+
DEFAULT_TIME_DISPLAY_FORMAT,
|
|
15
16
|
RuntimeConfigSchema,
|
|
16
17
|
type RuntimeConfig,
|
|
17
18
|
} from "../lib/runtimeConfig";
|
|
@@ -79,6 +80,7 @@ function resolveInitialConfig(): RuntimeConfig {
|
|
|
79
80
|
stripClaudeCodeBillingHeader: true,
|
|
80
81
|
hasSeenOnboarding: false,
|
|
81
82
|
slowResponseThresholdSeconds: DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
83
|
+
timeDisplayFormat: DEFAULT_TIME_DISPLAY_FORMAT,
|
|
82
84
|
};
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -87,6 +89,7 @@ function resolveInitialConfig(): RuntimeConfig {
|
|
|
87
89
|
stripClaudeCodeBillingHeader: false,
|
|
88
90
|
hasSeenOnboarding: false,
|
|
89
91
|
slowResponseThresholdSeconds: DEFAULT_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
92
|
+
timeDisplayFormat: DEFAULT_TIME_DISPLAY_FORMAT,
|
|
90
93
|
};
|
|
91
94
|
}
|
|
92
95
|
|
package/src/proxy/logger.ts
CHANGED
|
@@ -3,23 +3,14 @@ import { writeFileSync, mkdirSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { getDataDir } from "./dataDir";
|
|
5
5
|
|
|
6
|
-
const LOG_DIR_ENV = process.env["LOG_DIR"];
|
|
7
6
|
const RETENTION_DAYS = Number(process.env["LOG_RETENTION_DAYS"] ?? "7");
|
|
8
|
-
const LOG_FILE_ENV = process.env["AGENT_INSPECTOR_LOG_FILE"];
|
|
9
|
-
|
|
10
|
-
let resolvedLogDir: string | null = null;
|
|
11
7
|
|
|
12
8
|
export function resolveLogDir(): string {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
resolvedLogDir = path.isAbsolute(LOG_DIR_ENV)
|
|
17
|
-
? LOG_DIR_ENV
|
|
18
|
-
: path.join(getDataDir(), LOG_DIR_ENV);
|
|
19
|
-
} else {
|
|
20
|
-
resolvedLogDir = path.join(getDataDir(), "logs");
|
|
9
|
+
const logDirEnv = process.env["LOG_DIR"];
|
|
10
|
+
if (logDirEnv !== undefined && logDirEnv !== "") {
|
|
11
|
+
return path.isAbsolute(logDirEnv) ? logDirEnv : path.join(getDataDir(), logDirEnv);
|
|
21
12
|
}
|
|
22
|
-
return
|
|
13
|
+
return path.join(getDataDir(), "logs");
|
|
23
14
|
}
|
|
24
15
|
|
|
25
16
|
export function getLogFilePath(): string {
|
|
@@ -31,8 +22,9 @@ export function getLogFilePath(): string {
|
|
|
31
22
|
}
|
|
32
23
|
|
|
33
24
|
function getInspectorLogPath(): string {
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
const logFileEnv = process.env["AGENT_INSPECTOR_LOG_FILE"];
|
|
26
|
+
if (logFileEnv !== undefined && logFileEnv !== "") {
|
|
27
|
+
return logFileEnv;
|
|
36
28
|
}
|
|
37
29
|
return path.join(getDataDir(), "logs", "inspector.log");
|
|
38
30
|
}
|
|
@@ -43,6 +35,7 @@ export async function initLogger(): Promise<void> {
|
|
|
43
35
|
const cutoff = Date.now() - retentionMs;
|
|
44
36
|
|
|
45
37
|
try {
|
|
38
|
+
await mkdir(dir, { recursive: true });
|
|
46
39
|
const entries = await readdir(dir);
|
|
47
40
|
for (const entry of entries) {
|
|
48
41
|
if (!entry.endsWith(".jsonl")) continue;
|
package/src/proxy/store.ts
CHANGED
|
@@ -5,14 +5,7 @@ import { createInterface } from "node:readline";
|
|
|
5
5
|
import { Buffer } from "node:buffer";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { appendLogEntry, resolveLogDir, logger } from "./logger";
|
|
8
|
-
import {
|
|
9
|
-
addToIndex,
|
|
10
|
-
findInIndex,
|
|
11
|
-
getNextLogId,
|
|
12
|
-
getCurrentLogFile,
|
|
13
|
-
saveIndex,
|
|
14
|
-
loadIndex,
|
|
15
|
-
} from "./logIndex";
|
|
8
|
+
import { addToIndex, findInIndex, getNextLogId, getCurrentLogFile } from "./logIndex";
|
|
16
9
|
import { writeChunks } from "./chunkStorage";
|
|
17
10
|
import type { CapturedLog } from "./schemas";
|
|
18
11
|
import { CapturedLogSchema } from "./schemas";
|
|
@@ -67,17 +60,11 @@ function removeFromCache(id: number): void {
|
|
|
67
60
|
}
|
|
68
61
|
|
|
69
62
|
/**
|
|
70
|
-
* Add a test log entry
|
|
71
|
-
* This is used by the provider test endpoint
|
|
63
|
+
* Add a test log entry to the in-memory store and persistent log file.
|
|
64
|
+
* This is used by the provider test endpoint to seed the provider-test session.
|
|
72
65
|
*/
|
|
73
66
|
export async function addTestLogEntry(entry: Omit<CapturedLog, "id">): Promise<CapturedLog> {
|
|
74
67
|
const id = await getNextLogId();
|
|
75
|
-
// Update the index with the new maxId so subsequent calls get unique IDs
|
|
76
|
-
const index = await loadIndex();
|
|
77
|
-
if (id > index.maxId) {
|
|
78
|
-
index.maxId = id;
|
|
79
|
-
await saveIndex(index);
|
|
80
|
-
}
|
|
81
68
|
|
|
82
69
|
// Persist streaming chunks to disk if present
|
|
83
70
|
let streamingChunksPath: string | null = null;
|
|
@@ -100,6 +87,11 @@ export async function addTestLogEntry(entry: Omit<CapturedLog, "id">): Promise<C
|
|
|
100
87
|
sessionId: session.id,
|
|
101
88
|
streamingChunksPath,
|
|
102
89
|
};
|
|
90
|
+
|
|
91
|
+
const logFile = getCurrentLogFile();
|
|
92
|
+
appendLogEntry(log);
|
|
93
|
+
await addToIndex(id, logFile, -1, -1);
|
|
94
|
+
|
|
103
95
|
addToCache(log);
|
|
104
96
|
observeSessionLog(log, session.source);
|
|
105
97
|
emitLogUpdate(log);
|
package/src/routes/api/config.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
MAX_SLOW_RESPONSE_THRESHOLD_SECONDS,
|
|
5
|
+
TimeDisplayFormatSchema,
|
|
6
|
+
} from "../../lib/runtimeConfig";
|
|
4
7
|
import { getConfig, setConfig, RuntimeConfigSchema } from "../../proxy/config";
|
|
5
8
|
|
|
6
9
|
// Partial schema for PATCH: at least one known field must be present.
|
|
@@ -14,6 +17,7 @@ const RuntimeConfigPatchSchema = z
|
|
|
14
17
|
.min(0)
|
|
15
18
|
.max(MAX_SLOW_RESPONSE_THRESHOLD_SECONDS)
|
|
16
19
|
.optional(),
|
|
20
|
+
timeDisplayFormat: TimeDisplayFormatSchema.optional(),
|
|
17
21
|
})
|
|
18
22
|
.strict()
|
|
19
23
|
.refine((v) => Object.keys(v).length > 0, {
|