@buoy-gg/images 7.0.14 → 7.0.16

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 (74) hide show
  1. package/lib/commonjs/capture/actions.js +15 -0
  2. package/lib/commonjs/capture/actions.js.map +1 -1
  3. package/lib/commonjs/capture/install.js +5 -1
  4. package/lib/commonjs/capture/install.js.map +1 -1
  5. package/lib/commonjs/capture/overrides.js +70 -10
  6. package/lib/commonjs/capture/overrides.js.map +1 -1
  7. package/lib/commonjs/capture/savings.js +16 -0
  8. package/lib/commonjs/capture/savings.js.map +1 -1
  9. package/lib/commonjs/components/ImageDetail.js +11 -1
  10. package/lib/commonjs/components/ImageDetail.js.map +1 -1
  11. package/lib/commonjs/components/ImagesModal.js +103 -38
  12. package/lib/commonjs/components/ImagesModal.js.map +1 -1
  13. package/lib/commonjs/index.js +26 -1
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/remote.js +42 -0
  16. package/lib/commonjs/remote.js.map +1 -0
  17. package/lib/commonjs/state.js +5 -1
  18. package/lib/commonjs/state.js.map +1 -1
  19. package/lib/commonjs/store/imageEventStore.js +79 -1
  20. package/lib/commonjs/store/imageEventStore.js.map +1 -1
  21. package/lib/commonjs/sync/imagesSyncAdapter.js.map +1 -1
  22. package/lib/module/capture/actions.js +15 -0
  23. package/lib/module/capture/actions.js.map +1 -1
  24. package/lib/module/capture/install.js +5 -1
  25. package/lib/module/capture/install.js.map +1 -1
  26. package/lib/module/capture/overrides.js +70 -10
  27. package/lib/module/capture/overrides.js.map +1 -1
  28. package/lib/module/capture/savings.js +16 -0
  29. package/lib/module/capture/savings.js.map +1 -1
  30. package/lib/module/components/ImageDetail.js +13 -3
  31. package/lib/module/components/ImageDetail.js.map +1 -1
  32. package/lib/module/components/ImagesModal.js +104 -40
  33. package/lib/module/components/ImagesModal.js.map +1 -1
  34. package/lib/module/index.js +6 -0
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/module/remote.js +36 -0
  37. package/lib/module/remote.js.map +1 -0
  38. package/lib/module/state.js +5 -1
  39. package/lib/module/state.js.map +1 -1
  40. package/lib/module/store/imageEventStore.js +76 -1
  41. package/lib/module/store/imageEventStore.js.map +1 -1
  42. package/lib/module/sync/imagesSyncAdapter.js.map +1 -1
  43. package/lib/typescript/capture/actions.d.ts.map +1 -1
  44. package/lib/typescript/capture/install.d.ts.map +1 -1
  45. package/lib/typescript/capture/overrides.d.ts.map +1 -1
  46. package/lib/typescript/capture/savings.d.ts.map +1 -1
  47. package/lib/typescript/components/ImageDetail.d.ts.map +1 -1
  48. package/lib/typescript/components/ImagesModal.d.ts +9 -1
  49. package/lib/typescript/components/ImagesModal.d.ts.map +1 -1
  50. package/lib/typescript/index.d.ts +5 -0
  51. package/lib/typescript/index.d.ts.map +1 -1
  52. package/lib/typescript/remote.d.ts +16 -0
  53. package/lib/typescript/remote.d.ts.map +1 -0
  54. package/lib/typescript/state.d.ts +9 -1
  55. package/lib/typescript/state.d.ts.map +1 -1
  56. package/lib/typescript/store/imageEventStore.d.ts +15 -1
  57. package/lib/typescript/store/imageEventStore.d.ts.map +1 -1
  58. package/lib/typescript/sync/imagesSyncAdapter.d.ts +3 -70
  59. package/lib/typescript/sync/imagesSyncAdapter.d.ts.map +1 -1
  60. package/lib/typescript/types.d.ts +53 -0
  61. package/lib/typescript/types.d.ts.map +1 -1
  62. package/package.json +2 -2
  63. package/src/capture/actions.ts +9 -0
  64. package/src/capture/install.ts +5 -1
  65. package/src/capture/overrides.ts +59 -9
  66. package/src/capture/savings.ts +11 -0
  67. package/src/components/ImageDetail.tsx +12 -2
  68. package/src/components/ImagesModal.tsx +107 -39
  69. package/src/index.tsx +12 -0
  70. package/src/remote.ts +42 -0
  71. package/src/state.ts +16 -1
  72. package/src/store/imageEventStore.ts +76 -2
  73. package/src/sync/imagesSyncAdapter.ts +2 -2
  74. package/src/types.ts +43 -0
@@ -5,7 +5,7 @@
5
5
  * instead of silently empty.
6
6
  */
7
7
 
8
- import { useState } from "react";
8
+ import { useEffect, useState } from "react";
9
9
  import { FlatList, StyleSheet, Text, View } from "react-native";
10
10
  import { JsModal, macOSColors } from "@buoy-gg/shared-ui";
11
11
  import type { ModalState } from "@buoy-gg/shared-ui";
@@ -13,7 +13,8 @@ import { getCaptureStatus, installImageCapture } from "../capture/install";
13
13
  import { getGlobalModes } from "../capture/overrides";
14
14
  import { formatRecordMarkdown, formatRegistryMarkdown } from "../export";
15
15
  import { useImageRecords } from "../hooks/useImageRecords";
16
- import { clearRecords, getRecord } from "../store/imageEventStore";
16
+ import { remoteImagesDispatch } from "../remote";
17
+ import { clearRecords, getRecord, updateRecord } from "../store/imageEventStore";
17
18
  import { ImageDetail } from "./ImageDetail";
18
19
  import { ImageDetailFooter } from "./ImageDetailFooter";
19
20
  import { ImageRow } from "./ImageRow";
@@ -29,6 +30,8 @@ interface ImagesModalProps {
29
30
  onBack?: () => void;
30
31
  onMinimize?: (modalState: ModalState) => void;
31
32
  persistenceKey?: string;
33
+ /** Render as a plain header-bar + body stack instead of a JsModal (desktop). */
34
+ inline?: boolean;
32
35
  }
33
36
 
34
37
  function CaptureNotice() {
@@ -72,6 +75,7 @@ export function ImagesModal({
72
75
  onBack,
73
76
  onMinimize,
74
77
  persistenceKey,
78
+ inline = false,
75
79
  }: ImagesModalProps) {
76
80
  // Safety net: ensures capture is live even without the early register
77
81
  // import (expo-image capture is timing-immune, so this still gets most data).
@@ -81,6 +85,30 @@ export function ImagesModal({
81
85
  const [selectedId, setSelectedId] = useState<number | null>(null);
82
86
  const selected = selectedId != null ? getRecord(selectedId) : undefined;
83
87
 
88
+ // Mirror mode: the synced wire records omit errorHeaders (detail-only data)
89
+ // — pull them from the device the first time a failed record is opened.
90
+ const wantHeadersFor =
91
+ selected?.status === "error" && selected.errorHeaders == null
92
+ ? selected.id
93
+ : null;
94
+ useEffect(() => {
95
+ const remote = remoteImagesDispatch();
96
+ if (wantHeadersFor == null || !remote) return;
97
+ let cancelled = false;
98
+ remote("getDetail", { id: wantHeadersFor })
99
+ .then((detail) => {
100
+ if (cancelled) return;
101
+ const headers = (detail as { errorHeaders?: Record<string, string> | null })
102
+ ?.errorHeaders;
103
+ // `{}` (not undefined) so a headerless error doesn't refetch forever.
104
+ updateRecord(wantHeadersFor, { errorHeaders: headers ?? {} });
105
+ })
106
+ .catch(() => {});
107
+ return () => {
108
+ cancelled = true;
109
+ };
110
+ }, [wantHeadersFor]);
111
+
84
112
  // Stepping is relative to the live list (newest first): previous = newer.
85
113
  const selectedIndex = selected
86
114
  ? records.findIndex((r) => r.id === selected.id)
@@ -104,6 +132,58 @@ export function ImagesModal({
104
132
  getGlobalModes()
105
133
  );
106
134
 
135
+ const headerContent = (
136
+ <ImagesHeader
137
+ stats={stats}
138
+ detailOpen={selected != null}
139
+ detailIndex={selectedIndex}
140
+ detailTotal={records.length}
141
+ onCloseDetail={() => setSelectedId(null)}
142
+ onClear={clearRecords}
143
+ getReport={getReport}
144
+ onBack={onBack}
145
+ />
146
+ );
147
+
148
+ const body = selected ? (
149
+ <View style={styles.detailContainer}>
150
+ <ImageDetail record={selected} />
151
+ <ImageDetailFooter
152
+ key={selected.id}
153
+ record={selected}
154
+ hasPrevious={selectedIndex > 0}
155
+ hasNext={selectedIndex >= 0 && selectedIndex < records.length - 1}
156
+ onPrevious={() => stepTo(selectedIndex - 1)}
157
+ onNext={() => stepTo(selectedIndex + 1)}
158
+ />
159
+ </View>
160
+ ) : (
161
+ <View style={styles.listContainer}>
162
+ <InsightsBar insights={computeInsights(records)} />
163
+ <CaptureNotice />
164
+ <FlatList
165
+ data={records}
166
+ keyExtractor={(item: ImageRecord) => String(item.id)}
167
+ renderItem={({ item }) => <ImageRow record={item} onPress={setSelectedId} />}
168
+ ListEmptyComponent={<EmptyState />}
169
+ />
170
+ <ImagesListFooter />
171
+ </View>
172
+ );
173
+
174
+ // The ONLY difference between the phone and the desktop dashboard is which
175
+ // shell the header and body go in. Everything above this is shared.
176
+ if (inline) {
177
+ return (
178
+ <View style={styles.inlineRoot}>
179
+ {/* Flex-less wrapper — `ModalHeader` is `flex: 1` for JsModal's
180
+ ROW-direction header slot and would grow vertically here. */}
181
+ <View style={styles.inlineHeader}>{headerContent}</View>
182
+ {body}
183
+ </View>
184
+ );
185
+ }
186
+
107
187
  return (
108
188
  <JsModal
109
189
  visible={visible}
@@ -112,53 +192,41 @@ export function ImagesModal({
112
192
  persistenceKey={persistenceKey}
113
193
  header={{
114
194
  showToggleButton: true,
115
- customContent: (
116
- <ImagesHeader
117
- stats={stats}
118
- detailOpen={selected != null}
119
- detailIndex={selectedIndex}
120
- detailTotal={records.length}
121
- onCloseDetail={() => setSelectedId(null)}
122
- onClear={clearRecords}
123
- getReport={getReport}
124
- onBack={onBack}
125
- />
126
- ),
195
+ customContent: headerContent,
127
196
  }}
128
197
  enablePersistence
129
198
  initialMode="bottomSheet"
130
199
  disableScrollWrapper
131
200
  >
132
- {selected ? (
133
- <View style={styles.detailContainer}>
134
- <ImageDetail record={selected} />
135
- <ImageDetailFooter
136
- key={selected.id}
137
- record={selected}
138
- hasPrevious={selectedIndex > 0}
139
- hasNext={selectedIndex >= 0 && selectedIndex < records.length - 1}
140
- onPrevious={() => stepTo(selectedIndex - 1)}
141
- onNext={() => stepTo(selectedIndex + 1)}
142
- />
143
- </View>
144
- ) : (
145
- <View style={styles.listContainer}>
146
- <InsightsBar insights={computeInsights(records)} />
147
- <CaptureNotice />
148
- <FlatList
149
- data={records}
150
- keyExtractor={(item: ImageRecord) => String(item.id)}
151
- renderItem={({ item }) => <ImageRow record={item} onPress={setSelectedId} />}
152
- ListEmptyComponent={<EmptyState />}
153
- />
154
- <ImagesListFooter />
155
- </View>
156
- )}
201
+ {body}
157
202
  </JsModal>
158
203
  );
159
204
  }
160
205
 
206
+ /**
207
+ * The same Images tool as `ImagesModal`, rendered INLINE for hosts that place
208
+ * tools in a panel rather than a modal (the desktop dashboard). Exists so
209
+ * there is exactly one implementation of the tool's header, list and detail.
210
+ */
211
+ export function ImagesToolView(
212
+ props: Omit<ImagesModalProps, "visible" | "onClose" | "inline"> = {}
213
+ ) {
214
+ return <ImagesModal {...props} inline visible onClose={() => {}} />;
215
+ }
216
+
161
217
  const styles = StyleSheet.create({
218
+ inlineRoot: {
219
+ flex: 1,
220
+ minHeight: 0,
221
+ backgroundColor: macOSColors.background.base,
222
+ },
223
+ inlineHeader: {
224
+ paddingHorizontal: 12,
225
+ paddingVertical: 8,
226
+ borderBottomWidth: 1,
227
+ borderBottomColor: macOSColors.border.default,
228
+ backgroundColor: macOSColors.background.card,
229
+ },
162
230
  listContainer: {
163
231
  flex: 1,
164
232
  },
package/src/index.tsx CHANGED
@@ -79,6 +79,18 @@ export { formatRecordMarkdown, formatRegistryMarkdown } from "./export";
79
79
  // =============================================================================
80
80
  export { imagesSyncAdapter } from "./sync/imagesSyncAdapter";
81
81
 
82
+ // =============================================================================
83
+ // DASHBOARD MIRROR (Buoy Desktop renders ImagesToolView fed by these)
84
+ // =============================================================================
85
+ export { ImagesToolView } from "./components/ImagesModal";
86
+ export {
87
+ disableImagesCapture,
88
+ replaceImagesFromSnapshot,
89
+ } from "./store/imageEventStore";
90
+ export { setRemoteImagesHandler } from "./remote";
91
+ export type { ImagesRemoteDispatch } from "./remote";
92
+ export type { ImageRecordWire, ImagesSnapshotPayload } from "./types";
93
+
82
94
  // =============================================================================
83
95
  // STORE + HOOKS
84
96
  // =============================================================================
package/src/remote.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Remote-dispatch seam for dashboard mirrors (Buoy Desktop).
3
+ *
4
+ * On device this stays null and every action in capture/ runs locally. A
5
+ * dashboard sets a handler that forwards the action name + params over the
6
+ * external-sync protocol to the SAME adapter actions the MCP uses
7
+ * (sync/imagesSyncAdapter.ts), so one action vocabulary serves phone UI,
8
+ * desktop UI, and LLM automation.
9
+ */
10
+
11
+ import { imagesState } from "./state";
12
+ import type { ActionResult } from "./capture/actions";
13
+
14
+ export type ImagesRemoteDispatch = (
15
+ action: string,
16
+ params?: Record<string, unknown>
17
+ ) => Promise<unknown>;
18
+
19
+ export function setRemoteImagesHandler(
20
+ handler: ImagesRemoteDispatch | null
21
+ ): void {
22
+ imagesState().remoteDispatch = handler;
23
+ }
24
+
25
+ export function remoteImagesDispatch(): ImagesRemoteDispatch | null {
26
+ return imagesState().remoteDispatch;
27
+ }
28
+
29
+ /** Coerce an adapter action's `{ ok, message }` reply into an ActionResult. */
30
+ export async function remoteActionResult(
31
+ call: Promise<unknown>
32
+ ): Promise<ActionResult> {
33
+ try {
34
+ const result = (await call) as { ok?: unknown; message?: unknown } | null;
35
+ return {
36
+ ok: !!result?.ok,
37
+ message: String(result?.message ?? ""),
38
+ };
39
+ } catch (error) {
40
+ return { ok: false, message: String((error as Error)?.message ?? error) };
41
+ }
42
+ }
package/src/state.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * every copy operate on the same data.
11
11
  */
12
12
 
13
- import type { ImageRecord } from "./types";
13
+ import type { CaptureStatus, ImageRecord } from "./types";
14
14
 
15
15
  export interface ImagesGlobalState {
16
16
  // store
@@ -51,6 +51,17 @@ export interface ImagesGlobalState {
51
51
  versionCounter: number;
52
52
  /** App-wide simulation modes. */
53
53
  globalModes: { network: "normal" | "offline" | "cold"; blank: boolean };
54
+ // mirror mode (desktop dashboard)
55
+ /** True on dashboards: local capture must never install (mirror only). */
56
+ captureDisabled: boolean;
57
+ /** Forwards actions to the synced device instead of running them locally. */
58
+ remoteDispatch:
59
+ | ((action: string, params?: Record<string, unknown>) => Promise<unknown>)
60
+ | null;
61
+ /** Device PixelRatio derived from the synced snapshot (layout → neededPx). */
62
+ mirrorPixelRatio: number | null;
63
+ /** Device-reported capture status (overrides the local probe on mirrors). */
64
+ mirrorCaptureStatus: CaptureStatus | null;
54
65
  }
55
66
 
56
67
  const KEY = Symbol.for("buoy.images.globalState.v1");
@@ -82,6 +93,10 @@ export function imagesState(): ImagesGlobalState {
82
93
  globalVersion: 0,
83
94
  versionCounter: 0,
84
95
  globalModes: { network: "normal", blank: false },
96
+ captureDisabled: false,
97
+ remoteDispatch: null,
98
+ mirrorPixelRatio: null,
99
+ mirrorCaptureStatus: null,
85
100
  };
86
101
  }
87
102
  return g[KEY];
@@ -10,8 +10,9 @@
10
10
  */
11
11
 
12
12
  import { PixelRatio } from "react-native";
13
+ import { remoteImagesDispatch } from "../remote";
13
14
  import { imagesState } from "../state";
14
- import type { ImageRecord, ImageStats } from "../types";
15
+ import type { ImageRecord, ImageStats, ImagesSnapshotPayload } from "../types";
15
16
 
16
17
  /** Max records retained before the oldest are dropped. */
17
18
  const MAX_RECORDS = 500;
@@ -83,6 +84,9 @@ export function subscribe(listener: () => void): () => void {
83
84
  }
84
85
 
85
86
  export function clearRecords(): void {
87
+ // Mirror mode: clear the device's registry too, or the next snapshot would
88
+ // refill this store straight away.
89
+ void remoteImagesDispatch()?.("clearRecords");
86
90
  const s = imagesState();
87
91
  // Keep records still actively loading so in-flight events don't orphan.
88
92
  s.records = s.records.filter(
@@ -92,14 +96,84 @@ export function clearRecords(): void {
92
96
  invalidate();
93
97
  }
94
98
 
99
+ // ---------------------------------------------------------------------------
100
+ // Mirror mode (desktop dashboard)
101
+ // ---------------------------------------------------------------------------
102
+
103
+ /**
104
+ * Dashboards never instrument their own images — the store is purely a mirror
105
+ * of the synced device. Makes installImageCapture() a permanent no-op.
106
+ */
107
+ export function disableImagesCapture(): void {
108
+ imagesState().captureDisabled = true;
109
+ }
110
+
111
+ /**
112
+ * Replace the whole store from a synced adapter snapshot (wire records are
113
+ * newest-first; the store keeps oldest-first and useImageRecords reverses).
114
+ * Also adopts the device's global modes / capture status and derives the
115
+ * DEVICE's PixelRatio so oversize math doesn't use the dashboard display's.
116
+ */
117
+ export function replaceImagesFromSnapshot(payload: ImagesSnapshotPayload): void {
118
+ const s = imagesState();
119
+ const wire = payload.records ?? [];
120
+
121
+ // neededPx was computed on device as round(layout * PixelRatio.get()), so
122
+ // the widest record recovers the device scale with the least rounding error.
123
+ let bestWidth = 0;
124
+ for (const w of wire) {
125
+ if (w.layout && w.neededPx && w.layout.width > bestWidth) {
126
+ bestWidth = w.layout.width;
127
+ s.mirrorPixelRatio = w.neededPx.width / w.layout.width;
128
+ }
129
+ }
130
+
131
+ const now = Date.now();
132
+ s.records = [...wire].reverse().map((w) => ({
133
+ id: w.id,
134
+ lib: w.lib,
135
+ uri: w.uri,
136
+ sourceKind: w.kind,
137
+ status: w.status,
138
+ mounted: w.mounted,
139
+ createdAt: now - w.ageMs,
140
+ durationMs: w.ms ?? undefined,
141
+ intrinsic: w.intrinsic ?? undefined,
142
+ layout: w.layout ?? undefined,
143
+ cacheVerdict: w.cache ?? undefined,
144
+ cacheVerdictSource: w.cacheSource ?? undefined,
145
+ progressSeen: w.progressSeen,
146
+ // The wire carries only the total; a settled download loaded all of it.
147
+ bytesLoaded: w.status === "loaded" ? (w.bytesTotal ?? undefined) : undefined,
148
+ bytesTotal: w.bytesTotal ?? undefined,
149
+ error: w.error ?? undefined,
150
+ errorCode: w.errorCode ?? undefined,
151
+ loadCount: w.loadCount,
152
+ overrideLabel: w.overrideLabel ?? undefined,
153
+ hasAltText: w.hasAltText ?? undefined,
154
+ layoutShifts: w.layoutShifts,
155
+ }));
156
+ s.byId = new Map(s.records.map((r) => [r.id, r]));
157
+ if (payload.globalModes) s.globalModes = { ...payload.globalModes };
158
+ if (payload.captureStatus) s.mirrorCaptureStatus = payload.captureStatus;
159
+ invalidate();
160
+ // Mode toggles render through the actions bus, not the record store.
161
+ for (const l of s.actionListeners) l();
162
+ }
163
+
95
164
  // ---------------------------------------------------------------------------
96
165
  // Derived math (shared by rows, detail view, and stats)
97
166
  // ---------------------------------------------------------------------------
98
167
 
168
+ /** Device PixelRatio on device; the SYNCED device's ratio on a mirror. */
169
+ export function effectivePixelRatio(): number {
170
+ return imagesState().mirrorPixelRatio ?? PixelRatio.get();
171
+ }
172
+
99
173
  /** Physical pixels actually needed to fill the laid-out view 1:1. */
100
174
  export function neededPixels(record: ImageRecord): { width: number; height: number } | null {
101
175
  if (!record.layout || record.layout.width <= 0 || record.layout.height <= 0) return null;
102
- const scale = PixelRatio.get();
176
+ const scale = effectivePixelRatio();
103
177
  return {
104
178
  width: Math.round(record.layout.width * scale),
105
179
  height: Math.round(record.layout.height * scale),
@@ -40,10 +40,10 @@ import {
40
40
  oversizeFactor,
41
41
  subscribe,
42
42
  } from "../store/imageEventStore";
43
- import type { ImageRecord } from "../types";
43
+ import type { ImageRecord, ImageRecordWire } from "../types";
44
44
 
45
45
  /** Slim wire form of a record (heavy/derived fields precomputed + rounded). */
46
- function toWire(record: ImageRecord) {
46
+ function toWire(record: ImageRecord): ImageRecordWire {
47
47
  const factor = oversizeFactor(record);
48
48
  return {
49
49
  id: record.id,
package/src/types.ts CHANGED
@@ -97,6 +97,49 @@ export interface ImageStats {
97
97
  estWastedBytes: number;
98
98
  }
99
99
 
100
+ /**
101
+ * Slim wire form of a record as pushed by the external-sync adapter (heavy
102
+ * derived math precomputed on device). This is what dashboards receive and
103
+ * what `replaceFromSnapshot` reconstructs ImageRecords from — keep it in
104
+ * lockstep with `toWire` in sync/imagesSyncAdapter.ts (and the Flutter port).
105
+ */
106
+ export interface ImageRecordWire {
107
+ id: number;
108
+ lib: ImageLib;
109
+ uri: string;
110
+ kind: SourceKind;
111
+ status: ImageStatus;
112
+ mounted: boolean;
113
+ cache: CacheVerdict | null;
114
+ cacheSource: "event" | "queryCache" | null;
115
+ ms: number | null;
116
+ intrinsic: { width: number; height: number } | null;
117
+ layout: { width: number; height: number } | null;
118
+ neededPx: { width: number; height: number } | null;
119
+ oversizeFactor: number | null;
120
+ decodedKB: number;
121
+ wastedKB: number;
122
+ progressSeen: boolean;
123
+ bytesTotal: number | null;
124
+ error: string | null;
125
+ errorCode: number | null;
126
+ loadCount: number;
127
+ overrideLabel: string | null;
128
+ hasAltText: boolean | null;
129
+ layoutShifts: number;
130
+ ageMs: number;
131
+ }
132
+
133
+ /** Full snapshot payload the adapter pushes to watching dashboards. */
134
+ export interface ImagesSnapshotPayload {
135
+ stats?: ImageStats;
136
+ captureStatus?: CaptureStatus;
137
+ globalModes?: { network: "normal" | "offline" | "cold"; blank: boolean };
138
+ insights?: ImageInsights;
139
+ /** Newest first, capped at 100. */
140
+ records?: ImageRecordWire[];
141
+ }
142
+
100
143
  /** Capture capability report — what the interceptors managed to hook. */
101
144
  export interface CaptureStatus {
102
145
  installed: boolean;