@buoy-gg/network 7.0.32 → 7.0.34

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 (36) hide show
  1. package/lib/commonjs/index.js +6 -0
  2. package/lib/commonjs/network/components/NetworkCopySettingsView.js +10 -121
  3. package/lib/commonjs/network/components/NetworkHeaderLive.js +16 -0
  4. package/lib/commonjs/network/components/NetworkListScreen.js +34 -46
  5. package/lib/commonjs/network/components/NetworkModal.js +24 -53
  6. package/lib/commonjs/network/components/NetworkOverridesScreen.js +11 -2
  7. package/lib/commonjs/network/components/NetworkSavedScreen.js +19 -38
  8. package/lib/commonjs/network/hooks/useNetworkEventList.js +7 -4
  9. package/lib/commonjs/network/sync/networkSyncAdapter.js +8 -0
  10. package/lib/commonjs/preset.js +6 -21
  11. package/lib/module/index.js +1 -1
  12. package/lib/module/network/components/NetworkCopySettingsView.js +13 -124
  13. package/lib/module/network/components/NetworkHeaderLive.js +15 -0
  14. package/lib/module/network/components/NetworkListScreen.js +35 -47
  15. package/lib/module/network/components/NetworkModal.js +26 -55
  16. package/lib/module/network/components/NetworkOverridesScreen.js +12 -3
  17. package/lib/module/network/components/NetworkSavedScreen.js +20 -39
  18. package/lib/module/network/hooks/useNetworkEventList.js +7 -4
  19. package/lib/module/network/sync/networkSyncAdapter.js +8 -0
  20. package/lib/module/preset.js +6 -20
  21. package/lib/typescript/index.d.ts +1 -1
  22. package/lib/typescript/index.d.ts.map +1 -1
  23. package/lib/typescript/network/components/NetworkCopySettingsView.d.ts.map +1 -1
  24. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +11 -0
  25. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  26. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  27. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  28. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts.map +1 -1
  29. package/lib/typescript/network/components/NetworkSavedScreen.d.ts.map +1 -1
  30. package/lib/typescript/network/hooks/useNetworkEventList.d.ts +11 -0
  31. package/lib/typescript/network/hooks/useNetworkEventList.d.ts.map +1 -1
  32. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +4 -0
  33. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  34. package/lib/typescript/preset.d.ts +2 -8
  35. package/lib/typescript/preset.d.ts.map +1 -1
  36. package/package.json +2 -2
@@ -1,11 +1,11 @@
1
1
  "use strict";
2
2
 
3
- import { DynamicFilterView, macOSColors, FileText, FileCode, Link, Zap, Settings, Hash, Eye, ChevronDown, ChevronUp, AlertTriangle, ToolbarCopyButton, formatRelativeTime, useFeatureGate, ProFeatureBanner } from "@buoy-gg/shared-ui";
3
+ import { DynamicFilterView, macOSColors, FileText, FileCode, Link, Zap, Settings, Hash, Eye, ToolbarCopyButton, formatRelativeTime, useFeatureGate, ProFeatureBanner, ExportPreview } from "@buoy-gg/shared-ui";
4
4
  import { DataViewer } from "@buoy-gg/shared-ui/dataViewer";
5
5
  import { useCallback, useMemo, useState } from "react";
6
- import { Text, StyleSheet, ScrollView, View, TouchableOpacity } from "react-native";
6
+ import { Text, StyleSheet, ScrollView, View } from "react-native";
7
7
  import { useTickEveryMinute } from "../hooks/useTickEveryMinute";
8
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
8
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  export const DEFAULT_COPY_SETTINGS = {
10
10
  includeMethod: true,
11
11
  includeStatus: true,
@@ -541,62 +541,16 @@ export function NetworkCopySettingsView({
541
541
  const generatePreviewContent = useCallback(() => {
542
542
  const dataSource = hasLiveData ? events : [mockRequestData];
543
543
  const requestCount = dataSource.length;
544
-
545
- // Collapsed state - show summary with expand button
546
- if (!isPreviewExpanded) {
547
- const warningColor = sizeWarningLevel === 'danger' ? macOSColors.semantic.error : sizeWarningLevel === 'warning' ? macOSColors.semantic.warning : macOSColors.text.secondary;
548
- return /*#__PURE__*/_jsxs(View, {
549
- style: styles.collapsedPreview,
550
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
551
- style: styles.expandButton,
552
- onPress: () => setIsPreviewExpanded(true),
553
- children: /*#__PURE__*/_jsxs(View, {
554
- style: styles.expandButtonContent,
555
- children: [/*#__PURE__*/_jsx(Eye, {
556
- size: 16,
557
- color: macOSColors.semantic.info
558
- }), /*#__PURE__*/_jsxs(View, {
559
- style: styles.expandButtonTextContainer,
560
- children: [/*#__PURE__*/_jsx(Text, {
561
- style: styles.expandButtonTitle,
562
- children: "Show Preview"
563
- }), /*#__PURE__*/_jsxs(Text, {
564
- style: styles.expandButtonSubtitle,
565
- children: [requestCount, " request", requestCount !== 1 ? 's' : '', " \u2022 ~", formatBytes(estimatedSize)]
566
- })]
567
- }), /*#__PURE__*/_jsx(ChevronDown, {
568
- size: 16,
569
- color: macOSColors.text.muted
570
- })]
571
- })
572
- }), sizeWarningLevel !== 'none' && /*#__PURE__*/_jsxs(View, {
573
- style: [styles.sizeWarning, sizeWarningLevel === 'danger' && styles.sizeWarningDanger],
574
- children: [/*#__PURE__*/_jsx(AlertTriangle, {
575
- size: 12,
576
- color: warningColor
577
- }), /*#__PURE__*/_jsx(Text, {
578
- style: [styles.sizeWarningText, {
579
- color: warningColor
580
- }],
581
- children: sizeWarningLevel === 'danger' ? 'Very large payload - may cause performance issues' : 'Large payload - preview may be slow'
582
- })]
583
- })]
584
- });
585
- }
586
-
587
- // Expanded state - show collapse button + actual content
588
- return /*#__PURE__*/_jsxs(View, {
589
- children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
590
- style: styles.collapseButton,
591
- onPress: () => setIsPreviewExpanded(false),
592
- children: [/*#__PURE__*/_jsx(Text, {
593
- style: styles.collapseButtonText,
594
- children: "Hide Preview"
595
- }), /*#__PURE__*/_jsx(ChevronUp, {
596
- size: 14,
597
- color: macOSColors.text.secondary
598
- })]
599
- }), generateExpandedPreviewContent()]
544
+ return /*#__PURE__*/_jsx(ExportPreview, {
545
+ expanded: isPreviewExpanded,
546
+ onExpandedChange: setIsPreviewExpanded,
547
+ summary: `${requestCount} request${requestCount !== 1 ? "s" : ""} • ~${formatBytes(estimatedSize)}`,
548
+ warningLevel: sizeWarningLevel,
549
+ warningText: {
550
+ warning: "Large payload - preview may be slow",
551
+ danger: "Very large payload - may cause performance issues"
552
+ },
553
+ renderBody: generateExpandedPreviewContent
600
554
  });
601
555
  }, [settings, hasLiveData, events, mockRequestData, isPreviewExpanded, estimatedSize, sizeWarningLevel, generateExpandedPreviewContent]);
602
556
 
@@ -785,71 +739,6 @@ const styles = StyleSheet.create({
785
739
  marginRight: 8
786
740
  },
787
741
  // Collapsible preview styles
788
- collapsedPreview: {
789
- gap: 8
790
- },
791
- expandButton: {
792
- backgroundColor: macOSColors.background.input,
793
- borderRadius: 8,
794
- borderWidth: 1,
795
- borderColor: macOSColors.border.default,
796
- padding: 12
797
- },
798
- expandButtonContent: {
799
- flexDirection: 'row',
800
- alignItems: 'center',
801
- gap: 10
802
- },
803
- expandButtonTextContainer: {
804
- flex: 1
805
- },
806
- expandButtonTitle: {
807
- fontSize: 13,
808
- fontWeight: '600',
809
- color: macOSColors.text.primary
810
- },
811
- expandButtonSubtitle: {
812
- fontSize: 11,
813
- color: macOSColors.text.secondary,
814
- marginTop: 2
815
- },
816
- collapseButton: {
817
- flexDirection: 'row',
818
- alignItems: 'center',
819
- justifyContent: 'center',
820
- gap: 6,
821
- paddingVertical: 8,
822
- marginBottom: 8,
823
- backgroundColor: macOSColors.background.hover,
824
- borderRadius: 6,
825
- borderWidth: 1,
826
- borderColor: macOSColors.border.default
827
- },
828
- collapseButtonText: {
829
- fontSize: 12,
830
- fontWeight: '500',
831
- color: macOSColors.text.secondary
832
- },
833
- sizeWarning: {
834
- flexDirection: 'row',
835
- alignItems: 'center',
836
- gap: 6,
837
- paddingHorizontal: 10,
838
- paddingVertical: 6,
839
- backgroundColor: macOSColors.semantic.warningBackground,
840
- borderRadius: 6,
841
- borderWidth: 1,
842
- borderColor: macOSColors.semantic.warning + '30'
843
- },
844
- sizeWarningDanger: {
845
- backgroundColor: macOSColors.semantic.errorBackground,
846
- borderColor: macOSColors.semantic.error + '30'
847
- },
848
- sizeWarningText: {
849
- fontSize: 11,
850
- fontWeight: '500',
851
- flex: 1
852
- },
853
742
  // Gated container styles for free users
854
743
  gatedContainer: {
855
744
  flex: 1,
@@ -40,6 +40,21 @@ export function NetworkHeaderBadges({
40
40
  ignoredPatterns,
41
41
  paused
42
42
  });
43
+ return /*#__PURE__*/_jsx(NetworkHeaderBadgesView, {
44
+ counts: counts,
45
+ activeStatus: activeStatus,
46
+ onToggleStatus: onToggleStatus
47
+ });
48
+ }
49
+ /**
50
+ * The chips themselves, fed by props — what the parity sheet renders with
51
+ * fixture counts (`NetworkHeaderBadges` reads the live store).
52
+ */
53
+ export function NetworkHeaderBadgesView({
54
+ counts,
55
+ activeStatus,
56
+ onToggleStatus
57
+ }) {
43
58
  return /*#__PURE__*/_jsxs(View, {
44
59
  style: styles.headerChipRow,
45
60
  children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
@@ -14,7 +14,7 @@
14
14
 
15
15
  import { useCallback } from "react";
16
16
  import { FlatList, StyleSheet, Text, TouchableOpacity, View } from "react-native";
17
- import { FlaskConical, Globe, Pin, Power, macOSColors } from "@buoy-gg/shared-ui";
17
+ import { FlaskConical, Pin, Power, macOSColors, ToolEmptyState, resolveListEmptyState, EVENT_LIST_PRESET } from "@buoy-gg/shared-ui";
18
18
  import { NetworkEventItemCompact } from "./NetworkEventItemCompact";
19
19
  import { NetworkUpgradeBanner } from "./NetworkUpgradeBanner";
20
20
  import { useNetworkEventList } from "../hooks/useNetworkEventList";
@@ -23,23 +23,6 @@ import { useOverrideRules } from "../hooks/useOverrideRules";
23
23
  import { ruleMatches } from "../overrides/matchRule";
24
24
  import { splitPinned } from "../utils/withPinnedFirst";
25
25
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
26
- function EmptyState({
27
- isEnabled
28
- }) {
29
- return /*#__PURE__*/_jsxs(View, {
30
- style: styles.emptyState,
31
- children: [/*#__PURE__*/_jsx(Globe, {
32
- size: 32,
33
- color: macOSColors.text.muted
34
- }), /*#__PURE__*/_jsx(Text, {
35
- style: styles.emptyTitle,
36
- children: "No network events"
37
- }), /*#__PURE__*/_jsx(Text, {
38
- style: styles.emptyText,
39
- children: isEnabled ? "Network requests will appear here" : "Enable interception to start capturing"
40
- })]
41
- });
42
- }
43
26
  const keyExtractor = item => item.id;
44
27
 
45
28
  /**
@@ -160,7 +143,8 @@ export function NetworkListScreen({
160
143
  const {
161
144
  visibleEvents,
162
145
  hasLockedEvents,
163
- lockedEventCount
146
+ lockedEventCount,
147
+ totalEvents
164
148
  } = useNetworkEventList({
165
149
  filter,
166
150
  ignoredPatterns,
@@ -222,6 +206,34 @@ export function NetworkListScreen({
222
206
  onLongPress: handleLongPress,
223
207
  saved: savedLiveIds.has(item.id)
224
208
  });
209
+
210
+ // Which flavour of "nothing here" this is. Pins and overrides render
211
+ // outside the list, so a screen showing only those is not empty at all.
212
+ const renderEmpty = () => {
213
+ // Pins and overrides render outside the list, so a screen showing only
214
+ // those is not empty at all.
215
+ if (pinnedRows.length > 0 || overridden.length > 0) return null;
216
+ return /*#__PURE__*/_jsx(ToolEmptyState, {
217
+ testID: "buoy-network-empty",
218
+ ...resolveListEmptyState({
219
+ subject: "requests",
220
+ search: searchText,
221
+ total: totalEvents,
222
+ capturing: isEnabled,
223
+ waiting: {
224
+ // fishing = capture is on and nothing has arrived.
225
+ otto: "fishing",
226
+ title: "No network events",
227
+ message: "Network requests will appear here"
228
+ },
229
+ paused: {
230
+ // paused = capture off. Both are literally what the copy says.
231
+ title: "No network events",
232
+ message: "Enable interception to start capturing"
233
+ }
234
+ })
235
+ });
236
+ };
225
237
  const renderListFooter = () => {
226
238
  if (!hasLockedEvents) return null;
227
239
  const isSearching = searchText.length > 0;
@@ -266,10 +278,8 @@ export function NetworkListScreen({
266
278
  showsVerticalScrollIndicator: true
267
279
  // Clipped cells falsely fail Detox's hittability check (known
268
280
  // RN+Detox issue), so e2e bundles (Metro started with
269
- // EXPO_PUBLIC_E2E=1) turn clipping off. Inlined at bundle time;
270
- // guarded so a non-inlining bundler keeps the prod behavior.
281
+ // EXPO_PUBLIC_E2E=1) turn clipping off — see EVENT_LIST_PRESET.
271
282
  ,
272
- removeClippedSubviews: !(typeof process !== "undefined" && process.env?.EXPO_PUBLIC_E2E),
273
283
  onEndReachedThreshold: 0.8
274
284
  // The list OWNS scrolling (NetworkModal passes
275
285
  // disableScrollWrapper while this screen is visible) so FlatList
@@ -278,24 +288,18 @@ export function NetworkListScreen({
278
288
  // rows: at the store's 500-event cap that measured 17 JS FPS /
279
289
  // 133% CPU / 1.4GB vs ~53 JS FPS / 52% / 360MB virtualized
280
290
  // (buoy-optimize batch 2026-07-06, batch-1783396983529-6l7w).
281
- // Window/batch values are the batch's "tuned" winner.
291
+ // Window/batch values are the batch's "tuned" winner (EVENT_LIST_PRESET).
282
292
  ,
283
293
  scrollEnabled: true,
284
- initialNumToRender: 12,
285
- maxToRenderPerBatch: 8,
286
- windowSize: 5,
287
- updateCellsBatchingPeriod: 50
294
+ ...EVENT_LIST_PRESET,
288
295
  // Saving a request from the detail view changes a row's glyph
289
296
  // without changing `data`. VirtualizedList is a PureComponent, so
290
297
  // the Set has to be declared as a re-render marker rather than
291
298
  // relying on `renderItem` getting a fresh identity.
292
- ,
293
299
  extraData: savedLiveIds,
294
300
  "sentry-label": "ignore network events list",
295
301
  ListFooterComponent: renderListFooter
296
- }) : pinnedRows.length > 0 || overridden.length > 0 ? null : /*#__PURE__*/_jsx(EmptyState, {
297
- isEnabled: isEnabled
298
- })]
302
+ }) : renderEmpty()]
299
303
  });
300
304
  }
301
305
  const styles = StyleSheet.create({
@@ -392,21 +396,5 @@ const styles = StyleSheet.create({
392
396
  },
393
397
  listContent: {
394
398
  paddingTop: 8
395
- },
396
- emptyState: {
397
- alignItems: "center",
398
- paddingVertical: 40
399
- },
400
- emptyTitle: {
401
- color: macOSColors.text.primary,
402
- fontSize: 14,
403
- fontWeight: "600",
404
- marginTop: 12,
405
- marginBottom: 6
406
- },
407
- emptyText: {
408
- color: macOSColors.text.muted,
409
- fontSize: 12,
410
- textAlign: "center"
411
399
  }
412
400
  });
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  import { useState, useRef, useEffect, useCallback } from "react";
4
- import { View, StyleSheet, TouchableOpacity, TextInput } from "react-native";
5
- import { Search, Filter, Bookmark, X, JsModal, ModalHeader, devToolsStorageKeys, macOSColors, night, persistentStorage, NightSegmented, useFeatureGate, UpgradeModal, PowerToggleButton, useIgnoredPatterns, windowDragProps } from "@buoy-gg/shared-ui";
4
+ import { View, StyleSheet, TouchableOpacity } from "react-native";
5
+ import { Search, Filter, Bookmark, JsModal, ModalHeader, devToolsStorageKeys, macOSColors, night, persistentStorage, NightSegmented, useFeatureGate, UpgradeModal, PowerToggleButton, useIgnoredPatterns, windowDragProps } from "@buoy-gg/shared-ui";
6
6
  import { NetworkFilterViewV3 } from "./NetworkFilterViewV3";
7
7
  import { TickProvider } from "../hooks/useTickEveryMinute";
8
8
  import { NetworkEventDetailView } from "./NetworkEventDetailView";
@@ -230,6 +230,12 @@ function NetworkModalInner({
230
230
  const [activeTab, setActiveTab] = useState("filters");
231
231
  const [searchText, setSearchText] = useState("");
232
232
  const [isSearchActive, setIsSearchActive] = useState(false);
233
+ // True while the header's clear ✕ is being pressed. With a mouse, pressing
234
+ // the ✕ blurs the input FIRST, and the blur handler below closes search —
235
+ // which unmounts the ✕ before its own press can fire, so the query was
236
+ // never cleared (desktop). Press-in lands before the blur, so the blur
237
+ // handler can tell this dismissal apart from a click elsewhere.
238
+ const clearPressRef = useRef(false);
233
239
  // The Saved screen's search box lives HERE, not in that screen, because the
234
240
  // detail footer steps through the same list the user was just looking at —
235
241
  // and the Saved screen is unmounted while a detail is open.
@@ -441,38 +447,24 @@ function NetworkModalInner({
441
447
  onBack: onBack
442
448
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
443
449
  title: "",
444
- children: isSearchActive ? /*#__PURE__*/_jsxs(View, {
445
- style: styles.headerSearchContainer,
446
- children: [/*#__PURE__*/_jsx(Search, {
447
- size: 14,
448
- color: night.color.textSecondary
449
- }), /*#__PURE__*/_jsx(TextInput, {
450
- ref: searchInputRef,
451
- style: styles.headerSearchInput,
452
- placeholder: "Search URL, method, operation, error...",
453
- placeholderTextColor: night.color.placeholder,
454
- value: searchText,
455
- onChangeText: handleSearch,
456
- onSubmitEditing: () => setIsSearchActive(false),
457
- onBlur: () => setIsSearchActive(false),
458
- "sentry-label": "ignore network search header",
459
- accessibilityLabel: "Search network requests",
460
- testID: "buoy-network-search-input",
461
- autoCapitalize: "none",
462
- autoCorrect: false,
463
- returnKeyType: "search"
464
- }), searchText.length > 0 ? /*#__PURE__*/_jsx(TouchableOpacity, {
465
- onPress: () => {
466
- handleSearch("");
467
- setIsSearchActive(false);
468
- },
469
- "sentry-label": "ignore clear search header",
470
- style: styles.headerSearchClear,
471
- children: /*#__PURE__*/_jsx(X, {
472
- size: 14,
473
- color: night.color.textSecondary
474
- })
475
- }) : null]
450
+ children: isSearchActive ? /*#__PURE__*/_jsx(ModalHeader.Search, {
451
+ value: searchText,
452
+ onChangeText: handleSearch,
453
+ onCancel: () => {
454
+ handleSearch("");
455
+ setIsSearchActive(false);
456
+ },
457
+ onDismiss: () => setIsSearchActive(false),
458
+ dismissOnBlur: true,
459
+ clearOnlyWhenText: true,
460
+ placeholder: "Search URL, method, operation, error...",
461
+ accessibilityLabel: "Search network requests",
462
+ iconSize: 14,
463
+ testID: "buoy-network-search-input",
464
+ clearTestID: "buoy-network-search-clear",
465
+ inputProps: {
466
+ "sentry-label": "ignore network search header"
467
+ }
476
468
  }) : /*#__PURE__*/_jsx(NetworkHeaderBadges, {
477
469
  activeStatus: filter.status,
478
470
  onToggleStatus: toggleStatusFilter,
@@ -717,27 +709,6 @@ const styles = StyleSheet.create({
717
709
  },
718
710
  // Night chrome — the header is glass over the star field, so the search
719
711
  // field and action chips sit on the Everlights dark-glass fill.
720
- headerSearchContainer: {
721
- flexDirection: "row",
722
- alignItems: "center",
723
- backgroundColor: night.color.surfaceElevated,
724
- borderRadius: night.radius.row,
725
- borderWidth: 1,
726
- borderColor: night.color.border,
727
- paddingHorizontal: 12,
728
- paddingVertical: 5
729
- },
730
- headerSearchInput: {
731
- flex: 1,
732
- color: night.color.text,
733
- fontSize: 13,
734
- marginLeft: 6,
735
- paddingVertical: 2
736
- },
737
- headerSearchClear: {
738
- marginLeft: 6,
739
- padding: 4
740
- },
741
712
  menuCopyRow: {
742
713
  paddingHorizontal: 6
743
714
  },
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useCallback, useEffect, useState } from "react";
4
4
  import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
5
- import { AlertCircle, AlertTriangle, DevToolsCard, EmptyState, FlaskConical, MethodBadge, Plus, Trash2, macOSColors, useLimit } from "@buoy-gg/shared-ui";
5
+ import { AlertCircle, AlertTriangle, DevToolsCard, EmptyState, MethodBadge, Plus, Trash2, macOSColors, useLimit } from "@buoy-gg/shared-ui";
6
6
  import { useOverrideRules } from "../hooks/useOverrideRules";
7
7
  import { NetworkOverrideEditor } from "./NetworkOverrideEditor";
8
8
  import { overrideRulesStore } from "../overrides/overrideRulesStore";
@@ -361,8 +361,17 @@ function OverridesBody({
361
361
  })]
362
362
  }), !hasRules ? /*#__PURE__*/_jsx(View, {
363
363
  style: styles.emptyWrap,
364
- children: /*#__PURE__*/_jsx(EmptyState, {
365
- icon: FlaskConical,
364
+ children: /*#__PURE__*/_jsx(EmptyState
365
+ // Otto instead of the flask: nothing is faked yet, so he peers
366
+ // into an empty box. Truthful by construction — this branch only
367
+ // renders when the rule list is genuinely empty, and the master
368
+ // switch bar above is hidden in exactly that case.
369
+ , {
370
+ otto: "empty"
371
+ // 32 → pixelSize 2, the size the saved/list screens use, so the
372
+ // three network empties read as one family.
373
+ ,
374
+ iconSize: 32,
366
375
  title: "Nothing overridden"
367
376
  // Names the control that actually exists. This said "tap Force 500"
368
377
  // — a button from the deleted preset list, which sent anyone
@@ -19,7 +19,7 @@
19
19
 
20
20
  import { useState } from "react";
21
21
  import { FlatList, StyleSheet, Text, TextInput, View, TouchableOpacity } from "react-native";
22
- import { Bookmark, CopyButton, Search, macOSColors, useLimit } from "@buoy-gg/shared-ui";
22
+ import { Bookmark, CopyButton, Search, macOSColors, useLimit, ToolEmptyState, resolveListEmptyState, EVENT_LIST_PRESET } from "@buoy-gg/shared-ui";
23
23
  import { NetworkEventItemCompact } from "./NetworkEventItemCompact";
24
24
  import { NetworkUpgradeBanner } from "./NetworkUpgradeBanner";
25
25
  import { generateNetworkCopyText } from "../utils/generateNetworkCopyText";
@@ -28,21 +28,6 @@ import { useNetworkSaved } from "../hooks/useNetworkSaved";
28
28
  import { MAX_SAVED } from "../utils/networkSavedStore";
29
29
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
30
30
  const keyExtractor = item => item.id;
31
- function EmptyState() {
32
- return /*#__PURE__*/_jsxs(View, {
33
- style: styles.emptyState,
34
- children: [/*#__PURE__*/_jsx(Bookmark, {
35
- size: 32,
36
- color: macOSColors.text.muted
37
- }), /*#__PURE__*/_jsx(Text, {
38
- style: styles.emptyTitle,
39
- children: "No saved requests"
40
- }), /*#__PURE__*/_jsx(Text, {
41
- style: styles.emptyText,
42
- children: "Open a request and tap the bookmark to keep it here \u2014 saved requests survive Clear and app restarts."
43
- })]
44
- });
45
- }
46
31
  export function NetworkSavedScreen({
47
32
  visible,
48
33
  isPro,
@@ -142,17 +127,31 @@ export function NetworkSavedScreen({
142
127
  autoCorrect: false,
143
128
  returnKeyType: "search"
144
129
  })]
145
- }) : null, savedRecords.length === 0 ? /*#__PURE__*/_jsx(EmptyState, {}) : /*#__PURE__*/_jsx(FlatList, {
130
+ }) : null, savedRecords.length === 0 ||
131
+ // lockedCount > 0 falls through to the list so the upgrade banner (its
132
+ // ListFooterComponent) still renders — "0 shown, N locked" is an
133
+ // answer, not an empty screen.
134
+ search.trim().length > 0 && filtered.length === 0 && lockedCount === 0 ? /*#__PURE__*/_jsx(ToolEmptyState, {
135
+ testID: "buoy-network-saved-empty",
136
+ ...resolveListEmptyState({
137
+ subject: "saved requests",
138
+ verb: "kept",
139
+ search: savedRecords.length === 0 ? "" : search,
140
+ total: savedRecords.length,
141
+ waiting: {
142
+ otto: "empty",
143
+ title: "No saved requests",
144
+ message: "Open a request and tap the bookmark to keep it here — saved requests survive Clear and app restarts."
145
+ }
146
+ })
147
+ }) : /*#__PURE__*/_jsx(FlatList, {
146
148
  data: filtered,
147
149
  renderItem: renderItem,
148
150
  keyExtractor: keyExtractor,
149
151
  contentContainerStyle: styles.listContent,
150
152
  showsVerticalScrollIndicator: true,
151
- removeClippedSubviews: true,
152
153
  scrollEnabled: true,
153
- initialNumToRender: 12,
154
- maxToRenderPerBatch: 8,
155
- windowSize: 5,
154
+ ...EVENT_LIST_PRESET,
156
155
  extraData: pinnedLiveIds,
157
156
  "sentry-label": "ignore saved requests list",
158
157
  ListFooterComponent: renderListFooter
@@ -219,23 +218,5 @@ const styles = StyleSheet.create({
219
218
  },
220
219
  listContent: {
221
220
  paddingTop: 4
222
- },
223
- emptyState: {
224
- alignItems: "center",
225
- paddingVertical: 40,
226
- paddingHorizontal: 24
227
- },
228
- emptyTitle: {
229
- color: macOSColors.text.primary,
230
- fontSize: 14,
231
- fontWeight: "600",
232
- marginTop: 12,
233
- marginBottom: 6
234
- },
235
- emptyText: {
236
- color: macOSColors.text.muted,
237
- fontSize: 12,
238
- textAlign: "center",
239
- lineHeight: 17
240
221
  }
241
222
  });
@@ -74,9 +74,11 @@ export function useNetworkEventList(options) {
74
74
 
75
75
  // Derivations are plain useMemo over the subscribed array — linear
76
76
  // passes over ≤maxEvents items, recomputed only when inputs change.
77
- const filteredEvents = useMemo(() => {
78
- return filterNetworkEvents(applyIgnoredPatterns(events, ignoredPatterns), filter);
79
- }, [events, ignoredPatterns, filter]);
77
+ // Split in two so the pre-filter count is observable: `totalEvents` below
78
+ // is the denominator the empty state needs, and recomputing it separately
79
+ // would mean a second pass over the same array.
80
+ const candidateEvents = useMemo(() => applyIgnoredPatterns(events, ignoredPatterns), [events, ignoredPatterns]);
81
+ const filteredEvents = useMemo(() => filterNetworkEvents(candidateEvents, filter), [candidateEvents, filter]);
80
82
 
81
83
  // Free tier: events beyond the limit in the ORIGINAL order are locked,
82
84
  // so locked events stay locked even when searching (ported verbatim).
@@ -96,6 +98,7 @@ export function useNetworkEventList(options) {
96
98
  events: filteredEvents,
97
99
  visibleEvents,
98
100
  hasLockedEvents: lockedEventCount > 0,
99
- lockedEventCount
101
+ lockedEventCount,
102
+ totalEvents: candidateEvents.length
100
103
  };
101
104
  }
@@ -428,6 +428,14 @@ export const networkSyncAdapter = {
428
428
  const event = resolveEvent(id);
429
429
  if (!event) return null;
430
430
  return {
431
+ // Identity + the override mark ride along so a caller (Ask Buoy's
432
+ // receipt, an MCP session) can say WHOSE body this is and whether an
433
+ // override rule produced it — a mocked response rendered as "live"
434
+ // is the fabrication problem wearing a chrome suit.
435
+ url: event.url ?? null,
436
+ method: event.method ?? null,
437
+ status: event.status ?? null,
438
+ override: event.override ?? null,
431
439
  requestData: event.requestData ?? null,
432
440
  responseData: event.responseData ?? null,
433
441
  requestHeaders: event.requestHeaders ?? null,
@@ -17,7 +17,7 @@
17
17
  * ```
18
18
  */
19
19
 
20
- import { NetworkIcon } from "@buoy-gg/shared-ui";
20
+ import { NetworkIcon, createToolFactory, defineToolPreset } from "@buoy-gg/shared-ui";
21
21
  import { NetworkModal } from "./network/components/NetworkModal";
22
22
  import { overrideRulesStore } from "./network/overrides/overrideRulesStore";
23
23
  import { networkEventStore } from "./network/utils/networkEventStore";
@@ -63,7 +63,7 @@ if (isReactNativeRuntime && typeof __DEV__ !== "undefined" && __DEV__) {
63
63
  * - Filter by status, method, URL
64
64
  * - Timing information
65
65
  */
66
- export const networkToolPreset = {
66
+ export const networkToolPreset = defineToolPreset({
67
67
  id: "network",
68
68
  name: "NET",
69
69
  description: "Network request logger",
@@ -77,7 +77,7 @@ export const networkToolPreset = {
77
77
  props: {
78
78
  enableSharedModalDimensions: false
79
79
  }
80
- };
80
+ });
81
81
 
82
82
  /**
83
83
  * Create a custom network monitoring tool configuration.
@@ -94,20 +94,6 @@ export const networkToolPreset = {
94
94
  * });
95
95
  * ```
96
96
  */
97
- export function createNetworkTool(options) {
98
- return {
99
- id: options?.id || "network",
100
- name: options?.name || "NET",
101
- description: options?.description || "Network request logger",
102
- slot: "both",
103
- icon: ({
104
- size
105
- }) => /*#__PURE__*/_jsx(NetworkIcon, {
106
- size: size
107
- }),
108
- component: NetworkModal,
109
- props: {
110
- enableSharedModalDimensions: options?.enableSharedModalDimensions !== undefined ? options.enableSharedModalDimensions : false
111
- }
112
- };
113
- }
97
+ export const createNetworkTool = createToolFactory(networkToolPreset, options => ({
98
+ enableSharedModalDimensions: options.enableSharedModalDimensions ?? false
99
+ }));
@@ -13,7 +13,7 @@ export type { NetworkToolViewProps } from "./network/components/NetworkModal";
13
13
  export { NetworkEventDetailView } from "./network/components/NetworkEventDetailView";
14
14
  export { NetworkEventItemCompact } from "./network/components/NetworkEventItemCompact";
15
15
  export { NetworkSavedScreen } from "./network/components/NetworkSavedScreen";
16
- export { NetworkSavedButton, NetworkDetailPinActions, } from "./network/components/NetworkHeaderLive";
16
+ export { NetworkSavedButton, NetworkDetailPinActions, NetworkHeaderBadgesView, } from "./network/components/NetworkHeaderLive";
17
17
  export { useNetworkEvents } from "./network/hooks/useNetworkEvents";
18
18
  export { useNetworkEventList } from "./network/hooks/useNetworkEventList";
19
19
  export { useLiveNetworkEvent } from "./network/hooks/useLiveNetworkEvent";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAOhE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAClF,YAAY,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EACL,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,wCAAwC,CAAC;AAKhD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAIpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAI9E,OAAO,EACL,eAAe,EACf,KAAK,qBAAqB,GAC3B,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAIhF,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,KAAK,qBAAqB,GAC3B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAKtF,OAAO,EACL,WAAW,EACX,cAAc,EACd,gBAAgB,GACjB,MAAM,4BAA4B,CAAC;AAKpC,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,uBAAuB,GACxB,MAAM,iBAAiB,CAAC;AAQzB,OAAO,EACL,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB;AACtB;;;GAGG;AACH,KAAK,mBAAmB,GACzB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EACL,SAAS,IAAI,kBAAkB,EAC/B,oBAAoB,IAAI,wBAAwB,EAChD,UAAU,IAAI,mBAAmB,EACjC,UAAU,IAAI,mBAAmB,EACjC,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,2BAA2B,CAAC;AAQnC,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,wBAAwB,EACxB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EACL,2BAA2B,EAC3B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,GACzB,MAAM,oCAAoC,CAAC;AAO5C,gBAAgB;AAChB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE;;;GAGG;AACH,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,qBAAqB,EACrB,UAAU,EACV,SAAS,EACT,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,gBAAgB;AAChB,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,iCAAiC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAOhE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAClF,YAAY,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,wCAAwC,CAAC;AAKhD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAIpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAI9E,OAAO,EACL,eAAe,EACf,KAAK,qBAAqB,GAC3B,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAIhF,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,KAAK,qBAAqB,GAC3B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAKtF,OAAO,EACL,WAAW,EACX,cAAc,EACd,gBAAgB,GACjB,MAAM,4BAA4B,CAAC;AAKpC,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,uBAAuB,GACxB,MAAM,iBAAiB,CAAC;AAQzB,OAAO,EACL,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB;AACtB;;;GAGG;AACH,KAAK,mBAAmB,GACzB,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EACL,SAAS,IAAI,kBAAkB,EAC/B,oBAAoB,IAAI,wBAAwB,EAChD,UAAU,IAAI,mBAAmB,EACjC,UAAU,IAAI,mBAAmB,EACjC,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,2BAA2B,CAAC;AAQnC,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,wBAAwB,EACxB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EACL,2BAA2B,EAC3B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,GACzB,MAAM,oCAAoC,CAAC;AAO5C,gBAAgB;AAChB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE;;;GAGG;AACH,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,qBAAqB,EACrB,UAAU,EACV,SAAS,EACT,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,GACvB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,gBAAgB;AAChB,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,iCAAiC,CAAC"}