@tonyclaw/llm-inspector 1.14.6 → 1.14.8
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/index-CdnotuLh.js +105 -0
- package/.output/public/assets/index-vP91146S.css +1 -0
- package/.output/public/assets/{main-Dp5657Eq.js → main-CJ4MreBr.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +87 -79
- package/.output/server/_libs/radix-ui__react-id.mjs +1 -1
- package/.output/server/_libs/radix-ui__react-tooltip.mjs +1 -1
- package/.output/server/_ssr/{index-D2yS8VvO.mjs → index-9uTJ4xYR.mjs} +813 -595
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-DCfjmmJu.mjs → router-BKnjB_zi.mjs} +2 -2
- package/.output/server/{_tanstack-start-manifest_v-DupqJc5d.mjs → _tanstack-start-manifest_v-IsglLVKy.mjs} +1 -1
- package/.output/server/index.mjs +26 -26
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +114 -146
- package/src/components/providers/ImportWizardDialog.tsx +6 -0
- package/src/components/providers/ProviderCard.tsx +79 -26
- package/src/components/providers/ProviderForm.tsx +37 -22
- package/src/components/providers/ProvidersPanel.tsx +118 -58
- package/src/components/providers/SettingsDialog.tsx +25 -15
- package/src/components/proxy-viewer/ConversationGroup.tsx +50 -10
- package/src/components/proxy-viewer/ConversationHeader.tsx +48 -2
- package/src/components/proxy-viewer/LogEntry.tsx +116 -45
- package/src/components/proxy-viewer/LogEntryHeader.tsx +89 -71
- package/src/components/proxy-viewer/ReplayDialog.tsx +16 -6
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +24 -16
- package/src/components/proxy-viewer/ThreadConnector.tsx +104 -0
- package/src/components/proxy-viewer/index.ts +2 -1
- package/src/components/ui/confirm-dialog.tsx +51 -0
- package/src/lib/stopReason.ts +57 -0
- package/.output/public/assets/index-BFNoWwFI.css +0 -1
- package/.output/public/assets/index-LH-YtFEM.js +0 -105
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type JSX, type ReactNode, useCallback, useRef, useState } from "react";
|
|
2
2
|
import { Button } from "../ui/button";
|
|
3
|
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
3
4
|
import {
|
|
4
5
|
Eye,
|
|
5
6
|
EyeOff,
|
|
@@ -109,16 +110,30 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
|
|
|
109
110
|
}
|
|
110
111
|
if (result.cacheCreationInputTokens !== undefined && result.cacheCreationInputTokens > 0) {
|
|
111
112
|
tokenParts.push(
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
<TooltipProvider key="cache-create">
|
|
114
|
+
<Tooltip>
|
|
115
|
+
<TooltipTrigger asChild>
|
|
116
|
+
<span className="font-mono tabular-nums text-emerald-400">
|
|
117
|
+
+{result.cacheCreationInputTokens} cache
|
|
118
|
+
</span>
|
|
119
|
+
</TooltipTrigger>
|
|
120
|
+
<TooltipContent>Tokens cached for reuse, reducing future API cost</TooltipContent>
|
|
121
|
+
</Tooltip>
|
|
122
|
+
</TooltipProvider>,
|
|
115
123
|
);
|
|
116
124
|
}
|
|
117
125
|
if (result.cacheReadInputTokens !== undefined && result.cacheReadInputTokens > 0) {
|
|
118
126
|
tokenParts.push(
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
<TooltipProvider key="cache-read">
|
|
128
|
+
<Tooltip>
|
|
129
|
+
<TooltipTrigger asChild>
|
|
130
|
+
<span className="font-mono tabular-nums text-purple-400">
|
|
131
|
+
~{result.cacheReadInputTokens} cached
|
|
132
|
+
</span>
|
|
133
|
+
</TooltipTrigger>
|
|
134
|
+
<TooltipContent>Tokens served from cache, reducing API cost</TooltipContent>
|
|
135
|
+
</Tooltip>
|
|
136
|
+
</TooltipProvider>,
|
|
122
137
|
);
|
|
123
138
|
}
|
|
124
139
|
const displayTokens: ReactNode[] = [];
|
|
@@ -127,11 +142,20 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
|
|
|
127
142
|
displayTokens.push(tokenParts[i]);
|
|
128
143
|
}
|
|
129
144
|
return (
|
|
130
|
-
<
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
<TooltipProvider>
|
|
146
|
+
<Tooltip>
|
|
147
|
+
<TooltipTrigger asChild>
|
|
148
|
+
<div className="flex items-center gap-1 text-xs text-green-600 shrink-0">
|
|
149
|
+
<CheckCircle className="size-3" />
|
|
150
|
+
<span>Connected</span>
|
|
151
|
+
{tokenParts.length > 0 && (
|
|
152
|
+
<span className="text-muted-foreground">({displayTokens})</span>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
</TooltipTrigger>
|
|
156
|
+
<TooltipContent>Connection test passed</TooltipContent>
|
|
157
|
+
</Tooltip>
|
|
158
|
+
</TooltipProvider>
|
|
135
159
|
);
|
|
136
160
|
}
|
|
137
161
|
|
|
@@ -145,16 +169,31 @@ function TestStatus({ result }: { result: ProviderTestState }): JSX.Element {
|
|
|
145
169
|
<div className="flex flex-col gap-1 shrink-0">
|
|
146
170
|
<div className="flex items-center gap-1 text-xs text-red-600 max-w-[200px]">
|
|
147
171
|
{getErrorIcon(errorType)}
|
|
148
|
-
<
|
|
172
|
+
<TooltipProvider>
|
|
173
|
+
<Tooltip>
|
|
174
|
+
<TooltipTrigger asChild>
|
|
175
|
+
<span className="truncate">{errorMessage}</span>
|
|
176
|
+
</TooltipTrigger>
|
|
177
|
+
<TooltipContent>Connection test failed</TooltipContent>
|
|
178
|
+
</Tooltip>
|
|
179
|
+
</TooltipProvider>
|
|
149
180
|
{errorDetails !== undefined && (
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
181
|
+
<TooltipProvider>
|
|
182
|
+
<Tooltip>
|
|
183
|
+
<TooltipTrigger asChild>
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={() => setShowDetails(!showDetails)}
|
|
187
|
+
className="shrink-0 text-muted-foreground hover:text-foreground transition-colors"
|
|
188
|
+
>
|
|
189
|
+
{showDetails ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
|
|
190
|
+
</button>
|
|
191
|
+
</TooltipTrigger>
|
|
192
|
+
<TooltipContent>
|
|
193
|
+
{showDetails ? "Hide error details" : "Show detailed error information"}
|
|
194
|
+
</TooltipContent>
|
|
195
|
+
</Tooltip>
|
|
196
|
+
</TooltipProvider>
|
|
158
197
|
)}
|
|
159
198
|
</div>
|
|
160
199
|
{showDetails && errorDetails !== undefined && (
|
|
@@ -243,14 +282,28 @@ export function ProviderCard({
|
|
|
243
282
|
<div className="flex items-center gap-2 min-w-0">
|
|
244
283
|
<span className="font-medium truncate">{provider.name}</span>
|
|
245
284
|
{provider.source === "company" && (
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
285
|
+
<TooltipProvider>
|
|
286
|
+
<Tooltip>
|
|
287
|
+
<TooltipTrigger asChild>
|
|
288
|
+
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 shrink-0">
|
|
289
|
+
公司
|
|
290
|
+
</span>
|
|
291
|
+
</TooltipTrigger>
|
|
292
|
+
<TooltipContent>Company-provided API key</TooltipContent>
|
|
293
|
+
</Tooltip>
|
|
294
|
+
</TooltipProvider>
|
|
249
295
|
)}
|
|
250
296
|
{provider.source === "personal" && (
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
297
|
+
<TooltipProvider>
|
|
298
|
+
<Tooltip>
|
|
299
|
+
<TooltipTrigger asChild>
|
|
300
|
+
<span className="text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 shrink-0">
|
|
301
|
+
个人
|
|
302
|
+
</span>
|
|
303
|
+
</TooltipTrigger>
|
|
304
|
+
<TooltipContent>Your personal API key</TooltipContent>
|
|
305
|
+
</Tooltip>
|
|
306
|
+
</TooltipProvider>
|
|
254
307
|
)}
|
|
255
308
|
</div>
|
|
256
309
|
{docsUrl !== undefined && (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type JSX, useState, useEffect, useRef } from "react";
|
|
2
2
|
import { Button } from "../ui/button";
|
|
3
|
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
3
4
|
import { Eye, EyeOff, Copy, Check, ChevronDown } from "lucide-react";
|
|
4
5
|
import type { ProviderConfig } from "../../proxy/providers";
|
|
5
6
|
import { maskApiKey } from "../../lib/mask";
|
|
@@ -371,28 +372,42 @@ export function ProviderForm({ provider, onSubmit, onCancel }: ProviderFormProps
|
|
|
371
372
|
|
|
372
373
|
<div className="space-y-2">
|
|
373
374
|
<div className="flex gap-1 border-b border-border">
|
|
374
|
-
<
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
375
|
+
<TooltipProvider>
|
|
376
|
+
<Tooltip>
|
|
377
|
+
<TooltipTrigger asChild>
|
|
378
|
+
<button
|
|
379
|
+
type="button"
|
|
380
|
+
onClick={() => setActiveTab("anthropic")}
|
|
381
|
+
className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
382
|
+
activeTab === "anthropic"
|
|
383
|
+
? "border-primary text-primary"
|
|
384
|
+
: "border-transparent text-muted-foreground hover:text-foreground"
|
|
385
|
+
}`}
|
|
386
|
+
>
|
|
387
|
+
Anthropic Format
|
|
388
|
+
</button>
|
|
389
|
+
</TooltipTrigger>
|
|
390
|
+
<TooltipContent>Anthropic Messages API protocol</TooltipContent>
|
|
391
|
+
</Tooltip>
|
|
392
|
+
</TooltipProvider>
|
|
393
|
+
<TooltipProvider>
|
|
394
|
+
<Tooltip>
|
|
395
|
+
<TooltipTrigger asChild>
|
|
396
|
+
<button
|
|
397
|
+
type="button"
|
|
398
|
+
onClick={() => setActiveTab("openai")}
|
|
399
|
+
className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
400
|
+
activeTab === "openai"
|
|
401
|
+
? "border-primary text-primary"
|
|
402
|
+
: "border-transparent text-muted-foreground hover:text-foreground"
|
|
403
|
+
}`}
|
|
404
|
+
>
|
|
405
|
+
OpenAI Format
|
|
406
|
+
</button>
|
|
407
|
+
</TooltipTrigger>
|
|
408
|
+
<TooltipContent>OpenAI Chat Completions API protocol</TooltipContent>
|
|
409
|
+
</Tooltip>
|
|
410
|
+
</TooltipProvider>
|
|
396
411
|
</div>
|
|
397
412
|
{errors.format !== undefined && <p className="text-xs text-destructive">{errors.format}</p>}
|
|
398
413
|
</div>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type JSX, useState, useEffect, useCallback, useMemo, useRef } from "react";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { Button } from "../ui/button";
|
|
4
|
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
4
5
|
import { Plus, AlertCircle, Copy, Check, Download, Upload, Scan } from "lucide-react";
|
|
5
6
|
import { ImportWizardDialog } from "./ImportWizardDialog";
|
|
7
|
+
import { ConfirmDialog } from "../ui/confirm-dialog";
|
|
6
8
|
import { ProviderCard } from "./ProviderCard";
|
|
7
9
|
import { ProviderForm } from "./ProviderForm";
|
|
8
10
|
import { parseJsonResponse, readApiError } from "../../lib/apiClient";
|
|
@@ -87,6 +89,8 @@ export function ProvidersPanel({
|
|
|
87
89
|
const [configPathCopied, setConfigPathCopied] = useState(false);
|
|
88
90
|
const [highlightedProviderId, setHighlightedProviderId] = useState<string | null>(null);
|
|
89
91
|
const [showImportWizard, setShowImportWizard] = useState(false);
|
|
92
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
93
|
+
const [deletingProviderId, setDeletingProviderId] = useState<string | null>(null);
|
|
90
94
|
const [sourceFilter, setSourceFilter] = useState<"all" | "personal" | "company">("all");
|
|
91
95
|
const listScrollRef = useRef<HTMLDivElement>(null);
|
|
92
96
|
const highlightTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -309,8 +313,13 @@ export function ProvidersPanel({
|
|
|
309
313
|
}
|
|
310
314
|
|
|
311
315
|
function handleDeleteProvider(providerId: string): void {
|
|
312
|
-
|
|
313
|
-
|
|
316
|
+
setDeletingProviderId(providerId);
|
|
317
|
+
setConfirmOpen(true);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function executeDelete(): void {
|
|
321
|
+
const providerId = deletingProviderId;
|
|
322
|
+
if (providerId === null) return;
|
|
314
323
|
void (async () => {
|
|
315
324
|
let res: Response;
|
|
316
325
|
try {
|
|
@@ -425,24 +434,38 @@ export function ProvidersPanel({
|
|
|
425
434
|
<div className="flex items-center justify-between sticky top-0 z-10 bg-background pb-2">
|
|
426
435
|
<h3 className="text-lg font-medium">Providers</h3>
|
|
427
436
|
<div className="flex items-center gap-2">
|
|
428
|
-
<
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
437
|
+
<TooltipProvider>
|
|
438
|
+
<Tooltip>
|
|
439
|
+
<TooltipTrigger asChild>
|
|
440
|
+
<Button
|
|
441
|
+
variant="outline"
|
|
442
|
+
size="sm"
|
|
443
|
+
onClick={() => handleExport(false)}
|
|
444
|
+
className="gap-1 hover:bg-muted"
|
|
445
|
+
>
|
|
446
|
+
<Download className="size-3" />
|
|
447
|
+
Export
|
|
448
|
+
</Button>
|
|
449
|
+
</TooltipTrigger>
|
|
450
|
+
<TooltipContent>Download providers as JSON for backup or sharing</TooltipContent>
|
|
451
|
+
</Tooltip>
|
|
452
|
+
</TooltipProvider>
|
|
453
|
+
<TooltipProvider>
|
|
454
|
+
<Tooltip>
|
|
455
|
+
<TooltipTrigger asChild>
|
|
456
|
+
<Button
|
|
457
|
+
variant="outline"
|
|
458
|
+
size="sm"
|
|
459
|
+
onClick={handleImportClick}
|
|
460
|
+
className="gap-1 hover:bg-muted"
|
|
461
|
+
>
|
|
462
|
+
<Upload className="size-3" />
|
|
463
|
+
Import
|
|
464
|
+
</Button>
|
|
465
|
+
</TooltipTrigger>
|
|
466
|
+
<TooltipContent>Import providers from an exported JSON file</TooltipContent>
|
|
467
|
+
</Tooltip>
|
|
468
|
+
</TooltipProvider>
|
|
446
469
|
<input
|
|
447
470
|
type="file"
|
|
448
471
|
ref={fileInputRef}
|
|
@@ -450,15 +473,24 @@ export function ProvidersPanel({
|
|
|
450
473
|
onChange={handleFileChange}
|
|
451
474
|
style={{ display: "none" }}
|
|
452
475
|
/>
|
|
453
|
-
<
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
476
|
+
<TooltipProvider>
|
|
477
|
+
<Tooltip>
|
|
478
|
+
<TooltipTrigger asChild>
|
|
479
|
+
<Button
|
|
480
|
+
variant="outline"
|
|
481
|
+
size="sm"
|
|
482
|
+
onClick={() => setShowImportWizard(true)}
|
|
483
|
+
className="gap-1"
|
|
484
|
+
>
|
|
485
|
+
<Scan className="size-3" />
|
|
486
|
+
Scan
|
|
487
|
+
</Button>
|
|
488
|
+
</TooltipTrigger>
|
|
489
|
+
<TooltipContent>
|
|
490
|
+
Detect and import provider configs from Claude Code and OpenCode
|
|
491
|
+
</TooltipContent>
|
|
492
|
+
</Tooltip>
|
|
493
|
+
</TooltipProvider>
|
|
462
494
|
<Button onClick={() => setShowForm(true)} size="sm" className="gap-1">
|
|
463
495
|
<Plus className="size-4" />
|
|
464
496
|
Add Provider
|
|
@@ -472,23 +504,29 @@ export function ProvidersPanel({
|
|
|
472
504
|
<span className="font-mono truncate" title={configPath}>
|
|
473
505
|
{configPath}
|
|
474
506
|
</span>
|
|
475
|
-
<
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
507
|
+
<TooltipProvider>
|
|
508
|
+
<Tooltip>
|
|
509
|
+
<TooltipTrigger asChild>
|
|
510
|
+
<button
|
|
511
|
+
type="button"
|
|
512
|
+
onClick={() => {
|
|
513
|
+
void window.navigator.clipboard.writeText(configPath).then(() => {
|
|
514
|
+
setConfigPathCopied(true);
|
|
515
|
+
setTimeout(() => setConfigPathCopied(false), 2000);
|
|
516
|
+
});
|
|
517
|
+
}}
|
|
518
|
+
className="shrink-0 ml-auto text-muted-foreground hover:text-foreground transition-colors"
|
|
519
|
+
>
|
|
520
|
+
{configPathCopied ? (
|
|
521
|
+
<Check className="size-3 text-green-500" />
|
|
522
|
+
) : (
|
|
523
|
+
<Copy className="size-3" />
|
|
524
|
+
)}
|
|
525
|
+
</button>
|
|
526
|
+
</TooltipTrigger>
|
|
527
|
+
<TooltipContent>Copy config file path to clipboard</TooltipContent>
|
|
528
|
+
</Tooltip>
|
|
529
|
+
</TooltipProvider>
|
|
492
530
|
</div>
|
|
493
531
|
)}
|
|
494
532
|
|
|
@@ -511,18 +549,30 @@ export function ProvidersPanel({
|
|
|
511
549
|
<div className="space-y-3">
|
|
512
550
|
<div className="flex gap-1 border-b border-border">
|
|
513
551
|
{(["all", "personal", "company"] as const).map((tab) => (
|
|
514
|
-
<
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
552
|
+
<TooltipProvider key={tab}>
|
|
553
|
+
<Tooltip>
|
|
554
|
+
<TooltipTrigger asChild>
|
|
555
|
+
<button
|
|
556
|
+
type="button"
|
|
557
|
+
onClick={() => setSourceFilter(tab)}
|
|
558
|
+
className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
559
|
+
sourceFilter === tab
|
|
560
|
+
? "border-primary text-primary"
|
|
561
|
+
: "border-transparent text-muted-foreground hover:text-foreground"
|
|
562
|
+
}`}
|
|
563
|
+
>
|
|
564
|
+
{tab === "all" ? "All" : tab === "personal" ? "Personal" : "Company"}
|
|
565
|
+
</button>
|
|
566
|
+
</TooltipTrigger>
|
|
567
|
+
<TooltipContent>
|
|
568
|
+
{tab === "all"
|
|
569
|
+
? "Show all providers"
|
|
570
|
+
: tab === "personal"
|
|
571
|
+
? "Providers you configured yourself"
|
|
572
|
+
: "Providers set by your organization"}
|
|
573
|
+
</TooltipContent>
|
|
574
|
+
</Tooltip>
|
|
575
|
+
</TooltipProvider>
|
|
526
576
|
))}
|
|
527
577
|
</div>
|
|
528
578
|
<div ref={listScrollRef} className="space-y-3">
|
|
@@ -554,6 +604,16 @@ export function ProvidersPanel({
|
|
|
554
604
|
}
|
|
555
605
|
}}
|
|
556
606
|
/>
|
|
607
|
+
|
|
608
|
+
<ConfirmDialog
|
|
609
|
+
open={confirmOpen}
|
|
610
|
+
onOpenChange={setConfirmOpen}
|
|
611
|
+
title="Delete Provider"
|
|
612
|
+
description="Are you sure you want to delete this provider? This action cannot be undone."
|
|
613
|
+
confirmLabel="Delete"
|
|
614
|
+
variant="destructive"
|
|
615
|
+
onConfirm={executeDelete}
|
|
616
|
+
/>
|
|
557
617
|
</div>
|
|
558
618
|
);
|
|
559
619
|
}
|
|
@@ -3,6 +3,7 @@ import { Settings } from "lucide-react";
|
|
|
3
3
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
|
|
4
4
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
|
|
5
5
|
import { Button } from "../ui/button";
|
|
6
|
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
|
|
6
7
|
import { ProvidersPanel } from "./ProvidersPanel";
|
|
7
8
|
import { useProviders } from "../../lib/useProviders";
|
|
8
9
|
import { useStripConfig } from "../../lib/useStripConfig";
|
|
@@ -129,21 +130,30 @@ function ProxySettingsTab(): JSX.Element {
|
|
|
129
130
|
</p>
|
|
130
131
|
</div>
|
|
131
132
|
|
|
132
|
-
<
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
<TooltipProvider>
|
|
134
|
+
<Tooltip>
|
|
135
|
+
<TooltipTrigger asChild>
|
|
136
|
+
<label className="flex items-center gap-3">
|
|
137
|
+
<input
|
|
138
|
+
type="checkbox"
|
|
139
|
+
role="switch"
|
|
140
|
+
checked={strip}
|
|
141
|
+
disabled={isLoading || pending}
|
|
142
|
+
onChange={(e) => {
|
|
143
|
+
void handleToggle(e.currentTarget.checked);
|
|
144
|
+
}}
|
|
145
|
+
className="size-4 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
|
|
146
|
+
/>
|
|
147
|
+
<span className="text-sm">
|
|
148
|
+
{isLoading ? "Loading…" : strip ? "Stripping enabled" : "Stripping disabled"}
|
|
149
|
+
</span>
|
|
150
|
+
</label>
|
|
151
|
+
</TooltipTrigger>
|
|
152
|
+
<TooltipContent>
|
|
153
|
+
Strip Claude Code billing header to improve cache hit rates
|
|
154
|
+
</TooltipContent>
|
|
155
|
+
</Tooltip>
|
|
156
|
+
</TooltipProvider>
|
|
147
157
|
|
|
148
158
|
{error !== null && <p className="text-xs text-destructive">Failed to save: {error}</p>}
|
|
149
159
|
</div>
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { useState, memo } from "react";
|
|
1
|
+
import { useState, memo, useMemo, useEffect } from "react";
|
|
2
2
|
import type { JSX } from "react";
|
|
3
3
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
4
|
+
import { extractStopReason } from "../../lib/stopReason";
|
|
4
5
|
import {
|
|
5
6
|
ConversationHeader,
|
|
6
7
|
getConversationId,
|
|
7
8
|
getGroupApiFormat,
|
|
8
9
|
hasMixedApiFormat,
|
|
9
10
|
type ConversationGroupData,
|
|
11
|
+
type ViewMode,
|
|
10
12
|
} from "./ConversationHeader";
|
|
11
13
|
import { LogEntry } from "./LogEntry";
|
|
14
|
+
import { ThreadConnector } from "./ThreadConnector";
|
|
12
15
|
import type { CacheTrendEntry } from "./cacheTrend";
|
|
13
16
|
|
|
14
17
|
export type ConversationGroupProps = {
|
|
@@ -21,10 +24,10 @@ export type ConversationGroupProps = {
|
|
|
21
24
|
* across the whole viewer. Each `LogEntry` looks up its own entry.
|
|
22
25
|
*/
|
|
23
26
|
cacheTrends?: Map<number, CacheTrendEntry>;
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
|
|
27
|
+
/** Callback to open CompareDrawer with a log and its immediate predecessor. */
|
|
28
|
+
onCompareWithPrevious: (log: CapturedLog) => void;
|
|
29
|
+
/** Default display mode for new groups (from global toggle). */
|
|
30
|
+
defaultGroupViewMode?: ViewMode;
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
function computeStats(logs: CapturedLog[]): {
|
|
@@ -45,15 +48,25 @@ export const ConversationGroup = memo(function ({
|
|
|
45
48
|
viewMode = "simple",
|
|
46
49
|
strip,
|
|
47
50
|
cacheTrends,
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
onCompareWithPrevious,
|
|
52
|
+
defaultGroupViewMode = "thread",
|
|
50
53
|
}: ConversationGroupProps): JSX.Element {
|
|
51
54
|
const [expanded, setExpanded] = useState(false);
|
|
55
|
+
const [groupViewMode, setGroupViewMode] = useState<ViewMode>(defaultGroupViewMode);
|
|
56
|
+
|
|
57
|
+
// Sync local view mode when global toggle changes (it only acts as default on mount otherwise)
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setGroupViewMode(defaultGroupViewMode);
|
|
60
|
+
}, [defaultGroupViewMode]);
|
|
52
61
|
|
|
53
62
|
const stats = computeStats(group.logs);
|
|
54
63
|
const startTime = group.logs[0]?.timestamp ?? new Date().toISOString();
|
|
55
64
|
const endTime = group.logs[group.logs.length - 1]?.timestamp ?? new Date().toISOString();
|
|
56
65
|
const mixed = hasMixedApiFormat(group.logs);
|
|
66
|
+
const isLoading = group.logs.some((log) => log.responseStatus === null);
|
|
67
|
+
|
|
68
|
+
// Pre-compute stop reasons for each log — used by ThreadConnector
|
|
69
|
+
const stopReasons = useMemo(() => group.logs.map((log) => extractStopReason(log)), [group.logs]);
|
|
57
70
|
|
|
58
71
|
const displayId =
|
|
59
72
|
group.conversationId.startsWith("PID:") || group.conversationId.includes("|")
|
|
@@ -75,9 +88,12 @@ export const ConversationGroup = memo(function ({
|
|
|
75
88
|
expanded={expanded}
|
|
76
89
|
onToggle={() => setExpanded(!expanded)}
|
|
77
90
|
hideApiFormat={mixed}
|
|
91
|
+
isLoading={isLoading}
|
|
92
|
+
viewMode={groupViewMode}
|
|
93
|
+
onToggleViewMode={() => setGroupViewMode((prev) => (prev === "thread" ? "flat" : "thread"))}
|
|
78
94
|
/>
|
|
79
95
|
|
|
80
|
-
{expanded && (
|
|
96
|
+
{expanded && groupViewMode === "flat" && (
|
|
81
97
|
<div className="pl-4 border-l-2 border-muted ml-3">
|
|
82
98
|
{group.logs.map((log) => (
|
|
83
99
|
<LogEntry
|
|
@@ -87,12 +103,36 @@ export const ConversationGroup = memo(function ({
|
|
|
87
103
|
suppressApiFormatBadge={!mixed}
|
|
88
104
|
strip={strip}
|
|
89
105
|
cacheTrend={cacheTrends?.get(log.id) ?? null}
|
|
90
|
-
|
|
91
|
-
onToggleSelect={onToggleSelect}
|
|
106
|
+
onCompareWithPrevious={() => onCompareWithPrevious(log)}
|
|
92
107
|
/>
|
|
93
108
|
))}
|
|
94
109
|
</div>
|
|
95
110
|
)}
|
|
111
|
+
|
|
112
|
+
{expanded && groupViewMode === "thread" && (
|
|
113
|
+
<div className="ml-3">
|
|
114
|
+
{group.logs.map((log, idx) => (
|
|
115
|
+
<div key={log.id} className="flex items-stretch">
|
|
116
|
+
<ThreadConnector
|
|
117
|
+
stopReason={stopReasons[idx] ?? null}
|
|
118
|
+
isPending={log.responseStatus === null}
|
|
119
|
+
isFirst={idx === 0}
|
|
120
|
+
isLast={idx === group.logs.length - 1}
|
|
121
|
+
/>
|
|
122
|
+
<div className="flex-1 min-w-0 mb-2">
|
|
123
|
+
<LogEntry
|
|
124
|
+
log={log}
|
|
125
|
+
viewMode={viewMode}
|
|
126
|
+
suppressApiFormatBadge={!mixed}
|
|
127
|
+
strip={strip}
|
|
128
|
+
cacheTrend={cacheTrends?.get(log.id) ?? null}
|
|
129
|
+
onCompareWithPrevious={() => onCompareWithPrevious(log)}
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
96
136
|
</div>
|
|
97
137
|
);
|
|
98
138
|
});
|