@buoy-gg/network 7.0.3 → 7.0.5

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 (69) hide show
  1. package/README.md +3 -2
  2. package/lib/commonjs/index.js +85 -0
  3. package/lib/commonjs/network/components/NetworkEventDetailView.js +201 -40
  4. package/lib/commonjs/network/components/NetworkEventItemCompact.js +52 -5
  5. package/lib/commonjs/network/components/NetworkHeaderLive.js +132 -0
  6. package/lib/commonjs/network/components/NetworkListScreen.js +134 -56
  7. package/lib/commonjs/network/components/NetworkModal.js +154 -36
  8. package/lib/commonjs/network/components/NetworkSavedScreen.js +245 -0
  9. package/lib/commonjs/network/components/NetworkUpgradeBanner.js +76 -0
  10. package/lib/commonjs/network/hooks/useLiveNetworkEvent.js +62 -0
  11. package/lib/commonjs/network/hooks/useNetworkSaved.js +38 -0
  12. package/lib/commonjs/network/index.js +84 -0
  13. package/lib/commonjs/network/sync/networkSyncAdapter.js +85 -7
  14. package/lib/commonjs/network/utils/generateCurl.js +71 -0
  15. package/lib/commonjs/network/utils/networkEventStore.js +10 -1
  16. package/lib/commonjs/network/utils/networkListener.js +54 -1
  17. package/lib/commonjs/network/utils/networkSavedStore.js +575 -0
  18. package/lib/commonjs/network/utils/withPinnedFirst.js +32 -0
  19. package/lib/module/index.js +17 -1
  20. package/lib/module/network/components/NetworkEventDetailView.js +204 -41
  21. package/lib/module/network/components/NetworkEventItemCompact.js +53 -6
  22. package/lib/module/network/components/NetworkHeaderLive.js +132 -2
  23. package/lib/module/network/components/NetworkListScreen.js +136 -58
  24. package/lib/module/network/components/NetworkModal.js +156 -39
  25. package/lib/module/network/components/NetworkSavedScreen.js +241 -0
  26. package/lib/module/network/components/NetworkUpgradeBanner.js +72 -0
  27. package/lib/module/network/hooks/useLiveNetworkEvent.js +58 -0
  28. package/lib/module/network/hooks/useNetworkSaved.js +34 -0
  29. package/lib/module/network/index.js +7 -1
  30. package/lib/module/network/sync/networkSyncAdapter.js +85 -7
  31. package/lib/module/network/utils/generateCurl.js +67 -0
  32. package/lib/module/network/utils/networkEventStore.js +10 -1
  33. package/lib/module/network/utils/networkListener.js +54 -1
  34. package/lib/module/network/utils/networkSavedStore.js +569 -0
  35. package/lib/module/network/utils/withPinnedFirst.js +28 -0
  36. package/lib/typescript/index.d.ts +13 -1
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  39. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +12 -0
  40. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  41. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +28 -0
  42. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  43. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  44. package/lib/typescript/network/components/NetworkModal.d.ts +34 -0
  45. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  46. package/lib/typescript/network/components/NetworkSavedScreen.d.ts +29 -0
  47. package/lib/typescript/network/components/NetworkSavedScreen.d.ts.map +1 -0
  48. package/lib/typescript/network/components/NetworkUpgradeBanner.d.ts +17 -0
  49. package/lib/typescript/network/components/NetworkUpgradeBanner.d.ts.map +1 -0
  50. package/lib/typescript/network/hooks/useLiveNetworkEvent.d.ts +19 -0
  51. package/lib/typescript/network/hooks/useLiveNetworkEvent.d.ts.map +1 -0
  52. package/lib/typescript/network/hooks/useNetworkSaved.d.ts +21 -0
  53. package/lib/typescript/network/hooks/useNetworkSaved.d.ts.map +1 -0
  54. package/lib/typescript/network/index.d.ts +8 -1
  55. package/lib/typescript/network/index.d.ts.map +1 -1
  56. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +52 -3
  57. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  58. package/lib/typescript/network/types/index.d.ts +43 -0
  59. package/lib/typescript/network/types/index.d.ts.map +1 -1
  60. package/lib/typescript/network/utils/generateCurl.d.ts +3 -0
  61. package/lib/typescript/network/utils/generateCurl.d.ts.map +1 -0
  62. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  63. package/lib/typescript/network/utils/networkListener.d.ts +4 -0
  64. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  65. package/lib/typescript/network/utils/networkSavedStore.d.ts +186 -0
  66. package/lib/typescript/network/utils/networkSavedStore.d.ts.map +1 -0
  67. package/lib/typescript/network/utils/withPinnedFirst.d.ts +24 -0
  68. package/lib/typescript/network/utils/withPinnedFirst.d.ts.map +1 -0
  69. package/package.json +2 -2
package/README.md CHANGED
@@ -39,8 +39,9 @@ Want a different label or color? Build the tool with `createNetworkTool({ name,
39
39
  - **GraphQL gets special treatment** — operation names are extracted from queries, mutations, and subscriptions and shown with variables in arrow notation: `GetUser › 123`. No more guessing which `/graphql` request is which.
40
40
  - **Full request detail** — request/response headers, payloads, HTTP status, and duration for every call, with status-colored rows.
41
41
  - **Filter and search** — narrow by status code, HTTP method, or URL pattern; search captured events; pause/resume capture; clear history.
42
+ - **Pin the one that failed** — pin a request to hold it at the top of the list (ignoring your filters), or save it to a separate favorites list. Both keep a full snapshot, so it survives Clear, the 500-request cap, and an app restart. Long-press a row, or use the buttons in the request detail header.
42
43
  - **Ignore the noise** — a shared ignored-domains/URL filter (`useIgnoredPatterns`) hides endpoints you never want to see, shared with the Events tool.
43
- - **Hooks for custom UIs** — `useNetworkEvents`, `useNetworkEventList`, and `useNetworkBadgeCounts` expose the capture stream with narrow per-consumer subscriptions.
44
+ - **Hooks for custom UIs** — `useNetworkEvents`, `useNetworkEventList`, `useNetworkBadgeCounts`, and `useNetworkSaved` expose the capture stream with narrow per-consumer subscriptions.
44
45
 
45
46
  ## Desktop & AI
46
47
 
@@ -50,7 +51,7 @@ The same live session streams to [Buoy Desktop](https://github.com/Buoy-gg/Buoy-
50
51
 
51
52
  Every tool is free. [Pro](https://buoy.gg/pricing) unlocks production builds, the MCP server, and unlimited capture. Every weekend, Pro features unlock free for everyone.
52
53
 
53
- > 🎯 **Flutter (beta):** Buoy is now available for Flutter — `flutter pub add buoy`. The network inspector ships first, streaming to the same desktop dashboard and MCP server. [Details →](https://buoy.gg/flutter)
54
+ > 🎯 **Flutter (beta):** Buoy is now a full suite for Flutter — `flutter pub add buoy`. Network, storage, console, env, routes, images, Riverpod, events, and a live perf HUD, all streaming to the same desktop dashboard and MCP server. [Details →](https://buoy.gg/flutter)
54
55
 
55
56
  ---
56
57
 
@@ -3,12 +3,36 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ Object.defineProperty(exports, "FREE_TIER_SAVED_LIMIT", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _networkSavedStore.FREE_TIER_SAVED_LIMIT;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "MAX_PINNED", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _networkSavedStore.MAX_PINNED;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "MAX_SAVED", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _networkSavedStore.MAX_SAVED;
22
+ }
23
+ });
6
24
  Object.defineProperty(exports, "NetworkBodyResolverProvider", {
7
25
  enumerable: true,
8
26
  get: function () {
9
27
  return _networkBodyResolver.NetworkBodyResolverProvider;
10
28
  }
11
29
  });
30
+ Object.defineProperty(exports, "NetworkDetailPinActions", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _NetworkHeaderLive.NetworkDetailPinActions;
34
+ }
35
+ });
12
36
  Object.defineProperty(exports, "NetworkEventDetailView", {
13
37
  enumerable: true,
14
38
  get: function () {
@@ -27,6 +51,24 @@ Object.defineProperty(exports, "NetworkModal", {
27
51
  return _NetworkModal.NetworkModal;
28
52
  }
29
53
  });
54
+ Object.defineProperty(exports, "NetworkSavedButton", {
55
+ enumerable: true,
56
+ get: function () {
57
+ return _NetworkHeaderLive.NetworkSavedButton;
58
+ }
59
+ });
60
+ Object.defineProperty(exports, "NetworkSavedScreen", {
61
+ enumerable: true,
62
+ get: function () {
63
+ return _NetworkSavedScreen.NetworkSavedScreen;
64
+ }
65
+ });
66
+ Object.defineProperty(exports, "NetworkToolView", {
67
+ enumerable: true,
68
+ get: function () {
69
+ return _NetworkModal.NetworkToolView;
70
+ }
71
+ });
30
72
  Object.defineProperty(exports, "TickProvider", {
31
73
  enumerable: true,
32
74
  get: function () {
@@ -45,6 +87,12 @@ Object.defineProperty(exports, "createNetworkTool", {
45
87
  return _preset.createNetworkTool;
46
88
  }
47
89
  });
90
+ Object.defineProperty(exports, "flagsForEventId", {
91
+ enumerable: true,
92
+ get: function () {
93
+ return _networkSavedStore.flagsForEventId;
94
+ }
95
+ });
48
96
  Object.defineProperty(exports, "formatBytes", {
49
97
  enumerable: true,
50
98
  get: function () {
@@ -63,6 +111,12 @@ Object.defineProperty(exports, "formatHttpStatus", {
63
111
  return _formatting.formatHttpStatus;
64
112
  }
65
113
  });
114
+ Object.defineProperty(exports, "generateCurl", {
115
+ enumerable: true,
116
+ get: function () {
117
+ return _generateCurl.generateCurl;
118
+ }
119
+ });
66
120
  Object.defineProperty(exports, "ignoredPatternsStore", {
67
121
  enumerable: true,
68
122
  get: function () {
@@ -87,6 +141,12 @@ Object.defineProperty(exports, "networkListener", {
87
141
  return _networkListener.networkListener;
88
142
  }
89
143
  });
144
+ Object.defineProperty(exports, "networkSavedStore", {
145
+ enumerable: true,
146
+ get: function () {
147
+ return _networkSavedStore.networkSavedStore;
148
+ }
149
+ });
90
150
  Object.defineProperty(exports, "networkSyncAdapter", {
91
151
  enumerable: true,
92
152
  get: function () {
@@ -105,6 +165,12 @@ Object.defineProperty(exports, "removeAllNetworkListeners", {
105
165
  return _networkListener.removeAllNetworkListeners;
106
166
  }
107
167
  });
168
+ Object.defineProperty(exports, "splitPinned", {
169
+ enumerable: true,
170
+ get: function () {
171
+ return _withPinnedFirst.splitPinned;
172
+ }
173
+ });
108
174
  Object.defineProperty(exports, "urlMatchesIgnoredPattern", {
109
175
  enumerable: true,
110
176
  get: function () {
@@ -117,6 +183,12 @@ Object.defineProperty(exports, "useIgnoredPatterns", {
117
183
  return _sharedUi.useIgnoredPatterns;
118
184
  }
119
185
  });
186
+ Object.defineProperty(exports, "useLiveNetworkEvent", {
187
+ enumerable: true,
188
+ get: function () {
189
+ return _useLiveNetworkEvent.useLiveNetworkEvent;
190
+ }
191
+ });
120
192
  Object.defineProperty(exports, "useNetworkBadgeCounts", {
121
193
  enumerable: true,
122
194
  get: function () {
@@ -135,6 +207,12 @@ Object.defineProperty(exports, "useNetworkEvents", {
135
207
  return _useNetworkEvents.useNetworkEvents;
136
208
  }
137
209
  });
210
+ Object.defineProperty(exports, "useNetworkSaved", {
211
+ enumerable: true,
212
+ get: function () {
213
+ return _useNetworkSaved.useNetworkSaved;
214
+ }
215
+ });
138
216
  Object.defineProperty(exports, "useTickEveryMinute", {
139
217
  enumerable: true,
140
218
  get: function () {
@@ -145,13 +223,20 @@ var _preset = require("./preset");
145
223
  var _NetworkModal = require("./network/components/NetworkModal");
146
224
  var _NetworkEventDetailView = require("./network/components/NetworkEventDetailView");
147
225
  var _NetworkEventItemCompact = require("./network/components/NetworkEventItemCompact");
226
+ var _NetworkSavedScreen = require("./network/components/NetworkSavedScreen");
227
+ var _NetworkHeaderLive = require("./network/components/NetworkHeaderLive");
148
228
  var _useNetworkEvents = require("./network/hooks/useNetworkEvents");
149
229
  var _useNetworkEventList = require("./network/hooks/useNetworkEventList");
230
+ var _useLiveNetworkEvent = require("./network/hooks/useLiveNetworkEvent");
150
231
  var _useNetworkBadgeCounts = require("./network/hooks/useNetworkBadgeCounts");
232
+ var _useNetworkSaved = require("./network/hooks/useNetworkSaved");
233
+ var _withPinnedFirst = require("./network/utils/withPinnedFirst");
151
234
  var _useTickEveryMinute = require("./network/hooks/useTickEveryMinute");
152
235
  var _formatting = require("./network/utils/formatting");
153
236
  var _sharedUi = require("@buoy-gg/shared-ui");
154
237
  var _networkSyncAdapter = require("./network/sync/networkSyncAdapter");
155
238
  var _networkBodyResolver = require("./network/sync/networkBodyResolver");
156
239
  var _networkEventStore = require("./network/utils/networkEventStore");
240
+ var _networkSavedStore = require("./network/utils/networkSavedStore");
241
+ var _generateCurl = require("./network/utils/generateCurl");
157
242
  var _networkListener = require("./network/utils/networkListener");
@@ -12,12 +12,83 @@ var _dataViewer = require("@buoy-gg/shared-ui/dataViewer");
12
12
  var _formatGraphQLVariables = require("../utils/formatGraphQLVariables");
13
13
  var _networkBodyResolver = require("../sync/networkBodyResolver");
14
14
  var _jsxRuntime = require("react/jsx-runtime");
15
+ /**
16
+ * Colour for the icons that merely LABEL a section (Sent/Received, the JSON
17
+ * body glyphs). The Lock/Unlock pair keeps its colour because
18
+ * secure-vs-insecure is a genuine signal, as does the status pill.
19
+ */const NEUTRAL_ICON = _sharedUi.macOSColors.semantic.success;
20
+ /**
21
+ * The measurable slice of a request's lifetime, as proportional bars.
22
+ *
23
+ * Renders NOTHING when the interceptor couldn't measure it — an older device
24
+ * talking to a newer dashboard, or a request that died before headers. A phase
25
+ * row reading `0ms` would claim we measured zero, which is a different and
26
+ * false statement from "we didn't measure this".
27
+ *
28
+ * Only two phases, on purpose. Queueing / DNS / TLS / request-send all happen
29
+ * below the JS layer with nothing to read them from — see NetworkTimings. The
30
+ * design this came from shows five, but derives the other three by multiplying
31
+ * the total by fixed fractions, i.e. they are invented. Two real ones are worth
32
+ * more than five where three are decoration.
33
+ */
34
+ const TimingPhases = ({
35
+ timings,
36
+ total
37
+ }) => {
38
+ // Guard the divisor: a sub-millisecond request would otherwise divide by 0.
39
+ const span = Math.max(1, total);
40
+ const phases = [{
41
+ // Deliberately NOT called "server time". Measured from JS, this includes
42
+ // queueing, DNS, TLS and sending the request — everything before the
43
+ // headers land. The subtitle says so rather than implying Chrome-grade
44
+ // precision we don't have.
45
+ label: "Waiting (TTFB)",
46
+ hint: "incl. queue, DNS, TLS",
47
+ value: timings.ttfb,
48
+ color: _sharedUi.macOSColors.semantic.warning
49
+ }, {
50
+ label: "Content download",
51
+ hint: undefined,
52
+ value: timings.download,
53
+ color: _sharedUi.macOSColors.semantic.success
54
+ }];
55
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
56
+ style: styles.phaseList,
57
+ children: phases.map(phase => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
58
+ style: styles.phaseRow,
59
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
60
+ style: styles.phaseLabelGroup,
61
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
62
+ style: styles.phaseLabel,
63
+ children: phase.label
64
+ }), phase.hint ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
65
+ style: styles.phaseHint,
66
+ children: phase.hint
67
+ }) : null]
68
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
69
+ style: styles.phaseTrack,
70
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
71
+ style: [styles.phaseBar, {
72
+ backgroundColor: phase.color,
73
+ // A floor so a 1ms phase is still a visible tick.
74
+ width: `${Math.max(1, phase.value / span * 100)}%`
75
+ }]
76
+ })
77
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
78
+ style: styles.phaseValue,
79
+ children: (0, _formatting.formatDuration)(phase.value)
80
+ })]
81
+ }, phase.label))
82
+ });
83
+ };
84
+
15
85
  // Component for collapsible sections matching Sentry style
16
86
  const CollapsibleSection = ({
17
87
  title,
18
88
  icon,
19
89
  children,
20
- defaultOpen = false
90
+ defaultOpen = false,
91
+ count
21
92
  }) => {
22
93
  const [isOpen, setIsOpen] = (0, _react.useState)(defaultOpen);
23
94
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
@@ -30,7 +101,10 @@ const CollapsibleSection = ({
30
101
  children: [icon, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
31
102
  style: styles.collapsibleTitleText,
32
103
  children: title
33
- })]
104
+ }), count !== undefined ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
105
+ style: styles.collapsibleCount,
106
+ children: count
107
+ }) : null]
34
108
  }), isOpen ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ChevronUp, {
35
109
  size: 16,
36
110
  color: _sharedUi.macOSColors.text.secondary
@@ -98,7 +172,23 @@ const UrlBreakdown = ({
98
172
  };
99
173
  }
100
174
  };
101
- const urlParts = parseUrl(url);
175
+
176
+ /**
177
+ * MUST be memoized. `parseUrl` builds a fresh `params` object every call, and
178
+ * that object is handed to `DataViewer` as its `data` prop — whose flattening
179
+ * effect keys on `data` by IDENTITY. Computed inline, every render of this
180
+ * component produced a new object, so the effect re-fired and re-entered
181
+ * "Processing data…" forever. The detail view re-renders constantly on the
182
+ * dashboard (it tracks the live event as the device pushes snapshots ~4×/sec),
183
+ * so this pinned the query-parameter viewer in a permanent processing loop.
184
+ *
185
+ * `url` is a string, so this is stable by value.
186
+ */
187
+ const urlParts = (0, _react.useMemo)(() => parseUrl(url),
188
+ // parseUrl closes over the graphql/operation props, which are what change
189
+ // the derived pathname.
190
+ // eslint-disable-next-line react-hooks/exhaustive-deps
191
+ [url, requestClient, operationName, graphqlVariables]);
102
192
  const [showParams, setShowParams] = (0, _react.useState)(false);
103
193
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
104
194
  style: styles.urlBreakdown,
@@ -147,6 +237,9 @@ const UrlBreakdown = ({
147
237
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
148
238
  style: styles.queryParamsToggleText,
149
239
  children: "Query Parameters"
240
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
241
+ style: styles.collapsibleCount,
242
+ children: Object.keys(urlParts.params).length
150
243
  }), showParams ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ChevronUp, {
151
244
  size: 14,
152
245
  color: _sharedUi.macOSColors.text.secondary
@@ -362,34 +455,41 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
362
455
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
363
456
  style: styles.timingExact,
364
457
  children: ["(", new Date(event.timestamp).toLocaleTimeString(), ")"]
365
- })]
366
- }), event.requestSize || event.responseSize ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
367
- style: styles.sizeRow,
368
- children: [event.requestSize !== undefined ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
369
- style: styles.sizeItem,
370
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Upload, {
371
- size: 10,
372
- color: _sharedUi.macOSColors.semantic.info
373
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
374
- style: styles.sizeLabel,
375
- children: "Sent:"
376
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
377
- style: styles.sizeValue,
378
- children: (0, _formatting.formatBytes)(event.requestSize)
379
- })]
380
- }) : null, event.responseSize !== undefined ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
381
- style: styles.sizeItem,
382
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Download, {
383
- size: 10,
384
- color: _sharedUi.macOSColors.semantic.success
385
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
386
- style: styles.sizeLabel,
387
- children: "Received:"
388
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
389
- style: styles.sizeValue,
390
- children: (0, _formatting.formatBytes)(event.responseSize)
391
- })]
458
+ }), event.requestSize || event.responseSize ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
459
+ style: styles.sizeRow,
460
+ children: [event.requestSize !== undefined ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
461
+ style: styles.sizeItem,
462
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Upload, {
463
+ size: 10,
464
+ color: _sharedUi.macOSColors.semantic.info
465
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
466
+ style: styles.sizeLabel,
467
+ children: "Sent:"
468
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
469
+ style: styles.sizeValue,
470
+ children: (0, _formatting.formatBytes)(event.requestSize)
471
+ })]
472
+ }) : null, event.responseSize !== undefined ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
473
+ style: styles.sizeItem,
474
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Download, {
475
+ size: 10,
476
+ color: NEUTRAL_ICON
477
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
478
+ style: styles.sizeLabel,
479
+ children: "Received:"
480
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
481
+ style: styles.sizeValue,
482
+ children: (0, _formatting.formatBytes)(event.responseSize)
483
+ })]
484
+ }) : null]
392
485
  }) : null]
486
+ }), event.timings ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
487
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
488
+ style: styles.phaseDivider
489
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(TimingPhases, {
490
+ timings: event.timings,
491
+ total: event.duration ?? event.timings.ttfb + event.timings.download
492
+ })]
393
493
  }) : null]
394
494
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(CollapsibleSection, {
395
495
  title: "Request Headers",
@@ -398,6 +498,7 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
398
498
  color: _sharedUi.macOSColors.semantic.info
399
499
  }),
400
500
  defaultOpen: false,
501
+ count: Object.keys(event.requestHeaders).length,
401
502
  children: Object.keys(event.requestHeaders).length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
402
503
  style: styles.dataViewerContainer,
403
504
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_dataViewer.DataViewer, {
@@ -416,9 +517,10 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
416
517
  title: "Response Headers",
417
518
  icon: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Download, {
418
519
  size: 14,
419
- color: _sharedUi.macOSColors.semantic.success
520
+ color: NEUTRAL_ICON
420
521
  }),
421
522
  defaultOpen: false,
523
+ count: Object.keys(event.responseHeaders).length,
422
524
  children: Object.keys(event.responseHeaders).length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
423
525
  style: styles.dataViewerContainer,
424
526
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_dataViewer.DataViewer, {
@@ -466,9 +568,12 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
466
568
  title: "Response Body",
467
569
  icon: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.FileJson, {
468
570
  size: 14,
469
- color: _sharedUi.macOSColors.semantic.success
470
- }),
471
- defaultOpen: false,
571
+ color: NEUTRAL_ICON
572
+ })
573
+ // The body is what you opened the row FOR — the design has it
574
+ // expanded on arrival, everything else closed.
575
+ ,
576
+ defaultOpen: true,
472
577
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
473
578
  style: styles.dataViewerContainer,
474
579
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_dataViewer.DataViewer, {
@@ -484,7 +589,7 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
484
589
  title: "Response Body",
485
590
  icon: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.FileJson, {
486
591
  size: 14,
487
- color: _sharedUi.macOSColors.semantic.success
592
+ color: NEUTRAL_ICON
488
593
  }),
489
594
  defaultOpen: true,
490
595
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
@@ -710,6 +815,7 @@ const styles = _reactNative.StyleSheet.create({
710
815
  marginBottom: 8
711
816
  },
712
817
  queryParamsToggleText: {
818
+ flexGrow: 0,
713
819
  color: _sharedUi.macOSColors.text.primary,
714
820
  fontSize: 12,
715
821
  fontWeight: "600",
@@ -728,7 +834,10 @@ const styles = _reactNative.StyleSheet.create({
728
834
  timingRow: {
729
835
  flexDirection: "row",
730
836
  alignItems: "center",
731
- gap: 6
837
+ gap: 6,
838
+ // Wraps rather than squashing when the card is narrow (a phone, or a
839
+ // half-width panel) — the sizes drop to their own line there.
840
+ flexWrap: "wrap"
732
841
  },
733
842
  timingLabel: {
734
843
  color: _sharedUi.macOSColors.text.secondary,
@@ -746,11 +855,11 @@ const styles = _reactNative.StyleSheet.create({
746
855
  },
747
856
  sizeRow: {
748
857
  flexDirection: "row",
858
+ alignItems: "center",
749
859
  gap: 16,
750
- marginTop: 8,
751
- paddingTop: 8,
752
- borderTopWidth: 1,
753
- borderTopColor: _sharedUi.macOSColors.border.default
860
+ // Pushed to the far end of the Started row. No top border/padding — it is
861
+ // no longer a separate strip.
862
+ marginLeft: "auto"
754
863
  },
755
864
  sizeItem: {
756
865
  flexDirection: "row",
@@ -767,6 +876,50 @@ const styles = _reactNative.StyleSheet.create({
767
876
  fontFamily: "monospace",
768
877
  fontWeight: "600"
769
878
  },
879
+ /* ---- Timing phases ---- */
880
+ phaseDivider: {
881
+ height: 1,
882
+ backgroundColor: _sharedUi.macOSColors.border.default,
883
+ marginTop: 12,
884
+ marginBottom: 12
885
+ },
886
+ phaseList: {
887
+ gap: 8
888
+ },
889
+ phaseRow: {
890
+ flexDirection: "row",
891
+ alignItems: "center",
892
+ gap: 14
893
+ },
894
+ phaseLabelGroup: {
895
+ width: 150
896
+ },
897
+ phaseLabel: {
898
+ fontSize: 12,
899
+ color: _sharedUi.macOSColors.text.secondary
900
+ },
901
+ phaseHint: {
902
+ fontSize: 10,
903
+ color: _sharedUi.macOSColors.text.muted
904
+ },
905
+ phaseTrack: {
906
+ flex: 1,
907
+ height: 7,
908
+ borderRadius: 4,
909
+ backgroundColor: _sharedUi.macOSColors.background.hover,
910
+ justifyContent: "center"
911
+ },
912
+ phaseBar: {
913
+ height: 7,
914
+ borderRadius: 4
915
+ },
916
+ phaseValue: {
917
+ width: 72,
918
+ textAlign: "right",
919
+ fontSize: 12,
920
+ fontFamily: "monospace",
921
+ color: _sharedUi.macOSColors.text.primary
922
+ },
770
923
  // Collapsible section styles
771
924
  collapsibleSection: {
772
925
  marginHorizontal: 12,
@@ -782,6 +935,8 @@ const styles = _reactNative.StyleSheet.create({
782
935
  alignItems: "center",
783
936
  justifyContent: "space-between",
784
937
  padding: 12,
938
+ // `background.hover` is the SELECTION grey — heavy enough here that every
939
+ // section header read as a toolbar. The design uses a near-white strip.
785
940
  backgroundColor: _sharedUi.macOSColors.background.hover
786
941
  },
787
942
  collapsibleTitle: {
@@ -794,6 +949,12 @@ const styles = _reactNative.StyleSheet.create({
794
949
  fontSize: 13,
795
950
  fontWeight: "600"
796
951
  },
952
+ /** The "how much is in here" number beside a section title. */
953
+ collapsibleCount: {
954
+ color: _sharedUi.macOSColors.text.muted,
955
+ fontSize: 11.5,
956
+ fontFamily: "monospace"
957
+ },
797
958
  collapsibleContent: {
798
959
  padding: 12
799
960
  },
@@ -35,6 +35,39 @@ function getContentTypeBadge(headers) {
35
35
  return null;
36
36
  }
37
37
 
38
+ /**
39
+ * The transport chip (`fetch` / `GQL` / `gRPC` / `XHR`), one brand colour each.
40
+ */
41
+ function getClientChipColors(requestClient) {
42
+ switch (requestClient) {
43
+ case "fetch":
44
+ return {
45
+ backgroundColor: "rgba(74, 144, 226, 0.15)",
46
+ color: "#4A90E2"
47
+ };
48
+ case "graphql":
49
+ return {
50
+ backgroundColor: "rgba(229, 53, 171, 0.15)",
51
+ color: "#E535AB"
52
+ };
53
+ case "grpc-web":
54
+ return {
55
+ backgroundColor: "rgba(16, 185, 129, 0.15)",
56
+ color: "#10B981"
57
+ };
58
+ case "xhr":
59
+ return {
60
+ backgroundColor: "rgba(245, 158, 11, 0.15)",
61
+ color: "#F59E0B"
62
+ };
63
+ default:
64
+ return {
65
+ backgroundColor: "rgba(147, 51, 234, 0.15)",
66
+ color: "#9333EA"
67
+ };
68
+ }
69
+ }
70
+
38
71
  // Decomposed components following rule3 - Component Composition
39
72
 
40
73
  // Status indicator component - single responsibility
@@ -137,7 +170,10 @@ const NetworkEventItemCompact = exports.NetworkEventItemCompact = /*#__PURE__*/(
137
170
  // renders to this component instead of a compiled anonymous name.
138
171
  function NetworkEventItemCompact({
139
172
  event,
140
- onPress
173
+ onPress,
174
+ pinned = false,
175
+ saved = false,
176
+ onLongPress
141
177
  }) {
142
178
  const statusColor = getStatusColor(event.status, event.error);
143
179
  const isPending = !event.status && !event.error;
@@ -162,23 +198,30 @@ function NetworkEventItemCompact({
162
198
  }
163
199
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.DevToolsCard, {
164
200
  onPress: () => onPress(event),
201
+ onLongPress: onLongPress ? () => onLongPress(event) : undefined,
165
202
  style: styles.cardWrapper,
166
203
  contentStyle: [styles.container, {
167
204
  borderLeftColor: statusColor
168
205
  }],
169
206
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
170
207
  style: styles.topRightContainer,
171
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(StatusIndicator, {
208
+ children: [pinned ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Pin, {
209
+ size: 10,
210
+ color: _sharedUi.macOSColors.semantic.info
211
+ }) : null, saved ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Bookmark, {
212
+ size: 10,
213
+ color: _sharedUi.macOSColors.semantic.debug
214
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(StatusIndicator, {
172
215
  event: event,
173
216
  isPending: isPending,
174
217
  statusColor: statusColor
175
218
  }), event.requestClient ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
176
219
  style: [styles.topRightBadge, {
177
- backgroundColor: event.requestClient === "fetch" ? "rgba(74, 144, 226, 0.15)" : event.requestClient === "graphql" ? "rgba(229, 53, 171, 0.15)" : event.requestClient === "grpc-web" ? "rgba(16, 185, 129, 0.15)" : event.requestClient === "xhr" ? "rgba(245, 158, 11, 0.15)" : "rgba(147, 51, 234, 0.15)"
220
+ backgroundColor: getClientChipColors(event.requestClient).backgroundColor
178
221
  }],
179
222
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
180
223
  style: [styles.clientText, {
181
- color: event.requestClient === "fetch" ? "#4A90E2" : event.requestClient === "graphql" ? "#E535AB" : event.requestClient === "grpc-web" ? "#10B981" : event.requestClient === "xhr" ? "#F59E0B" : "#9333EA"
224
+ color: getClientChipColors(event.requestClient).color
182
225
  }],
183
226
  children: event.requestClient === "graphql" ? "GQL" : event.requestClient === "grpc-web" ? "gRPC" : event.requestClient === "xhr" ? "XHR" : event.requestClient
184
227
  })
@@ -223,13 +266,17 @@ function NetworkEventItemCompact({
223
266
  NetworkEventItemCompact.displayName = "NetworkEventItemCompact";
224
267
  const styles = _reactNative.StyleSheet.create({
225
268
  cardWrapper: {
269
+ // Overrides DevToolsCard's own wrapper margin, so it has to opt into the
270
+ // edge-to-edge chat layout too or the rows keep their gutter.
226
271
  marginHorizontal: 8
227
272
  },
228
273
  container: {
229
274
  flexDirection: "row",
230
275
  alignItems: "center",
231
276
  borderLeftWidth: 3,
232
- borderLeftColor: "transparent"
277
+ borderLeftColor: "transparent",
278
+ // One hairline per row — the same value DevToolsCard's border uses.
279
+ borderBottomWidth: 1
233
280
  },
234
281
  leftSection: {
235
282
  marginRight: 8,