@buoy-gg/network 7.0.13 → 7.0.15

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 (123) hide show
  1. package/lib/commonjs/index.js +39 -0
  2. package/lib/commonjs/network/components/NetworkEventDetailView.js +30 -3
  3. package/lib/commonjs/network/components/NetworkEventItemCompact.js +4 -0
  4. package/lib/commonjs/network/components/NetworkHeaderLive.js +26 -6
  5. package/lib/commonjs/network/components/NetworkHeaderMenu.js +183 -0
  6. package/lib/commonjs/network/components/NetworkListScreen.js +126 -5
  7. package/lib/commonjs/network/components/NetworkModal.js +136 -29
  8. package/lib/commonjs/network/components/NetworkNumberPrompt.js +178 -0
  9. package/lib/commonjs/network/components/NetworkOverrideEditor.js +797 -0
  10. package/lib/commonjs/network/components/NetworkOverrideHeaderButton.js +102 -0
  11. package/lib/commonjs/network/components/NetworkOverridesButton.js +88 -0
  12. package/lib/commonjs/network/components/NetworkOverridesScreen.js +489 -0
  13. package/lib/commonjs/network/components/response-editor/Explorer.js +973 -0
  14. package/lib/commonjs/network/components/response-editor/deleteNestedDataByPath.js +56 -0
  15. package/lib/commonjs/network/components/response-editor/editorContext.js +33 -0
  16. package/lib/commonjs/network/components/response-editor/updateNestedDataByPath.js +61 -0
  17. package/lib/commonjs/network/hooks/useOverrideRules.js +36 -0
  18. package/lib/commonjs/network/overrides/applyToFetch.js +80 -0
  19. package/lib/commonjs/network/overrides/applyToXhr.js +190 -0
  20. package/lib/commonjs/network/overrides/engine.js +70 -0
  21. package/lib/commonjs/network/overrides/matchRule.js +81 -0
  22. package/lib/commonjs/network/overrides/overrideRulesStore.js +550 -0
  23. package/lib/commonjs/network/overrides/presets.js +70 -0
  24. package/lib/commonjs/network/overrides/resolveOverride.js +158 -0
  25. package/lib/commonjs/network/overrides/types.js +85 -0
  26. package/lib/commonjs/network/sync/networkSyncAdapter.js +230 -1
  27. package/lib/commonjs/network/types/index.js +5 -1
  28. package/lib/commonjs/network/utils/networkEventStore.js +52 -6
  29. package/lib/commonjs/network/utils/networkListener.js +90 -10
  30. package/lib/commonjs/preset.js +11 -0
  31. package/lib/module/index.js +15 -0
  32. package/lib/module/network/components/NetworkEventDetailView.js +31 -4
  33. package/lib/module/network/components/NetworkEventItemCompact.js +5 -1
  34. package/lib/module/network/components/NetworkHeaderLive.js +26 -6
  35. package/lib/module/network/components/NetworkHeaderMenu.js +177 -0
  36. package/lib/module/network/components/NetworkListScreen.js +127 -6
  37. package/lib/module/network/components/NetworkModal.js +138 -31
  38. package/lib/module/network/components/NetworkNumberPrompt.js +174 -0
  39. package/lib/module/network/components/NetworkOverrideEditor.js +794 -0
  40. package/lib/module/network/components/NetworkOverrideHeaderButton.js +99 -0
  41. package/lib/module/network/components/NetworkOverridesButton.js +85 -0
  42. package/lib/module/network/components/NetworkOverridesScreen.js +485 -0
  43. package/lib/module/network/components/response-editor/Explorer.js +972 -0
  44. package/lib/module/network/components/response-editor/deleteNestedDataByPath.js +51 -0
  45. package/lib/module/network/components/response-editor/editorContext.js +29 -0
  46. package/lib/module/network/components/response-editor/updateNestedDataByPath.js +56 -0
  47. package/lib/module/network/hooks/useOverrideRules.js +32 -0
  48. package/lib/module/network/overrides/applyToFetch.js +72 -0
  49. package/lib/module/network/overrides/applyToXhr.js +185 -0
  50. package/lib/module/network/overrides/engine.js +64 -0
  51. package/lib/module/network/overrides/matchRule.js +75 -0
  52. package/lib/module/network/overrides/overrideRulesStore.js +547 -0
  53. package/lib/module/network/overrides/presets.js +65 -0
  54. package/lib/module/network/overrides/resolveOverride.js +152 -0
  55. package/lib/module/network/overrides/types.js +80 -0
  56. package/lib/module/network/sync/networkSyncAdapter.js +230 -1
  57. package/lib/module/network/types/index.js +3 -1
  58. package/lib/module/network/utils/networkEventStore.js +52 -6
  59. package/lib/module/network/utils/networkListener.js +91 -10
  60. package/lib/module/preset.js +13 -1
  61. package/lib/typescript/index.d.ts +8 -0
  62. package/lib/typescript/index.d.ts.map +1 -1
  63. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  64. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +6 -0
  65. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  66. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +8 -1
  67. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  68. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +46 -0
  69. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -0
  70. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  71. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  72. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts +33 -0
  73. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts.map +1 -0
  74. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts +17 -0
  75. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts.map +1 -0
  76. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts +27 -0
  77. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts.map +1 -0
  78. package/lib/typescript/network/components/NetworkOverridesButton.d.ts +17 -0
  79. package/lib/typescript/network/components/NetworkOverridesButton.d.ts.map +1 -0
  80. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts +29 -0
  81. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts.map +1 -0
  82. package/lib/typescript/network/components/response-editor/Explorer.d.ts +17 -0
  83. package/lib/typescript/network/components/response-editor/Explorer.d.ts.map +1 -0
  84. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts +9 -0
  85. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts.map +1 -0
  86. package/lib/typescript/network/components/response-editor/editorContext.d.ts +29 -0
  87. package/lib/typescript/network/components/response-editor/editorContext.d.ts.map +1 -0
  88. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts +10 -0
  89. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts.map +1 -0
  90. package/lib/typescript/network/hooks/useOverrideRules.d.ts +29 -0
  91. package/lib/typescript/network/hooks/useOverrideRules.d.ts.map +1 -0
  92. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts +16 -0
  93. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts.map +1 -0
  94. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts +19 -0
  95. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts.map +1 -0
  96. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts +22 -0
  97. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts.map +1 -0
  98. package/lib/typescript/network/overrides/applyToFetch.d.ts +50 -0
  99. package/lib/typescript/network/overrides/applyToFetch.d.ts.map +1 -0
  100. package/lib/typescript/network/overrides/applyToXhr.d.ts +43 -0
  101. package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -0
  102. package/lib/typescript/network/overrides/engine.d.ts +47 -0
  103. package/lib/typescript/network/overrides/engine.d.ts.map +1 -0
  104. package/lib/typescript/network/overrides/matchRule.d.ts +22 -0
  105. package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -0
  106. package/lib/typescript/network/overrides/overrideRulesStore.d.ts +205 -0
  107. package/lib/typescript/network/overrides/overrideRulesStore.d.ts.map +1 -0
  108. package/lib/typescript/network/overrides/presets.d.ts +38 -0
  109. package/lib/typescript/network/overrides/presets.d.ts.map +1 -0
  110. package/lib/typescript/network/overrides/resolveOverride.d.ts +46 -0
  111. package/lib/typescript/network/overrides/resolveOverride.d.ts.map +1 -0
  112. package/lib/typescript/network/overrides/types.d.ts +169 -0
  113. package/lib/typescript/network/overrides/types.d.ts.map +1 -0
  114. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +73 -0
  115. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  116. package/lib/typescript/network/types/index.d.ts +10 -0
  117. package/lib/typescript/network/types/index.d.ts.map +1 -1
  118. package/lib/typescript/network/utils/networkEventStore.d.ts +14 -0
  119. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  120. package/lib/typescript/network/utils/networkListener.d.ts +3 -0
  121. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  122. package/lib/typescript/preset.d.ts.map +1 -1
  123. package/package.json +2 -2
@@ -14,11 +14,13 @@
14
14
 
15
15
  import { useCallback } from "react";
16
16
  import { FlatList, StyleSheet, Text, TouchableOpacity, View } from "react-native";
17
- import { Globe, Pin, Power, macOSColors } from "@buoy-gg/shared-ui";
17
+ import { FlaskConical, Globe, Pin, Power, macOSColors } from "@buoy-gg/shared-ui";
18
18
  import { NetworkEventItemCompact } from "./NetworkEventItemCompact";
19
19
  import { NetworkUpgradeBanner } from "./NetworkUpgradeBanner";
20
20
  import { useNetworkEventList } from "../hooks/useNetworkEventList";
21
21
  import { useNetworkSaved } from "../hooks/useNetworkSaved";
22
+ import { useOverrideRules } from "../hooks/useOverrideRules";
23
+ import { ruleMatches } from "../overrides/matchRule";
22
24
  import { splitPinned } from "../utils/withPinnedFirst";
23
25
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
24
26
  function EmptyState({
@@ -87,6 +89,62 @@ function PinnedSection({
87
89
  }, event.id))]
88
90
  });
89
91
  }
92
+
93
+ /**
94
+ * The OVERRIDDEN strip, above the pins.
95
+ *
96
+ * An overridden request is the one thing in this list that isn't true — the app
97
+ * got something the server never sent. That has to be impossible to miss, and a
98
+ * glyph on a row twenty scroll-positions down isn't. It sits ABOVE pins because
99
+ * a stale override explains a bug, whereas a pin only marks one.
100
+ *
101
+ * Rows are matched against the RULES rather than filtered on the `override`
102
+ * mark, so the request you built a rule from hoists immediately instead of
103
+ * waiting for the endpoint to be called again.
104
+ *
105
+ * The header button is PAUSE, not "Manage". Flipping overrides off and on is by
106
+ * far the most repeated action in a testing session — force the failure, check
107
+ * the UI, turn it off, check the happy path, turn it back on — and it used to
108
+ * cost a trip to another screen each way. The manager is still one tap away on
109
+ * the toolbar; it just isn't the thing you reach for twenty times an hour.
110
+ */
111
+ function OverriddenSection({
112
+ events,
113
+ onEventPress,
114
+ onLongPress,
115
+ savedLiveIds,
116
+ paused,
117
+ onTogglePaused
118
+ }) {
119
+ if (events.length === 0) return null;
120
+ return /*#__PURE__*/_jsxs(View, {
121
+ style: [styles.overriddenSection, paused && styles.overriddenSectionOff],
122
+ children: [/*#__PURE__*/_jsxs(View, {
123
+ style: styles.overriddenHeader,
124
+ children: [/*#__PURE__*/_jsx(FlaskConical, {
125
+ size: 11,
126
+ color: paused ? macOSColors.text.muted : macOSColors.semantic.warning
127
+ }), /*#__PURE__*/_jsx(Text, {
128
+ style: [styles.overriddenTitle, paused && styles.overriddenTitleOff],
129
+ children: paused ? `PAUSED · ${events.length}` : `OVERRIDDEN · ${events.length}`
130
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
131
+ onPress: onTogglePaused,
132
+ "sentry-label": "ignore pause overrides",
133
+ style: styles.overriddenManage,
134
+ children: /*#__PURE__*/_jsx(Text, {
135
+ style: [styles.overriddenManageText, paused && styles.overriddenTitleOff],
136
+ children: paused ? "Resume" : "Pause"
137
+ })
138
+ })]
139
+ }), events.map(event => /*#__PURE__*/_jsx(NetworkEventItemCompact, {
140
+ event: event,
141
+ onPress: onEventPress,
142
+ onLongPress: onLongPress,
143
+ overridden: true,
144
+ saved: savedLiveIds.has(event.id)
145
+ }, event.id))]
146
+ });
147
+ }
90
148
  export function NetworkListScreen({
91
149
  visible,
92
150
  paused,
@@ -118,6 +176,15 @@ export function NetworkListScreen({
118
176
  clearPinned
119
177
  } = useNetworkSaved();
120
178
 
179
+ // Which rules are live right now — the strip below reflects the rules, not
180
+ // the history, so it's correct the moment a rule is made or removed.
181
+ const {
182
+ rules,
183
+ enabled: overridesEnabled,
184
+ setEnabled,
185
+ isRuleSpent
186
+ } = useOverrideRules();
187
+
121
188
  // Stable identity: handed to memoized rows.
122
189
  const handleLongPress = useCallback(event => {
123
190
  togglePin(event);
@@ -132,6 +199,17 @@ export function NetworkListScreen({
132
199
  pinned,
133
200
  rest
134
201
  } = splitPinned(visibleEvents, pinnedEvents, pinnedLiveIds);
202
+
203
+ // Hoist anything an active rule covers. Done after `splitPinned` so a request
204
+ // that is both pinned and overridden appears once, in the louder section.
205
+ // Paused rules still populate the strip — that's how you find the thing you
206
+ // paused in order to resume it. The header says PAUSED instead.
207
+ const activeRules = rules.filter(rule => rule.enabled && !isRuleSpent(rule));
208
+ const isOverridden = event => activeRules.some(rule => ruleMatches(rule, event.url, String(event.method || "GET")));
209
+ const overridden = activeRules.length ? [...pinned, ...rest].filter(isOverridden) : [];
210
+ const overriddenIds = new Set(overridden.map(event => event.id));
211
+ const pinnedRows = pinned.filter(event => !overriddenIds.has(event.id));
212
+ const restRows = overriddenIds.size ? rest.filter(event => !overriddenIds.has(event.id)) : rest;
135
213
  const isEnabled = !paused;
136
214
  const renderItem = ({
137
215
  item
@@ -163,14 +241,21 @@ export function NetworkListScreen({
163
241
  style: styles.disabledText,
164
242
  children: "Network interception is disabled"
165
243
  })]
166
- }) : null, /*#__PURE__*/_jsx(PinnedSection, {
167
- events: pinned,
244
+ }) : null, /*#__PURE__*/_jsx(OverriddenSection, {
245
+ events: overridden,
246
+ onEventPress: onEventPress,
247
+ onLongPress: handleLongPress,
248
+ savedLiveIds: savedLiveIds,
249
+ paused: !overridesEnabled,
250
+ onTogglePaused: () => setEnabled(!overridesEnabled)
251
+ }), /*#__PURE__*/_jsx(PinnedSection, {
252
+ events: pinnedRows,
168
253
  onEventPress: onEventPress,
169
254
  onLongPress: handleLongPress,
170
255
  savedLiveIds: savedLiveIds,
171
256
  onClearPinned: clearPinned
172
- }), rest.length > 0 || hasLockedEvents ? /*#__PURE__*/_jsx(FlatList, {
173
- data: rest,
257
+ }), restRows.length > 0 || hasLockedEvents ? /*#__PURE__*/_jsx(FlatList, {
258
+ data: restRows,
174
259
  renderItem: renderItem,
175
260
  keyExtractor: keyExtractor,
176
261
  contentContainerStyle: styles.listContent,
@@ -199,7 +284,7 @@ export function NetworkListScreen({
199
284
  extraData: savedLiveIds,
200
285
  "sentry-label": "ignore network events list",
201
286
  ListFooterComponent: renderListFooter
202
- }) : pinned.length > 0 ? null : /*#__PURE__*/_jsx(EmptyState, {
287
+ }) : pinnedRows.length > 0 || overridden.length > 0 ? null : /*#__PURE__*/_jsx(EmptyState, {
203
288
  isEnabled: isEnabled
204
289
  })]
205
290
  });
@@ -214,6 +299,42 @@ const styles = StyleSheet.create({
214
299
  * these rows do NOT obey the filters, and the boundary is what tells the
215
300
  * user why a request they just filtered out is still on screen.
216
301
  */
302
+ overriddenSection: {
303
+ borderBottomWidth: 1,
304
+ borderBottomColor: macOSColors.semantic.warning + "44",
305
+ backgroundColor: macOSColors.semantic.warningBackground
306
+ },
307
+ overriddenSectionOff: {
308
+ borderBottomColor: macOSColors.border.default,
309
+ backgroundColor: macOSColors.background.card
310
+ },
311
+ overriddenTitleOff: {
312
+ color: macOSColors.text.muted
313
+ },
314
+ overriddenHeader: {
315
+ flexDirection: "row",
316
+ alignItems: "center",
317
+ gap: 6,
318
+ paddingHorizontal: 12,
319
+ paddingTop: 8,
320
+ paddingBottom: 4
321
+ },
322
+ overriddenTitle: {
323
+ flex: 1,
324
+ fontSize: 10,
325
+ fontWeight: "700",
326
+ letterSpacing: 0.8,
327
+ color: macOSColors.semantic.warning
328
+ },
329
+ overriddenManage: {
330
+ paddingHorizontal: 6,
331
+ paddingVertical: 2
332
+ },
333
+ overriddenManageText: {
334
+ fontSize: 10,
335
+ fontWeight: "600",
336
+ color: macOSColors.semantic.warning
337
+ },
217
338
  pinnedSection: {
218
339
  borderBottomWidth: 1,
219
340
  borderBottomColor: macOSColors.border.default,
@@ -2,20 +2,25 @@
2
2
 
3
3
  import { useState, useRef, useEffect, useCallback } from "react";
4
4
  import { View, StyleSheet, TouchableOpacity, TextInput } from "react-native";
5
- import { Search, Filter, X, JsModal, ModalHeader, devToolsStorageKeys, macOSColors, persistentStorage, TabSelector, useFeatureGate, UpgradeModal, PowerToggleButton, useIgnoredPatterns } from "@buoy-gg/shared-ui";
5
+ import { Search, Filter, Bookmark, X, JsModal, ModalHeader, devToolsStorageKeys, macOSColors, persistentStorage, TabSelector, useFeatureGate, UpgradeModal, PowerToggleButton, useIgnoredPatterns } from "@buoy-gg/shared-ui";
6
6
  import { NetworkFilterViewV3 } from "./NetworkFilterViewV3";
7
7
  import { TickProvider } from "../hooks/useTickEveryMinute";
8
8
  import { NetworkEventDetailView } from "./NetworkEventDetailView";
9
9
  import { NetworkCopySettingsView, DEFAULT_COPY_SETTINGS } from "./NetworkCopySettingsView";
10
- import { NetworkHeaderBadges, NetworkClearButton, NetworkDetailPinActions, NetworkHeaderCopyButton, NetworkSavedButton } from "./NetworkHeaderLive";
10
+ import { NetworkHeaderBadges, NetworkClearButton, NetworkDetailPinActions, NetworkHeaderCopyButton } from "./NetworkHeaderLive";
11
11
  import { NetworkListScreen } from "./NetworkListScreen";
12
12
  import { NetworkSavedScreen } from "./NetworkSavedScreen";
13
+ import { NetworkOverridesScreen } from "./NetworkOverridesScreen";
14
+ import { NetworkOverridesButton } from "./NetworkOverridesButton";
15
+ import { NetworkOverrideHeaderButton } from "./NetworkOverrideHeaderButton";
16
+ import { NetworkHeaderMenu, NetworkHeaderMenuButton, NetworkHeaderMenuItem } from "./NetworkHeaderMenu";
13
17
  import { NetworkDetailStepper } from "./NetworkDetailStepper";
14
18
  import { useNetworkEventList } from "../hooks/useNetworkEventList";
15
19
  import { useLiveNetworkEvent } from "../hooks/useLiveNetworkEvent";
16
20
  import { useHasNetworkSelection, useNetworkBodyEventId, useSelectedNetworkEventId } from "../hooks/useNetworkSelection";
17
21
  import { networkSelectionStore } from "../utils/networkSelectionStore";
18
22
  import { FREE_TIER_SAVED_LIMIT, MAX_SAVED } from "../utils/networkSavedStore";
23
+ import { FREE_TIER_RULE_LIMIT as FREE_TIER_OVERRIDE_LIMIT, MAX_RULES as MAX_OVERRIDE_RULES } from "../overrides/types";
19
24
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
20
25
  // ============================================================================
21
26
  // Screens (rule4 stable router: each gates itself with `return null`; the
@@ -199,6 +204,27 @@ function NetworkModalInner({
199
204
  const hasSelection = useHasNetworkSelection();
200
205
  const [showFilterView, setShowFilterView] = useState(false);
201
206
  const [showSavedView, setShowSavedView] = useState(false);
207
+ const [showOverridesView, setShowOverridesView] = useState(false);
208
+ const [showHeaderMenu, setShowHeaderMenu] = useState(false);
209
+ /**
210
+ * Overrides are a DEVICE capability — a rule runs inside the app's own
211
+ * `fetch`/`XMLHttpRequest`, which the dashboard doesn't have.
212
+ *
213
+ * That doesn't mean the dashboard can't AUTHOR them: in `inline` mode the
214
+ * rule store runs as a mirror of the connected device (see
215
+ * `overrideRulesStore.enableRemoteMode`), so the screens below are the same
216
+ * ones the phone renders and every edit is forwarded. The one thing the
217
+ * dashboard never does is apply a rule to its own traffic —
218
+ * `disableEngine()` guarantees that.
219
+ */
220
+ const overridesAvailable = true;
221
+ /**
222
+ * A rule prefilled from a request the user was looking at, handed to the
223
+ * Overrides screen when it mounts. Lives here rather than in that screen
224
+ * because the request that seeds it is on a DIFFERENT screen — this is the
225
+ * one place both are in scope.
226
+ */
227
+ const [pendingOverrideDraft, setPendingOverrideDraft] = useState(null);
202
228
  const [activeTab, setActiveTab] = useState("filters");
203
229
  const [searchText, setSearchText] = useState("");
204
230
  const [isSearchActive, setIsSearchActive] = useState(false);
@@ -277,6 +303,25 @@ function NetworkModalInner({
277
303
  const handleEventPress = useCallback(event => {
278
304
  networkSelectionStore.select(event.id);
279
305
  }, []);
306
+
307
+ /**
308
+ * "Custom…" in a request's Override section. Stable identity because the
309
+ * detail view is memoized on its props and an unstable callback would defeat
310
+ * the bail-out that keeps prev/next stepping cheap.
311
+ */
312
+ const handleOverridePress = useCallback((draft, ruleId) => {
313
+ setPendingOverrideDraft(ruleId ? {
314
+ ...draft,
315
+ id: ruleId
316
+ } : draft);
317
+ setShowOverridesView(true);
318
+ // Leave the detail view, or its header would sit on top of the editor.
319
+ networkSelectionStore.clear();
320
+ }, []);
321
+
322
+ // Stable identity: the Overrides screen adopts the draft in an effect keyed
323
+ // on both, so an unstable callback would re-run it every render.
324
+ const handlePendingOverrideConsumed = useCallback(() => setPendingOverrideDraft(null), []);
280
325
  const handleSearch = text => {
281
326
  setSearchText(text);
282
327
  setFilter(prev => ({
@@ -340,11 +385,28 @@ function NetworkModalInner({
340
385
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
341
386
  title: "Request Details",
342
387
  centered: true
343
- }), /*#__PURE__*/_jsx(ModalHeader.Actions, {
344
- children: /*#__PURE__*/_jsx(NetworkDetailPinActions, {
388
+ }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
389
+ children: [overridesAvailable ? /*#__PURE__*/_jsx(NetworkOverrideHeaderButton, {
390
+ onOpen: handleOverridePress
391
+ }) : null, /*#__PURE__*/_jsx(NetworkDetailPinActions, {
345
392
  isPro: isPro,
346
393
  onUpgradePress: () => showUpgrade("saved")
347
- })
394
+ })]
395
+ })]
396
+ });
397
+ }
398
+
399
+ // Overrides manager header.
400
+ if (showOverridesView && overridesAvailable) {
401
+ return /*#__PURE__*/_jsxs(ModalHeader, {
402
+ children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
403
+ onBack: () => {
404
+ setShowOverridesView(false);
405
+ setPendingOverrideDraft(null);
406
+ }
407
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
408
+ title: "Response Overrides",
409
+ centered: true
348
410
  })]
349
411
  });
350
412
  }
@@ -416,35 +478,26 @@ function NetworkModalInner({
416
478
  size: 14,
417
479
  color: macOSColors.text.secondary
418
480
  })
419
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
420
- "sentry-label": "ignore filter",
421
- onPress: () => {
422
- setShowFilterView(true);
423
- },
424
- style: [styles.headerActionButton, (filter.status || filter.method || filter.contentType) && styles.activeFilterButton],
425
- children: /*#__PURE__*/_jsx(Filter, {
426
- size: 14,
427
- color: filter.status || filter.method || filter.contentType ? macOSColors.semantic.info : macOSColors.text.muted
428
- })
429
- }), /*#__PURE__*/_jsx(NetworkSavedButton, {
430
- onPress: () => setShowSavedView(true)
431
- }), /*#__PURE__*/_jsx(NetworkHeaderCopyButton, {
432
- isPro: isPro,
433
- filter: filter,
434
- ignoredPatterns: ignoredPatterns,
435
- copySettings: copySettings,
436
- paused: paused,
437
- onUpgradePress: () => showUpgrade("export")
438
- }), /*#__PURE__*/_jsx(PowerToggleButton, {
481
+ }), overridesAvailable ? /*#__PURE__*/_jsx(NetworkOverridesButton, {
482
+ onPress: () => setShowOverridesView(true)
483
+ }) : null, /*#__PURE__*/_jsx(PowerToggleButton, {
439
484
  isEnabled: !paused,
440
485
  onToggle: () => setPaused(prev => !prev),
441
486
  accessibilityLabel: "Toggle network interception"
442
487
  }), /*#__PURE__*/_jsx(NetworkClearButton, {
443
488
  paused: paused
489
+ }), /*#__PURE__*/_jsx(NetworkHeaderMenuButton, {
490
+ onPress: () => setShowHeaderMenu(prev => !prev),
491
+ isOpen: showHeaderMenu,
492
+ hasIndicator: hasActiveFilter
444
493
  })]
445
494
  })]
446
495
  });
447
496
  };
497
+
498
+ // Surfaced on the ⋯ so moving Filters into the menu doesn't hide the one
499
+ // thing that button was communicating.
500
+ const hasActiveFilter = !!(filter.status || filter.method || filter.contentType);
448
501
  const persistenceKey = enableSharedModalDimensions ? devToolsStorageKeys.modal.root() : devToolsStorageKeys.network.modal();
449
502
  if (!visible) return null;
450
503
 
@@ -493,8 +546,20 @@ function NetworkModalInner({
493
546
  ignoredPatterns: ignoredPatterns,
494
547
  settings: copySettings,
495
548
  onSettingsChange: setCopySettings
549
+ }), /*#__PURE__*/_jsx(NetworkOverridesScreen, {
550
+ visible: overridesAvailable && showOverridesView && !hasSelection && !showFilterView,
551
+ isPro: isPro,
552
+ onUpgradePress: () => showUpgrade("overrides"),
553
+ pendingDraft: pendingOverrideDraft,
554
+ onPendingConsumed: handlePendingOverrideConsumed
555
+ // Inline (desktop panel) has no home indicator to clear.
556
+ ,
557
+ applySafeAreaInset: !inline && modalMode === "bottomSheet"
496
558
  }), /*#__PURE__*/_jsx(NetworkSavedScreen, {
497
- visible: showSavedView && !hasSelection && !showFilterView,
559
+ visible: showSavedView && !hasSelection && !showFilterView &&
560
+ // Overrides wins in the header branch too — the two must agree, or
561
+ // one screen renders under the other's header.
562
+ !(showOverridesView && overridesAvailable),
498
563
  isPro: isPro,
499
564
  copySettings: copySettings,
500
565
  search: savedSearch,
@@ -502,7 +567,7 @@ function NetworkModalInner({
502
567
  onEventPress: handleEventPress,
503
568
  onUpgradePress: () => showUpgrade("saved")
504
569
  }), /*#__PURE__*/_jsx(NetworkListScreen, {
505
- visible: !hasSelection && !showFilterView && !showSavedView,
570
+ visible: !hasSelection && !showFilterView && !showSavedView && !(showOverridesView && overridesAvailable),
506
571
  paused: paused,
507
572
  filter: filter,
508
573
  ignoredPatterns: ignoredPatterns,
@@ -511,12 +576,51 @@ function NetworkModalInner({
511
576
  onEventPress: handleEventPress,
512
577
  onUpgradePress: () => showUpgrade("export")
513
578
  })]
514
- }), /*#__PURE__*/_jsx(UpgradeModal, {
579
+ }), showHeaderMenu ? /*#__PURE__*/_jsxs(NetworkHeaderMenu, {
580
+ onClose: () => setShowHeaderMenu(false),
581
+ children: [/*#__PURE__*/_jsx(NetworkHeaderMenuItem, {
582
+ icon: /*#__PURE__*/_jsx(Filter, {
583
+ size: 14,
584
+ color: hasActiveFilter ? macOSColors.semantic.info : macOSColors.text.secondary
585
+ }),
586
+ label: "Filters",
587
+ detail: hasActiveFilter ? "Active" : undefined,
588
+ onPress: () => {
589
+ setShowHeaderMenu(false);
590
+ setActiveTab("filters");
591
+ setShowFilterView(true);
592
+ }
593
+ }), /*#__PURE__*/_jsx(NetworkHeaderMenuItem, {
594
+ icon: /*#__PURE__*/_jsx(Bookmark, {
595
+ size: 14,
596
+ color: macOSColors.text.secondary
597
+ }),
598
+ label: "Saved requests",
599
+ onPress: () => {
600
+ setShowHeaderMenu(false);
601
+ setShowSavedView(true);
602
+ }
603
+ }), /*#__PURE__*/_jsx(View, {
604
+ style: styles.menuCopyRow,
605
+ children: /*#__PURE__*/_jsx(NetworkHeaderCopyButton, {
606
+ isPro: isPro,
607
+ filter: filter,
608
+ ignoredPatterns: ignoredPatterns,
609
+ copySettings: copySettings,
610
+ paused: paused,
611
+ onUpgradePress: () => {
612
+ setShowHeaderMenu(false);
613
+ showUpgrade("export");
614
+ },
615
+ label: "Copy requests"
616
+ })
617
+ })]
618
+ }) : null, /*#__PURE__*/_jsx(UpgradeModal, {
515
619
  visible: showUpgradeModal,
516
620
  onClose: () => setShowUpgradeModal(false),
517
- featureName: upgradeReason === "saved" ? "Saved Requests" : "Network Export",
518
- 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.",
519
- benefits: upgradeReason === "saved" ? [`Save up to ${MAX_SAVED} requests (free keeps ${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"]
621
+ featureName: upgradeReason === "saved" ? "Saved Requests" : upgradeReason === "overrides" ? "Response Overrides" : "Network Export",
622
+ 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.",
623
+ benefits: upgradeReason === "overrides" ? [`Run ${MAX_OVERRIDE_RULES} rules at once (free keeps ${FREE_TIER_OVERRIDE_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 ${MAX_SAVED} requests (free keeps ${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"]
520
624
  })]
521
625
  });
522
626
 
@@ -599,6 +703,9 @@ const styles = StyleSheet.create({
599
703
  marginLeft: 6,
600
704
  padding: 4
601
705
  },
706
+ menuCopyRow: {
707
+ paddingHorizontal: 6
708
+ },
602
709
  headerActionButton: {
603
710
  width: 32,
604
711
  height: 32,
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+
3
+ import { useState } from "react";
4
+ import { StyleSheet, Text, TextInput, TouchableOpacity, View } from "react-native";
5
+ import { absoluteFill, macOSColors } from "@buoy-gg/shared-ui";
6
+
7
+ /**
8
+ * Entering one number, without the keyboard eating the screen.
9
+ *
10
+ * The custom status / delay / times fields used to be inline inputs sitting
11
+ * mid-form. Focusing one raises the keyboard, which on a phone covers roughly
12
+ * the bottom half of the tool — including, depending on where you'd scrolled,
13
+ * the field you were typing into and the Save button you needed next.
14
+ *
15
+ * Pinning the panel to the TOP is the fix, and it's the same one
16
+ * `StorageValueEditModal` reached for: a centred panel gets shoved up or
17
+ * covered the moment the field focuses, and a bottom sheet is simply
18
+ * underneath it. At the top it stays put and stays readable.
19
+ *
20
+ * Deliberately not a reuse of that component — this asks for a number and
21
+ * nothing else. It has no type inference, no raw-JSON escape hatch and no
22
+ * shape-preservation rules, so borrowing 400 lines to avoid writing 100 would
23
+ * be the wrong trade.
24
+ */
25
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
26
+ export function NetworkNumberPrompt({
27
+ title,
28
+ hint,
29
+ value,
30
+ placeholder = "Number",
31
+ min,
32
+ max,
33
+ onSubmit,
34
+ onCancel
35
+ }) {
36
+ const [text, setText] = useState(value != null ? String(value) : "");
37
+ const submit = () => {
38
+ const trimmed = text.trim();
39
+ if (!trimmed) {
40
+ onSubmit(null);
41
+ return;
42
+ }
43
+ const parsed = Number(trimmed);
44
+ if (!Number.isFinite(parsed)) {
45
+ onSubmit(null);
46
+ return;
47
+ }
48
+ // Clamped here rather than accepted-then-silently-changed: a field that
49
+ // takes 999 and stores 599 is a field that lies.
50
+ let next = parsed;
51
+ if (min != null) next = Math.max(min, next);
52
+ if (max != null) next = Math.min(max, next);
53
+ onSubmit(next);
54
+ };
55
+ return /*#__PURE__*/_jsxs(View, {
56
+ style: styles.overlay,
57
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
58
+ style: styles.backdrop,
59
+ activeOpacity: 1,
60
+ onPress: onCancel,
61
+ "sentry-label": "ignore dismiss number prompt"
62
+ }), /*#__PURE__*/_jsxs(View, {
63
+ style: styles.panel,
64
+ children: [/*#__PURE__*/_jsx(Text, {
65
+ style: styles.title,
66
+ children: title
67
+ }), /*#__PURE__*/_jsx(TextInput, {
68
+ style: styles.input,
69
+ value: text,
70
+ onChangeText: setText,
71
+ placeholder: placeholder,
72
+ placeholderTextColor: macOSColors.text.muted,
73
+ keyboardType: "number-pad",
74
+ autoFocus: true,
75
+ selectTextOnFocus: true,
76
+ onSubmitEditing: submit,
77
+ returnKeyType: "done",
78
+ "sentry-label": "ignore number prompt input",
79
+ accessibilityLabel: title
80
+ }), hint ? /*#__PURE__*/_jsx(Text, {
81
+ style: styles.hint,
82
+ children: hint
83
+ }) : null, /*#__PURE__*/_jsxs(View, {
84
+ style: styles.actions,
85
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
86
+ onPress: onCancel,
87
+ "sentry-label": "ignore cancel number prompt",
88
+ style: [styles.button, styles.cancelButton],
89
+ children: /*#__PURE__*/_jsx(Text, {
90
+ style: styles.cancelText,
91
+ children: "Cancel"
92
+ })
93
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
94
+ onPress: submit,
95
+ "sentry-label": "ignore save number prompt",
96
+ style: [styles.button, styles.saveButton],
97
+ children: /*#__PURE__*/_jsx(Text, {
98
+ style: styles.saveText,
99
+ children: "Set"
100
+ })
101
+ })]
102
+ })]
103
+ })]
104
+ });
105
+ }
106
+ const styles = StyleSheet.create({
107
+ overlay: {
108
+ ...absoluteFill,
109
+ zIndex: 40
110
+ },
111
+ backdrop: {
112
+ ...absoluteFill,
113
+ backgroundColor: "rgba(0,0,0,0.6)"
114
+ },
115
+ panel: {
116
+ marginHorizontal: 12,
117
+ marginTop: 12,
118
+ padding: 14,
119
+ borderRadius: 10,
120
+ borderWidth: 1,
121
+ borderColor: macOSColors.border.hover,
122
+ backgroundColor: macOSColors.background.card,
123
+ gap: 10
124
+ },
125
+ title: {
126
+ fontSize: 13,
127
+ fontWeight: "700",
128
+ color: macOSColors.text.primary
129
+ },
130
+ input: {
131
+ backgroundColor: macOSColors.background.input,
132
+ borderWidth: 1,
133
+ borderColor: macOSColors.border.input,
134
+ borderRadius: 6,
135
+ paddingHorizontal: 12,
136
+ paddingVertical: 10,
137
+ fontSize: 16,
138
+ fontWeight: "700",
139
+ color: macOSColors.text.primary
140
+ },
141
+ hint: {
142
+ fontSize: 10,
143
+ color: macOSColors.text.muted
144
+ },
145
+ actions: {
146
+ flexDirection: "row",
147
+ gap: 8
148
+ },
149
+ button: {
150
+ flex: 1,
151
+ paddingVertical: 10,
152
+ borderRadius: 6,
153
+ alignItems: "center",
154
+ borderWidth: 1
155
+ },
156
+ cancelButton: {
157
+ borderColor: macOSColors.border.default,
158
+ backgroundColor: macOSColors.background.base
159
+ },
160
+ cancelText: {
161
+ fontSize: 12,
162
+ fontWeight: "600",
163
+ color: macOSColors.text.secondary
164
+ },
165
+ saveButton: {
166
+ borderColor: macOSColors.semantic.info,
167
+ backgroundColor: `${macOSColors.semantic.info}22`
168
+ },
169
+ saveText: {
170
+ fontSize: 12,
171
+ fontWeight: "700",
172
+ color: macOSColors.semantic.info
173
+ }
174
+ });