@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
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
 
3
- import { useState, useEffect, useMemo } from "react";
3
+ import { useState, useEffect, useMemo, memo } from "react";
4
4
  import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from "react-native";
5
- import { Clock, Upload, Download, AlertCircle, ChevronDown, ChevronUp, Lock, Unlock, FileJson, Filter, Globe, Link, Copy, InlineCopyButton, macOSColors, useFeatureGate, UpgradeModal } from "@buoy-gg/shared-ui";
5
+ import { Clock, Upload, Download, AlertCircle, ChevronDown, ChevronUp, Lock, Unlock, FileJson, Filter, Globe, Link, Copy, FlaskConical, InlineCopyButton, macOSColors, useFeatureGate, UpgradeModal } from "@buoy-gg/shared-ui";
6
6
  import { formatBytes, formatDuration, formatHttpStatus } from "../utils/formatting";
7
7
  import { formatRelativeTime } from "@buoy-gg/shared-ui";
8
8
  import { DataViewer } from "@buoy-gg/shared-ui/dataViewer";
@@ -16,6 +16,10 @@ import { useNetworkBodyResolver } from "../sync/networkBodyResolver";
16
16
  */
17
17
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
18
  const NEUTRAL_ICON = macOSColors.semantic.success;
19
+
20
+ /** Hoisted so the defaults don't mint a new Set/function per render. */
21
+ const EMPTY_PATTERNS = new Set();
22
+ const noop = () => {};
19
23
  /**
20
24
  * The measurable slice of a request's lifetime, as proportional bars.
21
25
  *
@@ -69,8 +73,11 @@ const TimingPhases = ({
69
73
  children: /*#__PURE__*/_jsx(View, {
70
74
  style: [styles.phaseBar, {
71
75
  backgroundColor: phase.color,
72
- // A floor so a 1ms phase is still a visible tick.
73
- width: `${Math.max(1, phase.value / span * 100)}%`
76
+ // A floor so a 1ms phase is still a visible tick, and a
77
+ // ceiling because `total` and the phases are measured at
78
+ // different points — ttfb can exceed event.duration, which
79
+ // would push the bar past its track and over the label.
80
+ width: `${Math.min(100, Math.max(1, phase.value / span * 100))}%`
74
81
  }]
75
82
  })
76
83
  }), /*#__PURE__*/_jsx(Text, {
@@ -81,6 +88,17 @@ const TimingPhases = ({
81
88
  });
82
89
  };
83
90
 
91
+ /**
92
+ * Stand-in for a body tree while the prev/next cursor is still moving.
93
+ *
94
+ * Deliberately a fixed-height nothing: it must cost ~0 to render (that is the
95
+ * entire point) and it must not resize the scroll view as the real tree swaps
96
+ * in, or fast stepping would make the page jump under the reader.
97
+ */
98
+ const BodyDeferred = () => /*#__PURE__*/_jsx(View, {
99
+ style: styles.bodyDeferred
100
+ });
101
+
84
102
  // Component for collapsible sections matching Sentry style
85
103
  const CollapsibleSection = ({
86
104
  title,
@@ -260,11 +278,30 @@ const UrlBreakdown = ({
260
278
  /**
261
279
  * Detailed inspector rendering request/response metadata for a single network event. Designed to
262
280
  * mimic Sentry’s layout with collapsible sections, diff views, and quick copy affordances.
281
+ *
282
+ * MEMOIZED with profiling evidence, not by reflex. Two render paths hit this
283
+ * subtree with unchanged props:
284
+ *
285
+ * 1. `useLiveNetworkEvent` re-subscribes when the selected ID changes, and
286
+ * `subscribeToEvents` replays immediately — so every step through the
287
+ * prev/next footer committed the SAME event object twice, rendering this
288
+ * whole tree (expanded DataViewer included) a second time for nothing.
289
+ * 2. Any modal-root state change (header search, mode toggle) re-renders the
290
+ * shell, and the detail body has nothing to do with it.
291
+ *
292
+ * Both props besides `event` are stable by construction — `values` is memoized
293
+ * in `useIgnoredPatterns` and `toggle` is module-scope there — so the bail-out
294
+ * actually fires.
295
+ *
296
+ * Measured baseline (js-top, example-dev-build, 5 steps over 109–324KB
297
+ * pokeapi responses): 307ms of React work across 5 steps ≈ 61ms of JS per
298
+ * step. Case 1 alone is half of that, since it re-ran the whole body render.
263
299
  */
264
- export function NetworkEventDetailView({
300
+ export const NetworkEventDetailView = /*#__PURE__*/memo(function NetworkEventDetailView({
265
301
  event,
266
- ignoredPatterns = new Set(),
267
- onTogglePattern = () => {}
302
+ ignoredPatterns = EMPTY_PATTERNS,
303
+ onTogglePattern = noop,
304
+ showBodies = true
268
305
  }) {
269
306
  const status = event.status ? formatHttpStatus(event.status) : null;
270
307
  const isPending = !event.status && !event.error;
@@ -408,8 +445,24 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
408
445
  style: styles.httpDurationText,
409
446
  children: formatDuration(event.duration)
410
447
  })]
411
- }) : null, isPro ? /*#__PURE__*/_jsx(InlineCopyButton, {
412
- value: getFullRequestDetails(),
448
+ }) : null, event.override ? /*#__PURE__*/_jsxs(View, {
449
+ style: styles.overrideBadge,
450
+ children: [/*#__PURE__*/_jsx(FlaskConical, {
451
+ size: 10,
452
+ color: macOSColors.semantic.warning
453
+ }), /*#__PURE__*/_jsx(Text, {
454
+ style: styles.overrideBadgeText,
455
+ children: event.override.ruleName || "Overridden"
456
+ })]
457
+ }) : null, isPro ? /*#__PURE__*/_jsx(InlineCopyButton
458
+ // LAZY, not `getFullRequestDetails()`. CopyButton resolves a
459
+ // function value at press time; calling it here instead
460
+ // pretty-printed the headers AND both bodies on every render —
461
+ // four JSON.stringify passes over the full payload just to have
462
+ // a string nobody asked for yet. Same shape NetworkSavedScreen
463
+ // already uses for its copy button.
464
+ , {
465
+ value: getFullRequestDetails,
413
466
  buttonStyle: styles.copyFullButton
414
467
  }) : /*#__PURE__*/_jsx(TouchableOpacity, {
415
468
  style: styles.copyFullButton,
@@ -543,14 +596,14 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
543
596
  defaultOpen: false,
544
597
  children: /*#__PURE__*/_jsx(View, {
545
598
  style: styles.dataViewerContainer,
546
- children: /*#__PURE__*/_jsx(DataViewer, {
599
+ children: showBodies ? /*#__PURE__*/_jsx(DataViewer, {
547
600
  title: "",
548
601
  data: requestData,
549
602
  showTypeFilter: true,
550
603
  rawMode: true,
551
604
  initialExpanded: true,
552
605
  disableCopy: !isPro
553
- })
606
+ }) : /*#__PURE__*/_jsx(BodyDeferred, {})
554
607
  })
555
608
  }) : requestBodyOmitted ? /*#__PURE__*/_jsx(CollapsibleSection, {
556
609
  title: "Request Body",
@@ -575,14 +628,14 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
575
628
  defaultOpen: true,
576
629
  children: /*#__PURE__*/_jsx(View, {
577
630
  style: styles.dataViewerContainer,
578
- children: /*#__PURE__*/_jsx(DataViewer, {
631
+ children: showBodies ? /*#__PURE__*/_jsx(DataViewer, {
579
632
  title: "",
580
633
  data: responseData,
581
634
  showTypeFilter: true,
582
635
  rawMode: true,
583
636
  initialExpanded: true,
584
637
  disableCopy: !isPro
585
- })
638
+ }) : /*#__PURE__*/_jsx(BodyDeferred, {})
586
639
  })
587
640
  }) : responseBodyOmitted ? /*#__PURE__*/_jsx(CollapsibleSection, {
588
641
  title: "Response Body",
@@ -687,7 +740,7 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
687
740
  benefits: ["Copy full request details including headers and body", "Export to JSON, Markdown, or plain text", "Share request data with teammates", "Feed data to AI assistants for debugging"]
688
741
  })]
689
742
  });
690
- }
743
+ });
691
744
  const styles = StyleSheet.create({
692
745
  container: {
693
746
  flex: 1,
@@ -754,6 +807,20 @@ const styles = StyleSheet.create({
754
807
  color: macOSColors.text.secondary,
755
808
  fontSize: 11
756
809
  },
810
+ overrideBadge: {
811
+ flexDirection: "row",
812
+ alignItems: "center",
813
+ gap: 4,
814
+ paddingHorizontal: 6,
815
+ paddingVertical: 3,
816
+ borderRadius: 4,
817
+ backgroundColor: macOSColors.semantic.warningBackground
818
+ },
819
+ overrideBadgeText: {
820
+ color: macOSColors.semantic.warning,
821
+ fontSize: 10,
822
+ fontWeight: "700"
823
+ },
757
824
  copyFullButton: {
758
825
  marginLeft: "auto",
759
826
  flexShrink: 0,
@@ -906,7 +973,8 @@ const styles = StyleSheet.create({
906
973
  height: 7,
907
974
  borderRadius: 4,
908
975
  backgroundColor: macOSColors.background.hover,
909
- justifyContent: "center"
976
+ justifyContent: "center",
977
+ overflow: "hidden"
910
978
  },
911
979
  phaseBar: {
912
980
  height: 7,
@@ -963,6 +1031,10 @@ const styles = StyleSheet.create({
963
1031
  marginHorizontal: -12,
964
1032
  marginBottom: -12
965
1033
  },
1034
+ /** Placeholder for a not-yet-mounted body tree. See BodyDeferred. */
1035
+ bodyDeferred: {
1036
+ height: 120
1037
+ },
966
1038
  // Error box
967
1039
  errorBox: {
968
1040
  flexDirection: "row",
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { memo } from "react";
4
4
  import { StyleSheet, View, Text } from "react-native";
5
- import { ChevronRight, Upload, Download, Clock, AlertCircle, Bookmark, Pin, DevToolsCard, MethodBadge, TypeBadge, macOSColors } from "@buoy-gg/shared-ui";
5
+ import { ChevronRight, Upload, Download, Clock, AlertCircle, Bookmark, FlaskConical, Pin, DevToolsCard, MethodBadge, TypeBadge, macOSColors } from "@buoy-gg/shared-ui";
6
6
  import { formatBytes, formatDuration } from "../utils/formatting";
7
7
  import { formatRelativeTime } from "@buoy-gg/shared-ui";
8
8
  import { useTickEveryMinute } from "../hooks/useTickEveryMinute";
@@ -170,6 +170,7 @@ function NetworkEventItemCompact({
170
170
  onPress,
171
171
  pinned = false,
172
172
  saved = false,
173
+ overridden = false,
173
174
  onLongPress
174
175
  }) {
175
176
  const statusColor = getStatusColor(event.status, event.error);
@@ -208,6 +209,9 @@ function NetworkEventItemCompact({
208
209
  }) : null, saved ? /*#__PURE__*/_jsx(Bookmark, {
209
210
  size: 10,
210
211
  color: macOSColors.semantic.debug
212
+ }) : null, overridden || event.override ? /*#__PURE__*/_jsx(FlaskConical, {
213
+ size: 10,
214
+ color: macOSColors.semantic.warning
211
215
  }) : null, /*#__PURE__*/_jsx(StatusIndicator, {
212
216
  event: event,
213
217
  isPending: isPending,
@@ -23,6 +23,7 @@ import { Bookmark, CheckCircle, XCircle, Clock, Copy, Pin, Trash2, CopyButton, m
23
23
  import { networkEventStore } from "../utils/networkEventStore";
24
24
  import { flagsForEventId, networkSavedStore, FREE_TIER_SAVED_LIMIT, MAX_PINNED, MAX_SAVED } from "../utils/networkSavedStore";
25
25
  import { useNetworkSaved } from "../hooks/useNetworkSaved";
26
+ import { useSelectedNetworkEventId } from "../hooks/useNetworkSelection";
26
27
  import { useNetworkBadgeCounts } from "../hooks/useNetworkBadgeCounts";
27
28
  import { useNetworkHasEvents, useNetworkHasMatches } from "../hooks/useNetworkHasMatches";
28
29
  import { generateNetworkCopyText } from "../utils/generateNetworkCopyText";
@@ -141,12 +142,16 @@ export function NetworkSavedButton({
141
142
  * The event object is resolved at PRESS time rather than held as a prop: the
142
143
  * live store has the fresh copy while the request is in flight, and the saved
143
144
  * store is the only copy once it has been cleared or aged out.
145
+ *
146
+ * The selected id is SUBSCRIBED here rather than passed down, so stepping
147
+ * through requests doesn't have to re-render the modal root just to rebuild
148
+ * the header around a new prop.
144
149
  */
145
150
  export function NetworkDetailPinActions({
146
- eventId,
147
151
  isPro,
148
152
  onUpgradePress
149
153
  }) {
154
+ const eventId = useSelectedNetworkEventId();
150
155
  const state = useNetworkSaved();
151
156
  const {
152
157
  pinned,
@@ -209,7 +214,8 @@ export function NetworkHeaderCopyButton({
209
214
  ignoredPatterns,
210
215
  copySettings,
211
216
  paused,
212
- onUpgradePress
217
+ onUpgradePress,
218
+ label
213
219
  }) {
214
220
  const hasEvents = useNetworkHasMatches({
215
221
  filter,
@@ -217,15 +223,18 @@ export function NetworkHeaderCopyButton({
217
223
  paused
218
224
  });
219
225
  if (!isPro) {
220
- return /*#__PURE__*/_jsx(TouchableOpacity, {
226
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
221
227
  "sentry-label": "ignore copy pro",
222
228
  onPress: onUpgradePress,
223
- style: [styles.headerActionButton, !hasEvents && styles.headerActionButtonDisabled],
229
+ style: [label ? styles.copyMenuRow : styles.headerActionButton, !hasEvents && styles.headerActionButtonDisabled],
224
230
  disabled: !hasEvents,
225
- children: /*#__PURE__*/_jsx(Copy, {
231
+ children: [/*#__PURE__*/_jsx(Copy, {
226
232
  size: 14,
227
233
  color: hasEvents ? macOSColors.text.secondary : macOSColors.text.disabled
228
- })
234
+ }), label ? /*#__PURE__*/_jsx(Text, {
235
+ style: styles.copyMenuLabel,
236
+ children: label
237
+ }) : null]
229
238
  });
230
239
  }
231
240
  return /*#__PURE__*/_jsx(CopyButton
@@ -234,7 +243,9 @@ export function NetworkHeaderCopyButton({
234
243
  , {
235
244
  value: () => generateNetworkCopyText(filterNetworkEvents(applyIgnoredPatterns(networkEventStore.getEvents(), ignoredPatterns), filter), copySettings),
236
245
  size: 14,
237
- buttonStyle: styles.headerActionButton,
246
+ label: label,
247
+ buttonStyle: label ? styles.copyMenuRow : styles.headerActionButton,
248
+ labelStyle: styles.copyMenuLabel,
238
249
  colors: {
239
250
  idle: hasEvents ? macOSColors.text.secondary : macOSColors.text.disabled,
240
251
  success: macOSColors.semantic.success,
@@ -244,6 +255,20 @@ export function NetworkHeaderCopyButton({
244
255
  });
245
256
  }
246
257
  const styles = StyleSheet.create({
258
+ /** The overflow-menu variant: a full-width row, matching the other items. */
259
+ copyMenuRow: {
260
+ flexDirection: "row",
261
+ alignItems: "center",
262
+ gap: 10,
263
+ paddingHorizontal: 6,
264
+ paddingVertical: 11,
265
+ justifyContent: "flex-start"
266
+ },
267
+ copyMenuLabel: {
268
+ fontSize: 13,
269
+ fontWeight: "600",
270
+ color: macOSColors.text.primary
271
+ },
247
272
  headerChipRow: {
248
273
  flexDirection: "row",
249
274
  alignItems: "center",
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+
3
+ import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
4
+ import { absoluteFill, macOSColors } from "@buoy-gg/shared-ui";
5
+
6
+ /**
7
+ * The header's overflow menu.
8
+ *
9
+ * The toolbar had grown to three status badges and six icon buttons, which on a
10
+ * 402pt screen meant the pending badge was being overlapped by the search
11
+ * button. Something had to leave, and the rule for deciding what is:
12
+ *
13
+ * **A control that COMMUNICATES STATE stays visible. A destination or a rare
14
+ * action goes in the menu.**
15
+ *
16
+ * That keeps the power toggle (are we capturing?), the overrides flask (is the
17
+ * app being lied to, and how many rules?), the status badges (what happened)
18
+ * and Clear (the reproduce loop is clear → repro → read, and burying the first
19
+ * step doubles it). Search stays because it's the only way through 500 rows.
20
+ *
21
+ * Filters, Saved and Copy leave. Filters is the interesting one — it does show
22
+ * state, so the ⋯ carries a dot when a filter is applied rather than hiding
23
+ * that fact.
24
+ */
25
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
26
+ export function NetworkHeaderMenuButton({
27
+ onPress,
28
+ isOpen,
29
+ hasIndicator
30
+ }) {
31
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
32
+ onPress: onPress,
33
+ "sentry-label": "ignore toggle header menu",
34
+ accessibilityLabel: isOpen ? "Close menu" : "More actions",
35
+ style: [styles.button, isOpen && styles.buttonOpen],
36
+ children: [/*#__PURE__*/_jsxs(View, {
37
+ style: styles.dots,
38
+ children: [/*#__PURE__*/_jsx(View, {
39
+ style: [styles.dot, isOpen && styles.dotOpen]
40
+ }), /*#__PURE__*/_jsx(View, {
41
+ style: [styles.dot, isOpen && styles.dotOpen]
42
+ }), /*#__PURE__*/_jsx(View, {
43
+ style: [styles.dot, isOpen && styles.dotOpen]
44
+ })]
45
+ }), hasIndicator ? /*#__PURE__*/_jsx(View, {
46
+ style: styles.indicator
47
+ }) : null]
48
+ });
49
+ }
50
+ /**
51
+ * The dropdown itself, rendered in the modal BODY rather than the header —
52
+ * the header is a fixed-height row and anything overflowing it is clipped, so
53
+ * a panel anchored to the top-right of the body lands exactly where a dropdown
54
+ * from the ⋯ should appear.
55
+ */
56
+ export function NetworkHeaderMenu({
57
+ onClose,
58
+ children
59
+ }) {
60
+ return /*#__PURE__*/_jsxs(View, {
61
+ style: styles.overlay,
62
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
63
+ style: styles.backdrop,
64
+ activeOpacity: 1,
65
+ onPress: onClose,
66
+ "sentry-label": "ignore dismiss header menu"
67
+ }), /*#__PURE__*/_jsx(View, {
68
+ style: styles.panel,
69
+ children: children
70
+ })]
71
+ });
72
+ }
73
+ export function NetworkHeaderMenuItem({
74
+ icon,
75
+ label,
76
+ detail,
77
+ onPress
78
+ }) {
79
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
80
+ onPress: onPress,
81
+ "sentry-label": "ignore header menu item",
82
+ style: styles.item,
83
+ children: [/*#__PURE__*/_jsx(View, {
84
+ style: styles.itemIcon,
85
+ children: icon
86
+ }), /*#__PURE__*/_jsx(Text, {
87
+ style: styles.itemLabel,
88
+ children: label
89
+ }), detail ? /*#__PURE__*/_jsx(Text, {
90
+ style: styles.itemDetail,
91
+ children: detail
92
+ }) : null]
93
+ });
94
+ }
95
+ const styles = StyleSheet.create({
96
+ // The same 32×32 box every other header action uses, minus the fill and
97
+ // border. Padding alone gave a 3pt-wide hit area AND a shorter box than its
98
+ // neighbours, which is why the dots sat high in the row; a fixed square
99
+ // centres the glyph on the same baseline as the search/power/clear icons.
100
+ button: {
101
+ width: 32,
102
+ height: 32,
103
+ borderRadius: 8,
104
+ alignItems: "center",
105
+ justifyContent: "center"
106
+ },
107
+ // Only lit while the menu is down — the button is the one thing not covered
108
+ // by the backdrop, so it has to look like the thing that is currently open.
109
+ buttonOpen: {
110
+ backgroundColor: macOSColors.background.hover
111
+ },
112
+ dots: {
113
+ flexDirection: "column",
114
+ gap: 2.5,
115
+ alignItems: "center"
116
+ },
117
+ dot: {
118
+ width: 3,
119
+ height: 3,
120
+ borderRadius: 2,
121
+ backgroundColor: macOSColors.text.secondary
122
+ },
123
+ dotOpen: {
124
+ backgroundColor: macOSColors.text.primary
125
+ },
126
+ indicator: {
127
+ position: "absolute",
128
+ // Sits just up-and-right of the top dot. Anchored to the 32pt box's corner
129
+ // it would float off on its own, since the dot column is only 3pt wide.
130
+ top: 6,
131
+ right: 8,
132
+ width: 6,
133
+ height: 6,
134
+ borderRadius: 3,
135
+ backgroundColor: macOSColors.semantic.info
136
+ },
137
+ overlay: {
138
+ ...absoluteFill,
139
+ zIndex: 50
140
+ },
141
+ backdrop: {
142
+ ...absoluteFill,
143
+ backgroundColor: "rgba(0,0,0,0.5)"
144
+ },
145
+ panel: {
146
+ position: "absolute",
147
+ top: 6,
148
+ right: 8,
149
+ minWidth: 196,
150
+ paddingVertical: 4,
151
+ borderRadius: 8,
152
+ borderWidth: 1,
153
+ borderColor: macOSColors.border.hover,
154
+ backgroundColor: macOSColors.background.card
155
+ },
156
+ item: {
157
+ flexDirection: "row",
158
+ alignItems: "center",
159
+ gap: 10,
160
+ paddingHorizontal: 12,
161
+ paddingVertical: 11
162
+ },
163
+ itemIcon: {
164
+ width: 16,
165
+ alignItems: "center"
166
+ },
167
+ itemLabel: {
168
+ flex: 1,
169
+ fontSize: 13,
170
+ fontWeight: "600",
171
+ color: macOSColors.text.primary
172
+ },
173
+ itemDetail: {
174
+ fontSize: 11,
175
+ color: macOSColors.text.muted
176
+ }
177
+ });
@@ -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,
@@ -117,16 +175,41 @@ export function NetworkListScreen({
117
175
  togglePin,
118
176
  clearPinned
119
177
  } = useNetworkSaved();
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.
120
181
  const {
121
- pinned,
122
- rest
123
- } = splitPinned(visibleEvents, pinnedEvents, pinnedLiveIds);
182
+ rules,
183
+ enabled: overridesEnabled,
184
+ setEnabled,
185
+ isRuleSpent
186
+ } = useOverrideRules();
124
187
 
125
188
  // Stable identity: handed to memoized rows.
126
189
  const handleLongPress = useCallback(event => {
127
190
  togglePin(event);
128
191
  }, [togglePin]);
192
+
193
+ // Everything above is a hook and must run unconditionally; everything below
194
+ // is work. `splitPinned` is an O(n) pass over the whole list, and this screen
195
+ // re-renders on every modal-root state change — including every prev/next
196
+ // step, where it is hidden and the result is thrown away.
129
197
  if (!visible) return null;
198
+ const {
199
+ pinned,
200
+ rest
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;
130
213
  const isEnabled = !paused;
131
214
  const renderItem = ({
132
215
  item
@@ -158,14 +241,21 @@ export function NetworkListScreen({
158
241
  style: styles.disabledText,
159
242
  children: "Network interception is disabled"
160
243
  })]
161
- }) : null, /*#__PURE__*/_jsx(PinnedSection, {
162
- 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,
163
253
  onEventPress: onEventPress,
164
254
  onLongPress: handleLongPress,
165
255
  savedLiveIds: savedLiveIds,
166
256
  onClearPinned: clearPinned
167
- }), rest.length > 0 || hasLockedEvents ? /*#__PURE__*/_jsx(FlatList, {
168
- data: rest,
257
+ }), restRows.length > 0 || hasLockedEvents ? /*#__PURE__*/_jsx(FlatList, {
258
+ data: restRows,
169
259
  renderItem: renderItem,
170
260
  keyExtractor: keyExtractor,
171
261
  contentContainerStyle: styles.listContent,
@@ -194,7 +284,7 @@ export function NetworkListScreen({
194
284
  extraData: savedLiveIds,
195
285
  "sentry-label": "ignore network events list",
196
286
  ListFooterComponent: renderListFooter
197
- }) : pinned.length > 0 ? null : /*#__PURE__*/_jsx(EmptyState, {
287
+ }) : pinnedRows.length > 0 || overridden.length > 0 ? null : /*#__PURE__*/_jsx(EmptyState, {
198
288
  isEnabled: isEnabled
199
289
  })]
200
290
  });
@@ -209,6 +299,42 @@ const styles = StyleSheet.create({
209
299
  * these rows do NOT obey the filters, and the boundary is what tells the
210
300
  * user why a request they just filtered out is still on screen.
211
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
+ },
212
338
  pinnedSection: {
213
339
  borderBottomWidth: 1,
214
340
  borderBottomColor: macOSColors.border.default,