@tonyclaw/agent-inspector 3.0.2 → 3.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-Dqpwaku6.js → CompareDrawer-Bs2kzUSp.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-BUnlrfiS.js +106 -0
- package/.output/public/assets/{ReplayDialog-eaMk-WFG.js → ReplayDialog-B6xy7pl5.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-DCnoqam_.js → RequestAnatomy-BSvSmpCO.js} +1 -1
- package/.output/public/assets/{ResponseView-Q7HW0fAd.js → ResponseView-BFnUva7Z.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-K7qKD9VX.js → StreamingChunkSequence-u7ZaJOqn.js} +1 -1
- package/.output/public/assets/{_sessionId-CHwYmqKk.js → _sessionId-CH4nsMOA.js} +1 -1
- package/.output/public/assets/{index-EI9mxK7O.js → index-B5HS6AxR.js} +1 -1
- package/.output/public/assets/index-BqVL284D.js +1 -0
- package/.output/public/assets/index-DJW6qbnH.css +1 -0
- package/.output/public/assets/{json-viewer-Dbu2fh6-.js → json-viewer-5Tj5vkqe.js} +1 -1
- package/.output/public/assets/{main-CLNurSfs.js → main-ChxZWG8K.js} +2 -2
- package/.output/server/{_sessionId-1KajCUiB.mjs → _sessionId-B3r-VBdM.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-iM778CsC.mjs → CompareDrawer-DAYrKHKe.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-DzVBZMbn.mjs → ProxyViewerContainer-CYmNmQK9.mjs} +357 -214
- package/.output/server/_ssr/{ReplayDialog-BxPm6uxq.mjs → ReplayDialog-CdKvdXxk.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-D5aE8Qas.mjs → RequestAnatomy-BtmTUq7O.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-D0mzqj7J.mjs → ResponseView-DuD_z5hd.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-BbeKs1PZ.mjs → StreamingChunkSequence-DpIV2wt_.mjs} +2 -2
- package/.output/server/_ssr/{index-BIrW_uL8.mjs → index-B4_8vH8k.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-DVAdcOds.mjs → json-viewer-gcNJEB7y.mjs} +2 -2
- package/.output/server/_ssr/{router-DtMA9SLr.mjs → router-CYjIsElo.mjs} +4 -4
- package/.output/server/{_tanstack-start-manifest_v-Rq5BNh_J.mjs → _tanstack-start-manifest_v-BRdOxY6q.mjs} +1 -1
- package/.output/server/index.mjs +72 -72
- package/package.json +1 -1
- package/src/assets/IDE/claudecode-color.svg +1 -0
- package/src/assets/IDE/codex-color.svg +1 -0
- package/src/assets/IDE/opencode.svg +1 -0
- package/src/assets/IDE/xiaomimimo.svg +1 -0
- package/src/assets/logos/opencode.svg +4 -4
- package/src/components/ProxyViewer.tsx +92 -25
- package/src/components/ProxyViewerContainer.tsx +154 -55
- package/src/components/clients/ClientLogo.tsx +14 -52
- package/src/components/providers/ProviderCard.tsx +83 -36
- package/src/components/proxy-viewer/ConversationGroup.tsx +1 -1
- package/src/components/proxy-viewer/ConversationHeader.tsx +4 -5
- package/src/components/proxy-viewer/LogEntryHeader.tsx +1 -7
- package/.output/public/assets/ProxyViewerContainer-tu_yG20v.js +0 -106
- package/.output/public/assets/index-CZnCJkDN.js +0 -1
- package/.output/public/assets/index-Tq5s9NV0.css +0 -1
|
@@ -47,6 +47,8 @@ const LogCursorPageSchema = z.object({
|
|
|
47
47
|
|
|
48
48
|
type LogCursorPage = z.infer<typeof LogCursorPageSchema>;
|
|
49
49
|
|
|
50
|
+
const SessionsResponseSchema = z.array(z.string());
|
|
51
|
+
|
|
50
52
|
type SessionPageRequest =
|
|
51
53
|
| { kind: "newest" }
|
|
52
54
|
| { kind: "oldest" }
|
|
@@ -114,6 +116,8 @@ const DEBOUNCE_MS = 100;
|
|
|
114
116
|
const HASH_SCROLL_ATTEMPTS = 12;
|
|
115
117
|
const HASH_HIGHLIGHT_MS = 1800;
|
|
116
118
|
const SESSION_PAGE_LIMIT = 100;
|
|
119
|
+
const SESSION_RELOAD_PAGE_LIMIT = 500;
|
|
120
|
+
const MAX_SESSION_RELOAD_PAGES = 200;
|
|
117
121
|
|
|
118
122
|
function buildLogsStreamUrl(sessionId: string | undefined): string {
|
|
119
123
|
const params = new URLSearchParams();
|
|
@@ -125,11 +129,15 @@ function buildLogsStreamUrl(sessionId: string | undefined): string {
|
|
|
125
129
|
return query.length > 0 ? `/api/logs/stream?${query}` : "/api/logs/stream";
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
function buildSessionLogsPageUrl(
|
|
132
|
+
function buildSessionLogsPageUrl(
|
|
133
|
+
sessionId: string,
|
|
134
|
+
request: SessionPageRequest,
|
|
135
|
+
limit = SESSION_PAGE_LIMIT,
|
|
136
|
+
): string {
|
|
129
137
|
const params = new URLSearchParams({
|
|
130
138
|
cursor: "1",
|
|
131
139
|
compact: "1",
|
|
132
|
-
limit: String(
|
|
140
|
+
limit: String(limit),
|
|
133
141
|
sessionId,
|
|
134
142
|
});
|
|
135
143
|
|
|
@@ -174,6 +182,20 @@ export function mergeLogsById(
|
|
|
174
182
|
return [...byId.values()].sort((left, right) => left.id - right.id);
|
|
175
183
|
}
|
|
176
184
|
|
|
185
|
+
export function mergeSessionIds(
|
|
186
|
+
previousSessions: readonly string[],
|
|
187
|
+
incomingSessions: readonly string[],
|
|
188
|
+
): string[] {
|
|
189
|
+
const sessions = new Set<string>();
|
|
190
|
+
for (const session of previousSessions) {
|
|
191
|
+
if (session !== "") sessions.add(session);
|
|
192
|
+
}
|
|
193
|
+
for (const session of incomingSessions) {
|
|
194
|
+
if (session !== "") sessions.add(session);
|
|
195
|
+
}
|
|
196
|
+
return [...sessions].sort();
|
|
197
|
+
}
|
|
198
|
+
|
|
177
199
|
export function ProxyViewerContainer({
|
|
178
200
|
initialSessionId,
|
|
179
201
|
}: {
|
|
@@ -195,6 +217,7 @@ export function ProxyViewerContainer({
|
|
|
195
217
|
const [allLogs, setAllLogs] = useState<CapturedLog[]>([]);
|
|
196
218
|
const [selectedSession, setSelectedSession] = useState(initialSessionId ?? "__all__");
|
|
197
219
|
const [selectedModel, setSelectedModel] = useState("__all__");
|
|
220
|
+
const [knownSessions, setKnownSessions] = useState<string[]>([]);
|
|
198
221
|
const { notice, showToast, dismissToast } = useTransientToast();
|
|
199
222
|
const [streamInitialized, setStreamInitialized] = useState(initialSessionId === undefined);
|
|
200
223
|
const [logPage, setLogPage] = useState<LogCursorPage | null>(null);
|
|
@@ -225,9 +248,22 @@ export function ProxyViewerContainer({
|
|
|
225
248
|
() => filterLogs(allLogs, selectedSession, selectedModel),
|
|
226
249
|
[allLogs, selectedSession, selectedModel],
|
|
227
250
|
);
|
|
228
|
-
const sessions = useMemo(
|
|
251
|
+
const sessions = useMemo(
|
|
252
|
+
() => mergeSessionIds(knownSessions, extractSessions(allLogs)),
|
|
253
|
+
[allLogs, knownSessions],
|
|
254
|
+
);
|
|
229
255
|
const models = useMemo(() => extractModels(allLogs), [allLogs]);
|
|
230
256
|
|
|
257
|
+
const refreshSessions = useCallback(() => {
|
|
258
|
+
void fetchJson("/api/sessions", SessionsResponseSchema)
|
|
259
|
+
.then((sessionIds) => {
|
|
260
|
+
setKnownSessions((prev) => mergeSessionIds(prev, sessionIds));
|
|
261
|
+
})
|
|
262
|
+
.catch(() => {
|
|
263
|
+
// Session discovery is best-effort. Live SSE and session reloads still populate this list.
|
|
264
|
+
});
|
|
265
|
+
}, []);
|
|
266
|
+
|
|
231
267
|
const flushUpdates = useCallback(() => {
|
|
232
268
|
flushTimerRef.current = null;
|
|
233
269
|
const updates = pendingUpdatesRef.current;
|
|
@@ -286,9 +322,14 @@ export function ProxyViewerContainer({
|
|
|
286
322
|
logIndexRef.current = buildLogIndex(nextLogs);
|
|
287
323
|
return nextLogs;
|
|
288
324
|
});
|
|
325
|
+
setKnownSessions((prev) => mergeSessionIds(prev, extractSessions(update.logs)));
|
|
289
326
|
setStreamInitialized(true);
|
|
290
327
|
dismissToast();
|
|
291
328
|
} else if (update.type === "update") {
|
|
329
|
+
setKnownSessions((prev) => {
|
|
330
|
+
if (update.log.sessionId === null || update.log.sessionId === "") return prev;
|
|
331
|
+
return mergeSessionIds(prev, [update.log.sessionId]);
|
|
332
|
+
});
|
|
292
333
|
scheduleUpdate(update.log);
|
|
293
334
|
}
|
|
294
335
|
} catch {
|
|
@@ -338,6 +379,64 @@ export function ProxyViewerContainer({
|
|
|
338
379
|
[dismissToast, initialSessionId, showErrorToast],
|
|
339
380
|
);
|
|
340
381
|
|
|
382
|
+
const reloadSessionLogs = useCallback(
|
|
383
|
+
(sessionId: string) => {
|
|
384
|
+
const requestId = sessionPageRequestIdRef.current + 1;
|
|
385
|
+
sessionPageRequestIdRef.current = requestId;
|
|
386
|
+
setSessionPageLoading(true);
|
|
387
|
+
dismissToast();
|
|
388
|
+
|
|
389
|
+
void (async () => {
|
|
390
|
+
const collected: CapturedLog[] = [];
|
|
391
|
+
let page = await fetchJson(
|
|
392
|
+
buildSessionLogsPageUrl(sessionId, { kind: "oldest" }, SESSION_RELOAD_PAGE_LIMIT),
|
|
393
|
+
LogCursorPageSchema,
|
|
394
|
+
);
|
|
395
|
+
collected.push(...page.logs);
|
|
396
|
+
|
|
397
|
+
let loadedPages = 1;
|
|
398
|
+
while (
|
|
399
|
+
page.hasNewer &&
|
|
400
|
+
page.newestLogId !== null &&
|
|
401
|
+
loadedPages < MAX_SESSION_RELOAD_PAGES
|
|
402
|
+
) {
|
|
403
|
+
page = await fetchJson(
|
|
404
|
+
buildSessionLogsPageUrl(
|
|
405
|
+
sessionId,
|
|
406
|
+
{ kind: "newer", afterLogId: page.newestLogId },
|
|
407
|
+
SESSION_RELOAD_PAGE_LIMIT,
|
|
408
|
+
),
|
|
409
|
+
LogCursorPageSchema,
|
|
410
|
+
);
|
|
411
|
+
collected.push(...page.logs);
|
|
412
|
+
loadedPages += 1;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (sessionPageRequestIdRef.current !== requestId) return;
|
|
416
|
+
setAllLogs((prev) => {
|
|
417
|
+
const next = mergeLogsById(prev, collected);
|
|
418
|
+
logIndexRef.current = buildLogIndex(next);
|
|
419
|
+
return next;
|
|
420
|
+
});
|
|
421
|
+
setKnownSessions((prev) => mergeSessionIds(prev, [sessionId]));
|
|
422
|
+
setSessionPageLoading(false);
|
|
423
|
+
if (page.hasNewer) {
|
|
424
|
+
showToast({
|
|
425
|
+
message: `Loaded ${String(collected.length)} logs for this session. More history is available.`,
|
|
426
|
+
tone: "success",
|
|
427
|
+
});
|
|
428
|
+
} else {
|
|
429
|
+
dismissToast();
|
|
430
|
+
}
|
|
431
|
+
})().catch((err: unknown) => {
|
|
432
|
+
if (sessionPageRequestIdRef.current !== requestId) return;
|
|
433
|
+
setSessionPageLoading(false);
|
|
434
|
+
showErrorToast(err instanceof Error ? err.message : "Failed to reload session logs");
|
|
435
|
+
});
|
|
436
|
+
},
|
|
437
|
+
[dismissToast, showErrorToast, showToast],
|
|
438
|
+
);
|
|
439
|
+
|
|
341
440
|
useEffect(() => {
|
|
342
441
|
if (initialSessionId === undefined) {
|
|
343
442
|
setSessionMemberships([]);
|
|
@@ -365,11 +464,21 @@ export function ProxyViewerContainer({
|
|
|
365
464
|
};
|
|
366
465
|
}, [initialSessionId]);
|
|
367
466
|
|
|
467
|
+
useEffect(() => {
|
|
468
|
+
refreshSessions();
|
|
469
|
+
}, [refreshSessions]);
|
|
470
|
+
|
|
368
471
|
useEffect(() => {
|
|
369
472
|
if (initialSessionId === undefined) return;
|
|
370
473
|
loadSessionPage({ kind: "newest" });
|
|
371
474
|
}, [initialSessionId, loadSessionPage]);
|
|
372
475
|
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
if (initialSessionId !== undefined) return;
|
|
478
|
+
if (selectedSession === "__all__") return;
|
|
479
|
+
reloadSessionLogs(selectedSession);
|
|
480
|
+
}, [initialSessionId, reloadSessionLogs, selectedSession]);
|
|
481
|
+
|
|
373
482
|
useEffect(() => {
|
|
374
483
|
if (initialSessionId !== undefined) return undefined;
|
|
375
484
|
connectSSE();
|
|
@@ -443,64 +552,45 @@ export function ProxyViewerContainer({
|
|
|
443
552
|
}, [logs.length]);
|
|
444
553
|
|
|
445
554
|
const handleClearAll = useCallback(() => {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
? {}
|
|
457
|
-
: {
|
|
458
|
-
headers: { "Content-Type": "application/json" },
|
|
459
|
-
body,
|
|
460
|
-
}),
|
|
461
|
-
});
|
|
462
|
-
if (!res.ok) {
|
|
463
|
-
showErrorToast("Failed to clear logs");
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
logIndexRef.current.clear();
|
|
467
|
-
setAllLogs([]);
|
|
468
|
-
dismissToast();
|
|
469
|
-
} catch (err) {
|
|
470
|
-
showErrorToast(err instanceof Error ? err.message : "Unknown error clearing logs");
|
|
471
|
-
}
|
|
472
|
-
})();
|
|
473
|
-
}, [allLogs, dismissToast, initialSessionId, showErrorToast]);
|
|
555
|
+
const clearedSessions = extractSessions(allLogs);
|
|
556
|
+
logIndexRef.current.clear();
|
|
557
|
+
setAllLogs([]);
|
|
558
|
+
setLogPage(null);
|
|
559
|
+
setKnownSessions((prev) => mergeSessionIds(prev, clearedSessions));
|
|
560
|
+
showToast({
|
|
561
|
+
message: "Cleared the current view. Stored logs are still reloadable.",
|
|
562
|
+
tone: "success",
|
|
563
|
+
});
|
|
564
|
+
}, [allLogs, showToast]);
|
|
474
565
|
|
|
475
566
|
const handleClearGroup = useCallback(
|
|
476
567
|
(ids: number[]) => {
|
|
477
568
|
if (ids.length === 0) return;
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
const idSet = new Set(ids);
|
|
490
|
-
setAllLogs((prev) => {
|
|
491
|
-
const remaining = prev.filter((l) => !idSet.has(l.id));
|
|
492
|
-
logIndexRef.current = buildLogIndex(remaining);
|
|
493
|
-
return remaining;
|
|
494
|
-
});
|
|
495
|
-
dismissToast();
|
|
496
|
-
} catch (err) {
|
|
497
|
-
showErrorToast(err instanceof Error ? err.message : "Unknown error clearing group");
|
|
498
|
-
}
|
|
499
|
-
})();
|
|
569
|
+
const idSet = new Set(ids);
|
|
570
|
+
setAllLogs((prev) => {
|
|
571
|
+
const removed = prev.filter((log) => idSet.has(log.id));
|
|
572
|
+
const remaining = prev.filter((log) => !idSet.has(log.id));
|
|
573
|
+
setKnownSessions((known) => mergeSessionIds(known, extractSessions(removed)));
|
|
574
|
+
logIndexRef.current = buildLogIndex(remaining);
|
|
575
|
+
return remaining;
|
|
576
|
+
});
|
|
577
|
+
showToast({ message: "Cleared this group from the current view.", tone: "success" });
|
|
500
578
|
},
|
|
501
|
-
[
|
|
579
|
+
[showToast],
|
|
502
580
|
);
|
|
503
581
|
|
|
582
|
+
const handleReloadSession = useCallback(() => {
|
|
583
|
+
if (initialSessionId !== undefined) {
|
|
584
|
+
loadSessionPage({ kind: "newest" });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (selectedSession === "__all__") {
|
|
588
|
+
refreshSessions();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
reloadSessionLogs(selectedSession);
|
|
592
|
+
}, [initialSessionId, loadSessionPage, refreshSessions, reloadSessionLogs, selectedSession]);
|
|
593
|
+
|
|
504
594
|
const handleImportLogs = useCallback(
|
|
505
595
|
async (file: File): Promise<ImportLogsResponse> => {
|
|
506
596
|
const formData = new FormData();
|
|
@@ -588,10 +678,19 @@ export function ProxyViewerContainer({
|
|
|
588
678
|
onSessionChange={setSelectedSession}
|
|
589
679
|
onModelChange={setSelectedModel}
|
|
590
680
|
onClearAll={handleClearAll}
|
|
681
|
+
onReloadSession={
|
|
682
|
+
initialSessionId !== undefined || selectedSession !== "__all__"
|
|
683
|
+
? handleReloadSession
|
|
684
|
+
: undefined
|
|
685
|
+
}
|
|
591
686
|
onClearGroup={handleClearGroup}
|
|
592
687
|
onNotify={showToast}
|
|
593
688
|
onImportLogs={initialSessionId === undefined ? handleImportLogs : undefined}
|
|
594
|
-
isLoading={
|
|
689
|
+
isLoading={
|
|
690
|
+
initialSessionId === undefined
|
|
691
|
+
? !streamInitialized || sessionPageLoading
|
|
692
|
+
: sessionPageLoading
|
|
693
|
+
}
|
|
595
694
|
pagination={pagination}
|
|
596
695
|
sessionMemberships={sessionMemberships}
|
|
597
696
|
viewMode={viewMode}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
2
|
import { cn } from "../../lib/utils";
|
|
3
|
+
import ClaudeCodeLogoSvg from "../../assets/IDE/claudecode-color.svg";
|
|
4
|
+
import CodexLogoSvg from "../../assets/IDE/codex-color.svg";
|
|
5
|
+
import OpenCodeLogoSvg from "../../assets/IDE/opencode.svg";
|
|
6
|
+
import MiMoCodeLogoSvg from "../../assets/IDE/xiaomimimo.svg";
|
|
3
7
|
|
|
4
8
|
export type ClientApp = "codex" | "opencode" | "mimo-code" | "claude-code" | "unknown";
|
|
5
9
|
|
|
@@ -56,53 +60,12 @@ export function clientAppLabel(client: ClientApp): string | null {
|
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
fill="none"
|
|
66
|
-
stroke="currentColor"
|
|
67
|
-
strokeLinecap="round"
|
|
68
|
-
strokeWidth="2.2"
|
|
69
|
-
/>
|
|
70
|
-
</svg>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function OpenCodeMark(): JSX.Element {
|
|
75
|
-
return (
|
|
76
|
-
<svg viewBox="0 0 24 24" className="size-full" aria-hidden="true">
|
|
77
|
-
<path d="M7 4h10v16H7z" fill="none" stroke="currentColor" strokeWidth="2.2" />
|
|
78
|
-
<path d="M10 8h4M10 12h4M10 16h2" stroke="currentColor" strokeLinecap="round" />
|
|
79
|
-
</svg>
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function MiMoCodeMark(): JSX.Element {
|
|
84
|
-
return (
|
|
85
|
-
<svg viewBox="0 0 24 24" className="size-full" aria-hidden="true">
|
|
86
|
-
<path
|
|
87
|
-
d="M5 18V6h3.2l3.8 6.1L15.8 6H19v12h-3V11.3l-3.1 5h-1.8L8 11.3V18z"
|
|
88
|
-
fill="currentColor"
|
|
89
|
-
/>
|
|
90
|
-
</svg>
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function ClaudeCodeMark(): JSX.Element {
|
|
95
|
-
return (
|
|
96
|
-
<svg viewBox="0 0 24 24" className="size-full" aria-hidden="true">
|
|
97
|
-
<path
|
|
98
|
-
fill="currentColor"
|
|
99
|
-
fillRule="evenodd"
|
|
100
|
-
clipRule="evenodd"
|
|
101
|
-
d="M21 10.95h3v3.1h-3v3.03h-1.49V20H18v-2.92h-1.49V20H15v-2.92H9V20H7.49v-2.92H6V20H4.49v-2.92H3v-3.03H0v-3.1h3V5h18v5.95zM6 10.95h1.49V8.1H6v2.85zm10.51 0H18V8.1h-1.49v2.85z"
|
|
102
|
-
/>
|
|
103
|
-
</svg>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
63
|
+
const CLIENT_LOGOS: Record<Exclude<ClientApp, "unknown">, string> = {
|
|
64
|
+
codex: CodexLogoSvg,
|
|
65
|
+
opencode: OpenCodeLogoSvg,
|
|
66
|
+
"mimo-code": MiMoCodeLogoSvg,
|
|
67
|
+
"claude-code": ClaudeCodeLogoSvg,
|
|
68
|
+
};
|
|
106
69
|
|
|
107
70
|
export function ClientLogo({
|
|
108
71
|
client,
|
|
@@ -111,22 +74,21 @@ export function ClientLogo({
|
|
|
111
74
|
client: ClientApp;
|
|
112
75
|
className?: string;
|
|
113
76
|
}): JSX.Element | null {
|
|
77
|
+
if (client === "unknown") return null;
|
|
114
78
|
const label = clientAppLabel(client);
|
|
115
79
|
if (label === null) return null;
|
|
80
|
+
const logoSrc = CLIENT_LOGOS[client];
|
|
116
81
|
|
|
117
82
|
return (
|
|
118
83
|
<span
|
|
119
84
|
className={cn(
|
|
120
|
-
"border border-border bg-muted/40 inline-flex size-6 shrink-0 items-center justify-center rounded-md p-
|
|
85
|
+
"border border-border bg-muted/40 inline-flex size-6 shrink-0 items-center justify-center rounded-md p-0.5",
|
|
121
86
|
className,
|
|
122
87
|
)}
|
|
123
88
|
aria-label={label}
|
|
124
89
|
title={label}
|
|
125
90
|
>
|
|
126
|
-
{
|
|
127
|
-
{client === "opencode" && <OpenCodeMark />}
|
|
128
|
-
{client === "mimo-code" && <MiMoCodeMark />}
|
|
129
|
-
{client === "claude-code" && <ClaudeCodeMark />}
|
|
91
|
+
<img src={logoSrc} alt="" className="size-full object-contain" aria-hidden="true" />
|
|
130
92
|
</span>
|
|
131
93
|
);
|
|
132
94
|
}
|
|
@@ -275,6 +275,50 @@ function ProviderEndpointSummary({
|
|
|
275
275
|
);
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
function providerHasEndpoint(value: string | undefined): value is string {
|
|
279
|
+
return value !== undefined && value !== "";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function providerTestTargets(provider: ProviderConfig): string[] {
|
|
283
|
+
const targets: string[] = [];
|
|
284
|
+
if (providerHasEndpoint(provider.anthropicBaseUrl)) {
|
|
285
|
+
targets.push("Anthropic");
|
|
286
|
+
}
|
|
287
|
+
if (providerHasEndpoint(provider.openaiBaseUrl)) {
|
|
288
|
+
targets.push("Chat");
|
|
289
|
+
}
|
|
290
|
+
if (providerHasEndpoint(provider.openaiResponsesBaseUrl)) {
|
|
291
|
+
targets.push("Responses");
|
|
292
|
+
}
|
|
293
|
+
return targets;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function ProviderTestCoverage({ provider }: { provider: ProviderConfig }): JSX.Element {
|
|
297
|
+
const targets = providerTestTargets(provider);
|
|
298
|
+
if (targets.length === 0) {
|
|
299
|
+
return (
|
|
300
|
+
<div className="text-xs text-muted-foreground">
|
|
301
|
+
Test requires at least one configured endpoint.
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div className="flex min-w-0 flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
|
|
308
|
+
<span>Test covers</span>
|
|
309
|
+
{targets.map((target) => (
|
|
310
|
+
<span
|
|
311
|
+
key={target}
|
|
312
|
+
className="rounded border border-border/70 bg-muted/25 px-1.5 py-0.5 text-foreground/80"
|
|
313
|
+
>
|
|
314
|
+
{target}
|
|
315
|
+
</span>
|
|
316
|
+
))}
|
|
317
|
+
<span>non-stream + stream</span>
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
278
322
|
function getErrorIcon(type: ErrorType): JSX.Element {
|
|
279
323
|
const iconProps = { className: "size-3", strokeWidth: 2 };
|
|
280
324
|
switch (type) {
|
|
@@ -660,9 +704,9 @@ export function ProviderCard({
|
|
|
660
704
|
</button>
|
|
661
705
|
</div>
|
|
662
706
|
|
|
663
|
-
{provider.anthropicBaseUrl
|
|
707
|
+
{providerHasEndpoint(provider.anthropicBaseUrl) && (
|
|
664
708
|
<ProviderEndpointSummary
|
|
665
|
-
label="Anthropic"
|
|
709
|
+
label="Test: Anthropic"
|
|
666
710
|
baseUrl={provider.anthropicBaseUrl}
|
|
667
711
|
endpoint={PATH_V1_MESSAGES}
|
|
668
712
|
providerName={provider.name}
|
|
@@ -673,9 +717,9 @@ export function ProviderCard({
|
|
|
673
717
|
</ProviderEndpointSummary>
|
|
674
718
|
)}
|
|
675
719
|
|
|
676
|
-
{provider.openaiBaseUrl
|
|
720
|
+
{providerHasEndpoint(provider.openaiBaseUrl) && (
|
|
677
721
|
<ProviderEndpointSummary
|
|
678
|
-
label="
|
|
722
|
+
label="Test: Chat"
|
|
679
723
|
baseUrl={provider.openaiBaseUrl}
|
|
680
724
|
endpoint={PATH_V1_CHAT_COMPLETIONS}
|
|
681
725
|
providerName={provider.name}
|
|
@@ -686,9 +730,9 @@ export function ProviderCard({
|
|
|
686
730
|
</ProviderEndpointSummary>
|
|
687
731
|
)}
|
|
688
732
|
|
|
689
|
-
{provider.openaiResponsesBaseUrl
|
|
733
|
+
{providerHasEndpoint(provider.openaiResponsesBaseUrl) && (
|
|
690
734
|
<ProviderEndpointSummary
|
|
691
|
-
label="
|
|
735
|
+
label="Test: Responses"
|
|
692
736
|
baseUrl={provider.openaiResponsesBaseUrl}
|
|
693
737
|
endpoint={PATH_V1_RESPONSES}
|
|
694
738
|
providerName={provider.name}
|
|
@@ -774,43 +818,46 @@ export function ProviderCard({
|
|
|
774
818
|
</div>
|
|
775
819
|
)}
|
|
776
820
|
|
|
777
|
-
<div className="flex gap-2 pt-1 border-t">
|
|
778
|
-
{
|
|
821
|
+
<div className="flex flex-col gap-2 pt-1 border-t sm:flex-row sm:items-center sm:justify-between">
|
|
822
|
+
<ProviderTestCoverage provider={provider} />
|
|
823
|
+
<div className="flex gap-2">
|
|
824
|
+
{onTest !== undefined && (
|
|
825
|
+
<Button
|
|
826
|
+
variant="outline"
|
|
827
|
+
size="sm"
|
|
828
|
+
onClick={() => onTest(provider.id)}
|
|
829
|
+
className="text-xs h-7 gap-1"
|
|
830
|
+
disabled={isTesting ?? false}
|
|
831
|
+
>
|
|
832
|
+
<RotateCw className={`size-3 ${(isTesting ?? false) ? "animate-spin" : ""}`} />
|
|
833
|
+
{(isTesting ?? false)
|
|
834
|
+
? testingTimeLeft !== undefined
|
|
835
|
+
? `Testing (${testingTimeLeft}s)`
|
|
836
|
+
: "Testing..."
|
|
837
|
+
: "Test"}
|
|
838
|
+
</Button>
|
|
839
|
+
)}
|
|
779
840
|
<Button
|
|
780
841
|
variant="outline"
|
|
781
842
|
size="sm"
|
|
782
|
-
onClick={() =>
|
|
843
|
+
onClick={() => onEdit(provider)}
|
|
783
844
|
className="text-xs h-7 gap-1"
|
|
784
845
|
disabled={isTesting ?? false}
|
|
785
846
|
>
|
|
786
|
-
<
|
|
787
|
-
|
|
788
|
-
? testingTimeLeft !== undefined
|
|
789
|
-
? `Testing (${testingTimeLeft}s)`
|
|
790
|
-
: "Testing..."
|
|
791
|
-
: "Test"}
|
|
847
|
+
<Pencil className="size-3" />
|
|
848
|
+
Edit
|
|
792
849
|
</Button>
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
</
|
|
804
|
-
<Button
|
|
805
|
-
variant="outline"
|
|
806
|
-
size="sm"
|
|
807
|
-
onClick={() => onDelete(provider.id)}
|
|
808
|
-
className="text-xs h-7 gap-1 text-destructive hover:text-destructive"
|
|
809
|
-
disabled={isTesting ?? false}
|
|
810
|
-
>
|
|
811
|
-
<Trash2 className="size-3" />
|
|
812
|
-
Delete
|
|
813
|
-
</Button>
|
|
850
|
+
<Button
|
|
851
|
+
variant="outline"
|
|
852
|
+
size="sm"
|
|
853
|
+
onClick={() => onDelete(provider.id)}
|
|
854
|
+
className="text-xs h-7 gap-1 text-destructive hover:text-destructive"
|
|
855
|
+
disabled={isTesting ?? false}
|
|
856
|
+
>
|
|
857
|
+
<Trash2 className="size-3" />
|
|
858
|
+
Delete
|
|
859
|
+
</Button>
|
|
860
|
+
</div>
|
|
814
861
|
</div>
|
|
815
862
|
</div>
|
|
816
863
|
);
|
|
@@ -29,7 +29,7 @@ export type ConversationGroupProps = {
|
|
|
29
29
|
defaultExpanded?: boolean;
|
|
30
30
|
/** Controls whether timestamps render as compact local time or full ISO strings. */
|
|
31
31
|
timeDisplayFormat: TimeDisplayFormat;
|
|
32
|
-
/** Clear all logs that belong to this group. */
|
|
32
|
+
/** Clear all logs that belong to this group from the current view. */
|
|
33
33
|
onClearGroup: (ids: number[]) => void;
|
|
34
34
|
/** Show IDE identity in the session header. */
|
|
35
35
|
showClientIdentity?: boolean;
|
|
@@ -96,8 +96,7 @@ export type ConversationHeaderProps = {
|
|
|
96
96
|
showProcessMetadata?: boolean;
|
|
97
97
|
/** Controls whether timestamps render as compact local time or full ISO strings. */
|
|
98
98
|
timeDisplayFormat: TimeDisplayFormat;
|
|
99
|
-
/** Clear all logs in this group
|
|
100
|
-
* from the in-memory store; this header is then unmounted. */
|
|
99
|
+
/** Clear all logs in this group from the current view; this header is then unmounted. */
|
|
101
100
|
onClear?: () => void;
|
|
102
101
|
};
|
|
103
102
|
|
|
@@ -287,7 +286,7 @@ export function ConversationHeader({
|
|
|
287
286
|
type="button"
|
|
288
287
|
onClick={handleClearClick}
|
|
289
288
|
aria-label={`Clear group (${totalCalls} request${totalCalls !== 1 ? "s" : ""})`}
|
|
290
|
-
title="Clear this group"
|
|
289
|
+
title="Clear this group from the current view"
|
|
291
290
|
className={HEADER_ACTION_BUTTON_CLASS}
|
|
292
291
|
>
|
|
293
292
|
<Trash2 className="size-3.5" />
|
|
@@ -321,8 +320,8 @@ export function ConversationHeader({
|
|
|
321
320
|
<ConfirmDialog
|
|
322
321
|
open={confirmOpen}
|
|
323
322
|
onOpenChange={setConfirmOpen}
|
|
324
|
-
title="Clear this group?"
|
|
325
|
-
description={`This will
|
|
323
|
+
title="Clear this group from the current view?"
|
|
324
|
+
description={`This will hide ${totalCalls} request${totalCalls !== 1 ? "s" : ""} from the current view. Stored logs remain reloadable.`}
|
|
326
325
|
confirmLabel="Clear"
|
|
327
326
|
variant="destructive"
|
|
328
327
|
onConfirm={() => {
|
|
@@ -44,24 +44,19 @@ const STATUS_CUE_CLASSES: Record<
|
|
|
44
44
|
StatusCategory,
|
|
45
45
|
{
|
|
46
46
|
frame: string;
|
|
47
|
-
dot: string;
|
|
48
47
|
}
|
|
49
48
|
> = {
|
|
50
49
|
success: {
|
|
51
50
|
frame: "border-cyan-200/15 bg-white/[0.04] text-slate-100",
|
|
52
|
-
dot: "bg-cyan-200/80 shadow-[0_0_14px_rgba(125,211,252,0.65)]",
|
|
53
51
|
},
|
|
54
52
|
client_error: {
|
|
55
53
|
frame: "border-amber-300/25 bg-amber-300/[0.08] text-amber-100",
|
|
56
|
-
dot: "bg-amber-200 shadow-[0_0_14px_rgba(252,211,77,0.8)]",
|
|
57
54
|
},
|
|
58
55
|
server_error: {
|
|
59
56
|
frame: "border-rose-300/25 bg-rose-300/[0.08] text-rose-100",
|
|
60
|
-
dot: "bg-rose-300 shadow-[0_0_14px_rgba(253,164,175,0.8)]",
|
|
61
57
|
},
|
|
62
58
|
pending: {
|
|
63
59
|
frame: "border-cyan-300/25 bg-cyan-300/[0.08] text-cyan-100",
|
|
64
|
-
dot: "bg-cyan-200 shadow-[0_0_14px_rgba(103,232,249,0.8)]",
|
|
65
60
|
},
|
|
66
61
|
};
|
|
67
62
|
|
|
@@ -247,11 +242,10 @@ export const LogEntryHeader = memo(function ({
|
|
|
247
242
|
{/* Request ID */}
|
|
248
243
|
<span
|
|
249
244
|
className={cn(
|
|
250
|
-
"inline-flex h-8 shrink-0 items-center
|
|
245
|
+
"inline-flex h-8 shrink-0 items-center rounded-md px-2 font-mono",
|
|
251
246
|
statusCue.frame,
|
|
252
247
|
)}
|
|
253
248
|
>
|
|
254
|
-
<span className={cn("size-1.5 rounded-full", statusCue.dot)} aria-hidden="true" />
|
|
255
249
|
<span className="text-xs font-semibold tabular-nums" title={`Log ID ${String(log.id)}`}>
|
|
256
250
|
#{displayNumber}
|
|
257
251
|
</span>
|