@rozenite/network-activity-plugin 1.0.0-alpha.7 → 1.0.0-alpha.9

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 (96) hide show
  1. package/dist/App.html +2 -2
  2. package/dist/assets/{App-CIflVb88.js → App-CA1Fbh0I.js} +12009 -10809
  3. package/dist/assets/{App-Czu6Vt2P.css → App-DoHQsY5s.css} +43 -0
  4. package/dist/event-source.cjs +22 -0
  5. package/dist/event-source.js +23 -0
  6. package/dist/rozenite.json +1 -1
  7. package/dist/src/react-native/{network-inspector.d.ts → http/network-inspector.d.ts} +1 -1
  8. package/dist/src/react-native/sse/event-source.d.ts +2 -0
  9. package/dist/src/react-native/sse/sse-inspector.d.ts +9 -0
  10. package/dist/src/react-native/sse/sse-interceptor.d.ts +36 -0
  11. package/dist/src/react-native/sse/types.d.ts +6 -0
  12. package/dist/src/react-native/utils.d.ts +6 -0
  13. package/dist/src/react-native/websocket/websocket-inspector.d.ts +9 -0
  14. package/dist/src/react-native/websocket/websocket-interceptor.d.ts +74 -0
  15. package/dist/src/shared/client.d.ts +8 -4
  16. package/dist/src/shared/sse-events.d.ts +35 -0
  17. package/dist/src/shared/websocket-events.d.ts +60 -0
  18. package/dist/src/ui/components/Badge.d.ts +1 -1
  19. package/dist/src/ui/components/Button.d.ts +1 -1
  20. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +7 -0
  21. package/dist/src/ui/components/RequestList.d.ts +6 -26
  22. package/dist/src/ui/components/SidePanel.d.ts +1 -0
  23. package/dist/src/ui/components/Toolbar.d.ts +1 -0
  24. package/dist/src/ui/hooks/useCopyToClipboard.d.ts +4 -0
  25. package/dist/src/ui/state/derived.d.ts +5 -0
  26. package/dist/src/ui/state/hooks.d.ts +17 -0
  27. package/dist/src/ui/state/model.d.ts +98 -0
  28. package/dist/src/ui/state/store.d.ts +24 -0
  29. package/dist/src/ui/tabs/CookiesTab.d.ts +3 -6
  30. package/dist/src/ui/tabs/HeadersTab.d.ts +3 -15
  31. package/dist/src/ui/tabs/MessagesTab.d.ts +5 -0
  32. package/dist/src/ui/tabs/RequestTab.d.ts +2 -7
  33. package/dist/src/ui/tabs/ResponseTab.d.ts +2 -8
  34. package/dist/src/ui/tabs/SSEMessagesTab.d.ts +5 -0
  35. package/dist/src/ui/tabs/TimingTab.d.ts +3 -5
  36. package/dist/src/ui/types.d.ts +6 -3
  37. package/dist/src/ui/utils/assert.d.ts +1 -0
  38. package/dist/src/ui/utils/copyToClipboard.d.ts +1 -0
  39. package/dist/src/ui/utils/getHttpHeaderValue.d.ts +2 -0
  40. package/dist/src/ui/utils/getId.d.ts +1 -0
  41. package/dist/src/ui/utils/getStatusColor.d.ts +1 -0
  42. package/dist/useNetworkActivityDevTools.cjs +433 -34
  43. package/dist/useNetworkActivityDevTools.js +431 -34
  44. package/package.json +19 -8
  45. package/src/react-native/{network-inspector.ts → http/network-inspector.ts} +14 -32
  46. package/src/react-native/{xml-request.d.ts → http/xml-request.d.ts} +1 -0
  47. package/src/react-native/sse/event-source.ts +25 -0
  48. package/src/react-native/sse/sse-inspector.ts +117 -0
  49. package/src/react-native/sse/sse-interceptor.ts +162 -0
  50. package/src/react-native/sse/types.ts +9 -0
  51. package/src/react-native/useNetworkActivityDevTools.ts +75 -1
  52. package/src/react-native/utils.ts +43 -0
  53. package/src/react-native/websocket/websocket-inspector.ts +180 -0
  54. package/src/react-native/websocket/websocket-interceptor.d.ts +4 -0
  55. package/src/react-native/websocket/websocket-interceptor.ts +166 -0
  56. package/src/shared/client.ts +10 -4
  57. package/src/shared/sse-events.ts +44 -0
  58. package/src/shared/websocket-events.ts +79 -0
  59. package/src/ui/components/Badge.tsx +1 -1
  60. package/src/ui/components/Button.tsx +1 -1
  61. package/src/ui/components/Input.tsx +1 -1
  62. package/src/ui/components/JsonTree.tsx +13 -0
  63. package/src/ui/components/JsonTreeCopyableItem.tsx +33 -0
  64. package/src/ui/components/RequestList.tsx +42 -123
  65. package/src/ui/components/ScrollArea.tsx +1 -1
  66. package/src/ui/components/Separator.tsx +1 -1
  67. package/src/ui/components/SidePanel.tsx +323 -0
  68. package/src/ui/components/Tabs.tsx +2 -2
  69. package/src/ui/components/Toolbar.tsx +45 -0
  70. package/src/ui/hooks/useCopyToClipboard.ts +28 -0
  71. package/src/ui/state/derived.ts +112 -0
  72. package/src/ui/state/hooks.ts +44 -0
  73. package/src/ui/state/model.ts +129 -0
  74. package/src/ui/state/store.ts +559 -0
  75. package/src/ui/tabs/CookiesTab.tsx +168 -179
  76. package/src/ui/tabs/HeadersTab.tsx +24 -31
  77. package/src/ui/tabs/MessagesTab.tsx +276 -0
  78. package/src/ui/tabs/RequestTab.tsx +28 -31
  79. package/src/ui/tabs/ResponseTab.tsx +10 -12
  80. package/src/ui/tabs/SSEMessagesTab.tsx +213 -0
  81. package/src/ui/tabs/TimingTab.tsx +33 -44
  82. package/src/ui/types.ts +6 -2
  83. package/src/ui/utils/assert.ts +5 -0
  84. package/src/ui/utils/copyToClipboard.ts +3 -0
  85. package/src/ui/utils/getHttpHeaderValue.ts +14 -0
  86. package/src/ui/utils/getId.ts +10 -0
  87. package/src/ui/utils/getStatusColor.ts +15 -0
  88. package/src/ui/views/InspectorView.tsx +24 -320
  89. package/tailwind.config.ts +3 -0
  90. package/vite.config.ts +12 -0
  91. /package/dist/src/react-native/{network-requests-registry.d.ts → http/network-requests-registry.d.ts} +0 -0
  92. /package/dist/src/react-native/{xhr-interceptor.d.ts → http/xhr-interceptor.d.ts} +0 -0
  93. /package/dist/src/ui/{utils.d.ts → utils/cn.d.ts} +0 -0
  94. /package/src/react-native/{network-requests-registry.ts → http/network-requests-registry.ts} +0 -0
  95. /package/src/react-native/{xhr-interceptor.ts → http/xhr-interceptor.ts} +0 -0
  96. /package/src/ui/{utils.ts → utils/cn.ts} +0 -0
@@ -1,41 +1,33 @@
1
- import * as React from 'react';
1
+ import { useMemo, useState } from 'react';
2
2
  import {
3
3
  createColumnHelper,
4
4
  flexRender,
5
5
  getCoreRowModel,
6
6
  getSortedRowModel,
7
+ SortingFn,
7
8
  SortingState,
8
9
  useReactTable,
9
10
  } from '@tanstack/react-table';
10
- import { NetworkEntry } from '../types';
11
+ import { ProcessedRequest } from '../state/model';
11
12
  import { RequestId } from '../../shared/client';
13
+ import {
14
+ useNetworkActivityActions,
15
+ useProcessedRequests,
16
+ useSelectedRequestId,
17
+ } from '../state/hooks';
18
+ import { getStatusColor } from '../utils/getStatusColor';
12
19
 
13
20
  type NetworkRequest = {
14
- id: string;
21
+ id: RequestId;
15
22
  name: string;
16
- status: number;
23
+ status: string | number;
17
24
  method: string;
18
25
  domain: string;
19
26
  path: string;
20
27
  size: string;
21
28
  time: string;
22
29
  type: string;
23
- initiator: string;
24
30
  startTime: string;
25
- requestBody?: {
26
- type: string;
27
- data: string;
28
- };
29
- responseBody?: {
30
- type: string;
31
- data: string | null;
32
- };
33
- };
34
-
35
- type RequestListProps = {
36
- networkEntries: Map<RequestId, NetworkEntry>;
37
- selectedRequestId: RequestId | null;
38
- onRequestSelect: (requestId: RequestId) => void;
39
31
  };
40
32
 
41
33
  const formatSize = (bytes: number): string => {
@@ -67,10 +59,11 @@ const extractDomainAndPath = (
67
59
  url: string
68
60
  ): { domain: string; path: string } => {
69
61
  try {
70
- const urlObj = new URL(url);
62
+ const { hostname, pathname, search, hash, port } = new URL(url);
63
+
71
64
  return {
72
- domain: urlObj.hostname,
73
- path: urlObj.pathname + urlObj.search + urlObj.hash,
65
+ domain: `${hostname}${port ? `:${port}` : ''}`,
66
+ path: `${pathname}${search}${hash}`,
74
67
  };
75
68
  } catch {
76
69
  return { domain: 'unknown', path: url };
@@ -88,64 +81,7 @@ const generateName = (url: string): string => {
88
81
  }
89
82
  };
90
83
 
91
- const formatInitiator = (initiator: any): string => {
92
- if (!initiator) return 'Other';
93
- if (initiator.type === 'script' && initiator.url) {
94
- try {
95
- const url = new URL(initiator.url);
96
- const filename = url.pathname.split('/').pop() || url.hostname;
97
- const line = initiator.lineNumber ? `:${initiator.lineNumber}` : '';
98
- return `${filename}${line}`;
99
- } catch {
100
- return 'Script';
101
- }
102
- }
103
- return initiator.type || 'Other';
104
- };
105
-
106
- const mapResourceType = (type: string): string => {
107
- const typeMap: Record<string, string> = {
108
- Document: 'document',
109
- Stylesheet: 'stylesheet',
110
- Image: 'img',
111
- Media: 'media',
112
- Font: 'font',
113
- Script: 'script',
114
- XHR: 'xhr',
115
- Fetch: 'xhr',
116
- EventSource: 'eventsource',
117
- WebSocket: 'websocket',
118
- Manifest: 'manifest',
119
- Other: 'other',
120
- Ping: 'ping',
121
- CSPViolationReport: 'csp',
122
- Preflight: 'preflight',
123
- Subresource: 'subresource',
124
- };
125
- return typeMap[type] || 'other';
126
- };
127
-
128
- const getTypeColor = (type: string) => {
129
- const colors: Record<string, string> = {
130
- document: 'bg-blue-600',
131
- script: 'bg-yellow-600',
132
- stylesheet: 'bg-purple-600',
133
- xhr: 'bg-green-600',
134
- img: 'bg-pink-600',
135
- font: 'bg-orange-600',
136
- };
137
- return colors[type] || 'bg-gray-600';
138
- };
139
-
140
- const getStatusColor = (status: number) => {
141
- if (status >= 200 && status < 300) return 'text-green-400';
142
- if (status >= 300 && status < 400) return 'text-yellow-400';
143
- if (status >= 400) return 'text-red-400';
144
- return 'text-gray-400';
145
- };
146
-
147
- // Custom sorting functions
148
- const sortSize = (rowA: any, rowB: any, columnId: string) => {
84
+ const sortSize: SortingFn<NetworkRequest> = (rowA, rowB, columnId) => {
149
85
  const a = rowA.getValue(columnId) as string;
150
86
  const b = rowB.getValue(columnId) as string;
151
87
 
@@ -167,7 +103,7 @@ const sortSize = (rowA: any, rowB: any, columnId: string) => {
167
103
  return getNumericValue(a) - getNumericValue(b);
168
104
  };
169
105
 
170
- const sortTime = (rowA: any, rowB: any, columnId: string) => {
106
+ const sortTime: SortingFn<NetworkRequest> = (rowA, rowB, columnId) => {
171
107
  const a = rowA.getValue(columnId) as string;
172
108
  const b = rowB.getValue(columnId) as string;
173
109
 
@@ -183,38 +119,24 @@ const sortTime = (rowA: any, rowB: any, columnId: string) => {
183
119
  return getNumericValue(a) - getNumericValue(b);
184
120
  };
185
121
 
186
- // Convert NetworkEntry to NetworkRequest for UI display
187
- const processNetworkEntries = (
188
- networkEntries: Map<RequestId, NetworkEntry>
122
+ const processNetworkRequests = (
123
+ processedRequests: ProcessedRequest[]
189
124
  ): NetworkRequest[] => {
190
- return Array.from(networkEntries.values()).map((entry): NetworkRequest => {
191
- const { domain, path } = extractDomainAndPath(entry.url);
192
- const duration = entry.duration || 0;
125
+ return processedRequests.map((request): NetworkRequest => {
126
+ const { domain, path } = extractDomainAndPath(request.name);
127
+ const duration = request.duration || 0;
193
128
 
194
129
  return {
195
- id: entry.requestId,
196
- name: generateName(entry.url),
197
- status: entry.response?.status || 0,
198
- method: entry.request?.method || 'GET',
130
+ id: request.id,
131
+ name: generateName(request.name),
132
+ status: request.httpStatus || request.status,
133
+ method: request.method,
199
134
  domain,
200
135
  path,
201
- size: formatSize(entry.size || 0),
136
+ size: formatSize(request.size || 0),
202
137
  time: formatDuration(duration),
203
- type: mapResourceType(entry.type || 'Other'),
204
- initiator: formatInitiator(entry.initiator),
205
- startTime: formatStartTime(entry.startTime || 0),
206
- requestBody: entry.request?.postData
207
- ? {
208
- type: entry.request.headers['content-type'] || 'text/plain',
209
- data: entry.request.postData,
210
- }
211
- : undefined,
212
- responseBody: entry.responseBody
213
- ? {
214
- type: entry.response?.contentType || 'application/octet-stream',
215
- data: entry.responseBody.body,
216
- }
217
- : undefined,
138
+ type: request.type,
139
+ startTime: formatStartTime(request.timestamp),
218
140
  };
219
141
  });
220
142
  };
@@ -273,16 +195,15 @@ const columns = [
273
195
  }),
274
196
  ];
275
197
 
276
- export const RequestList: React.FC<RequestListProps> = ({
277
- networkEntries,
278
- selectedRequestId,
279
- onRequestSelect,
280
- }) => {
281
- const [sorting, setSorting] = React.useState<SortingState>([]);
198
+ export const RequestList = () => {
199
+ const actions = useNetworkActivityActions();
200
+ const processedRequests = useProcessedRequests();
201
+ const selectedRequestId = useSelectedRequestId();
202
+ const [sorting, setSorting] = useState<SortingState>([]);
282
203
 
283
- const requests = React.useMemo(() => {
284
- return processNetworkEntries(networkEntries);
285
- }, [networkEntries]);
204
+ const requests = useMemo(() => {
205
+ return processNetworkRequests(processedRequests);
206
+ }, [processedRequests]);
286
207
 
287
208
  const table = useReactTable({
288
209
  data: requests,
@@ -295,6 +216,10 @@ export const RequestList: React.FC<RequestListProps> = ({
295
216
  },
296
217
  });
297
218
 
219
+ const onRequestSelect = (requestId: RequestId): void => {
220
+ actions.setSelectedRequest(requestId);
221
+ };
222
+
298
223
  return (
299
224
  <div className="flex-1 overflow-auto">
300
225
  <table className="w-full">
@@ -366,11 +291,5 @@ export {
366
291
  formatStartTime,
367
292
  extractDomainAndPath,
368
293
  generateName,
369
- formatInitiator,
370
- mapResourceType,
371
- getTypeColor,
372
- getStatusColor,
373
- processNetworkEntries,
294
+ processNetworkRequests,
374
295
  };
375
-
376
- export type { NetworkRequest };
@@ -3,7 +3,7 @@
3
3
  import * as React from 'react';
4
4
  import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
5
5
 
6
- import { cn } from '../utils';
6
+ import { cn } from '../utils/cn';
7
7
 
8
8
  const ScrollArea = React.forwardRef<
9
9
  React.ElementRef<typeof ScrollAreaPrimitive.Root>,
@@ -3,7 +3,7 @@
3
3
  import * as React from 'react';
4
4
  import * as SeparatorPrimitive from '@radix-ui/react-separator';
5
5
 
6
- import { cn } from '../utils';
6
+ import { cn } from '../utils/cn';
7
7
 
8
8
  const Separator = React.forwardRef<
9
9
  React.ElementRef<typeof SeparatorPrimitive.Root>,
@@ -0,0 +1,323 @@
1
+ import { Badge } from './Badge';
2
+ import { Button } from './Button';
3
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from './Tabs';
4
+ import { HeadersTab } from '../tabs/HeadersTab';
5
+ import { RequestTab } from '../tabs/RequestTab';
6
+ import { ResponseTab } from '../tabs/ResponseTab';
7
+ import { CookiesTab } from '../tabs/CookiesTab';
8
+ import { TimingTab } from '../tabs/TimingTab';
9
+ import { X } from 'lucide-react';
10
+ import {
11
+ useNetworkActivityActions,
12
+ useNetworkActivityStore,
13
+ useSelectedRequest,
14
+ } from '../state/hooks';
15
+ import { NetworkEntry as OldNetworkEntry } from '../types';
16
+ import { getStatusColor } from '../utils/getStatusColor';
17
+ import { MessagesTab } from '../tabs/MessagesTab';
18
+ import { SSEMessagesTab } from '../tabs/SSEMessagesTab';
19
+
20
+ const getTypeColor = (type: string) => {
21
+ const colors: Record<string, string> = {
22
+ document: 'bg-blue-600',
23
+ script: 'bg-yellow-600',
24
+ stylesheet: 'bg-purple-600',
25
+ xhr: 'bg-green-600',
26
+ img: 'bg-pink-600',
27
+ font: 'bg-orange-600',
28
+ http: 'bg-green-600',
29
+ websocket: 'bg-blue-600',
30
+ sse: 'bg-purple-600',
31
+ };
32
+ return colors[type] || 'bg-gray-600';
33
+ };
34
+
35
+ // Adapter to convert new model to old format for tab components
36
+ const createLegacyNetworkEntry = (
37
+ selectedRequest: any,
38
+ httpDetails: any,
39
+ wsDetails: any
40
+ ): OldNetworkEntry | null => {
41
+ if (selectedRequest.type === 'http' && httpDetails) {
42
+ return {
43
+ requestId: httpDetails.id,
44
+ url: httpDetails.request?.url || '',
45
+ method: httpDetails.request?.method || 'GET',
46
+ headers: httpDetails.request?.headers || {},
47
+ body: httpDetails.request?.body,
48
+ status: httpDetails.status,
49
+ startTime: httpDetails.timestamp,
50
+ endTime: httpDetails.duration
51
+ ? httpDetails.timestamp + httpDetails.duration
52
+ : undefined,
53
+ duration: httpDetails.duration,
54
+ ttfb: httpDetails.ttfb,
55
+ type: httpDetails.resourceType,
56
+ initiator: httpDetails.initiator,
57
+ request: httpDetails.request,
58
+ response: httpDetails.response,
59
+ responseBody: httpDetails.response?.body
60
+ ? { body: httpDetails.response.body }
61
+ : undefined,
62
+ error: httpDetails.error,
63
+ canceled: httpDetails.canceled,
64
+ size: httpDetails.size,
65
+ };
66
+ } else if (selectedRequest.type === 'websocket' && wsDetails) {
67
+ // For WebSocket, create a minimal entry since tabs are designed for HTTP
68
+ return {
69
+ requestId: wsDetails.id,
70
+ url: wsDetails.connection?.url || '',
71
+ method: 'WS',
72
+ headers: {},
73
+ status: wsDetails.status === 'open' ? 'finished' : 'pending',
74
+ startTime: wsDetails.timestamp,
75
+ endTime: wsDetails.duration
76
+ ? wsDetails.timestamp + wsDetails.duration
77
+ : undefined,
78
+ duration: wsDetails.duration,
79
+ };
80
+ }
81
+ return null;
82
+ };
83
+
84
+ export const SidePanel = () => {
85
+ const actions = useNetworkActivityActions();
86
+ const selectedRequest = useSelectedRequest();
87
+ const client = useNetworkActivityStore((state) => state._client);
88
+
89
+ const onClose = (): void => {
90
+ actions.setSelectedRequest(null);
91
+ };
92
+
93
+ // Early return if no request is selected
94
+ if (!selectedRequest) {
95
+ return null;
96
+ }
97
+
98
+ // Get detailed information based on request type
99
+ const httpDetails = selectedRequest.type === 'http' ? selectedRequest : null;
100
+ const wsDetails =
101
+ selectedRequest.type === 'websocket' ? selectedRequest : null;
102
+ const sseDetails = selectedRequest.type === 'sse' ? selectedRequest : null;
103
+
104
+ // Extract name from the request
105
+ const requestName =
106
+ selectedRequest.type === 'http'
107
+ ? httpDetails?.request?.url || 'Unknown'
108
+ : selectedRequest.type === 'websocket'
109
+ ? wsDetails?.connection?.url || 'Unknown'
110
+ : sseDetails?.request?.url || 'Unknown';
111
+
112
+ // Extract status from the request
113
+ const requestStatus =
114
+ selectedRequest.type === 'http'
115
+ ? httpDetails?.response?.status || httpDetails?.status || 'pending'
116
+ : selectedRequest.type === 'websocket'
117
+ ? wsDetails?.status || 'unknown'
118
+ : sseDetails?.status || 'unknown';
119
+
120
+ // Create legacy network entry for tab components
121
+ const legacyEntry = createLegacyNetworkEntry(
122
+ selectedRequest,
123
+ httpDetails,
124
+ wsDetails
125
+ );
126
+ const legacyNetworkEntries = new Map<string, OldNetworkEntry>();
127
+ if (legacyEntry) {
128
+ legacyNetworkEntries.set(legacyEntry.requestId, legacyEntry);
129
+ }
130
+
131
+ const getTabsListTriggers = () => {
132
+ if (httpDetails) {
133
+ return (
134
+ <>
135
+ <TabsTrigger
136
+ value="headers"
137
+ className="data-[state=active]:bg-gray-700"
138
+ >
139
+ Headers
140
+ </TabsTrigger>
141
+ <TabsTrigger
142
+ value="request"
143
+ className="data-[state=active]:bg-gray-700"
144
+ >
145
+ Request
146
+ </TabsTrigger>
147
+ <TabsTrigger
148
+ value="response"
149
+ className="data-[state=active]:bg-gray-700"
150
+ >
151
+ Response
152
+ </TabsTrigger>
153
+ <TabsTrigger
154
+ value="cookies"
155
+ className="data-[state=active]:bg-gray-700"
156
+ >
157
+ Cookies
158
+ </TabsTrigger>
159
+ <TabsTrigger
160
+ value="timing"
161
+ className="data-[state=active]:bg-gray-700"
162
+ >
163
+ Timing
164
+ </TabsTrigger>
165
+ </>
166
+ );
167
+ }
168
+
169
+ if (sseDetails) {
170
+ return (
171
+ <>
172
+ <TabsTrigger
173
+ value="headers"
174
+ className="data-[state=active]:bg-gray-700"
175
+ >
176
+ Headers
177
+ </TabsTrigger>
178
+ <TabsTrigger
179
+ value="request"
180
+ className="data-[state=active]:bg-gray-700"
181
+ >
182
+ Request
183
+ </TabsTrigger>
184
+ <TabsTrigger
185
+ value="messages"
186
+ className="data-[state=active]:bg-gray-700"
187
+ >
188
+ Messages
189
+ </TabsTrigger>
190
+ </>
191
+ );
192
+ }
193
+
194
+ return (
195
+ <>
196
+ <TabsTrigger
197
+ value="messages"
198
+ className="data-[state=active]:bg-gray-700"
199
+ >
200
+ Messages
201
+ </TabsTrigger>
202
+ </>
203
+ );
204
+ };
205
+
206
+ const getTabsContent = () => {
207
+ if (httpDetails) {
208
+ return (
209
+ <>
210
+ <TabsContent value="headers" className="flex-1 m-0 overflow-hidden">
211
+ <HeadersTab selectedRequest={httpDetails} />
212
+ </TabsContent>
213
+
214
+ <TabsContent value="request" className="flex-1 m-0 overflow-hidden">
215
+ <RequestTab selectedRequest={httpDetails} />
216
+ </TabsContent>
217
+
218
+ <TabsContent value="response" className="flex-1 m-0 overflow-hidden">
219
+ <ResponseTab
220
+ selectedRequest={httpDetails}
221
+ onRequestResponseBody={(requestId) => {
222
+ if (client) {
223
+ client.send('get-response-body', {
224
+ requestId,
225
+ });
226
+ }
227
+ }}
228
+ />
229
+ </TabsContent>
230
+
231
+ <TabsContent value="cookies" className="flex-1 m-0 overflow-hidden">
232
+ <CookiesTab selectedRequest={httpDetails} />
233
+ </TabsContent>
234
+
235
+ <TabsContent value="timing" className="flex-1 m-0 overflow-hidden">
236
+ <TimingTab selectedRequest={httpDetails} />
237
+ </TabsContent>
238
+ </>
239
+ );
240
+ }
241
+
242
+ if (wsDetails) {
243
+ return (
244
+ <>
245
+ <TabsContent value="messages" className="flex-1 m-0 overflow-hidden">
246
+ <MessagesTab selectedRequest={wsDetails} />
247
+ </TabsContent>
248
+ </>
249
+ );
250
+ }
251
+
252
+ if (sseDetails) {
253
+ return (
254
+ <>
255
+ <TabsContent value="headers" className="flex-1 m-0 overflow-hidden">
256
+ <HeadersTab selectedRequest={sseDetails} />
257
+ </TabsContent>
258
+
259
+ <TabsContent value="request" className="flex-1 m-0 overflow-hidden">
260
+ <RequestTab selectedRequest={sseDetails} />
261
+ </TabsContent>
262
+
263
+ <TabsContent value="messages" className="flex-1 m-0 overflow-hidden">
264
+ <SSEMessagesTab selectedRequest={sseDetails} />
265
+ </TabsContent>
266
+
267
+ <TabsContent value="cookies" className="flex-1 m-0 overflow-hidden">
268
+ <CookiesTab selectedRequest={sseDetails} />
269
+ </TabsContent>
270
+ </>
271
+ );
272
+ }
273
+
274
+ throw new Error('Invalid request type');
275
+ };
276
+
277
+ return (
278
+ <div className="w-1/2 flex flex-col bg-gray-900">
279
+ {/* Side Panel Header */}
280
+ <div className="flex items-center justify-between p-3 border-b border-gray-700 bg-gray-800">
281
+ <div className="flex items-center gap-2">
282
+ <div
283
+ className={`w-3 h-3 rounded-full ${getTypeColor(
284
+ selectedRequest.type
285
+ )}`}
286
+ ></div>
287
+ <span className="font-medium">{requestName}</span>
288
+ <Badge
289
+ variant="outline"
290
+ className={`${getStatusColor(requestStatus)} border-current`}
291
+ >
292
+ {requestStatus}
293
+ </Badge>
294
+ </div>
295
+ <Button
296
+ variant="ghost"
297
+ size="sm"
298
+ onClick={onClose}
299
+ className="h-6 w-6 p-0 text-gray-400 hover:text-blue-400"
300
+ >
301
+ <X className="h-4 w-4" />
302
+ </Button>
303
+ </div>
304
+
305
+ {/* Side Panel Content */}
306
+ <div className="flex-1 overflow-hidden">
307
+ <Tabs
308
+ key={selectedRequest.id}
309
+ defaultValue={
310
+ selectedRequest.type === 'websocket' ? 'messages' : 'headers'
311
+ }
312
+ className="h-full flex flex-col"
313
+ >
314
+ <TabsList className="grid w-full grid-cols-5 bg-gray-800 rounded-none border-b border-gray-700">
315
+ {getTabsListTriggers()}
316
+ </TabsList>
317
+
318
+ {getTabsContent()}
319
+ </Tabs>
320
+ </div>
321
+ </div>
322
+ );
323
+ };
@@ -3,7 +3,7 @@
3
3
  import * as React from 'react';
4
4
  import * as TabsPrimitive from '@radix-ui/react-tabs';
5
5
 
6
- import { cn } from '../utils';
6
+ import { cn } from '../utils/cn';
7
7
 
8
8
  const Tabs = TabsPrimitive.Root;
9
9
 
@@ -44,7 +44,7 @@ const TabsContent = React.forwardRef<
44
44
  <TabsPrimitive.Content
45
45
  ref={ref}
46
46
  className={cn(
47
- 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
47
+ 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 min-h-0',
48
48
  className
49
49
  )}
50
50
  {...props}
@@ -0,0 +1,45 @@
1
+ import { Button } from './Button';
2
+ import { Circle, Square, Trash2 } from 'lucide-react';
3
+ import { useIsRecording, useNetworkActivityActions } from '../state/hooks';
4
+
5
+ export const Toolbar = () => {
6
+ const actions = useNetworkActivityActions();
7
+ const isRecording = useIsRecording();
8
+
9
+ const onToggleRecording = (): void => {
10
+ actions.setRecording(!isRecording);
11
+ };
12
+
13
+ const onClearRequests = (): void => {
14
+ actions.clearRequests();
15
+ };
16
+
17
+ return (
18
+ <div className="flex items-center gap-2 p-2 border-b border-gray-700 bg-gray-800">
19
+ <Button
20
+ variant="ghost"
21
+ size="sm"
22
+ onClick={onToggleRecording}
23
+ className={`h-8 w-8 p-0 ${
24
+ isRecording
25
+ ? 'text-red-400 hover:text-red-300'
26
+ : 'text-gray-400 hover:text-blue-400'
27
+ }`}
28
+ >
29
+ {isRecording ? (
30
+ <Circle className="h-4 w-4 fill-current" />
31
+ ) : (
32
+ <Square className="h-4 w-4" />
33
+ )}
34
+ </Button>
35
+ <Button
36
+ variant="ghost"
37
+ size="sm"
38
+ onClick={onClearRequests}
39
+ className="h-8 w-8 p-0 text-gray-400 hover:text-blue-400"
40
+ >
41
+ <Trash2 className="h-4 w-4" />
42
+ </Button>
43
+ </div>
44
+ );
45
+ };
@@ -0,0 +1,28 @@
1
+ import { useState, useRef, useEffect, useCallback } from 'react';
2
+
3
+ import { copyToClipboard } from '../utils/copyToClipboard';
4
+
5
+ export function useCopyToClipboard() {
6
+ const [isCopied, setIsCopied] = useState(false);
7
+
8
+ const timeoutRef = useRef<NodeJS.Timeout>();
9
+
10
+ useEffect(() => {
11
+ return () => clearTimeout(timeoutRef.current);
12
+ }, []);
13
+
14
+ const copy = useCallback(async (value: string) => {
15
+ try {
16
+ await copyToClipboard(value);
17
+
18
+ setIsCopied(true);
19
+
20
+ clearTimeout(timeoutRef.current);
21
+ timeoutRef.current = setTimeout(() => setIsCopied(false), 1000);
22
+ } catch (error) {
23
+ console.error('Failed to copy:', error);
24
+ }
25
+ }, []);
26
+
27
+ return { isCopied, copy };
28
+ }