@tonyclaw/agent-inspector 2.0.43 → 2.1.1

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.
Files changed (44) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-CxM1gCfL.js → CompareDrawer-DkDpl_Sg.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-D_-e1qn7.js +117 -0
  4. package/.output/public/assets/{ReplayDialog-UseUkucS.js → ReplayDialog-CKqYfD0A.js} +1 -1
  5. package/.output/public/assets/RequestAnatomy-CQZl-YIx.js +1 -0
  6. package/.output/public/assets/ResponseView-DW9tFPi0.js +1 -0
  7. package/.output/public/assets/{StreamingChunkSequence-JSQEPeNS.js → StreamingChunkSequence-m8zKc3L1.js} +1 -1
  8. package/.output/public/assets/_sessionId-DyfzgUXv.js +1 -0
  9. package/.output/public/assets/index-B6bobhTz.js +1 -0
  10. package/.output/public/assets/index-CLQKZ5A5.css +1 -0
  11. package/.output/public/assets/{main-BpGVJcpB.js → main-BjXd3DR-.js} +2 -2
  12. package/.output/server/_libs/lucide-react.mjs +219 -178
  13. package/.output/server/{_sessionId-DGn-TENM.mjs → _sessionId-LjzdyWF4.mjs} +3 -3
  14. package/.output/server/_ssr/{CompareDrawer-CFElCSYY.mjs → CompareDrawer-YX2uOwh9.mjs} +4 -4
  15. package/.output/server/_ssr/{ProxyViewerContainer-B7SR9mrD.mjs → ProxyViewerContainer-BcKHl2E3.mjs} +1016 -582
  16. package/.output/server/_ssr/{ReplayDialog-DfKapj0k.mjs → ReplayDialog-B9AxvV2j.mjs} +5 -5
  17. package/.output/server/_ssr/{RequestAnatomy-CUxTCg31.mjs → RequestAnatomy-DxmUOcz8.mjs} +4 -4
  18. package/.output/server/_ssr/{ResponseView-BXY0w197.mjs → ResponseView-B73PHDQ0.mjs} +4 -4
  19. package/.output/server/_ssr/{StreamingChunkSequence-CzMnES9H.mjs → StreamingChunkSequence-Bkwxu5Ev.mjs} +4 -4
  20. package/.output/server/_ssr/{index-CVlT-REW.mjs → index-Dfl6uxit.mjs} +3 -3
  21. package/.output/server/_ssr/index.mjs +2 -2
  22. package/.output/server/_ssr/{router-B2d1LUx6.mjs → router-B5cpU5Hi.mjs} +458 -10
  23. package/.output/server/{_tanstack-start-manifest_v-Drpi28BM.mjs → _tanstack-start-manifest_v-CHhxRQY_.mjs} +1 -1
  24. package/.output/server/index.mjs +62 -62
  25. package/package.json +1 -1
  26. package/src/components/OnboardingBanner.tsx +60 -70
  27. package/src/components/ProxyViewer.tsx +838 -366
  28. package/src/components/ProxyViewerContainer.tsx +136 -5
  29. package/src/components/providers/SettingsDialog.tsx +0 -13
  30. package/src/components/proxy-viewer/LogEntry.tsx +198 -89
  31. package/src/components/proxy-viewer/LogEntryHeader.tsx +23 -13
  32. package/src/components/proxy-viewer/RequestToolsPanel.tsx +8 -8
  33. package/src/components/ui/crab-logo.tsx +0 -50
  34. package/src/components/ui/json-viewer.tsx +6 -0
  35. package/src/proxy/logIndex.ts +188 -1
  36. package/src/proxy/store.ts +405 -3
  37. package/src/routes/api/logs.ts +36 -0
  38. package/styles/globals.css +53 -8
  39. package/.output/public/assets/ProxyViewerContainer-TtRG-0E7.js +0 -117
  40. package/.output/public/assets/RequestAnatomy-aPxgEJ2L.js +0 -1
  41. package/.output/public/assets/ResponseView-DA-F4F97.js +0 -1
  42. package/.output/public/assets/_sessionId-BEXuCWq5.js +0 -1
  43. package/.output/public/assets/index-Bhsa_2xX.js +0 -1
  44. package/.output/public/assets/index-DOWlRJ0W.css +0 -1
@@ -1,9 +1,10 @@
1
1
  import { AlertTriangle, GitCompareArrows } from "lucide-react";
2
- import { Suspense, type JSX } from "react";
2
+ import { Suspense, type JSX, type ReactNode } from "react";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState, memo } from "react";
4
4
  import { CapturedLogSchema, type CapturedLog } from "../../contracts";
5
5
  import { fetchJson } from "../../lib/apiClient";
6
6
  import type { TimeDisplayFormat } from "../../lib/runtimeConfig";
7
+ import { cn } from "../../lib/utils";
7
8
  import { stripClaudeCodeBillingHeader } from "../../proxy/claudeCodeStrip";
8
9
  import { Button } from "../ui/button";
9
10
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
@@ -53,10 +54,111 @@ function resolveFocusedTab(tab: LogFocusTab, anatomySegments: AnatomySegment[] |
53
54
 
54
55
  type BodyHydrationState = "idle" | "loading" | "failed";
55
56
 
57
+ const TAB_TRIGGER_CLASS =
58
+ "h-9 flex-none rounded-none border-0 border-b-2 border-transparent bg-transparent px-3 text-xs font-semibold text-muted-foreground shadow-none hover:bg-cyan-400/[0.04] hover:text-foreground data-[state=active]:border-cyan-300 data-[state=active]:bg-transparent data-[state=active]:text-cyan-100 data-[state=active]:shadow-none";
59
+
56
60
  function shouldHydrateLogBody(log: CapturedLog): boolean {
57
61
  return log.bodyContentMode === "compact" || log.bodyContentMode === "truncated";
58
62
  }
59
63
 
64
+ function ExpandedPanel({
65
+ children,
66
+ className,
67
+ }: {
68
+ children: ReactNode;
69
+ className?: string;
70
+ }): JSX.Element {
71
+ return (
72
+ <div
73
+ className={cn(
74
+ "inspector-scrollbar mx-3 mb-3 max-h-[72vh] overflow-auto rounded-md border border-white/10 bg-[#06080b] p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
75
+ className,
76
+ )}
77
+ >
78
+ {children}
79
+ </div>
80
+ );
81
+ }
82
+
83
+ function EmptyState({ children }: { children: ReactNode }): JSX.Element {
84
+ return (
85
+ <div className="rounded-md border border-dashed border-white/10 bg-white/[0.02] px-3 py-5 text-center text-xs italic text-muted-foreground">
86
+ {children}
87
+ </div>
88
+ );
89
+ }
90
+
91
+ function RawTextBlock({ children }: { children: ReactNode }): JSX.Element {
92
+ return (
93
+ <pre className="inspector-scrollbar max-h-[64vh] overflow-auto whitespace-pre-wrap break-words rounded-md border border-white/10 bg-black/30 p-3 font-mono text-xs leading-relaxed text-muted-foreground">
94
+ {children}
95
+ </pre>
96
+ );
97
+ }
98
+
99
+ type NoticeTone = "loading" | "warning" | "danger";
100
+
101
+ const NOTICE_TONE_CLASSES: Record<NoticeTone, string> = {
102
+ loading: "border-cyan-400/25 bg-cyan-400/[0.06] text-cyan-100",
103
+ warning: "border-amber-400/25 bg-amber-400/[0.08] text-amber-100",
104
+ danger: "border-rose-400/25 bg-rose-400/[0.08] text-rose-100",
105
+ };
106
+
107
+ function NoticeBlock({
108
+ tone,
109
+ title,
110
+ children,
111
+ }: {
112
+ tone: NoticeTone;
113
+ title?: string;
114
+ children: ReactNode;
115
+ }): JSX.Element {
116
+ return (
117
+ <div
118
+ className={cn(
119
+ "mb-3 rounded-md border px-3 py-2 text-xs shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
120
+ NOTICE_TONE_CLASSES[tone],
121
+ )}
122
+ >
123
+ {title !== undefined && <div className="mb-1 font-semibold">{title}</div>}
124
+ <div className="leading-relaxed text-muted-foreground">{children}</div>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ function HeaderRows({
130
+ headers,
131
+ emptyLabel,
132
+ }: {
133
+ headers: Record<string, string> | undefined;
134
+ emptyLabel: string;
135
+ }): JSX.Element {
136
+ const entries =
137
+ headers === undefined
138
+ ? []
139
+ : Object.entries(headers).sort(([left], [right]) => left.localeCompare(right));
140
+
141
+ if (entries.length === 0) return <EmptyState>{emptyLabel}</EmptyState>;
142
+
143
+ return (
144
+ <div className="overflow-hidden rounded-md border border-white/10 bg-black/20 font-mono text-xs">
145
+ {entries.map(([key, value]) => (
146
+ <div
147
+ key={key}
148
+ className="grid min-w-0 grid-cols-[minmax(140px,0.32fr)_minmax(0,1fr)] border-b border-white/5 last:border-b-0"
149
+ >
150
+ <div className="min-w-0 truncate border-r border-white/5 bg-white/[0.03] px-2.5 py-2 font-semibold text-cyan-200">
151
+ {key}
152
+ </div>
153
+ <div className="min-w-0 truncate px-2.5 py-2 text-muted-foreground" title={value}>
154
+ {value}
155
+ </div>
156
+ </div>
157
+ ))}
158
+ </div>
159
+ );
160
+ }
161
+
60
162
  export type LogEntryProps = {
61
163
  log: CapturedLog;
62
164
  viewMode?: "simple" | "full";
@@ -92,7 +194,12 @@ function DiffToggleButton({
92
194
  <Button
93
195
  variant={active ? "default" : "outline"}
94
196
  size="sm"
95
- className="h-8 text-xs"
197
+ className={cn(
198
+ "h-8 border-white/10 text-xs shadow-none",
199
+ active
200
+ ? "bg-cyan-300 text-black hover:bg-cyan-200"
201
+ : "bg-[#0a0d11] text-muted-foreground hover:border-cyan-300/30 hover:bg-cyan-400/10 hover:text-cyan-100",
202
+ )}
96
203
  onClick={onClick}
97
204
  aria-pressed={active}
98
205
  >
@@ -372,7 +479,14 @@ export const LogEntry = memo(function ({
372
479
 
373
480
  return (
374
481
  <TooltipProvider>
375
- <div className="border border-border rounded-lg mb-1 overflow-hidden">
482
+ <div
483
+ className={cn(
484
+ "mb-2 overflow-hidden rounded-md border bg-[#090a0d] transition-[background-color,border-color,box-shadow]",
485
+ expanded
486
+ ? "border-cyan-300/20 shadow-[0_18px_60px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.05)]"
487
+ : "border-border/70 hover:border-border",
488
+ )}
489
+ >
376
490
  <LogEntryHeader
377
491
  log={displayLog}
378
492
  messageCount={requestAnalysis.messageCount}
@@ -394,26 +508,55 @@ export const LogEntry = memo(function ({
394
508
  />
395
509
 
396
510
  {expanded && (
397
- <div onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>
398
- <Tabs value={activeTab} onValueChange={setActiveTab}>
399
- <TabsList className="mx-4 mt-2">
400
- {viewMode === "full" && <TabsTrigger value="raw-headers">Raw Headers</TabsTrigger>}
401
- {viewMode === "full" && <TabsTrigger value="headers">Headers</TabsTrigger>}
402
- {anatomySegments !== null && <TabsTrigger value="anatomy">Context</TabsTrigger>}
511
+ <div
512
+ className="border-t border-white/10 bg-[#050607]/80 pt-2"
513
+ onClick={(e) => e.stopPropagation()}
514
+ onKeyDown={(e) => e.stopPropagation()}
515
+ >
516
+ <Tabs value={activeTab} onValueChange={setActiveTab} className="gap-2">
517
+ <TabsList
518
+ variant="line"
519
+ className="inspector-scrollbar mx-3 h-auto w-[calc(100%-1.5rem)] justify-start overflow-x-auto rounded-none border-b border-white/10 bg-transparent p-0"
520
+ >
521
+ {viewMode === "full" && (
522
+ <TabsTrigger className={TAB_TRIGGER_CLASS} value="raw-headers">
523
+ Raw Headers
524
+ </TabsTrigger>
525
+ )}
526
+ {viewMode === "full" && (
527
+ <TabsTrigger className={TAB_TRIGGER_CLASS} value="headers">
528
+ Headers
529
+ </TabsTrigger>
530
+ )}
531
+ {anatomySegments !== null && (
532
+ <TabsTrigger className={TAB_TRIGGER_CLASS} value="anatomy">
533
+ Context
534
+ </TabsTrigger>
535
+ )}
403
536
  {shouldShowRawRequestTab(resolvedFormat, viewMode, strip) && (
404
- <TabsTrigger value="raw-request">Raw Request</TabsTrigger>
537
+ <TabsTrigger className={TAB_TRIGGER_CLASS} value="raw-request">
538
+ Raw Request
539
+ </TabsTrigger>
540
+ )}
541
+ <TabsTrigger className={TAB_TRIGGER_CLASS} value="request">
542
+ Request
543
+ </TabsTrigger>
544
+ {viewMode === "full" && (
545
+ <TabsTrigger className={TAB_TRIGGER_CLASS} value="raw">
546
+ Raw Response
547
+ </TabsTrigger>
405
548
  )}
406
- <TabsTrigger value="request">Request</TabsTrigger>
407
- {viewMode === "full" && <TabsTrigger value="raw">Raw Response</TabsTrigger>}
408
- <TabsTrigger value="parsed">Response</TabsTrigger>
549
+ <TabsTrigger className={TAB_TRIGGER_CLASS} value="parsed">
550
+ Response
551
+ </TabsTrigger>
409
552
  </TabsList>
410
553
 
411
554
  {shouldShowRawRequestTab(resolvedFormat, viewMode, strip) && (
412
555
  <TabsContent value="raw-request">
413
556
  {activeTab === "raw-request" && (
414
- <div className="px-4 pt-1 pb-3">
557
+ <ExpandedPanel>
415
558
  {displayLog.rawRequestBody === null ? (
416
- <p className="text-xs text-muted-foreground italic">No request body</p>
559
+ <EmptyState>No request body</EmptyState>
417
560
  ) : rawRequestExpansion.parsedData !== null ? (
418
561
  <Suspense fallback={<TabFallback />}>
419
562
  <LazyJsonViewer
@@ -423,31 +566,29 @@ export const LogEntry = memo(function ({
423
566
  />
424
567
  </Suspense>
425
568
  ) : (
426
- <pre className="font-mono text-xs whitespace-pre-wrap break-words">
427
- {displayLog.rawRequestBody}
428
- </pre>
569
+ <RawTextBlock>{displayLog.rawRequestBody}</RawTextBlock>
429
570
  )}
430
- </div>
571
+ </ExpandedPanel>
431
572
  )}
432
573
  </TabsContent>
433
574
  )}
434
575
 
435
576
  <TabsContent value="request">
436
577
  {activeTab === "request" && (
437
- <div className="px-4 pt-1 pb-3">
578
+ <ExpandedPanel>
438
579
  {bodyHydrationState === "loading" && (
439
- <div className="mb-3 rounded border border-border bg-muted/30 p-3 text-xs text-muted-foreground">
580
+ <NoticeBlock tone="loading">
440
581
  Loading full request and response bodies...
441
- </div>
582
+ </NoticeBlock>
442
583
  )}
443
584
  {bodyHydrationState === "failed" && (
444
- <div className="mb-3 rounded border border-amber-500/30 bg-amber-500/10 p-3 text-xs text-amber-200">
585
+ <NoticeBlock tone="warning">
445
586
  Full request and response bodies could not be loaded. Compact metadata is
446
587
  still available.
447
- </div>
588
+ </NoticeBlock>
448
589
  )}
449
590
  {warnings.length > 0 && (
450
- <div className="mb-3 rounded border border-amber-500/30 bg-amber-500/10 p-3 text-xs">
591
+ <NoticeBlock tone="warning">
451
592
  <div className="flex items-center gap-2 font-semibold text-amber-300">
452
593
  <AlertTriangle className="size-3.5" />
453
594
  Tool schema warnings
@@ -457,7 +598,7 @@ export const LogEntry = memo(function ({
457
598
  <li key={warning}>{warning}</li>
458
599
  ))}
459
600
  </ul>
460
- </div>
601
+ </NoticeBlock>
461
602
  )}
462
603
  <RequestTools
463
604
  summary={requestTools}
@@ -475,7 +616,7 @@ export const LogEntry = memo(function ({
475
616
  ) : (
476
617
  <div ref={requestJsonRef}>
477
618
  {displayedRequestBody === null ? (
478
- <p className="text-xs text-muted-foreground italic">No request body</p>
619
+ <EmptyState>No request body</EmptyState>
479
620
  ) : requestExpansion.parsedData !== null ? (
480
621
  <Suspense fallback={<TabFallback />}>
481
622
  <LazyJsonViewer
@@ -487,28 +628,28 @@ export const LogEntry = memo(function ({
487
628
  />
488
629
  </Suspense>
489
630
  ) : (
490
- <pre className="font-mono text-xs whitespace-pre-wrap break-words">
491
- {displayedRequestBody}
492
- </pre>
631
+ <RawTextBlock>{displayedRequestBody}</RawTextBlock>
493
632
  )}
494
633
  </div>
495
634
  )}
496
- </div>
635
+ </ExpandedPanel>
497
636
  )}
498
637
  </TabsContent>
499
638
 
500
639
  {anatomySegments !== null && (
501
640
  <TabsContent value="anatomy">
502
641
  {activeTab === "anatomy" && (
503
- <Suspense fallback={<TabFallback />}>
504
- <LazyRequestAnatomy
505
- parsed={requestExpansion.parsedData}
506
- inputTokens={displayLog.inputTokens ?? null}
507
- model={displayLog.model}
508
- segments={anatomySegments}
509
- onSegmentActivate={jumpToAnatomySegment}
510
- />
511
- </Suspense>
642
+ <ExpandedPanel className="p-0">
643
+ <Suspense fallback={<TabFallback />}>
644
+ <LazyRequestAnatomy
645
+ parsed={requestExpansion.parsedData}
646
+ inputTokens={displayLog.inputTokens ?? null}
647
+ model={displayLog.model}
648
+ segments={anatomySegments}
649
+ onSegmentActivate={jumpToAnatomySegment}
650
+ />
651
+ </Suspense>
652
+ </ExpandedPanel>
512
653
  )}
513
654
  </TabsContent>
514
655
  )}
@@ -516,7 +657,7 @@ export const LogEntry = memo(function ({
516
657
  {viewMode === "full" && (
517
658
  <TabsContent value="headers">
518
659
  {activeTab === "headers" && (
519
- <div className="px-4 pt-1 pb-3">
660
+ <ExpandedPanel>
520
661
  {/* Copy lives in the log header. */}
521
662
  <div className="flex justify-end gap-2 mb-2">
522
663
  {shouldShowHeadersDiffButton(
@@ -539,25 +680,10 @@ export const LogEntry = memo(function ({
539
680
  headers={displayLog.headers}
540
681
  emptyLabel="No transformation applied; raw and processed headers are identical."
541
682
  />
542
- ) : displayLog.headers && Object.keys(displayLog.headers).length > 0 ? (
543
- <div className="space-y-1 font-mono text-xs">
544
- {Object.entries(displayLog.headers)
545
- .sort(([a], [b]) => a.localeCompare(b))
546
- .map(([key, value]) => (
547
- <div key={key} className="flex gap-2">
548
- <span className="text-blue-600 dark:text-blue-400 font-semibold shrink-0">
549
- {key}:
550
- </span>
551
- <span className="text-muted-foreground truncate" title={value}>
552
- {value}
553
- </span>
554
- </div>
555
- ))}
556
- </div>
557
683
  ) : (
558
- <p className="text-xs text-muted-foreground italic">No headers captured</p>
684
+ <HeaderRows headers={displayLog.headers} emptyLabel="No headers captured" />
559
685
  )}
560
- </div>
686
+ </ExpandedPanel>
561
687
  )}
562
688
  </TabsContent>
563
689
  )}
@@ -565,41 +691,24 @@ export const LogEntry = memo(function ({
565
691
  {viewMode === "full" && (
566
692
  <TabsContent value="raw-headers">
567
693
  {activeTab === "raw-headers" && (
568
- <div className="px-4 pt-1 pb-3">
694
+ <ExpandedPanel>
569
695
  {/* Copy lives in the log header. */}
570
- {displayLog.rawHeaders && Object.keys(displayLog.rawHeaders).length > 0 ? (
571
- <div className="space-y-1 font-mono text-xs">
572
- {Object.entries(displayLog.rawHeaders)
573
- .sort(([a], [b]) => a.localeCompare(b))
574
- .map(([key, value]) => (
575
- <div key={key} className="flex gap-2">
576
- <span className="text-blue-600 dark:text-blue-400 font-semibold shrink-0">
577
- {key}:
578
- </span>
579
- <span className="text-muted-foreground truncate" title={value}>
580
- {value}
581
- </span>
582
- </div>
583
- ))}
584
- </div>
585
- ) : (
586
- <p className="text-xs text-muted-foreground italic">
587
- No raw headers captured
588
- </p>
589
- )}
590
- </div>
696
+ <HeaderRows
697
+ headers={displayLog.rawHeaders}
698
+ emptyLabel="No raw headers captured"
699
+ />
700
+ </ExpandedPanel>
591
701
  )}
592
702
  </TabsContent>
593
703
  )}
594
704
 
595
705
  <TabsContent value="raw">
596
706
  {activeTab === "raw" && (
597
- <div className="px-4 pt-1 pb-3 space-y-3">
707
+ <ExpandedPanel className="space-y-3">
598
708
  {displayLog.error !== undefined && displayLog.error !== null && (
599
- <div className="rounded border border-destructive/50 bg-destructive/10 p-3 text-xs">
600
- <div className="font-semibold text-destructive mb-1">SSE Error</div>
601
- <div className="text-muted-foreground font-mono">{displayLog.error}</div>
602
- </div>
709
+ <NoticeBlock tone="danger" title="SSE Error">
710
+ <span className="font-mono">{displayLog.error}</span>
711
+ </NoticeBlock>
603
712
  )}
604
713
  {displayLog.responseText !== null ? (
605
714
  <Suspense fallback={<TabFallback />}>
@@ -611,7 +720,7 @@ export const LogEntry = memo(function ({
611
720
  />
612
721
  </Suspense>
613
722
  ) : (
614
- <p className="text-xs text-muted-foreground italic">No response</p>
723
+ <EmptyState>No response</EmptyState>
615
724
  )}
616
725
  {displayLog.streaming === true && (
617
726
  <Suspense fallback={<TabFallback />}>
@@ -621,13 +730,13 @@ export const LogEntry = memo(function ({
621
730
  />
622
731
  </Suspense>
623
732
  )}
624
- </div>
733
+ </ExpandedPanel>
625
734
  )}
626
735
  </TabsContent>
627
736
 
628
737
  <TabsContent value="parsed">
629
738
  {activeTab === "parsed" && (
630
- <div className="px-4 pt-1 pb-3">
739
+ <ExpandedPanel>
631
740
  <Suspense fallback={<TabFallback />}>
632
741
  <LazyResponseView
633
742
  responseText={displayLog.responseText}
@@ -641,7 +750,7 @@ export const LogEntry = memo(function ({
641
750
  error={displayLog.error}
642
751
  />
643
752
  </Suspense>
644
- </div>
753
+ </ExpandedPanel>
645
754
  )}
646
755
  </TabsContent>
647
756
  </Tabs>
@@ -41,6 +41,9 @@ const STATUS_BADGE_CLASSES: Record<StatusCategory, string> = {
41
41
  pending: "bg-muted text-muted-foreground border-border",
42
42
  };
43
43
 
44
+ const HEADER_ACTION_BUTTON_CLASS =
45
+ "size-8 border-white/10 bg-[#0a0d11] text-muted-foreground shadow-none hover:border-cyan-300/30 hover:bg-cyan-400/10 hover:text-cyan-100";
46
+
44
47
  function formatElapsed(ms: number): string {
45
48
  if (ms < 1000) return `${ms}ms`;
46
49
  return `${(ms / 1000).toFixed(1)}s`;
@@ -170,8 +173,8 @@ export const LogEntryHeader = memo(function ({
170
173
  data-nav-id={`log-${log.id}`}
171
174
  data-nav-action={expanded ? "collapse" : "expand"}
172
175
  className={cn(
173
- "flex items-center gap-2 px-3 py-1 cursor-pointer transition-colors",
174
- "hover:bg-muted/50",
176
+ "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 px-3 py-1.5 cursor-pointer transition-colors",
177
+ expanded ? "bg-[#0c1015]" : "hover:bg-muted/30",
175
178
  "select-none",
176
179
  "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
177
180
  )}
@@ -302,17 +305,23 @@ export const LogEntryHeader = memo(function ({
302
305
 
303
306
  {/* Token counts */}
304
307
  {hasTokens && (
305
- <span className="flex items-center gap-1 text-xs shrink-0">
308
+ <span className="flex min-w-0 items-center gap-1 text-xs sm:shrink-0">
306
309
  <Zap className="size-3 text-muted-foreground" />
307
- <span className="font-mono tabular-nums">
310
+ <span className="flex min-w-0 flex-wrap items-center gap-x-1 font-mono tabular-nums">
308
311
  <span
309
- className={log.inputTokens !== null ? "text-blue-400" : "text-muted-foreground"}
312
+ className={cn(
313
+ "whitespace-nowrap",
314
+ log.inputTokens !== null ? "text-blue-400" : "text-muted-foreground",
315
+ )}
310
316
  >
311
317
  IN {log.inputTokens !== null ? formatTokens(log.inputTokens) : "—"}
312
318
  </span>
313
- {" / "}
319
+ <span className="text-muted-foreground">/</span>
314
320
  <span
315
- className={log.outputTokens !== null ? "text-amber-400" : "text-muted-foreground"}
321
+ className={cn(
322
+ "whitespace-nowrap",
323
+ log.outputTokens !== null ? "text-amber-400" : "text-muted-foreground",
324
+ )}
316
325
  >
317
326
  OUT {log.outputTokens !== null ? formatTokens(log.outputTokens) : "—"}
318
327
  </span>
@@ -421,7 +430,7 @@ export const LogEntryHeader = memo(function ({
421
430
  <Button
422
431
  variant="outline"
423
432
  size="icon"
424
- className="size-8"
433
+ className={HEADER_ACTION_BUTTON_CLASS}
425
434
  onClick={action.expansion.onToggle}
426
435
  disabled={action.expansion.isPending}
427
436
  aria-pressed={action.expansion.isExpanded}
@@ -450,8 +459,9 @@ export const LogEntryHeader = memo(function ({
450
459
  variant="outline"
451
460
  size="icon"
452
461
  className={cn(
453
- "size-8",
454
- action.diffWithRaw.active && "bg-accent text-accent-foreground",
462
+ HEADER_ACTION_BUTTON_CLASS,
463
+ action.diffWithRaw.active &&
464
+ "border-cyan-300/35 bg-cyan-400/15 text-cyan-100",
455
465
  )}
456
466
  onClick={action.diffWithRaw.onToggle}
457
467
  aria-pressed={action.diffWithRaw.active}
@@ -475,7 +485,7 @@ export const LogEntryHeader = memo(function ({
475
485
  <Button
476
486
  variant="outline"
477
487
  size="icon"
478
- className="size-8"
488
+ className={HEADER_ACTION_BUTTON_CLASS}
479
489
  onClick={action.diffWithPrevious}
480
490
  aria-label="Diff with previous"
481
491
  >
@@ -493,7 +503,7 @@ export const LogEntryHeader = memo(function ({
493
503
  <Button
494
504
  variant="outline"
495
505
  size="icon"
496
- className="size-8"
506
+ className={HEADER_ACTION_BUTTON_CLASS}
497
507
  onClick={action.onCopy}
498
508
  aria-label={action.copyCopied ? "Copied" : action.copyLabel}
499
509
  >
@@ -518,7 +528,7 @@ export const LogEntryHeader = memo(function ({
518
528
  <Button
519
529
  variant="outline"
520
530
  size="icon"
521
- className="size-8"
531
+ className={HEADER_ACTION_BUTTON_CLASS}
522
532
  onClick={onReplay}
523
533
  aria-label="Replay request"
524
534
  >
@@ -101,7 +101,7 @@ function ToolRow({ tool, called }: { tool: RequestToolDefinition; called: boolea
101
101
 
102
102
  return (
103
103
  <Collapsible open={open} onOpenChange={setOpen}>
104
- <div className="rounded-md border border-border/60 bg-background/40">
104
+ <div className="rounded-md border border-white/10 bg-black/20 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]">
105
105
  <div className="flex min-w-0 items-center gap-2 px-2.5 py-2">
106
106
  <button
107
107
  type="button"
@@ -152,13 +152,13 @@ function ToolRow({ tool, called }: { tool: RequestToolDefinition; called: boolea
152
152
  </button>
153
153
  </div>
154
154
  <CollapsibleContent>
155
- <div className="space-y-2 border-t border-border/60 px-2.5 py-2">
155
+ <div className="space-y-2 border-t border-white/10 bg-white/[0.02] px-2.5 py-2">
156
156
  {tool.description !== null && (
157
157
  <p className="text-xs leading-relaxed text-muted-foreground">{tool.description}</p>
158
158
  )}
159
159
  <RequiredParameters names={tool.requiredParameters} />
160
160
  {tool.schema !== null && (
161
- <ScrollArea className="max-h-64 rounded-md border border-border/60 bg-muted/20 p-2">
161
+ <ScrollArea className="max-h-64 rounded-md border border-white/10 bg-[#06080b] p-2">
162
162
  <JsonViewer data={safeJsonValue(tool.schema)} defaultExpandDepth={0} />
163
163
  </ScrollArea>
164
164
  )}
@@ -197,7 +197,7 @@ export const RequestTools = memo(function RequestTools({
197
197
  const hiddenPreviewCount = summary.tools.length - previewTools.length;
198
198
 
199
199
  return (
200
- <section className="mb-3 rounded-lg border border-border/70 bg-muted/20">
200
+ <section className="mb-3 overflow-hidden rounded-md border border-white/10 bg-[#080b0f] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]">
201
201
  <div className="flex min-w-0 flex-wrap items-center gap-2 px-3 py-2">
202
202
  <div className="flex min-w-0 flex-1 items-center gap-2">
203
203
  <Wrench className="size-4 shrink-0 text-sky-300/80" />
@@ -236,7 +236,7 @@ export const RequestTools = memo(function RequestTools({
236
236
  type="button"
237
237
  variant="ghost"
238
238
  size="sm"
239
- className="h-7 px-2"
239
+ className="h-7 border border-white/10 bg-black/20 px-2 text-muted-foreground hover:border-cyan-300/30 hover:bg-cyan-400/10 hover:text-cyan-100"
240
240
  onClick={() => setExpanded((value) => !value)}
241
241
  aria-expanded={expanded}
242
242
  >
@@ -245,7 +245,7 @@ export const RequestTools = memo(function RequestTools({
245
245
  </Button>
246
246
  </div>
247
247
 
248
- <div className="flex min-w-0 flex-wrap gap-1.5 border-t border-border/60 px-3 py-2">
248
+ <div className="flex min-w-0 flex-wrap gap-1.5 border-t border-white/10 bg-black/10 px-3 py-2">
249
249
  {previewTools.map((tool) => (
250
250
  <Badge
251
251
  key={tool.name}
@@ -266,8 +266,8 @@ export const RequestTools = memo(function RequestTools({
266
266
  </div>
267
267
 
268
268
  {expanded && (
269
- <div className="space-y-2 border-t border-border/60 px-3 py-3">
270
- <label className="flex h-8 items-center gap-2 rounded-md border border-border/70 bg-background/50 px-2 text-xs">
269
+ <div className="space-y-2 border-t border-white/10 bg-black/15 px-3 py-3">
270
+ <label className="flex h-8 items-center gap-2 rounded-md border border-white/10 bg-[#06080b] px-2 text-xs">
271
271
  <Search className="size-3.5 shrink-0 text-muted-foreground" />
272
272
  <input
273
273
  value={query}
@@ -40,56 +40,6 @@ export function CrabLogo({ className }: { className?: string }): JSX.Element {
40
40
  <line x1="9" y1="17.5" x2="8" y2="20.5" />
41
41
  <line x1="15" y1="17.5" x2="16" y2="20.5" />
42
42
  <line x1="17.5" y1="16" x2="19.5" y2="19.5" />
43
- {/* Duanwu zongzi charms */}
44
- <g>
45
- <path
46
- d="M5.6 17.7 L8.4 15.4 L10.8 18 C9.1 18.9 7.2 18.8 5.6 17.7 Z"
47
- fill="#2f6b3f"
48
- stroke="none"
49
- opacity="0.95"
50
- />
51
- <path
52
- d="M6.9 17.5 L8.4 16 L9.8 17.7 C8.9 18.1 7.8 18.1 6.9 17.5 Z"
53
- fill="#6fb36f"
54
- stroke="none"
55
- opacity="0.95"
56
- />
57
- <path d="M8.4 16 L8.4 18.4" stroke="#d6b45f" strokeWidth="0.34" />
58
- <circle cx="10.6" cy="17.75" r="0.36" fill="#c2412d" stroke="none" />
59
- </g>
60
- <g>
61
- <path
62
- d="M8 16.5 L11.9 13.6 L15.8 16.8 C13.3 18.4 10.4 18.3 8 16.5 Z"
63
- fill="#2f6b3f"
64
- stroke="none"
65
- opacity="0.97"
66
- />
67
- <path
68
- d="M9.8 16.3 L11.9 14.4 L14.3 16.5 C12.8 17.4 11.3 17.4 9.8 16.3 Z"
69
- fill="#6fb36f"
70
- stroke="none"
71
- opacity="0.95"
72
- />
73
- <path d="M11.9 14.3 L11.9 17.8" stroke="#d6b45f" strokeWidth="0.42" />
74
- <path d="M9.8 16.3 C11.2 15.8 12.9 15.9 14.3 16.5" stroke="#9fca78" strokeWidth="0.36" />
75
- <circle cx="15.6" cy="16.75" r="0.43" fill="#c2412d" stroke="none" />
76
- </g>
77
- <g>
78
- <path
79
- d="M13.6 18.1 L16.2 15.7 L18.5 18.2 C17.1 19.1 15.2 19 13.6 18.1 Z"
80
- fill="#2f6b3f"
81
- stroke="none"
82
- opacity="0.95"
83
- />
84
- <path
85
- d="M14.8 17.9 L16.2 16.3 L17.5 18 C16.6 18.4 15.7 18.4 14.8 17.9 Z"
86
- fill="#6fb36f"
87
- stroke="none"
88
- opacity="0.95"
89
- />
90
- <path d="M16.2 16.3 L16.2 18.6" stroke="#d6b45f" strokeWidth="0.34" />
91
- <circle cx="18.35" cy="18.15" r="0.36" fill="#c2412d" stroke="none" />
92
- </g>
93
43
  </svg>
94
44
  );
95
45
  }