@tonyclaw/llm-inspector 1.14.7 → 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-BV7uNIIz.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/_ssr/{index-BvHLASu8.mjs → index-9uTJ4xYR.mjs} +744 -581
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-lUOA8pi6.mjs → router-BKnjB_zi.mjs} +2 -2
- package/.output/server/{_tanstack-start-manifest_v-XNH7fVPN.mjs → _tanstack-start-manifest_v-IsglLVKy.mjs} +1 -1
- package/.output/server/index.mjs +28 -28
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +114 -146
- package/src/components/providers/ProviderCard.tsx +79 -26
- package/src/components/providers/ProviderForm.tsx +37 -22
- package/src/components/providers/ProvidersPanel.tsx +79 -47
- 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/lib/stopReason.ts +57 -0
- package/.output/public/assets/index-Cmi8TfeU.js +0 -105
- package/.output/public/assets/index-DXUNTCVh.css +0 -1
|
@@ -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>
|
|
@@ -434,24 +434,38 @@ export function ProvidersPanel({
|
|
|
434
434
|
<div className="flex items-center justify-between sticky top-0 z-10 bg-background pb-2">
|
|
435
435
|
<h3 className="text-lg font-medium">Providers</h3>
|
|
436
436
|
<div className="flex items-center gap-2">
|
|
437
|
-
<
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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>
|
|
455
469
|
<input
|
|
456
470
|
type="file"
|
|
457
471
|
ref={fileInputRef}
|
|
@@ -490,23 +504,29 @@ export function ProvidersPanel({
|
|
|
490
504
|
<span className="font-mono truncate" title={configPath}>
|
|
491
505
|
{configPath}
|
|
492
506
|
</span>
|
|
493
|
-
<
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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>
|
|
510
530
|
</div>
|
|
511
531
|
)}
|
|
512
532
|
|
|
@@ -529,18 +549,30 @@ export function ProvidersPanel({
|
|
|
529
549
|
<div className="space-y-3">
|
|
530
550
|
<div className="flex gap-1 border-b border-border">
|
|
531
551
|
{(["all", "personal", "company"] as const).map((tab) => (
|
|
532
|
-
<
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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>
|
|
544
576
|
))}
|
|
545
577
|
</div>
|
|
546
578
|
<div ref={listScrollRef} className="space-y-3">
|
|
@@ -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
|
});
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ChevronDown,
|
|
3
|
+
ChevronRight,
|
|
4
|
+
Clock,
|
|
5
|
+
GitBranch,
|
|
6
|
+
Loader2,
|
|
7
|
+
MessageSquare,
|
|
8
|
+
Zap,
|
|
9
|
+
} from "lucide-react";
|
|
2
10
|
import type { JSX } from "react";
|
|
3
11
|
import { cn, formatTokens } from "../../lib/utils";
|
|
4
12
|
import type { CapturedLog } from "../../proxy/schemas";
|
|
@@ -10,6 +18,8 @@ const API_FORMAT_LABELS: Record<"anthropic" | "openai" | "unknown", string> = {
|
|
|
10
18
|
unknown: "Unknown",
|
|
11
19
|
};
|
|
12
20
|
|
|
21
|
+
export type ViewMode = "flat" | "thread";
|
|
22
|
+
|
|
13
23
|
export type ConversationHeaderProps = {
|
|
14
24
|
conversationId: string;
|
|
15
25
|
startTime: string;
|
|
@@ -23,6 +33,13 @@ export type ConversationHeaderProps = {
|
|
|
23
33
|
/** Hide the API format badge on the header (used when the group contains
|
|
24
34
|
* mixed formats — the per-log badges are shown instead). */
|
|
25
35
|
hideApiFormat?: boolean;
|
|
36
|
+
/** When true and the group is collapsed, show a spinner instead of the
|
|
37
|
+
* expand chevron to indicate an in-flight request inside the group. */
|
|
38
|
+
isLoading?: boolean;
|
|
39
|
+
/** Current display mode for this group (flat cards or threaded timeline). */
|
|
40
|
+
viewMode?: ViewMode;
|
|
41
|
+
/** Toggle between flat and thread display modes for this group. */
|
|
42
|
+
onToggleViewMode?: () => void;
|
|
26
43
|
};
|
|
27
44
|
|
|
28
45
|
function formatTimestamp(iso: string): string {
|
|
@@ -41,6 +58,9 @@ export function ConversationHeader({
|
|
|
41
58
|
expanded,
|
|
42
59
|
onToggle,
|
|
43
60
|
hideApiFormat = false,
|
|
61
|
+
isLoading = false,
|
|
62
|
+
viewMode,
|
|
63
|
+
onToggleViewMode,
|
|
44
64
|
}: ConversationHeaderProps): JSX.Element {
|
|
45
65
|
return (
|
|
46
66
|
<div
|
|
@@ -60,13 +80,39 @@ export function ConversationHeader({
|
|
|
60
80
|
}
|
|
61
81
|
}}
|
|
62
82
|
>
|
|
63
|
-
{/* Expand chevron */}
|
|
83
|
+
{/* Expand chevron — shows spinner when collapsed and group has pending logs */}
|
|
64
84
|
{expanded ? (
|
|
65
85
|
<ChevronDown className="size-4 text-muted-foreground shrink-0" />
|
|
86
|
+
) : isLoading ? (
|
|
87
|
+
<Loader2 className="size-4 animate-spin text-muted-foreground shrink-0" />
|
|
66
88
|
) : (
|
|
67
89
|
<ChevronRight className="size-4 text-muted-foreground shrink-0" />
|
|
68
90
|
)}
|
|
69
91
|
|
|
92
|
+
{/* Thread/flat view toggle — only shown when expanded */}
|
|
93
|
+
{expanded && onToggleViewMode !== undefined && (
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={(e) => {
|
|
97
|
+
e.stopPropagation();
|
|
98
|
+
onToggleViewMode();
|
|
99
|
+
}}
|
|
100
|
+
className={cn(
|
|
101
|
+
"px-1.5 py-0.5 rounded text-[10px] font-mono transition-colors shrink-0 cursor-pointer",
|
|
102
|
+
viewMode === "thread"
|
|
103
|
+
? "bg-amber-500/15 text-amber-400 border border-amber-500/30"
|
|
104
|
+
: "bg-muted text-muted-foreground border border-border hover:text-foreground",
|
|
105
|
+
)}
|
|
106
|
+
title={
|
|
107
|
+
viewMode === "thread"
|
|
108
|
+
? "Thread view — click for flat view"
|
|
109
|
+
: "Flat view — click for thread view"
|
|
110
|
+
}
|
|
111
|
+
>
|
|
112
|
+
<GitBranch className="size-3" />
|
|
113
|
+
</button>
|
|
114
|
+
)}
|
|
115
|
+
|
|
70
116
|
{/* Conversation ID */}
|
|
71
117
|
<span
|
|
72
118
|
className="text-purple-400/90 font-mono text-xs font-semibold shrink-0"
|