@buoy-gg/network 7.0.3 → 7.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +3 -2
  2. package/lib/commonjs/index.js +85 -0
  3. package/lib/commonjs/network/components/NetworkEventDetailView.js +201 -40
  4. package/lib/commonjs/network/components/NetworkEventItemCompact.js +52 -5
  5. package/lib/commonjs/network/components/NetworkHeaderLive.js +132 -0
  6. package/lib/commonjs/network/components/NetworkListScreen.js +134 -56
  7. package/lib/commonjs/network/components/NetworkModal.js +154 -36
  8. package/lib/commonjs/network/components/NetworkSavedScreen.js +245 -0
  9. package/lib/commonjs/network/components/NetworkUpgradeBanner.js +76 -0
  10. package/lib/commonjs/network/hooks/useLiveNetworkEvent.js +62 -0
  11. package/lib/commonjs/network/hooks/useNetworkSaved.js +38 -0
  12. package/lib/commonjs/network/index.js +84 -0
  13. package/lib/commonjs/network/sync/networkSyncAdapter.js +85 -7
  14. package/lib/commonjs/network/utils/generateCurl.js +71 -0
  15. package/lib/commonjs/network/utils/networkEventStore.js +10 -1
  16. package/lib/commonjs/network/utils/networkListener.js +54 -1
  17. package/lib/commonjs/network/utils/networkSavedStore.js +575 -0
  18. package/lib/commonjs/network/utils/withPinnedFirst.js +32 -0
  19. package/lib/module/index.js +17 -1
  20. package/lib/module/network/components/NetworkEventDetailView.js +204 -41
  21. package/lib/module/network/components/NetworkEventItemCompact.js +53 -6
  22. package/lib/module/network/components/NetworkHeaderLive.js +132 -2
  23. package/lib/module/network/components/NetworkListScreen.js +136 -58
  24. package/lib/module/network/components/NetworkModal.js +156 -39
  25. package/lib/module/network/components/NetworkSavedScreen.js +241 -0
  26. package/lib/module/network/components/NetworkUpgradeBanner.js +72 -0
  27. package/lib/module/network/hooks/useLiveNetworkEvent.js +58 -0
  28. package/lib/module/network/hooks/useNetworkSaved.js +34 -0
  29. package/lib/module/network/index.js +7 -1
  30. package/lib/module/network/sync/networkSyncAdapter.js +85 -7
  31. package/lib/module/network/utils/generateCurl.js +67 -0
  32. package/lib/module/network/utils/networkEventStore.js +10 -1
  33. package/lib/module/network/utils/networkListener.js +54 -1
  34. package/lib/module/network/utils/networkSavedStore.js +569 -0
  35. package/lib/module/network/utils/withPinnedFirst.js +28 -0
  36. package/lib/typescript/index.d.ts +13 -1
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  39. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +12 -0
  40. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  41. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +28 -0
  42. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  43. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  44. package/lib/typescript/network/components/NetworkModal.d.ts +34 -0
  45. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  46. package/lib/typescript/network/components/NetworkSavedScreen.d.ts +29 -0
  47. package/lib/typescript/network/components/NetworkSavedScreen.d.ts.map +1 -0
  48. package/lib/typescript/network/components/NetworkUpgradeBanner.d.ts +17 -0
  49. package/lib/typescript/network/components/NetworkUpgradeBanner.d.ts.map +1 -0
  50. package/lib/typescript/network/hooks/useLiveNetworkEvent.d.ts +19 -0
  51. package/lib/typescript/network/hooks/useLiveNetworkEvent.d.ts.map +1 -0
  52. package/lib/typescript/network/hooks/useNetworkSaved.d.ts +21 -0
  53. package/lib/typescript/network/hooks/useNetworkSaved.d.ts.map +1 -0
  54. package/lib/typescript/network/index.d.ts +8 -1
  55. package/lib/typescript/network/index.d.ts.map +1 -1
  56. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +52 -3
  57. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  58. package/lib/typescript/network/types/index.d.ts +43 -0
  59. package/lib/typescript/network/types/index.d.ts.map +1 -1
  60. package/lib/typescript/network/utils/generateCurl.d.ts +3 -0
  61. package/lib/typescript/network/utils/generateCurl.d.ts.map +1 -0
  62. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  63. package/lib/typescript/network/utils/networkListener.d.ts +4 -0
  64. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  65. package/lib/typescript/network/utils/networkSavedStore.d.ts +186 -0
  66. package/lib/typescript/network/utils/networkSavedStore.d.ts.map +1 -0
  67. package/lib/typescript/network/utils/withPinnedFirst.d.ts +24 -0
  68. package/lib/typescript/network/utils/withPinnedFirst.d.ts.map +1 -0
  69. package/package.json +2 -2
@@ -12,11 +12,15 @@
12
12
  * `paused || !visible` so a hidden list doesn't subscribe at all.
13
13
  */
14
14
 
15
+ import { useCallback } from "react";
15
16
  import { FlatList, StyleSheet, Text, TouchableOpacity, View } from "react-native";
16
- import { Globe, Power, ProBadge, Zap, macOSColors } from "@buoy-gg/shared-ui";
17
+ import { Globe, Pin, Power, macOSColors } from "@buoy-gg/shared-ui";
17
18
  import { NetworkEventItemCompact } from "./NetworkEventItemCompact";
19
+ import { NetworkUpgradeBanner } from "./NetworkUpgradeBanner";
18
20
  import { useNetworkEventList } from "../hooks/useNetworkEventList";
19
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
21
+ import { useNetworkSaved } from "../hooks/useNetworkSaved";
22
+ import { splitPinned } from "../utils/withPinnedFirst";
23
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
24
  function EmptyState({
21
25
  isEnabled
22
26
  }) {
@@ -35,6 +39,54 @@ function EmptyState({
35
39
  });
36
40
  }
37
41
  const keyExtractor = item => item.id;
42
+
43
+ /**
44
+ * The PINNED strip above the live list.
45
+ *
46
+ * It renders OUTSIDE the FlatList on purpose. The list's window/batch values
47
+ * are a measured configuration (see the FlatList below), and merging pins into
48
+ * `data` would mean sentinel items in `keyExtractor`/`renderItem` and a
49
+ * section boundary that moves as events arrive. Keeping pins in their own
50
+ * bounded strip (capped at MAX_PINNED in the store) leaves that tuning
51
+ * untouched — and makes "pins ignore the filters" fall out for free, because
52
+ * this strip is never handed the filter.
53
+ */
54
+ function PinnedSection({
55
+ events,
56
+ onEventPress,
57
+ onLongPress,
58
+ savedLiveIds,
59
+ onClearPinned
60
+ }) {
61
+ if (events.length === 0) return null;
62
+ return /*#__PURE__*/_jsxs(View, {
63
+ style: styles.pinnedSection,
64
+ children: [/*#__PURE__*/_jsxs(View, {
65
+ style: styles.pinnedHeader,
66
+ children: [/*#__PURE__*/_jsx(Pin, {
67
+ size: 11,
68
+ color: macOSColors.semantic.info
69
+ }), /*#__PURE__*/_jsxs(Text, {
70
+ style: styles.pinnedTitle,
71
+ children: ["PINNED \xB7 ", events.length]
72
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
73
+ onPress: onClearPinned,
74
+ "sentry-label": "ignore unpin all",
75
+ style: styles.pinnedClear,
76
+ children: /*#__PURE__*/_jsx(Text, {
77
+ style: styles.pinnedClearText,
78
+ children: "Unpin all"
79
+ })
80
+ })]
81
+ }), events.map(event => /*#__PURE__*/_jsx(NetworkEventItemCompact, {
82
+ event: event,
83
+ onPress: onEventPress,
84
+ onLongPress: onLongPress,
85
+ pinned: true,
86
+ saved: savedLiveIds.has(event.id)
87
+ }, event.id))]
88
+ });
89
+ }
38
90
  export function NetworkListScreen({
39
91
  visible,
40
92
  paused,
@@ -55,42 +107,48 @@ export function NetworkListScreen({
55
107
  paused: paused || !visible,
56
108
  isPro
57
109
  });
110
+
111
+ // Pins come from the persisted saved store, NOT from the live list — that is
112
+ // what lets a pinned failure survive Clear, the 500-event cap and a reload.
113
+ const {
114
+ pinnedEvents,
115
+ pinnedLiveIds,
116
+ savedLiveIds,
117
+ togglePin,
118
+ clearPinned
119
+ } = useNetworkSaved();
120
+ const {
121
+ pinned,
122
+ rest
123
+ } = splitPinned(visibleEvents, pinnedEvents, pinnedLiveIds);
124
+
125
+ // Stable identity: handed to memoized rows.
126
+ const handleLongPress = useCallback(event => {
127
+ togglePin(event);
128
+ }, [togglePin]);
58
129
  if (!visible) return null;
59
130
  const isEnabled = !paused;
60
131
  const renderItem = ({
61
132
  item
62
133
  }) => /*#__PURE__*/_jsx(NetworkEventItemCompact, {
63
134
  event: item,
64
- onPress: onEventPress
135
+ onPress: onEventPress,
136
+ onLongPress: handleLongPress,
137
+ saved: savedLiveIds.has(item.id)
65
138
  });
66
139
  const renderListFooter = () => {
67
140
  if (!hasLockedEvents) return null;
68
141
  const isSearching = searchText.length > 0;
69
142
  const bannerText = isSearching ? `${lockedEventCount} matching ${lockedEventCount === 1 ? "request" : "requests"} in locked history` : `${lockedEventCount} requests locked`;
70
143
  const subtextMessage = isSearching ? "Upgrade to Pro to search your full request history" : "Upgrade to Pro to unlock full request history";
71
- return /*#__PURE__*/_jsxs(TouchableOpacity, {
72
- style: styles.limitBanner,
73
- onPress: onUpgradePress,
74
- activeOpacity: 0.8,
75
- children: [/*#__PURE__*/_jsxs(View, {
76
- style: styles.limitBannerContent,
77
- children: [/*#__PURE__*/_jsxs(View, {
78
- style: styles.limitBannerLeft,
79
- children: [/*#__PURE__*/_jsx(Zap, {
80
- size: 16,
81
- color: "#20C997"
82
- }), /*#__PURE__*/_jsx(Text, {
83
- style: styles.limitBannerText,
84
- children: bannerText
85
- })]
86
- }), /*#__PURE__*/_jsx(ProBadge, {})]
87
- }), /*#__PURE__*/_jsx(Text, {
88
- style: styles.limitBannerSubtext,
89
- children: subtextMessage
90
- })]
144
+ return /*#__PURE__*/_jsx(NetworkUpgradeBanner, {
145
+ title: bannerText,
146
+ subtitle: subtextMessage,
147
+ onPress: onUpgradePress
91
148
  });
92
149
  };
93
- return /*#__PURE__*/_jsxs(_Fragment, {
150
+ return /*#__PURE__*/_jsxs(View, {
151
+ style: styles.root,
94
152
  children: [!isEnabled ? /*#__PURE__*/_jsxs(View, {
95
153
  style: styles.disabledBanner,
96
154
  children: [/*#__PURE__*/_jsx(Power, {
@@ -100,8 +158,14 @@ export function NetworkListScreen({
100
158
  style: styles.disabledText,
101
159
  children: "Network interception is disabled"
102
160
  })]
103
- }) : null, visibleEvents.length > 0 || hasLockedEvents ? /*#__PURE__*/_jsx(FlatList, {
104
- data: visibleEvents,
161
+ }) : null, /*#__PURE__*/_jsx(PinnedSection, {
162
+ events: pinned,
163
+ onEventPress: onEventPress,
164
+ onLongPress: handleLongPress,
165
+ savedLiveIds: savedLiveIds,
166
+ onClearPinned: clearPinned
167
+ }), rest.length > 0 || hasLockedEvents ? /*#__PURE__*/_jsx(FlatList, {
168
+ data: rest,
105
169
  renderItem: renderItem,
106
170
  keyExtractor: keyExtractor,
107
171
  contentContainerStyle: styles.listContent,
@@ -121,15 +185,59 @@ export function NetworkListScreen({
121
185
  initialNumToRender: 12,
122
186
  maxToRenderPerBatch: 8,
123
187
  windowSize: 5,
124
- updateCellsBatchingPeriod: 50,
188
+ updateCellsBatchingPeriod: 50
189
+ // Saving a request from the detail view changes a row's glyph
190
+ // without changing `data`. VirtualizedList is a PureComponent, so
191
+ // the Set has to be declared as a re-render marker rather than
192
+ // relying on `renderItem` getting a fresh identity.
193
+ ,
194
+ extraData: savedLiveIds,
125
195
  "sentry-label": "ignore network events list",
126
196
  ListFooterComponent: renderListFooter
127
- }) : /*#__PURE__*/_jsx(EmptyState, {
197
+ }) : pinned.length > 0 ? null : /*#__PURE__*/_jsx(EmptyState, {
128
198
  isEnabled: isEnabled
129
199
  })]
130
200
  });
131
201
  }
132
202
  const styles = StyleSheet.create({
203
+ root: {
204
+ flex: 1,
205
+ minHeight: 0
206
+ },
207
+ /**
208
+ * The strip is separated from the live list by a real rule, not just space:
209
+ * these rows do NOT obey the filters, and the boundary is what tells the
210
+ * user why a request they just filtered out is still on screen.
211
+ */
212
+ pinnedSection: {
213
+ borderBottomWidth: 1,
214
+ borderBottomColor: macOSColors.border.default,
215
+ paddingBottom: 4
216
+ },
217
+ pinnedHeader: {
218
+ flexDirection: "row",
219
+ alignItems: "center",
220
+ gap: 6,
221
+ paddingHorizontal: 20,
222
+ paddingTop: 10,
223
+ paddingBottom: 6
224
+ },
225
+ pinnedTitle: {
226
+ flex: 1,
227
+ color: macOSColors.semantic.info,
228
+ fontSize: 10,
229
+ fontWeight: "700",
230
+ letterSpacing: 0.6,
231
+ fontFamily: "monospace"
232
+ },
233
+ pinnedClear: {
234
+ paddingHorizontal: 6,
235
+ paddingVertical: 2
236
+ },
237
+ pinnedClearText: {
238
+ color: macOSColors.text.muted,
239
+ fontSize: 10
240
+ },
133
241
  disabledBanner: {
134
242
  flexDirection: "row",
135
243
  alignItems: "center",
@@ -165,35 +273,5 @@ const styles = StyleSheet.create({
165
273
  color: macOSColors.text.muted,
166
274
  fontSize: 12,
167
275
  textAlign: "center"
168
- },
169
- limitBanner: {
170
- backgroundColor: "#1A1A1A",
171
- borderRadius: 10,
172
- borderWidth: 1,
173
- borderColor: "#20C997" + "40",
174
- padding: 12,
175
- marginHorizontal: 12,
176
- marginTop: 12,
177
- marginBottom: 34
178
- },
179
- limitBannerContent: {
180
- flexDirection: "row",
181
- alignItems: "center",
182
- justifyContent: "space-between",
183
- marginBottom: 4
184
- },
185
- limitBannerLeft: {
186
- flexDirection: "row",
187
- alignItems: "center",
188
- gap: 8
189
- },
190
- limitBannerText: {
191
- color: "#E0E0E0",
192
- fontSize: 13,
193
- fontWeight: "600"
194
- },
195
- limitBannerSubtext: {
196
- color: "#888888",
197
- fontSize: 11
198
276
  }
199
277
  });
@@ -1,16 +1,19 @@
1
1
  "use strict";
2
2
 
3
- import { useState, useRef, useEffect } from "react";
3
+ import { useState, useRef, useEffect, useCallback } from "react";
4
4
  import { View, StyleSheet, TouchableOpacity, TextInput } from "react-native";
5
5
  import { Search, Filter, 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, NetworkHeaderCopyButton } from "./NetworkHeaderLive";
10
+ import { NetworkHeaderBadges, NetworkClearButton, NetworkDetailPinActions, NetworkHeaderCopyButton, NetworkSavedButton } from "./NetworkHeaderLive";
11
11
  import { NetworkListScreen } from "./NetworkListScreen";
12
+ import { NetworkSavedScreen } from "./NetworkSavedScreen";
12
13
  import { useNetworkEventList } from "../hooks/useNetworkEventList";
13
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
+ import { useLiveNetworkEvent } from "../hooks/useLiveNetworkEvent";
15
+ import { FREE_TIER_SAVED_LIMIT, MAX_SAVED } from "../utils/networkSavedStore";
16
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
14
17
  // ============================================================================
15
18
  // Screens (rule4 stable router: each gates itself with `return null`; the
16
19
  // root renders all four as stable siblings instead of a nested ternary).
@@ -20,10 +23,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
23
 
21
24
  function NetworkDetailScreen({
22
25
  visible,
23
- event,
26
+ eventId,
27
+ paused,
24
28
  ignoredPatternValues,
25
29
  onTogglePattern
26
30
  }) {
31
+ // Resolve the ID to the live event so a request opened while pending picks
32
+ // up its response/error in place instead of freezing on the snapshot taken
33
+ // when the row was tapped.
34
+ const event = useLiveNetworkEvent(eventId, {
35
+ paused: paused || !visible
36
+ });
27
37
  if (!visible || !event) return null;
28
38
  return /*#__PURE__*/_jsx(NetworkEventDetailView, {
29
39
  event: event,
@@ -101,18 +111,31 @@ function NetworkModalInner({
101
111
  onClose,
102
112
  onBack,
103
113
  onMinimize,
104
- enableSharedModalDimensions = false
114
+ enableSharedModalDimensions = false,
115
+ inline = false,
116
+ paused: pausedProp,
117
+ onPausedChange
105
118
  }) {
106
119
  const {
107
120
  isPro
108
121
  } = useFeatureGate();
109
122
 
110
123
  // Capture pause (the power toggle's freeze-stream semantic) — gates
111
- // every subscriber in this modal's tree via props.
112
- const [paused, setPaused] = useState(false);
124
+ // every subscriber in this modal's tree via props. Controlled when the host
125
+ // supplies both halves; otherwise this local state is the source of truth.
126
+ const [pausedState, setPausedState] = useState(false);
127
+ const controlledPause = pausedProp != null && onPausedChange != null;
128
+ const paused = controlledPause ? pausedProp : pausedState;
129
+ const setPaused = next => {
130
+ const value = typeof next === "function" ? next(paused) : next;
131
+ if (controlledPause) onPausedChange(value);else setPausedState(value);
132
+ };
113
133
  const [filter, setFilter] = useState({});
114
- const [selectedEvent, setSelectedEvent] = useState(null);
134
+ // The SELECTION is UI state (an ID); the event object it points at is read
135
+ // live from the store by the detail screen — see useLiveNetworkEvent.
136
+ const [selectedEventId, setSelectedEventId] = useState(null);
115
137
  const [showFilterView, setShowFilterView] = useState(false);
138
+ const [showSavedView, setShowSavedView] = useState(false);
116
139
  const [activeTab, setActiveTab] = useState("filters");
117
140
  const [searchText, setSearchText] = useState("");
118
141
  const [isSearchActive, setIsSearchActive] = useState(false);
@@ -130,6 +153,13 @@ function NetworkModalInner({
130
153
  const [copySettings, setCopySettings] = useState(DEFAULT_COPY_SETTINGS);
131
154
  const hasLoadedCopySettings = useRef(false);
132
155
  const [showUpgradeModal, setShowUpgradeModal] = useState(false);
156
+ // Which ceiling the user hit — the upgrade sheet has to argue for the thing
157
+ // they were actually trying to do, not for exporting.
158
+ const [upgradeReason, setUpgradeReason] = useState("export");
159
+ const showUpgrade = reason => {
160
+ setUpgradeReason(reason);
161
+ setShowUpgradeModal(true);
162
+ };
133
163
 
134
164
  // Auto-close upgrade modal when user becomes Pro
135
165
  useEffect(() => {
@@ -173,8 +203,13 @@ function NetworkModalInner({
173
203
 
174
204
  // Simple handlers - no useCallback needed per rule2
175
205
  const handleBack = () => {
176
- setSelectedEvent(null);
206
+ setSelectedEventId(null);
177
207
  };
208
+
209
+ // Stable identity: this is passed down to the memoized list rows.
210
+ const handleEventPress = useCallback(event => {
211
+ setSelectedEventId(event.id);
212
+ }, []);
178
213
  const handleSearch = text => {
179
214
  setSearchText(text);
180
215
  setFilter(prev => ({
@@ -226,13 +261,32 @@ function NetworkModalInner({
226
261
  }
227
262
 
228
263
  // Event detail view header
229
- if (selectedEvent) {
264
+ if (selectedEventId) {
230
265
  return /*#__PURE__*/_jsxs(ModalHeader, {
231
266
  children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
232
267
  onBack: handleBack
233
268
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
234
269
  title: "Request Details",
235
270
  centered: true
271
+ }), /*#__PURE__*/_jsx(ModalHeader.Actions, {
272
+ children: /*#__PURE__*/_jsx(NetworkDetailPinActions, {
273
+ eventId: selectedEventId,
274
+ isPro: isPro,
275
+ onUpgradePress: () => showUpgrade("saved")
276
+ })
277
+ })]
278
+ });
279
+ }
280
+
281
+ // Saved (favorites) header. Checked AFTER the detail branch so opening a
282
+ // saved request still gets the detail header — and its pin/save toggles.
283
+ if (showSavedView) {
284
+ return /*#__PURE__*/_jsxs(ModalHeader, {
285
+ children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
286
+ onBack: () => setShowSavedView(false)
287
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
288
+ title: "Saved Requests",
289
+ centered: true
236
290
  })]
237
291
  });
238
292
  }
@@ -301,13 +355,15 @@ function NetworkModalInner({
301
355
  size: 14,
302
356
  color: filter.status || filter.method || filter.contentType ? macOSColors.semantic.info : macOSColors.text.muted
303
357
  })
358
+ }), /*#__PURE__*/_jsx(NetworkSavedButton, {
359
+ onPress: () => setShowSavedView(true)
304
360
  }), /*#__PURE__*/_jsx(NetworkHeaderCopyButton, {
305
361
  isPro: isPro,
306
362
  filter: filter,
307
363
  ignoredPatterns: ignoredPatterns,
308
364
  copySettings: copySettings,
309
365
  paused: paused,
310
- onUpgradePress: () => setShowUpgradeModal(true)
366
+ onUpgradePress: () => showUpgrade("export")
311
367
  }), /*#__PURE__*/_jsx(PowerToggleButton, {
312
368
  isEnabled: !paused,
313
369
  onToggle: () => setPaused(prev => !prev),
@@ -324,32 +380,20 @@ function NetworkModalInner({
324
380
  // The LIST screen owns its scrolling (FlatList virtualization requires
325
381
  // it — inside JsModal's ScrollView wrapper every row mounts, which at
326
382
  // the 500-event cap measured 17 JS FPS / 133% CPU / 1.4GB; see
327
- // NetworkListScreen). Detail/filter/copy still use the wrapper.
328
- const listOwnsScroll = selectedEvent == null && !showFilterView;
329
- return /*#__PURE__*/_jsxs(JsModal, {
330
- visible: visible,
331
- onClose: onClose,
332
- onMinimize: onMinimize,
333
- persistenceKey: persistenceKey,
334
- header: {
335
- showToggleButton: true,
336
- customContent: renderHeaderContent()
337
- },
338
- onModeChange: handleModeChange,
339
- enablePersistence: true,
340
- initialMode: "bottomSheet",
341
- enableGlitchEffects: true,
342
- disableScrollWrapper: listOwnsScroll,
343
- styles: {},
383
+ // NetworkListScreen). The Saved screen is a FlatList too, so it takes the
384
+ // same deal. Detail/filter/copy still use the wrapper.
385
+ const listOwnsScroll = selectedEventId == null && !showFilterView;
386
+ const body = /*#__PURE__*/_jsxs(_Fragment, {
344
387
  children: [/*#__PURE__*/_jsxs(View, {
345
388
  style: styles.container,
346
389
  children: [/*#__PURE__*/_jsx(NetworkDetailScreen, {
347
- visible: selectedEvent != null,
348
- event: selectedEvent,
390
+ visible: selectedEventId != null,
391
+ eventId: selectedEventId,
392
+ paused: paused,
349
393
  ignoredPatternValues: ignoredPatternValues,
350
394
  onTogglePattern: toggleIgnoredPattern
351
395
  }), /*#__PURE__*/_jsx(NetworkFilterScreen, {
352
- visible: showFilterView && activeTab === "filters" && !selectedEvent,
396
+ visible: showFilterView && activeTab === "filters" && !selectedEventId,
353
397
  paused: paused,
354
398
  isPro: isPro,
355
399
  filter: filter,
@@ -359,37 +403,86 @@ function NetworkModalInner({
359
403
  onAddPattern: addIgnoredPattern,
360
404
  onTogglePatternMode: toggleIgnoredPatternMode
361
405
  }), /*#__PURE__*/_jsx(NetworkCopyScreen, {
362
- visible: showFilterView && activeTab === "copy" && !selectedEvent,
406
+ visible: showFilterView && activeTab === "copy" && !selectedEventId,
363
407
  paused: paused,
364
408
  isPro: isPro,
365
409
  filter: filter,
366
410
  ignoredPatterns: ignoredPatterns,
367
411
  settings: copySettings,
368
412
  onSettingsChange: setCopySettings
413
+ }), /*#__PURE__*/_jsx(NetworkSavedScreen, {
414
+ visible: showSavedView && selectedEventId == null && !showFilterView,
415
+ isPro: isPro,
416
+ copySettings: copySettings,
417
+ onEventPress: handleEventPress,
418
+ onUpgradePress: () => showUpgrade("saved")
369
419
  }), /*#__PURE__*/_jsx(NetworkListScreen, {
370
- visible: selectedEvent == null && !showFilterView,
420
+ visible: selectedEventId == null && !showFilterView && !showSavedView,
371
421
  paused: paused,
372
422
  filter: filter,
373
423
  ignoredPatterns: ignoredPatterns,
374
424
  isPro: isPro,
375
425
  searchText: searchText,
376
- onEventPress: setSelectedEvent,
377
- onUpgradePress: () => setShowUpgradeModal(true)
426
+ onEventPress: handleEventPress,
427
+ onUpgradePress: () => showUpgrade("export")
378
428
  })]
379
429
  }), /*#__PURE__*/_jsx(UpgradeModal, {
380
430
  visible: showUpgradeModal,
381
431
  onClose: () => setShowUpgradeModal(false),
382
- featureName: "Network Export",
383
- description: "Export network requests to share with your team, debug APIs, or feed into AI tools.",
384
- benefits: ["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"]
432
+ featureName: upgradeReason === "saved" ? "Saved Requests" : "Network Export",
433
+ 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.",
434
+ 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"]
385
435
  })]
386
436
  });
437
+
438
+ // The ONLY thing that differs between the phone and the desktop dashboard is
439
+ // which shell the header and body go in. Everything above this line is shared.
440
+ if (inline) {
441
+ return /*#__PURE__*/_jsxs(View, {
442
+ style: styles.inlineRoot,
443
+ children: [/*#__PURE__*/_jsx(View, {
444
+ style: styles.inlineHeader,
445
+ children: renderHeaderContent()
446
+ }), body]
447
+ });
448
+ }
449
+ return /*#__PURE__*/_jsx(JsModal, {
450
+ visible: visible,
451
+ onClose: onClose,
452
+ onMinimize: onMinimize,
453
+ persistenceKey: persistenceKey,
454
+ header: {
455
+ showToggleButton: true,
456
+ customContent: renderHeaderContent()
457
+ },
458
+ onModeChange: handleModeChange,
459
+ enablePersistence: true,
460
+ initialMode: "bottomSheet",
461
+ enableGlitchEffects: true,
462
+ disableScrollWrapper: listOwnsScroll,
463
+ styles: {},
464
+ children: body
465
+ });
387
466
  }
388
467
  const styles = StyleSheet.create({
389
468
  container: {
390
469
  flex: 1,
391
470
  backgroundColor: macOSColors.background.base
392
471
  },
472
+ /** Inline shell — fills the host panel; `minHeight: 0` so the list's own
473
+ * FlatList can scroll instead of stretching the column. */
474
+ inlineRoot: {
475
+ flex: 1,
476
+ minHeight: 0,
477
+ backgroundColor: macOSColors.background.base
478
+ },
479
+ inlineHeader: {
480
+ paddingHorizontal: 12,
481
+ paddingVertical: 8,
482
+ borderBottomWidth: 1,
483
+ borderBottomColor: macOSColors.border.default,
484
+ backgroundColor: macOSColors.background.card
485
+ },
393
486
  headerSearchContainer: {
394
487
  flexDirection: "row",
395
488
  alignItems: "center",
@@ -437,4 +530,28 @@ export function NetworkModal(props) {
437
530
  ...props
438
531
  })
439
532
  });
440
- }
533
+ }
534
+ /**
535
+ * The same network tool as `NetworkModal`, rendered INLINE — header bar, then
536
+ * body — for hosts that place tools in a panel rather than a modal. Buoy Desktop
537
+ * is the only one today.
538
+ *
539
+ * This exists so there is exactly ONE network tool. The desktop dashboard used
540
+ * to ship its own 1,258-line copy of the modal's internals, which is how the two
541
+ * surfaces drifted apart in the first place; anything added here now shows up on
542
+ * both without a second edit.
543
+ */
544
+ export function NetworkToolView(props = {}) {
545
+ return /*#__PURE__*/_jsx(TickProvider, {
546
+ children: /*#__PURE__*/_jsx(NetworkModalInner, {
547
+ ...props,
548
+ inline: true,
549
+ visible: true
550
+ // Inline has no chrome to drive: there is no close button, no minimize
551
+ // and no outer back — the host panel owns all of that.
552
+ ,
553
+ onClose: noop
554
+ })
555
+ });
556
+ }
557
+ const noop = () => {};