@buoy-gg/jotai 7.0.17 → 7.0.18

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 (27) hide show
  1. package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +11 -3
  2. package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +4 -3
  3. package/lib/commonjs/jotai/components/JotaiModal.js +35 -4
  4. package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +139 -5
  5. package/lib/commonjs/jotai/utils/jotaiStateStore.js +73 -7
  6. package/lib/commonjs/jotai/utils/snapshotProvider.js +111 -2
  7. package/lib/module/jotai/components/JotaiAtomBrowser.js +11 -3
  8. package/lib/module/jotai/components/JotaiAtomDetailContent.js +4 -2
  9. package/lib/module/jotai/components/JotaiModal.js +36 -5
  10. package/lib/module/jotai/sync/jotaiSyncAdapter.js +138 -4
  11. package/lib/module/jotai/utils/jotaiStateStore.js +73 -7
  12. package/lib/module/jotai/utils/snapshotProvider.js +111 -2
  13. package/lib/typescript/index.d.ts +1 -1
  14. package/lib/typescript/index.d.ts.map +1 -1
  15. package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts +3 -1
  16. package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +1 -1
  17. package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts +7 -2
  18. package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -1
  19. package/lib/typescript/jotai/components/JotaiModal.d.ts +1 -1
  20. package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -1
  21. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +54 -2
  22. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -1
  23. package/lib/typescript/jotai/types/index.d.ts +28 -0
  24. package/lib/typescript/jotai/types/index.d.ts.map +1 -1
  25. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +10 -0
  26. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -1
  27. package/package.json +3 -3
@@ -20,6 +20,9 @@ var _jsxRuntime = require("react/jsx-runtime");
20
20
  function getValuePreview(atom) {
21
21
  try {
22
22
  const value = atom.getValue();
23
+ if (value && typeof value === "object" && value.__buoyValueOnDevice === true) {
24
+ return "on device";
25
+ }
23
26
  if (value === undefined) return "undefined";
24
27
  if (value === null) return "null";
25
28
  if (Array.isArray(value)) {
@@ -125,7 +128,8 @@ function EmptyBrowserState() {
125
128
  function JotaiAtomBrowser({
126
129
  atoms,
127
130
  searchQuery,
128
- onViewHistory
131
+ onViewHistory,
132
+ onAtomExpand
129
133
  }) {
130
134
  const [expandedAtom, setExpandedAtom] = (0, _react.useState)(null);
131
135
  const filteredAtoms = (0, _react.useMemo)(() => {
@@ -134,8 +138,12 @@ function JotaiAtomBrowser({
134
138
  return atoms.filter(a => a.label.toLowerCase().includes(search));
135
139
  }, [atoms, searchQuery]);
136
140
  const handleAtomPress = (0, _react.useCallback)(atom => {
137
- setExpandedAtom(prev => prev === atom.label ? null : atom.label);
138
- }, []);
141
+ setExpandedAtom(prev => {
142
+ const next = prev === atom.label ? null : atom.label;
143
+ if (next) onAtomExpand?.(atom.label);
144
+ return next;
145
+ });
146
+ }, [onAtomExpand]);
139
147
  if (filteredAtoms.length === 0 && !searchQuery) {
140
148
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyBrowserState, {});
141
149
  }
@@ -25,7 +25,6 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
25
25
  */
26
26
 
27
27
  // ---- View Toggle ----
28
-
29
28
  const VIEW_CONFIGS = [{
30
29
  key: "change",
31
30
  label: "CHANGE",
@@ -277,9 +276,11 @@ function JotaiAtomDetailContent({
277
276
  changes,
278
277
  selectedIndex,
279
278
  onIndexChange,
280
- disableInternalFooter = false
279
+ disableInternalFooter = false,
280
+ initialView = "change"
281
281
  }) {
282
- const [activeView, setActiveView] = (0, _react.useState)("change");
282
+ // View state (initialView seeds it once; taps own it afterwards)
283
+ const [activeView, setActiveView] = (0, _react.useState)(initialView);
283
284
  const [diffMode, setDiffMode] = (0, _react.useState)("tree");
284
285
  const totalChanges = changes.length;
285
286
  const atomColor = _jotaiStateStore.jotaiStateStore.getAtomColor(change.atomLabel);
@@ -47,11 +47,16 @@ function JotaiModal({
47
47
  onBack,
48
48
  onMinimize,
49
49
  enableSharedModalDimensions = false,
50
- inline = false
50
+ inline = false,
51
+ initialTab,
52
+ initialSelectedChangeId,
53
+ initialDetailView,
54
+ onChangeSelect,
55
+ onAtomExpand
51
56
  }) {
52
57
  const isPro = (0, _license.useIsPro)();
53
58
  const [showProModal, setShowProModal] = (0, _react.useState)(false);
54
- const [activeTab, setActiveTab] = (0, _react.useState)("atoms");
59
+ const [activeTab, setActiveTab] = (0, _react.useState)(initialTab ?? "atoms");
55
60
  const [selectedHistoryAtom, setSelectedHistoryAtom] = (0, _react.useState)(null);
56
61
  const [showFilters, setShowFilters] = (0, _react.useState)(false);
57
62
  const [ignoredPatterns, setIgnoredPatterns] = (0, _react.useState)(new Set());
@@ -103,6 +108,30 @@ function JotaiModal({
103
108
  }, [selectedChangeId, activeChanges]);
104
109
  const selectedChange = selectedChangeIndex !== null ? activeChanges[selectedChangeIndex] : null;
105
110
 
111
+ // Notify the host on selection (keyed by change id so re-renders with the
112
+ // same selection don't re-fire; remote mirrors fetch atom values here).
113
+ const lastNotifiedSelectionRef = (0, _react.useRef)(null);
114
+ (0, _react.useEffect)(() => {
115
+ if (!onChangeSelect) return;
116
+ const id = selectedChange?.id ?? null;
117
+ if (id === null || id === lastNotifiedSelectionRef.current) return;
118
+ lastNotifiedSelectionRef.current = id;
119
+ onChangeSelect(selectedChange);
120
+ }, [selectedChange, onChangeSelect]);
121
+
122
+ // Deep link into a change's detail (see initialSelectedChangeId). Applied
123
+ // at most once per id, so the list re-rendering — or the user pressing Back
124
+ // — never re-opens it.
125
+ const appliedInitialSelectionRef = (0, _react.useRef)(null);
126
+ (0, _react.useEffect)(() => {
127
+ if (!initialSelectedChangeId) return;
128
+ if (appliedInitialSelectionRef.current === initialSelectedChangeId) return;
129
+ const exists = activeChanges.some(c => c.id === initialSelectedChangeId);
130
+ if (!exists) return;
131
+ appliedInitialSelectionRef.current = initialSelectedChangeId;
132
+ setSelectedChangeId(initialSelectedChangeId);
133
+ }, [initialSelectedChangeId, activeChanges]);
134
+
106
135
  // Search state
107
136
  const [searchText, setSearchText] = (0, _react.useState)("");
108
137
  const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
@@ -343,7 +372,8 @@ function JotaiModal({
343
372
  changes: activeChanges,
344
373
  selectedIndex: selectedChangeIndex,
345
374
  onIndexChange: handleIndexChange,
346
- disableInternalFooter: true
375
+ disableInternalFooter: true,
376
+ initialView: initialDetailView
347
377
  });
348
378
  }
349
379
  if (selectedHistoryAtom) {
@@ -376,7 +406,8 @@ function JotaiModal({
376
406
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_JotaiAtomBrowser.JotaiAtomBrowser, {
377
407
  atoms: displayedAtoms,
378
408
  searchQuery: searchText,
379
- onViewHistory: handleViewHistory
409
+ onViewHistory: handleViewHistory,
410
+ onAtomExpand: onAtomExpand
380
411
  });
381
412
  }
382
413
 
@@ -3,8 +3,62 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.jotaiSyncAdapter = void 0;
6
+ exports.jotaiSyncAdapter = exports.VALUE_ON_DEVICE = void 0;
7
7
  var _jotaiStateStore = require("../utils/jotaiStateStore");
8
+ /**
9
+ * Sentinel used in WIRE-FORM snapshots in place of raw prevValue/nextValue.
10
+ * Same hazard as redux/zustand: up to 200 changes × 2 values on every
11
+ * snapshot. Dashboards fetch values via `getChangeDetail`.
12
+ */
13
+ const VALUE_ON_DEVICE = exports.VALUE_ON_DEVICE = Object.freeze({
14
+ __buoyValueOnDevice: true,
15
+ note: "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue."
16
+ });
17
+ const MAX_CHANGE_DETAIL_BYTES = 8 * 1024 * 1024;
18
+ const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
19
+ function jsonLongerThan(value, limit) {
20
+ let bytes = 0;
21
+ const stack = [value];
22
+ const seen = new Set();
23
+ while (stack.length > 0) {
24
+ if (bytes > limit) return true;
25
+ const v = stack.pop();
26
+ if (v === null || v === undefined) bytes += 4;else if (typeof v === "string") bytes += v.length + 2;else if (typeof v === "number") bytes += 8;else if (typeof v === "boolean") bytes += 5;else if (typeof v === "object") {
27
+ if (seen.has(v)) continue;
28
+ seen.add(v);
29
+ if (Array.isArray(v)) {
30
+ bytes += 2 + v.length;
31
+ for (let i = 0; i < v.length; i++) stack.push(v[i]);
32
+ } else {
33
+ bytes += 2;
34
+ for (const k in v) {
35
+ bytes += k.length + 4;
36
+ stack.push(v[k]);
37
+ }
38
+ }
39
+ } else bytes += 4;
40
+ }
41
+ return bytes > limit;
42
+ }
43
+ function toWireChange(change) {
44
+ return {
45
+ ...change,
46
+ prevValue: VALUE_ON_DEVICE,
47
+ nextValue: VALUE_ON_DEVICE
48
+ };
49
+ }
50
+ function toWireValue(value) {
51
+ try {
52
+ JSON.stringify(value);
53
+ } catch {
54
+ return VALUE_ON_DEVICE;
55
+ }
56
+ if (jsonLongerThan(value, MAX_WIRE_PAYLOAD_BYTES)) {
57
+ return VALUE_ON_DEVICE;
58
+ }
59
+ return value;
60
+ }
61
+
8
62
  /**
9
63
  * Sync adapter for the jotai tool, consumed by @buoy-gg/external-sync's
10
64
  * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
@@ -15,12 +69,19 @@ var _jotaiStateStore = require("../utils/jotaiStateStore");
15
69
  * already records. The snapshot carries both the change timeline and the
16
70
  * atom registry (with each atom's current value) so the dashboard can render
17
71
  * the atom browser.
72
+ *
73
+ * v2: change-log prevValue/nextValue are replaced with on-device markers.
74
+ * Dashboards fetch values via `getChangeDetail`. Oversized / unserializable
75
+ * `atoms[].currentValue` uses the same marker and is fetched via `getAtomValue`.
18
76
  */
19
77
  const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
20
- version: 1,
78
+ version: 2,
21
79
  getSnapshot: () => ({
22
- changes: _jotaiStateStore.jotaiStateStore.getAtomChanges(),
23
- atoms: _jotaiStateStore.jotaiStateStore.getAtomSnapshots()
80
+ changes: _jotaiStateStore.jotaiStateStore.getAtomChanges().map(toWireChange),
81
+ atoms: _jotaiStateStore.jotaiStateStore.getAtomSnapshots().map(atom => ({
82
+ ...atom,
83
+ currentValue: toWireValue(atom.currentValue)
84
+ }))
24
85
  }),
25
86
  subscribe: onChange => {
26
87
  const unsubscribeChanges = _jotaiStateStore.jotaiStateStore.subscribe(onChange);
@@ -34,6 +95,78 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
34
95
  clearEvents: () => {
35
96
  _jotaiStateStore.jotaiStateStore.clearAtomChanges();
36
97
  },
98
+ /**
99
+ * On-demand detail: one change's real prevValue/nextValue.
100
+ * The per-snapshot stream stays value-free; this is the explicit,
101
+ * size-guarded channel for the detail pane.
102
+ */
103
+ getChangeDetail: params => {
104
+ const {
105
+ id
106
+ } = params ?? {};
107
+ if (!id) return {
108
+ found: false,
109
+ reason: "missing id"
110
+ };
111
+ const change = _jotaiStateStore.jotaiStateStore.getAtomChangeById(id);
112
+ if (!change) return {
113
+ found: false,
114
+ reason: "unknown id"
115
+ };
116
+ const cappedNote = {
117
+ __buoyTruncated: true,
118
+ note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
119
+ };
120
+ const cap = value => jsonLongerThan(value, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : value;
121
+ return {
122
+ found: true,
123
+ id,
124
+ prevValue: cap(change.prevValue),
125
+ nextValue: cap(change.nextValue)
126
+ };
127
+ },
128
+ /**
129
+ * On-demand current value for one atom. Wire-form snapshots replace
130
+ * oversized / unserializable `currentValue` with VALUE_ON_DEVICE.
131
+ */
132
+ getAtomValue: params => {
133
+ const {
134
+ label
135
+ } = params ?? {};
136
+ if (!label) return {
137
+ found: false,
138
+ reason: "missing label"
139
+ };
140
+ const atom = _jotaiStateStore.jotaiStateStore.getAtom(label);
141
+ if (!atom) return {
142
+ found: false,
143
+ reason: "unknown label"
144
+ };
145
+ let currentValue;
146
+ try {
147
+ currentValue = atom.getValue();
148
+ } catch {
149
+ currentValue = undefined;
150
+ }
151
+ const cappedNote = {
152
+ __buoyTruncated: true,
153
+ note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
154
+ };
155
+ try {
156
+ JSON.stringify(currentValue);
157
+ } catch {
158
+ return {
159
+ found: true,
160
+ label,
161
+ currentValue: VALUE_ON_DEVICE
162
+ };
163
+ }
164
+ return {
165
+ found: true,
166
+ label,
167
+ currentValue: jsonLongerThan(currentValue, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : currentValue
168
+ };
169
+ },
37
170
  /**
38
171
  * Compact current-value reader for a remote driver (MCP/LLM): registered
39
172
  * atoms with light metadata; each atom's `currentValue` stays on the device
@@ -77,7 +210,8 @@ const jotaiSyncAdapter = exports.jotaiSyncAdapter = {
77
210
  const {
78
211
  label,
79
212
  value
80
- } = params;
213
+ } = params ?? {};
214
+ if (!label) throw new Error("setAtom requires a `label`");
81
215
  _jotaiStateStore.jotaiStateStore.setAtomValue(label, value);
82
216
  }
83
217
  }
@@ -290,17 +290,83 @@ class JotaiStateStore {
290
290
  */
291
291
  replaceFromSnapshot(changes, atomSnapshots) {
292
292
  if (!this.isEnabled) return;
293
- this.atomChanges = changes.slice(0, this.maxChanges);
294
- this.atoms = new Map(atomSnapshots.map(snapshot => [snapshot.label, {
295
- label: snapshot.label,
296
- changeCount: snapshot.changeCount,
297
- color: snapshot.color,
298
- getValue: () => snapshot.currentValue
299
- }]));
293
+
294
+ // Preserve values a remote mirror already hydrated — wire-form snapshots
295
+ // always carry markers, and a later snapshot must not blank the detail pane.
296
+ const hydrated = new Map();
297
+ for (const existing of this.atomChanges) {
298
+ const prev = existing.prevValue;
299
+ if (!prev || prev.__buoyValueOnDevice !== true) {
300
+ hydrated.set(existing.id, existing);
301
+ }
302
+ }
303
+ this.atomChanges = changes.slice(0, this.maxChanges).map(change => {
304
+ const kept = hydrated.get(change.id);
305
+ const incoming = change.prevValue;
306
+ return kept && incoming?.__buoyValueOnDevice === true ? {
307
+ ...change,
308
+ prevValue: kept.prevValue,
309
+ nextValue: kept.nextValue
310
+ } : change;
311
+ });
312
+
313
+ // Preserve currentValue a remote mirror already hydrated — oversized
314
+ // atoms arrive as VALUE_ON_DEVICE markers on every snapshot.
315
+ const hydratedAtoms = new Map();
316
+ for (const [label, atom] of this.atoms) {
317
+ try {
318
+ const value = atom.getValue();
319
+ const marker = value;
320
+ if (!marker || marker.__buoyValueOnDevice !== true) {
321
+ hydratedAtoms.set(label, value);
322
+ }
323
+ } catch {
324
+ // ignore
325
+ }
326
+ }
327
+ this.atoms = new Map(atomSnapshots.map(snapshot => {
328
+ const incoming = snapshot.currentValue;
329
+ const kept = hydratedAtoms.get(snapshot.label);
330
+ const currentValue = kept !== undefined && incoming?.__buoyValueOnDevice === true ? kept : snapshot.currentValue;
331
+ return [snapshot.label, {
332
+ label: snapshot.label,
333
+ changeCount: snapshot.changeCount,
334
+ color: snapshot.color,
335
+ getValue: () => currentValue
336
+ }];
337
+ }));
300
338
  this.notifyListeners();
301
339
  this.notifyAtomListeners();
302
340
  }
303
341
 
342
+ /**
343
+ * Hydrate one mirrored change with real values fetched on demand from the
344
+ * device via the adapter's `getChangeDetail`. Remote-mirror (desktop) only.
345
+ */
346
+ hydrateChange(id, patch) {
347
+ const index = this.atomChanges.findIndex(c => c.id === id);
348
+ if (index === -1) return;
349
+ this.atomChanges = [...this.atomChanges.slice(0, index), {
350
+ ...this.atomChanges[index],
351
+ ...patch
352
+ }, ...this.atomChanges.slice(index + 1)];
353
+ this.notifyListeners();
354
+ }
355
+
356
+ /**
357
+ * Hydrate one mirrored atom's currentValue fetched on demand from the
358
+ * device via the adapter's `getAtomValue`. Remote-mirror (desktop) only.
359
+ */
360
+ hydrateAtomValue(label, currentValue) {
361
+ const atom = this.atoms.get(label);
362
+ if (!atom) return;
363
+ this.atoms.set(label, {
364
+ ...atom,
365
+ getValue: () => currentValue
366
+ });
367
+ this.notifyAtomListeners();
368
+ }
369
+
304
370
  // ---- Atom Registry ----
305
371
 
306
372
  registerAtom(label, getValue, setValue) {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  var _sharedUi = require("@buoy-gg/shared-ui");
4
+ var _dataViewer = require("@buoy-gg/shared-ui/dataViewer");
4
5
  var _jotaiStateStore = require("./jotaiStateStore");
5
6
  /**
6
7
  * Time Machine snapshot provider for Jotai.
@@ -62,8 +63,12 @@ async function capture() {
62
63
  warnings
63
64
  };
64
65
  }
65
- async function restore(data) {
66
+ async function restore(data, options) {
66
67
  const snapshot = data;
68
+ const excludedSet = new Set(options?.excludeKeys ?? []);
69
+ const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
70
+ const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
71
+ let excludedHits = 0;
67
72
  const result = {
68
73
  ok: true,
69
74
  applied: 0,
@@ -80,6 +85,10 @@ async function restore(data) {
80
85
  }
81
86
  let derived = 0;
82
87
  for (const [label, entry] of Object.entries(snapshot.atoms)) {
88
+ if (!touch(label)) {
89
+ excludedHits += 1;
90
+ continue;
91
+ }
83
92
  if (!entry.writable) {
84
93
  derived += 1;
85
94
  continue;
@@ -113,12 +122,112 @@ async function restore(data) {
113
122
  if (derived > 0) {
114
123
  result.warnings.push(`${derived} derived atom(s) left to recompute`);
115
124
  }
125
+ if (excludedHits > 0) {
126
+ result.warnings.push(`${excludedHits} atom(s) excluded by you`);
127
+ }
116
128
  return result;
117
129
  }
130
+
131
+ /** Per-atom restore preview. `base` omitted = compare against live atoms. */
132
+ async function previewItems(target, base) {
133
+ const targetAtoms = target?.atoms ?? {};
134
+ const liveMode = base === undefined;
135
+ const baseAtoms = {};
136
+ if (liveMode) {
137
+ for (const info of _jotaiStateStore.jotaiStateStore.getAtoms()) {
138
+ try {
139
+ baseAtoms[info.label] = {
140
+ value: info.getValue(),
141
+ writable: Boolean(info.setValue)
142
+ };
143
+ } catch {
144
+ // unreadable atom — treated as absent
145
+ }
146
+ }
147
+ } else {
148
+ Object.assign(baseAtoms, base?.atoms ?? {});
149
+ }
150
+ const items = [];
151
+ const notes = [];
152
+ const labels = new Set([...Object.keys(baseAtoms), ...Object.keys(targetAtoms)]);
153
+ for (const label of labels) {
154
+ const inTarget = label in targetAtoms;
155
+ if (!inTarget) {
156
+ if (liveMode) notes.push(`atom "${label}" not in snapshot — untouched`);else items.push({
157
+ key: label,
158
+ label,
159
+ verdict: "removed",
160
+ oldValue: baseAtoms[label]?.value
161
+ });
162
+ continue;
163
+ }
164
+ const entry = targetAtoms[label];
165
+ const equal = label in baseAtoms && (0, _dataViewer.boundedDeepEqual)(baseAtoms[label].value, entry.value);
166
+ if (!entry.writable) {
167
+ items.push({
168
+ key: label,
169
+ label,
170
+ verdict: equal ? "unchanged" : "recomputes",
171
+ reason: equal ? undefined : "derived atom — recomputes from its sources",
172
+ oldValue: baseAtoms[label]?.value,
173
+ newValue: entry.value
174
+ });
175
+ continue;
176
+ }
177
+ if (liveMode) {
178
+ const info = _jotaiStateStore.jotaiStateStore.getAtom(label);
179
+ if (!info) {
180
+ items.push({
181
+ key: label,
182
+ label,
183
+ verdict: "wont-apply",
184
+ reason: "atom not registered (renamed, or unwatched since snapshot?)"
185
+ });
186
+ continue;
187
+ }
188
+ if (!info.setValue) {
189
+ items.push({
190
+ key: label,
191
+ label,
192
+ verdict: "wont-apply",
193
+ reason: "atom is now read-only (derived)"
194
+ });
195
+ continue;
196
+ }
197
+ }
198
+ if (!(label in baseAtoms)) {
199
+ items.push({
200
+ key: label,
201
+ label,
202
+ verdict: "added",
203
+ newValue: entry.value
204
+ });
205
+ } else if (equal) {
206
+ items.push({
207
+ key: label,
208
+ label,
209
+ verdict: "unchanged"
210
+ });
211
+ } else {
212
+ items.push({
213
+ key: label,
214
+ label,
215
+ verdict: "changed",
216
+ oldValue: baseAtoms[label].value,
217
+ newValue: entry.value
218
+ });
219
+ }
220
+ }
221
+ return {
222
+ items,
223
+ notes
224
+ };
225
+ }
118
226
  (0, _sharedUi.registerStateSnapshotProvider)({
119
227
  id: "jotai",
120
228
  label: "Jotai",
121
229
  status,
122
230
  capture,
123
- restore
231
+ restore,
232
+ previewItems
124
233
  });
@@ -16,6 +16,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
16
  function getValuePreview(atom) {
17
17
  try {
18
18
  const value = atom.getValue();
19
+ if (value && typeof value === "object" && value.__buoyValueOnDevice === true) {
20
+ return "on device";
21
+ }
19
22
  if (value === undefined) return "undefined";
20
23
  if (value === null) return "null";
21
24
  if (Array.isArray(value)) {
@@ -121,7 +124,8 @@ function EmptyBrowserState() {
121
124
  export function JotaiAtomBrowser({
122
125
  atoms,
123
126
  searchQuery,
124
- onViewHistory
127
+ onViewHistory,
128
+ onAtomExpand
125
129
  }) {
126
130
  const [expandedAtom, setExpandedAtom] = useState(null);
127
131
  const filteredAtoms = useMemo(() => {
@@ -130,8 +134,12 @@ export function JotaiAtomBrowser({
130
134
  return atoms.filter(a => a.label.toLowerCase().includes(search));
131
135
  }, [atoms, searchQuery]);
132
136
  const handleAtomPress = useCallback(atom => {
133
- setExpandedAtom(prev => prev === atom.label ? null : atom.label);
134
- }, []);
137
+ setExpandedAtom(prev => {
138
+ const next = prev === atom.label ? null : atom.label;
139
+ if (next) onAtomExpand?.(atom.label);
140
+ return next;
141
+ });
142
+ }, [onAtomExpand]);
135
143
  if (filteredAtoms.length === 0 && !searchQuery) {
136
144
  return /*#__PURE__*/_jsx(EmptyBrowserState, {});
137
145
  }
@@ -271,9 +271,11 @@ export function JotaiAtomDetailContent({
271
271
  changes,
272
272
  selectedIndex,
273
273
  onIndexChange,
274
- disableInternalFooter = false
274
+ disableInternalFooter = false,
275
+ initialView = "change"
275
276
  }) {
276
- const [activeView, setActiveView] = useState("change");
277
+ // View state (initialView seeds it once; taps own it afterwards)
278
+ const [activeView, setActiveView] = useState(initialView);
277
279
  const [diffMode, setDiffMode] = useState("tree");
278
280
  const totalChanges = changes.length;
279
281
  const atomColor = jotaiStateStore.getAtomColor(change.atomLabel);
@@ -8,7 +8,7 @@
8
8
  * - Events tab: Live atom change monitoring with diffs
9
9
  */
10
10
 
11
- import { useState, useMemo, useRef, useCallback } from "react";
11
+ import { useState, useMemo, useRef, useCallback, useEffect } from "react";
12
12
  import { View, Text, StyleSheet, FlatList, TextInput, TouchableOpacity } from "react-native";
13
13
  import { JsModal, ModalHeader, TabSelector, macOSColors, buoyColors, Search, Filter, Power, X, Box, devToolsStorageKeys, ProUpgradeModal, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider } from "@buoy-gg/shared-ui";
14
14
  import { useIsPro } from "@buoy-gg/license";
@@ -42,11 +42,16 @@ export function JotaiModal({
42
42
  onBack,
43
43
  onMinimize,
44
44
  enableSharedModalDimensions = false,
45
- inline = false
45
+ inline = false,
46
+ initialTab,
47
+ initialSelectedChangeId,
48
+ initialDetailView,
49
+ onChangeSelect,
50
+ onAtomExpand
46
51
  }) {
47
52
  const isPro = useIsPro();
48
53
  const [showProModal, setShowProModal] = useState(false);
49
- const [activeTab, setActiveTab] = useState("atoms");
54
+ const [activeTab, setActiveTab] = useState(initialTab ?? "atoms");
50
55
  const [selectedHistoryAtom, setSelectedHistoryAtom] = useState(null);
51
56
  const [showFilters, setShowFilters] = useState(false);
52
57
  const [ignoredPatterns, setIgnoredPatterns] = useState(new Set());
@@ -98,6 +103,30 @@ export function JotaiModal({
98
103
  }, [selectedChangeId, activeChanges]);
99
104
  const selectedChange = selectedChangeIndex !== null ? activeChanges[selectedChangeIndex] : null;
100
105
 
106
+ // Notify the host on selection (keyed by change id so re-renders with the
107
+ // same selection don't re-fire; remote mirrors fetch atom values here).
108
+ const lastNotifiedSelectionRef = useRef(null);
109
+ useEffect(() => {
110
+ if (!onChangeSelect) return;
111
+ const id = selectedChange?.id ?? null;
112
+ if (id === null || id === lastNotifiedSelectionRef.current) return;
113
+ lastNotifiedSelectionRef.current = id;
114
+ onChangeSelect(selectedChange);
115
+ }, [selectedChange, onChangeSelect]);
116
+
117
+ // Deep link into a change's detail (see initialSelectedChangeId). Applied
118
+ // at most once per id, so the list re-rendering — or the user pressing Back
119
+ // — never re-opens it.
120
+ const appliedInitialSelectionRef = useRef(null);
121
+ useEffect(() => {
122
+ if (!initialSelectedChangeId) return;
123
+ if (appliedInitialSelectionRef.current === initialSelectedChangeId) return;
124
+ const exists = activeChanges.some(c => c.id === initialSelectedChangeId);
125
+ if (!exists) return;
126
+ appliedInitialSelectionRef.current = initialSelectedChangeId;
127
+ setSelectedChangeId(initialSelectedChangeId);
128
+ }, [initialSelectedChangeId, activeChanges]);
129
+
101
130
  // Search state
102
131
  const [searchText, setSearchText] = useState("");
103
132
  const [isSearchActive, setIsSearchActive] = useState(false);
@@ -338,7 +367,8 @@ export function JotaiModal({
338
367
  changes: activeChanges,
339
368
  selectedIndex: selectedChangeIndex,
340
369
  onIndexChange: handleIndexChange,
341
- disableInternalFooter: true
370
+ disableInternalFooter: true,
371
+ initialView: initialDetailView
342
372
  });
343
373
  }
344
374
  if (selectedHistoryAtom) {
@@ -371,7 +401,8 @@ export function JotaiModal({
371
401
  return /*#__PURE__*/_jsx(JotaiAtomBrowser, {
372
402
  atoms: displayedAtoms,
373
403
  searchQuery: searchText,
374
- onViewHistory: handleViewHistory
404
+ onViewHistory: handleViewHistory,
405
+ onAtomExpand: onAtomExpand
375
406
  });
376
407
  }
377
408