@buoy-gg/network 7.0.12 → 7.0.14

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 (136) hide show
  1. package/lib/commonjs/index.js +52 -0
  2. package/lib/commonjs/network/components/NetworkDetailStepper.js +26 -14
  3. package/lib/commonjs/network/components/NetworkEventDetailView.js +86 -14
  4. package/lib/commonjs/network/components/NetworkEventItemCompact.js +4 -0
  5. package/lib/commonjs/network/components/NetworkHeaderLive.js +32 -7
  6. package/lib/commonjs/network/components/NetworkHeaderMenu.js +183 -0
  7. package/lib/commonjs/network/components/NetworkListScreen.js +134 -8
  8. package/lib/commonjs/network/components/NetworkModal.js +198 -51
  9. package/lib/commonjs/network/components/NetworkNumberPrompt.js +178 -0
  10. package/lib/commonjs/network/components/NetworkOverrideEditor.js +797 -0
  11. package/lib/commonjs/network/components/NetworkOverrideHeaderButton.js +102 -0
  12. package/lib/commonjs/network/components/NetworkOverridesButton.js +88 -0
  13. package/lib/commonjs/network/components/NetworkOverridesScreen.js +489 -0
  14. package/lib/commonjs/network/components/response-editor/Explorer.js +973 -0
  15. package/lib/commonjs/network/components/response-editor/deleteNestedDataByPath.js +56 -0
  16. package/lib/commonjs/network/components/response-editor/editorContext.js +33 -0
  17. package/lib/commonjs/network/components/response-editor/updateNestedDataByPath.js +61 -0
  18. package/lib/commonjs/network/hooks/useNetworkSelection.js +53 -0
  19. package/lib/commonjs/network/hooks/useOverrideRules.js +36 -0
  20. package/lib/commonjs/network/overrides/applyToFetch.js +80 -0
  21. package/lib/commonjs/network/overrides/applyToXhr.js +190 -0
  22. package/lib/commonjs/network/overrides/engine.js +70 -0
  23. package/lib/commonjs/network/overrides/matchRule.js +81 -0
  24. package/lib/commonjs/network/overrides/overrideRulesStore.js +550 -0
  25. package/lib/commonjs/network/overrides/presets.js +70 -0
  26. package/lib/commonjs/network/overrides/resolveOverride.js +158 -0
  27. package/lib/commonjs/network/overrides/types.js +85 -0
  28. package/lib/commonjs/network/sync/networkSyncAdapter.js +230 -1
  29. package/lib/commonjs/network/types/index.js +5 -1
  30. package/lib/commonjs/network/utils/networkEventStore.js +52 -6
  31. package/lib/commonjs/network/utils/networkListener.js +90 -10
  32. package/lib/commonjs/network/utils/networkSelectionStore.js +193 -0
  33. package/lib/commonjs/preset.js +11 -0
  34. package/lib/module/index.js +19 -0
  35. package/lib/module/network/components/NetworkDetailStepper.js +27 -15
  36. package/lib/module/network/components/NetworkEventDetailView.js +87 -15
  37. package/lib/module/network/components/NetworkEventItemCompact.js +5 -1
  38. package/lib/module/network/components/NetworkHeaderLive.js +32 -7
  39. package/lib/module/network/components/NetworkHeaderMenu.js +177 -0
  40. package/lib/module/network/components/NetworkListScreen.js +135 -9
  41. package/lib/module/network/components/NetworkModal.js +200 -53
  42. package/lib/module/network/components/NetworkNumberPrompt.js +174 -0
  43. package/lib/module/network/components/NetworkOverrideEditor.js +794 -0
  44. package/lib/module/network/components/NetworkOverrideHeaderButton.js +99 -0
  45. package/lib/module/network/components/NetworkOverridesButton.js +85 -0
  46. package/lib/module/network/components/NetworkOverridesScreen.js +485 -0
  47. package/lib/module/network/components/response-editor/Explorer.js +972 -0
  48. package/lib/module/network/components/response-editor/deleteNestedDataByPath.js +51 -0
  49. package/lib/module/network/components/response-editor/editorContext.js +29 -0
  50. package/lib/module/network/components/response-editor/updateNestedDataByPath.js +56 -0
  51. package/lib/module/network/hooks/useNetworkSelection.js +46 -0
  52. package/lib/module/network/hooks/useOverrideRules.js +32 -0
  53. package/lib/module/network/overrides/applyToFetch.js +72 -0
  54. package/lib/module/network/overrides/applyToXhr.js +185 -0
  55. package/lib/module/network/overrides/engine.js +64 -0
  56. package/lib/module/network/overrides/matchRule.js +75 -0
  57. package/lib/module/network/overrides/overrideRulesStore.js +547 -0
  58. package/lib/module/network/overrides/presets.js +65 -0
  59. package/lib/module/network/overrides/resolveOverride.js +152 -0
  60. package/lib/module/network/overrides/types.js +80 -0
  61. package/lib/module/network/sync/networkSyncAdapter.js +230 -1
  62. package/lib/module/network/types/index.js +3 -1
  63. package/lib/module/network/utils/networkEventStore.js +52 -6
  64. package/lib/module/network/utils/networkListener.js +91 -10
  65. package/lib/module/network/utils/networkSelectionStore.js +188 -0
  66. package/lib/module/preset.js +13 -1
  67. package/lib/typescript/index.d.ts +9 -0
  68. package/lib/typescript/index.d.ts.map +1 -1
  69. package/lib/typescript/network/components/NetworkDetailStepper.d.ts +8 -5
  70. package/lib/typescript/network/components/NetworkDetailStepper.d.ts.map +1 -1
  71. package/lib/typescript/network/components/NetworkEventDetailView.d.ts +36 -1
  72. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  73. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +6 -0
  74. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  75. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +13 -3
  76. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  77. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +46 -0
  78. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -0
  79. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  80. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  81. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts +33 -0
  82. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts.map +1 -0
  83. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts +17 -0
  84. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts.map +1 -0
  85. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts +27 -0
  86. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts.map +1 -0
  87. package/lib/typescript/network/components/NetworkOverridesButton.d.ts +17 -0
  88. package/lib/typescript/network/components/NetworkOverridesButton.d.ts.map +1 -0
  89. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts +29 -0
  90. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts.map +1 -0
  91. package/lib/typescript/network/components/response-editor/Explorer.d.ts +17 -0
  92. package/lib/typescript/network/components/response-editor/Explorer.d.ts.map +1 -0
  93. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts +9 -0
  94. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts.map +1 -0
  95. package/lib/typescript/network/components/response-editor/editorContext.d.ts +29 -0
  96. package/lib/typescript/network/components/response-editor/editorContext.d.ts.map +1 -0
  97. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts +10 -0
  98. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts.map +1 -0
  99. package/lib/typescript/network/hooks/useNetworkSelection.d.ts +26 -0
  100. package/lib/typescript/network/hooks/useNetworkSelection.d.ts.map +1 -0
  101. package/lib/typescript/network/hooks/useOverrideRules.d.ts +29 -0
  102. package/lib/typescript/network/hooks/useOverrideRules.d.ts.map +1 -0
  103. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts +16 -0
  104. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts.map +1 -0
  105. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts +19 -0
  106. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts.map +1 -0
  107. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts +22 -0
  108. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts.map +1 -0
  109. package/lib/typescript/network/overrides/applyToFetch.d.ts +50 -0
  110. package/lib/typescript/network/overrides/applyToFetch.d.ts.map +1 -0
  111. package/lib/typescript/network/overrides/applyToXhr.d.ts +43 -0
  112. package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -0
  113. package/lib/typescript/network/overrides/engine.d.ts +47 -0
  114. package/lib/typescript/network/overrides/engine.d.ts.map +1 -0
  115. package/lib/typescript/network/overrides/matchRule.d.ts +22 -0
  116. package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -0
  117. package/lib/typescript/network/overrides/overrideRulesStore.d.ts +205 -0
  118. package/lib/typescript/network/overrides/overrideRulesStore.d.ts.map +1 -0
  119. package/lib/typescript/network/overrides/presets.d.ts +38 -0
  120. package/lib/typescript/network/overrides/presets.d.ts.map +1 -0
  121. package/lib/typescript/network/overrides/resolveOverride.d.ts +46 -0
  122. package/lib/typescript/network/overrides/resolveOverride.d.ts.map +1 -0
  123. package/lib/typescript/network/overrides/types.d.ts +169 -0
  124. package/lib/typescript/network/overrides/types.d.ts.map +1 -0
  125. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +73 -0
  126. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  127. package/lib/typescript/network/types/index.d.ts +10 -0
  128. package/lib/typescript/network/types/index.d.ts.map +1 -1
  129. package/lib/typescript/network/utils/networkEventStore.d.ts +14 -0
  130. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  131. package/lib/typescript/network/utils/networkListener.d.ts +3 -0
  132. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  133. package/lib/typescript/network/utils/networkSelectionStore.d.ts +117 -0
  134. package/lib/typescript/network/utils/networkSelectionStore.d.ts.map +1 -0
  135. package/lib/typescript/preset.d.ts.map +1 -1
  136. package/package.json +2 -2
@@ -11,6 +11,8 @@ var _NetworkEventItemCompact = require("./NetworkEventItemCompact");
11
11
  var _NetworkUpgradeBanner = require("./NetworkUpgradeBanner");
12
12
  var _useNetworkEventList = require("../hooks/useNetworkEventList");
13
13
  var _useNetworkSaved = require("../hooks/useNetworkSaved");
14
+ var _useOverrideRules = require("../hooks/useOverrideRules");
15
+ var _matchRule = require("../overrides/matchRule");
14
16
  var _withPinnedFirst = require("../utils/withPinnedFirst");
15
17
  var _jsxRuntime = require("react/jsx-runtime");
16
18
  /**
@@ -91,6 +93,62 @@ function PinnedSection({
91
93
  }, event.id))]
92
94
  });
93
95
  }
96
+
97
+ /**
98
+ * The OVERRIDDEN strip, above the pins.
99
+ *
100
+ * An overridden request is the one thing in this list that isn't true — the app
101
+ * got something the server never sent. That has to be impossible to miss, and a
102
+ * glyph on a row twenty scroll-positions down isn't. It sits ABOVE pins because
103
+ * a stale override explains a bug, whereas a pin only marks one.
104
+ *
105
+ * Rows are matched against the RULES rather than filtered on the `override`
106
+ * mark, so the request you built a rule from hoists immediately instead of
107
+ * waiting for the endpoint to be called again.
108
+ *
109
+ * The header button is PAUSE, not "Manage". Flipping overrides off and on is by
110
+ * far the most repeated action in a testing session — force the failure, check
111
+ * the UI, turn it off, check the happy path, turn it back on — and it used to
112
+ * cost a trip to another screen each way. The manager is still one tap away on
113
+ * the toolbar; it just isn't the thing you reach for twenty times an hour.
114
+ */
115
+ function OverriddenSection({
116
+ events,
117
+ onEventPress,
118
+ onLongPress,
119
+ savedLiveIds,
120
+ paused,
121
+ onTogglePaused
122
+ }) {
123
+ if (events.length === 0) return null;
124
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
125
+ style: [styles.overriddenSection, paused && styles.overriddenSectionOff],
126
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
127
+ style: styles.overriddenHeader,
128
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.FlaskConical, {
129
+ size: 11,
130
+ color: paused ? _sharedUi.macOSColors.text.muted : _sharedUi.macOSColors.semantic.warning
131
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
132
+ style: [styles.overriddenTitle, paused && styles.overriddenTitleOff],
133
+ children: paused ? `PAUSED · ${events.length}` : `OVERRIDDEN · ${events.length}`
134
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
135
+ onPress: onTogglePaused,
136
+ "sentry-label": "ignore pause overrides",
137
+ style: styles.overriddenManage,
138
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
139
+ style: [styles.overriddenManageText, paused && styles.overriddenTitleOff],
140
+ children: paused ? "Resume" : "Pause"
141
+ })
142
+ })]
143
+ }), events.map(event => /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkEventItemCompact.NetworkEventItemCompact, {
144
+ event: event,
145
+ onPress: onEventPress,
146
+ onLongPress: onLongPress,
147
+ overridden: true,
148
+ saved: savedLiveIds.has(event.id)
149
+ }, event.id))]
150
+ });
151
+ }
94
152
  function NetworkListScreen({
95
153
  visible,
96
154
  paused,
@@ -121,16 +179,41 @@ function NetworkListScreen({
121
179
  togglePin,
122
180
  clearPinned
123
181
  } = (0, _useNetworkSaved.useNetworkSaved)();
182
+
183
+ // Which rules are live right now — the strip below reflects the rules, not
184
+ // the history, so it's correct the moment a rule is made or removed.
124
185
  const {
125
- pinned,
126
- rest
127
- } = (0, _withPinnedFirst.splitPinned)(visibleEvents, pinnedEvents, pinnedLiveIds);
186
+ rules,
187
+ enabled: overridesEnabled,
188
+ setEnabled,
189
+ isRuleSpent
190
+ } = (0, _useOverrideRules.useOverrideRules)();
128
191
 
129
192
  // Stable identity: handed to memoized rows.
130
193
  const handleLongPress = (0, _react.useCallback)(event => {
131
194
  togglePin(event);
132
195
  }, [togglePin]);
196
+
197
+ // Everything above is a hook and must run unconditionally; everything below
198
+ // is work. `splitPinned` is an O(n) pass over the whole list, and this screen
199
+ // re-renders on every modal-root state change — including every prev/next
200
+ // step, where it is hidden and the result is thrown away.
133
201
  if (!visible) return null;
202
+ const {
203
+ pinned,
204
+ rest
205
+ } = (0, _withPinnedFirst.splitPinned)(visibleEvents, pinnedEvents, pinnedLiveIds);
206
+
207
+ // Hoist anything an active rule covers. Done after `splitPinned` so a request
208
+ // that is both pinned and overridden appears once, in the louder section.
209
+ // Paused rules still populate the strip — that's how you find the thing you
210
+ // paused in order to resume it. The header says PAUSED instead.
211
+ const activeRules = rules.filter(rule => rule.enabled && !isRuleSpent(rule));
212
+ const isOverridden = event => activeRules.some(rule => (0, _matchRule.ruleMatches)(rule, event.url, String(event.method || "GET")));
213
+ const overridden = activeRules.length ? [...pinned, ...rest].filter(isOverridden) : [];
214
+ const overriddenIds = new Set(overridden.map(event => event.id));
215
+ const pinnedRows = pinned.filter(event => !overriddenIds.has(event.id));
216
+ const restRows = overriddenIds.size ? rest.filter(event => !overriddenIds.has(event.id)) : rest;
134
217
  const isEnabled = !paused;
135
218
  const renderItem = ({
136
219
  item
@@ -162,14 +245,21 @@ function NetworkListScreen({
162
245
  style: styles.disabledText,
163
246
  children: "Network interception is disabled"
164
247
  })]
165
- }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(PinnedSection, {
166
- events: pinned,
248
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(OverriddenSection, {
249
+ events: overridden,
250
+ onEventPress: onEventPress,
251
+ onLongPress: handleLongPress,
252
+ savedLiveIds: savedLiveIds,
253
+ paused: !overridesEnabled,
254
+ onTogglePaused: () => setEnabled(!overridesEnabled)
255
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(PinnedSection, {
256
+ events: pinnedRows,
167
257
  onEventPress: onEventPress,
168
258
  onLongPress: handleLongPress,
169
259
  savedLiveIds: savedLiveIds,
170
260
  onClearPinned: clearPinned
171
- }), rest.length > 0 || hasLockedEvents ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
172
- data: rest,
261
+ }), restRows.length > 0 || hasLockedEvents ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
262
+ data: restRows,
173
263
  renderItem: renderItem,
174
264
  keyExtractor: keyExtractor,
175
265
  contentContainerStyle: styles.listContent,
@@ -198,7 +288,7 @@ function NetworkListScreen({
198
288
  extraData: savedLiveIds,
199
289
  "sentry-label": "ignore network events list",
200
290
  ListFooterComponent: renderListFooter
201
- }) : pinned.length > 0 ? null : /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyState, {
291
+ }) : pinnedRows.length > 0 || overridden.length > 0 ? null : /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyState, {
202
292
  isEnabled: isEnabled
203
293
  })]
204
294
  });
@@ -213,6 +303,42 @@ const styles = _reactNative.StyleSheet.create({
213
303
  * these rows do NOT obey the filters, and the boundary is what tells the
214
304
  * user why a request they just filtered out is still on screen.
215
305
  */
306
+ overriddenSection: {
307
+ borderBottomWidth: 1,
308
+ borderBottomColor: _sharedUi.macOSColors.semantic.warning + "44",
309
+ backgroundColor: _sharedUi.macOSColors.semantic.warningBackground
310
+ },
311
+ overriddenSectionOff: {
312
+ borderBottomColor: _sharedUi.macOSColors.border.default,
313
+ backgroundColor: _sharedUi.macOSColors.background.card
314
+ },
315
+ overriddenTitleOff: {
316
+ color: _sharedUi.macOSColors.text.muted
317
+ },
318
+ overriddenHeader: {
319
+ flexDirection: "row",
320
+ alignItems: "center",
321
+ gap: 6,
322
+ paddingHorizontal: 12,
323
+ paddingTop: 8,
324
+ paddingBottom: 4
325
+ },
326
+ overriddenTitle: {
327
+ flex: 1,
328
+ fontSize: 10,
329
+ fontWeight: "700",
330
+ letterSpacing: 0.8,
331
+ color: _sharedUi.macOSColors.semantic.warning
332
+ },
333
+ overriddenManage: {
334
+ paddingHorizontal: 6,
335
+ paddingVertical: 2
336
+ },
337
+ overriddenManageText: {
338
+ fontSize: 10,
339
+ fontWeight: "600",
340
+ color: _sharedUi.macOSColors.semantic.warning
341
+ },
216
342
  pinnedSection: {
217
343
  borderBottomWidth: 1,
218
344
  borderBottomColor: _sharedUi.macOSColors.border.default,
@@ -15,10 +15,17 @@ var _NetworkCopySettingsView = require("./NetworkCopySettingsView");
15
15
  var _NetworkHeaderLive = require("./NetworkHeaderLive");
16
16
  var _NetworkListScreen = require("./NetworkListScreen");
17
17
  var _NetworkSavedScreen = require("./NetworkSavedScreen");
18
+ var _NetworkOverridesScreen = require("./NetworkOverridesScreen");
19
+ var _NetworkOverridesButton = require("./NetworkOverridesButton");
20
+ var _NetworkOverrideHeaderButton = require("./NetworkOverrideHeaderButton");
21
+ var _NetworkHeaderMenu = require("./NetworkHeaderMenu");
18
22
  var _NetworkDetailStepper = require("./NetworkDetailStepper");
19
23
  var _useNetworkEventList = require("../hooks/useNetworkEventList");
20
24
  var _useLiveNetworkEvent = require("../hooks/useLiveNetworkEvent");
25
+ var _useNetworkSelection = require("../hooks/useNetworkSelection");
26
+ var _networkSelectionStore = require("../utils/networkSelectionStore");
21
27
  var _networkSavedStore = require("../utils/networkSavedStore");
28
+ var _types = require("../overrides/types");
22
29
  var _jsxRuntime = require("react/jsx-runtime");
23
30
  // ============================================================================
24
31
  // Screens (rule4 stable router: each gates itself with `return null`; the
@@ -29,7 +36,6 @@ var _jsxRuntime = require("react/jsx-runtime");
29
36
 
30
37
  function NetworkDetailScreen({
31
38
  visible,
32
- eventId,
33
39
  paused,
34
40
  ignoredPatternValues,
35
41
  onTogglePattern,
@@ -38,34 +44,66 @@ function NetworkDetailScreen({
38
44
  ignoredPatterns,
39
45
  fromSaved,
40
46
  savedSearch,
41
- onSelectEvent,
42
47
  applySafeAreaInset
43
48
  }) {
49
+ /**
50
+ * The detail renders at the IMMEDIATE selection — every step, no delay.
51
+ * Measured, 100 steps at a 150ms tap interval over 64KB payloads:
52
+ *
53
+ * whole detail, bodies included ... 647ms/step
54
+ * detail with no bodies at all .... 43ms/step (over the 150ms tap gap)
55
+ *
56
+ * So ~93% of a step is the request/response BODY trees, and everything else
57
+ * — status, URL breakdown, timing, header counts — is affordable at any tap
58
+ * rate. Deferring the whole detail (which is what this used to do) therefore
59
+ * bought nothing a user could feel: it made the cheap 43ms wait alongside
60
+ * the expensive 450ms instead of splitting them.
61
+ *
62
+ * So only the expensive part waits. `bodyId` trails `selectedId` by the
63
+ * settle window, so `heavyReady` is false exactly while the cursor is still
64
+ * moving — the JSON trees are skipped during a burst and mount once you land.
65
+ */
66
+ const eventId = (0, _useNetworkSelection.useSelectedNetworkEventId)();
67
+ const settledId = (0, _useNetworkSelection.useNetworkBodyEventId)();
68
+ const heavyReady = eventId != null && eventId === settledId;
44
69
  // Resolve the ID to the live event so a request opened while pending picks
45
70
  // up its response/error in place instead of freezing on the snapshot taken
46
71
  // when the row was tapped.
47
72
  const event = (0, _useLiveNetworkEvent.useLiveNetworkEvent)(eventId, {
48
73
  paused: paused || !visible
49
74
  });
50
- if (!visible || !event) return null;
75
+
76
+ // Instrumentation for the benchmark harness. Gated on `heavyReady` on
77
+ // purpose: the cheap chrome now renders on every step, so reporting here
78
+ // unconditionally would make `bodyMs` a duplicate of `cursorMs`. What the
79
+ // harness needs to know is when the expensive TREES landed.
80
+ (0, _react.useEffect)(() => {
81
+ if (visible && event && heavyReady) {
82
+ _networkSelectionStore.networkSelectionStore.reportRendered(event.id);
83
+ }
84
+ }, [visible, event, heavyReady]);
85
+ if (!visible) return null;
51
86
  // Column: the detail body scrolls, the stepper stays pinned under it. The
52
87
  // body owns its scroll here (see `bodyOwnsScroll`) — inside JsModal's
53
- // wrapper the footer would scroll away with the content.
88
+ // wrapper the footer would scroll away with the content. The spacer keeps
89
+ // the footer at the bottom on the rare frame where the ID resolves to
90
+ // nothing (cleared, aged out, and not saved).
54
91
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
55
92
  style: styles.detailRoot,
56
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkEventDetailView.NetworkEventDetailView, {
93
+ children: [event ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkEventDetailView.NetworkEventDetailView, {
57
94
  event: event,
58
95
  ignoredPatterns: ignoredPatternValues,
59
- onTogglePattern: onTogglePattern
96
+ onTogglePattern: onTogglePattern,
97
+ showBodies: heavyReady
98
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
99
+ style: styles.detailBodyFallback
60
100
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkDetailStepper.NetworkDetailStepper, {
61
- eventId: event.id,
62
101
  paused: paused,
63
102
  filter: filter,
64
103
  ignoredPatterns: ignoredPatterns,
65
104
  isPro: isPro,
66
105
  fromSaved: fromSaved,
67
106
  savedSearch: savedSearch,
68
- onSelect: onSelectEvent,
69
107
  applySafeAreaInset: applySafeAreaInset
70
108
  })]
71
109
  });
@@ -160,11 +198,38 @@ function NetworkModalInner({
160
198
  if (controlledPause) onPausedChange(value);else setPausedState(value);
161
199
  };
162
200
  const [filter, setFilter] = (0, _react.useState)({});
163
- // The SELECTION is UI state (an ID); the event object it points at is read
164
- // live from the store by the detail screen — see useLiveNetworkEvent.
165
- const [selectedEventId, setSelectedEventId] = (0, _react.useState)(null);
201
+ /**
202
+ * The cursor lives in `networkSelectionStore`, and the root subscribes to
203
+ * the BOOLEAN only. That is the whole point: this component re-renders
204
+ * `JsModal` (which can never bail — its `header` prop is a fresh object
205
+ * every render), the entire header, and all five screens. None of them care
206
+ * WHICH request is selected, only THAT one is, and this flips twice a
207
+ * session instead of once per tap.
208
+ */
209
+ const hasSelection = (0, _useNetworkSelection.useHasNetworkSelection)();
166
210
  const [showFilterView, setShowFilterView] = (0, _react.useState)(false);
167
211
  const [showSavedView, setShowSavedView] = (0, _react.useState)(false);
212
+ const [showOverridesView, setShowOverridesView] = (0, _react.useState)(false);
213
+ const [showHeaderMenu, setShowHeaderMenu] = (0, _react.useState)(false);
214
+ /**
215
+ * Overrides are a DEVICE capability — a rule runs inside the app's own
216
+ * `fetch`/`XMLHttpRequest`, which the dashboard doesn't have.
217
+ *
218
+ * That doesn't mean the dashboard can't AUTHOR them: in `inline` mode the
219
+ * rule store runs as a mirror of the connected device (see
220
+ * `overrideRulesStore.enableRemoteMode`), so the screens below are the same
221
+ * ones the phone renders and every edit is forwarded. The one thing the
222
+ * dashboard never does is apply a rule to its own traffic —
223
+ * `disableEngine()` guarantees that.
224
+ */
225
+ const overridesAvailable = true;
226
+ /**
227
+ * A rule prefilled from a request the user was looking at, handed to the
228
+ * Overrides screen when it mounts. Lives here rather than in that screen
229
+ * because the request that seeds it is on a DIFFERENT screen — this is the
230
+ * one place both are in scope.
231
+ */
232
+ const [pendingOverrideDraft, setPendingOverrideDraft] = (0, _react.useState)(null);
168
233
  const [activeTab, setActiveTab] = (0, _react.useState)("filters");
169
234
  const [searchText, setSearchText] = (0, _react.useState)("");
170
235
  const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
@@ -236,13 +301,32 @@ function NetworkModalInner({
236
301
 
237
302
  // Simple handlers - no useCallback needed per rule2
238
303
  const handleBack = () => {
239
- setSelectedEventId(null);
304
+ _networkSelectionStore.networkSelectionStore.clear();
240
305
  };
241
306
 
242
307
  // Stable identity: this is passed down to the memoized list rows.
243
308
  const handleEventPress = (0, _react.useCallback)(event => {
244
- setSelectedEventId(event.id);
309
+ _networkSelectionStore.networkSelectionStore.select(event.id);
310
+ }, []);
311
+
312
+ /**
313
+ * "Custom…" in a request's Override section. Stable identity because the
314
+ * detail view is memoized on its props and an unstable callback would defeat
315
+ * the bail-out that keeps prev/next stepping cheap.
316
+ */
317
+ const handleOverridePress = (0, _react.useCallback)((draft, ruleId) => {
318
+ setPendingOverrideDraft(ruleId ? {
319
+ ...draft,
320
+ id: ruleId
321
+ } : draft);
322
+ setShowOverridesView(true);
323
+ // Leave the detail view, or its header would sit on top of the editor.
324
+ _networkSelectionStore.networkSelectionStore.clear();
245
325
  }, []);
326
+
327
+ // Stable identity: the Overrides screen adopts the draft in an effect keyed
328
+ // on both, so an unstable callback would re-run it every render.
329
+ const handlePendingOverrideConsumed = (0, _react.useCallback)(() => setPendingOverrideDraft(null), []);
246
330
  const handleSearch = text => {
247
331
  setSearchText(text);
248
332
  setFilter(prev => ({
@@ -299,19 +383,35 @@ function NetworkModalInner({
299
383
  }
300
384
 
301
385
  // Event detail view header
302
- if (selectedEventId) {
386
+ if (hasSelection) {
303
387
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
304
388
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
305
389
  onBack: handleBack
306
390
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
307
391
  title: "Request Details",
308
392
  centered: true
309
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Actions, {
310
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkDetailPinActions, {
311
- eventId: selectedEventId,
393
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader.Actions, {
394
+ children: [overridesAvailable ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkOverrideHeaderButton.NetworkOverrideHeaderButton, {
395
+ onOpen: handleOverridePress
396
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkDetailPinActions, {
312
397
  isPro: isPro,
313
398
  onUpgradePress: () => showUpgrade("saved")
314
- })
399
+ })]
400
+ })]
401
+ });
402
+ }
403
+
404
+ // Overrides manager header.
405
+ if (showOverridesView && overridesAvailable) {
406
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
407
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
408
+ onBack: () => {
409
+ setShowOverridesView(false);
410
+ setPendingOverrideDraft(null);
411
+ }
412
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
413
+ title: "Response Overrides",
414
+ centered: true
315
415
  })]
316
416
  });
317
417
  }
@@ -383,35 +483,26 @@ function NetworkModalInner({
383
483
  size: 14,
384
484
  color: _sharedUi.macOSColors.text.secondary
385
485
  })
386
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
387
- "sentry-label": "ignore filter",
388
- onPress: () => {
389
- setShowFilterView(true);
390
- },
391
- style: [styles.headerActionButton, (filter.status || filter.method || filter.contentType) && styles.activeFilterButton],
392
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Filter, {
393
- size: 14,
394
- color: filter.status || filter.method || filter.contentType ? _sharedUi.macOSColors.semantic.info : _sharedUi.macOSColors.text.muted
395
- })
396
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkSavedButton, {
397
- onPress: () => setShowSavedView(true)
398
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkHeaderCopyButton, {
399
- isPro: isPro,
400
- filter: filter,
401
- ignoredPatterns: ignoredPatterns,
402
- copySettings: copySettings,
403
- paused: paused,
404
- onUpgradePress: () => showUpgrade("export")
405
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
486
+ }), overridesAvailable ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkOverridesButton.NetworkOverridesButton, {
487
+ onPress: () => setShowOverridesView(true)
488
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
406
489
  isEnabled: !paused,
407
490
  onToggle: () => setPaused(prev => !prev),
408
491
  accessibilityLabel: "Toggle network interception"
409
492
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkClearButton, {
410
493
  paused: paused
494
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderMenu.NetworkHeaderMenuButton, {
495
+ onPress: () => setShowHeaderMenu(prev => !prev),
496
+ isOpen: showHeaderMenu,
497
+ hasIndicator: hasActiveFilter
411
498
  })]
412
499
  })]
413
500
  });
414
501
  };
502
+
503
+ // Surfaced on the ⋯ so moving Filters into the menu doesn't hide the one
504
+ // thing that button was communicating.
505
+ const hasActiveFilter = !!(filter.status || filter.method || filter.contentType);
415
506
  const persistenceKey = enableSharedModalDimensions ? _sharedUi.devToolsStorageKeys.modal.root() : _sharedUi.devToolsStorageKeys.network.modal();
416
507
  if (!visible) return null;
417
508
 
@@ -422,13 +513,12 @@ function NetworkModalInner({
422
513
  // same deal. The DETAIL screen owns it as well, so its prev/next footer
423
514
  // stays pinned instead of scrolling away with the body. Filter/copy still
424
515
  // use the wrapper.
425
- const bodyOwnsScroll = selectedEventId != null || !showFilterView;
516
+ const bodyOwnsScroll = hasSelection || !showFilterView;
426
517
  const body = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
427
518
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
428
519
  style: styles.container,
429
520
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkDetailScreen, {
430
- visible: selectedEventId != null,
431
- eventId: selectedEventId,
521
+ visible: hasSelection,
432
522
  paused: paused,
433
523
  ignoredPatternValues: ignoredPatternValues,
434
524
  onTogglePattern: toggleIgnoredPattern,
@@ -439,13 +529,12 @@ function NetworkModalInner({
439
529
  // the user came from — and therefore which one to step through.
440
530
  ,
441
531
  fromSaved: showSavedView,
442
- savedSearch: savedSearch,
443
- onSelectEvent: setSelectedEventId
532
+ savedSearch: savedSearch
444
533
  // Inline (desktop panel) has no home indicator to clear.
445
534
  ,
446
535
  applySafeAreaInset: !inline && modalMode === "bottomSheet"
447
536
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkFilterScreen, {
448
- visible: showFilterView && activeTab === "filters" && !selectedEventId,
537
+ visible: showFilterView && activeTab === "filters" && !hasSelection,
449
538
  paused: paused,
450
539
  isPro: isPro,
451
540
  filter: filter,
@@ -455,15 +544,27 @@ function NetworkModalInner({
455
544
  onAddPattern: addIgnoredPattern,
456
545
  onTogglePatternMode: toggleIgnoredPatternMode
457
546
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkCopyScreen, {
458
- visible: showFilterView && activeTab === "copy" && !selectedEventId,
547
+ visible: showFilterView && activeTab === "copy" && !hasSelection,
459
548
  paused: paused,
460
549
  isPro: isPro,
461
550
  filter: filter,
462
551
  ignoredPatterns: ignoredPatterns,
463
552
  settings: copySettings,
464
553
  onSettingsChange: setCopySettings
554
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkOverridesScreen.NetworkOverridesScreen, {
555
+ visible: overridesAvailable && showOverridesView && !hasSelection && !showFilterView,
556
+ isPro: isPro,
557
+ onUpgradePress: () => showUpgrade("overrides"),
558
+ pendingDraft: pendingOverrideDraft,
559
+ onPendingConsumed: handlePendingOverrideConsumed
560
+ // Inline (desktop panel) has no home indicator to clear.
561
+ ,
562
+ applySafeAreaInset: !inline && modalMode === "bottomSheet"
465
563
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkSavedScreen.NetworkSavedScreen, {
466
- visible: showSavedView && selectedEventId == null && !showFilterView,
564
+ visible: showSavedView && !hasSelection && !showFilterView &&
565
+ // Overrides wins in the header branch too — the two must agree, or
566
+ // one screen renders under the other's header.
567
+ !(showOverridesView && overridesAvailable),
467
568
  isPro: isPro,
468
569
  copySettings: copySettings,
469
570
  search: savedSearch,
@@ -471,7 +572,7 @@ function NetworkModalInner({
471
572
  onEventPress: handleEventPress,
472
573
  onUpgradePress: () => showUpgrade("saved")
473
574
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkListScreen.NetworkListScreen, {
474
- visible: selectedEventId == null && !showFilterView && !showSavedView,
575
+ visible: !hasSelection && !showFilterView && !showSavedView && !(showOverridesView && overridesAvailable),
475
576
  paused: paused,
476
577
  filter: filter,
477
578
  ignoredPatterns: ignoredPatterns,
@@ -480,12 +581,51 @@ function NetworkModalInner({
480
581
  onEventPress: handleEventPress,
481
582
  onUpgradePress: () => showUpgrade("export")
482
583
  })]
483
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.UpgradeModal, {
584
+ }), showHeaderMenu ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_NetworkHeaderMenu.NetworkHeaderMenu, {
585
+ onClose: () => setShowHeaderMenu(false),
586
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderMenu.NetworkHeaderMenuItem, {
587
+ icon: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Filter, {
588
+ size: 14,
589
+ color: hasActiveFilter ? _sharedUi.macOSColors.semantic.info : _sharedUi.macOSColors.text.secondary
590
+ }),
591
+ label: "Filters",
592
+ detail: hasActiveFilter ? "Active" : undefined,
593
+ onPress: () => {
594
+ setShowHeaderMenu(false);
595
+ setActiveTab("filters");
596
+ setShowFilterView(true);
597
+ }
598
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderMenu.NetworkHeaderMenuItem, {
599
+ icon: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Bookmark, {
600
+ size: 14,
601
+ color: _sharedUi.macOSColors.text.secondary
602
+ }),
603
+ label: "Saved requests",
604
+ onPress: () => {
605
+ setShowHeaderMenu(false);
606
+ setShowSavedView(true);
607
+ }
608
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
609
+ style: styles.menuCopyRow,
610
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkHeaderCopyButton, {
611
+ isPro: isPro,
612
+ filter: filter,
613
+ ignoredPatterns: ignoredPatterns,
614
+ copySettings: copySettings,
615
+ paused: paused,
616
+ onUpgradePress: () => {
617
+ setShowHeaderMenu(false);
618
+ showUpgrade("export");
619
+ },
620
+ label: "Copy requests"
621
+ })
622
+ })]
623
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.UpgradeModal, {
484
624
  visible: showUpgradeModal,
485
625
  onClose: () => setShowUpgradeModal(false),
486
- featureName: upgradeReason === "saved" ? "Saved Requests" : "Network Export",
487
- description: upgradeReason === "saved" ? "Keep the requests that matter. Saved requests survive Clear, app restarts, and the 500-request history cap." : "Export network requests to share with your team, debug APIs, or feed into AI tools.",
488
- benefits: upgradeReason === "saved" ? [`Save up to ${_networkSavedStore.MAX_SAVED} requests (free keeps ${_networkSavedStore.FREE_TIER_SAVED_LIMIT})`, "Failed requests stay put through a Clear", "Snapshots survive an app restart", "Export saved requests to teammates or AI assistants"] : ["Export to JSON, Markdown, or plain text", "Customizable copy presets (URLs only, LLM-ready, full data)", "Share request details with teammates", "Feed network data directly to AI assistants"]
626
+ featureName: upgradeReason === "saved" ? "Saved Requests" : upgradeReason === "overrides" ? "Response Overrides" : "Network Export",
627
+ description: upgradeReason === "saved" ? "Keep the requests that matter. Saved requests survive Clear, app restarts, and the 500-request history cap." : upgradeReason === "overrides" ? "Force any endpoint to fail, stall or return something else — no backend changes, no code changes. Free keeps one active rule." : "Export network requests to share with your team, debug APIs, or feed into AI tools.",
628
+ benefits: upgradeReason === "overrides" ? [`Run ${_types.MAX_RULES} rules at once (free keeps ${_types.FREE_TIER_RULE_LIMIT})`, "Force a 500, a 401 or an empty list in one tap", "Simulate offline, timeouts and slow connections", "Rules survive an app reload, so you can test the boot path"] : upgradeReason === "saved" ? [`Save up to ${_networkSavedStore.MAX_SAVED} requests (free keeps ${_networkSavedStore.FREE_TIER_SAVED_LIMIT})`, "Failed requests stay put through a Clear", "Snapshots survive an app restart", "Export saved requests to teammates or AI assistants"] : ["Export to JSON, Markdown, or plain text", "Customizable copy presets (URLs only, LLM-ready, full data)", "Share request details with teammates", "Feed network data directly to AI assistants"]
489
629
  })]
490
630
  });
491
631
 
@@ -529,6 +669,10 @@ const styles = _reactNative.StyleSheet.create({
529
669
  flex: 1,
530
670
  minHeight: 0
531
671
  },
672
+ /** Holds the footer down when the selected ID resolves to no event. */
673
+ detailBodyFallback: {
674
+ flex: 1
675
+ },
532
676
  /** Inline shell — fills the host panel; `minHeight: 0` so the list's own
533
677
  * FlatList can scroll instead of stretching the column. */
534
678
  inlineRoot: {
@@ -564,6 +708,9 @@ const styles = _reactNative.StyleSheet.create({
564
708
  marginLeft: 6,
565
709
  padding: 4
566
710
  },
711
+ menuCopyRow: {
712
+ paddingHorizontal: 6
713
+ },
567
714
  headerActionButton: {
568
715
  width: 32,
569
716
  height: 32,